ASP.NET MVC Template and Knockout.js

I am trying to get up to speed with knockout.js.  I am brining it into an MVC application.  I am trying to reduce the amount of JavaScript our team has to write, especially in large, complicated screens. 

At the same time, I want to leverage the power of MVC views.  Our application is using DataAnnotations, and I really love the fact that unobtrusive JavaScript validation is automatically injected into my views.  I do not want to give this up to use knockout. I could not find any reasonable solution on the internet, so I decided to create my own.

Let’s first walk through standard approach. I am going to use EntityFramework Code First to create a simple screen with a class that has drop down, some strings and a Boolean field.  I am going to build UI then for this class with a standard query (list) screen and an edit screen.  My class looks as following.

using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace WebDemo.Data
{
    public class Person
    {
        [Key]
        public int PersonId { get; set; }

        [Required(ErrorMessage = "First name is required")]
        [StringLength(30, ErrorMessage = "First name cannot be longer than 30 letters")]
        [DisplayName("First Name")]
        public string FirstName { get; set; }

        [Required(ErrorMessage = "Last name is required")]
        [StringLength(30, ErrorMessage = "Last name cannot be longer than 30 letters")]
        [DisplayName("Last Name")]
        public string LastName { get; set; }

        [DisplayName("Active")]
        public bool IsActive { get; set; }

        public int PersonCategoryId { get; set; }

        [ForeignKey("PersonCategoryId")]
        public PersonCategory PersonCategory { get; set; }

    }
}

Now I am going to use standard MVC UI to build a screen that uses entity framework to enter Person data. In Controller wizard I am using the following values.

image

Note: Ordinarily I would have used repository, but in this demo my focus point is not data.

Now let’s take a look at the Edit view for example (the same issues would exist in Create view as well).

@model WebDemo.Data.Person
@using (Html.BeginForm())
{
    @Html.ValidationSummary(true)
    
    <fieldset>
        <legend>Person</legend>

        @Html.HiddenFor(model => model.PersonId)

        <div class="editor-label">
            @Html.LabelFor(model => model.FirstName)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.FirstName)
            @Html.ValidationMessageFor(model => model.FirstName)
        </div>

        <div class="editor-label">
            @Html.LabelFor(model => model.LastName)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.LastName)
            @Html.ValidationMessageFor(model => model.LastName)
        </div>

        <div class="editor-label">
            @Html.LabelFor(model => model.IsActive)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.IsActive)
            @Html.ValidationMessageFor(model => model.IsActive)
        </div>

        <div class="editor-label">
            @Html.LabelFor(model => model.PersonCategoryId, "PersonCategory")
        </div>
        <div class="editor-field">
            @Html.DropDownList("PersonCategoryId", String.Empty)
            @Html.ValidationMessageFor(model => model.PersonCategoryId)
        </div>

        <p>
            <input type="submit" value="Save" />
        </p>
    </fieldset>
}

<div>
    @Html.ActionLink("Back to List", "Index")
</div>

The first issue is that each entry field take about 6 lines of html. My first step on my way to deal with knockout is centralize the code for fields. What I am going to use is custom templates capability in ASP.NET MVC.  I am going to start by creating a template for a text field.  To do that I am going to add EditorTemplates folder under ViewsShared folder in my project first.

image

Now, I am going to create view as a base for my template.  I am going to call it TextBox.cshtml.

image

To get a head start I am going to copy initial html from the Edit view, but them I have to modify because my Model passed to my edit template is just a single field, FirstName of a person for example.  Hence, I cannot use TextBoxFor for example, so I am just going to use TextBox method.  The other key to creating a successful editor template is the knowledge about metadata for the field.  It is available off ViewData project luckily.  Also, I have to do a bit of extra work to inject attributes manually, such as “for” attribute into the label.  Here is the final product.

<div class="editor-label">
    @Html.Label(ViewData.ModelMetadata.DisplayName,
        new Dictionary<string, object>
            {
                { "for", ViewData.ModelMetadata.PropertyName }
            })
</div>
<div class="editor-field">
    @Html.TextBox("", (object)Model,
        new Dictionary<string, object>
            {
                { "id", ViewData.ModelMetadata.PropertyName },
                { "name", ViewData.ModelMetadata.PropertyName },
                { "class", "text-box single-line"}
            })
    @Html.ValidationMessage(ViewData.ModelMetadata.PropertyName,
        new Dictionary<string, object>
            {
                { "data-valmsg-for", ViewData.ModelMetadata.PropertyName }
            })
</div>

 

I can now use this editor in my Edit view. To support first name entry for example I now have just one line of code.

@Html.EditorFor(model=>model.FirstName, "TextBox")

I do not quite like the look of this though – I have to rely on magic strings.  So, I am going to define an enumeration for custom edit templates and an extension method for EditorFor.

namespace WebDemo.Helper
{
    public enum EditorTemplate
    {
        TextBox,
        CheckBox
    }
}
namespace System.Web.Mvc.Html
{
    public static class CustomHtmlExtensions
    {
        public static MvcHtmlString EditorFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, EditorTemplate editorTemplate)
        {
            return html.EditorFor(expression, editorTemplate.ToString());
        }
    }
}

