Return of syntax highlighting and code completion for KnockoutJS in VS2010 (when using Razor)

OK, admittedly, this is a workaround for an issue where the syntax of jQuery Templates (used by KnockoutJS) doesn’t lend itself to the most pleasant editing experience in Visual Studio, but eh.

This was inspired after talking with Ryan a bit and seeing a recent post on his new web site. Here’s what I came up with.

Following a similar pattern to the BeginForm Helper, I created a “Template” helper. It’s simple to use as the code below demonstrates (the example is taken from the KnockoutJS web site).

<div data-bind='template: "personTemplate"'> </div>

@using (Html.Template("personTemplate"))
{ <text>    
    ${ name } is ${ age } years old
    <button data-bind='click: makeOlder'>Make older</button>
</text> }
     
<script type='text/javascript'>
    var viewModel = {
        name: ko.observable('Bert'),
        age: ko.observable(78),
        makeOlder: function () {
            this.age(this.age() + 1);
        }
    };
    ko.applyBindings(viewModel);
</script>

Sometimes, the Razor compiler/engine is confused by the template syntax however, so to work around that, you’ll need to add the <text>…</text> block to prevent the template syntax from being parsed as Razor syntax. The example above shouldn’t require it. The one that causes problems I’ve found mostly right now is the conditional {{ if }} block which apparently looks like Razor/C# code, and fails. The <text> tag syntax isn’t tragic. The most annoying part is that it highlights as bright yellow.

The Template Helper emits the start and end <script> tags appropriately. There’s an optional second parameter that allows the developer to override the default of the type being text/html.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;

namespace TestMVC.Web
{
    public static class MvcExtensions
    {
        /// <summary>
        /// Automatically generates a script block, useful for non-typical script
        /// tags that have HTML content inside (like those in jquery templates for example)
        /// Always use this within using statement as Dispose must be called to properly close
        /// the script tag.
        /// </summary>
        /// <param name="helper">Html Helper object</param>
        /// <param name="id">The ID for the generated script tag.</param>
        /// <returns>TemplateBlock object which must be disposed to properly emit
        /// the necessary script tags.</returns>
        public static TemplateBlock Template(this HtmlHelper helper, string id)
        {
            return Template(helper, id, "");
        }

        /// <summary>
        /// Automatically generates a script block, useful for non-typical script
        /// tags that have HTML content inside (like those in jquery templates for example)
        /// Always use this within using statement as Dispose must be called to properly close
        /// the script tag.
        /// </summary>
        /// <param name="helper">Html Helper object</param>
        /// <param name="id">The ID for the generated script tag.</param>
        /// <param name="type">Defaults to text/html, but may be overriden by setting
        /// this parameter.</param>
        /// <returns>TemplateBlock object which must be disposed to properly emit
        /// the necessary script tags.</returns>
        public static TemplateBlock Template(this HtmlHelper helper, string id, string type)
        {
            return new TemplateBlock(helper.ViewContext, id, type);
        }
    
    }

    public class TemplateBlock : IDisposable
    {
        private bool _disposed = false;
        public ViewContext ViewContext { get; private set; }
        
        public TemplateBlock(ViewContext context, string id, string type)
        {
            this.ViewContext = context;
            type = string.IsNullOrWhiteSpace(type) ? "text/html" : type;
            context.Writer.Write("<script type='{0}' id='{1}'>\n", type, id);
        }

        private void Disposing(bool disposing)
        {
            if (!_disposed)
            {
                _disposed = true;
                ViewContext.Writer.Write("</script>\n");
            }
        }
        public void Dispose()
        {
            this.Disposing(true);            
        }
    }

}

Enjoy.