Now, I can clean up my Edit view code some more.

@Html.EditorFor(model=>model.FirstName, EditorTemplate.TextBox)

Cool so far. Now, checkbox.

<div class="editor-label">
    @Html.Label(ViewData.ModelMetadata.DisplayName,
        new Dictionary<string, object>
            {
                { "for", ViewData.ModelMetadata.PropertyName }
            })
</div>
<div class="editor-field">
    @Html.CheckBox("", (bool)Model,
        new Dictionary<string, object>
            {
                { "id", ViewData.ModelMetadata.PropertyName },
                { "name", ViewData.ModelMetadata.PropertyName },
                { "class", "check-box"}
            })
    @Html.ValidationMessage(ViewData.ModelMetadata.PropertyName,
        new Dictionary<string, object>
            {
                { "data-valmsg-for", ViewData.ModelMetadata.PropertyName }
            })
</div>

Drop down is a bit complicated and now relies on some conventions, just like the Controller does.  For example, the drop down data is added to ViewBag in the controller.  I changed it a bit to simplify my Html. My Create method looks as follows.

        public ActionResult Create()
        {
            ViewData["PersonCategoryId"] = 
                new SelectList(db.PersonCategories, "PersonCategoryId", "Name", 0);
            return View(new Person());
        }

As you can see above, I create a list and add it to ViewData using the property name that the drop down is for.  Now my template just needs to pull that data back out and call DropDownList method.

<div class="editor-label">
    @Html.Label(ViewData.ModelMetadata.DisplayName,
        new Dictionary<string, object>
            {
                { "for", ViewData.ModelMetadata.PropertyName }
            })
</div>
<div class="editor-field">
    @Html.DropDownList("", (IEnumerable<SelectListItem>)ViewData[ViewData.ModelMetadata.PropertyName] ,
        new Dictionary<string, object>
            {
                { "id", ViewData.ModelMetadata.PropertyName },
                { "name", ViewData.ModelMetadata.PropertyName }
            })
    @Html.ValidationMessage(ViewData.ModelMetadata.PropertyName,
        new Dictionary<string, object>
            {
                { "data-valmsg-for", ViewData.ModelMetadata.PropertyName }
            })
</div>

Now, the view looks extremely clean.

@using WebDemo.Helper
@model WebDemo.Data.Person
@using (Html.BeginForm())
{
    @Html.ValidationSummary(true)
    
    <fieldset>
        <legend>Person</legend>

        @Html.HiddenFor(model => model.PersonId)
        @Html.EditorFor(model => model.FirstName, EditorTemplate.TextBox)
        @Html.EditorFor(model => model.LastName, EditorTemplate.TextBox)
        @Html.EditorFor(model => model.IsActive, EditorTemplate.CheckBox)
        @Html.EditorFor(model => model.PersonCategoryId, EditorTemplate.DropDown)

        <p>
            <input type="submit" value="Save" />
        </p>
    </fieldset>
}

<div>
    @Html.ActionLink("Back to List", "Index")
</div>

Very nice, right. 

To summarize, I just created a set of custom templates I can use in my views.  Now, adding knockout binding cannot be simpler.  Here is how I would do it for TextBox by simply adding data-bind attribute with the correct value.

<div class="editor-label">
    @Html.Label(ViewData.ModelMetadata.DisplayName,
        new Dictionary<string, object>
            {
                { "for", ViewData.ModelMetadata.PropertyName }
            })
</div>
<div class="editor-field">
    @Html.TextBox("", (object)Model,
        new Dictionary<string, object>
            {
                { "id", ViewData.ModelMetadata.PropertyName },
                { "name", ViewData.ModelMetadata.PropertyName },
                { "class", "text-box single-line"},
                { "data-bind", "value: " + ViewData.ModelMetadata.PropertyName },
            })
    @Html.ValidationMessage(ViewData.ModelMetadata.PropertyName,
        new Dictionary<string, object>
            {
                { "data-valmsg-for", ViewData.ModelMetadata.PropertyName }
            })
</div>

Voila!  The code for the dropdown and checkbox is identical. I used some conventions of course, and relied on the fact that DisplayName attribute was defined.  You can always customize to suit your needs.

Please let me know what you think about this approach.  Feel free to download the demo as it stands today.  I will work on adding knockout view model functionality with save in the next post.

Thanks.

 

 

 

 

 

39 Comments

  1. Hi Sergey,

    Very nice! Do you know if this approach works in ASP.NET MVC4 also?

    I’ve been struggling the whole day today with this layout http://forum.jquery.com/topic/why-one-code-always-firing-and-the-other-one-never#14737000003630988

    I am wondering if using your approach I can also do something similar for the Phone + Ext together?

    Also, do you know where this etc comes from? Is it something defined internally?

    Thanks again for your blog and I am going to try to implement it as is in my application also.

  2. Yes, I downloaded the demo and looking at it right now.

    My question – sort of – say you have Phone and Ext fields in your model and you want to show them as one nice control in UI (e.g.
    Phone [ ] Ext [ ]

    It is a bit off-topic, but this is what I may need for the project I am working on right now.

  3. —————————
    Microsoft Visual Studio
    —————————
    Unable to launch the IIS Express Web server.
    Sergey,

    When I am attempting to run your application by pressing F5 I am getting this error

    Failed to register URL “http://localhost:33167/” for site “WebDemo” application “/”. Error description: The process cannot access the file because it is being used by another process. (0x80070020)

    I am going to research later on, but may be you know an easy fix?

  4. Is it OK to have one partial view invoke another? Say, if I create my phone + ext as a partial view, can I insert it into another already partial view (_ClientForm)?

    I am also going to try using your editors – I like much cleaner view. One quick question, though:
    In the model I used the following annotation:
    [Email]
    [StringLength(100)]
    [Column(“C2_Email”, TypeName = “varchar”)]
    public virtual string Email2 { get; set; }

    (This is from DataAnnotationsExtensions library). It automatically handles e-mail validation and shows it right away when I navigate to another control. Will your new editors also handle that automatically?

  5. It is practically the same as textbox

    @Html.Label(ViewData.ModelMetadata.DisplayName,
    new Dictionary
    {
    { “for”, ViewData.ModelMetadata.PropertyName }
    })
    @Html.TextArea(“”, (string)Model,
    new Dictionary
    {
    { “id”, ViewData.ModelMetadata.PropertyName },
    { “name”, ViewData.ModelMetadata.PropertyName },
    { “class”, “text-box multi-line”},
    { “data-bind”, “value: ” + ViewData.ModelMetadata.PropertyName },
    })
    @Html.ValidationMessage(ViewData.ModelMetadata.PropertyName,
    new Dictionary
    {
    { “data-valmsg-for”, ViewData.ModelMetadata.PropertyName }
    })
  6. @Beezo
    Depends on your view model. You can use Html.RadioButton and pass in the name you would like to use for all bool properties that are grouped together. If so, you can always set name for Radio button group to the value you can pass in when calling the EditorFor as dictionary. This will translated into ViewData. So your code will look something like
    @Html.RadioButton(
    ViewData[“GroupName”],
    (bool)Model,
    (bool)Model,
    new Dictionary
    {
    { “id”, ViewData.ModelMetadata.PropertyName },
    {“data-bind”, “checked: ” + ViewData.ModelMetadata.PropertyName },
    { “class”, “radio-button”}
    })

  7. Do you know if it’s possible to somehow access the column name assigned through the mapping and set Id to be the column’s name instead of the property name?

    Say, I have Client class that consists of 2 complex types. If I use the editor as is, the validation messages are not shown.

    This is what I have in the mapping
    this.Property(t => t.Contact1.Email).IsUnicode(false).HasColumnName(“C1_Email”);

    So, in the EditorFor I want to access that column name and use it for Id instead of the EMail property. Do you know if this is possible and how?

  8. I typically just use IIS locally, not express. I try to develop on environment as close to production as I can. I have never seen this error using iIS. Just go to project properties web tab and create a virtual directory, then you can just run it. I am not sure about IIS Express error, but this should get you going.

  9. So, it appears that the id attribute plays a significant role in this pattern. The behavior I’m seeing is this:

    1. when the property being bound does not have an associated Knockout observable, the field is bound to itself resulting in a string value similar of [object HTMLInputElement] or [object HTMLCollection] if there are more than one element on the page with the same id.

    2. the assignment of the id attribute in the template does not account for nested view model properties. So, if my vm has two Address properties, the fields within the Address properties will be assigned duplicate id values. Additionally, if the KO vm doesn’t have an associated property, the ApplyBindings command will now fail since KO can’t even self-reference anymore.

    Have you come across these issues? If so, have you come up with an elegant solution?

    Thanks,
    Vinney

  10. Hi Paul,

    Sorry, I haven’t visited this blog and didn’t see your question till today. I think I found at that time some solution through internet and also I assigned a specific port to my application. I forgot right now the exact steps of how I did it, but hopefully you can google as how to assign the port.

  11. In other words, my new question now is this – how did you create your “EditorFor” (starting from “this is the final product”) and what is the code in the core ASP.NET MVC for this?
    I think your own EditorFor (which I’ve been using) is good, but it has certain limitations compared with the “native” EditorFor which can figure out more information from the model. I think it can automatically handle UIHint and DataType. Have you tried taking all the best from the native ASP.NET editor and incorporate into yours?

    Do you have an update to that blog post?

  12. Pingback: KnockoutMVC Extension Support for DataAnnotations C# | El Yacare

  13. You can always customize your observable model or add additional attributes to pass to the template or enhance the template to pay attention to new attributes if you implement events via attributes.

  14. Pingback: Validation in Angular forms – Part 2 | Sergey Barskiy's Blog

Leave a Reply to Naomi Cancel reply

Your email address will not be published. Required fields are marked *