ASP.NET MVC Tutorial

ASP.NET MVC Model to Knockout Viewmodel

knowventBanner

This is a quick article on how to share your ASP.NET MVC Model to Knockout Viewmodel structure with the MVC framework and Knockout’s ViewModel. Keeping with the DRY methodology we don’t want to maintain multiple definitions of the same view model in JavaScript and in C#.

How to get ASP.NET MVC Model to Knockout Viewmodel

Along with the sharing of view model code I have a simple set of extension methods used to glue the C# Model to Knockouts MVVM.

So the rule is: Define your Model in the MVC model and share this model to your web app

The View

<div id="cntrWidgetUserStats">

    <h2>@ViewBag.Title<small><a class="glyphicon glyphicon-refresh pull-right" href="#" data-bind="click: refresh"></a></small></h2>

    <ul class="nav nav-pills nav-stacked">
        <li>
            <span class="badge pull-right" data-bind="text: VisitCount"></span>
            Total Visits
        </li>
        <li>
            <span class="badge pull-right" data-bind="text: ConsecutiveDaysVisited"></span>
            Consecutive Visits
        </li>
        <li>
            <span class="badge pull-right" data-bind="text: UserScore"></span>
            User Score
        </li>
    </ul>
</div>
@Html.RenderPartialKnockoutResourceWithScope("UserStats", "Widget", (string)Model.ToJson(), "cntrWidgetUserStats")

This view is sharing it’s MVC model using the custom extension method @Html.RenderPartialKnockoutResourceWithScope by passing it a serialized Json representation of the model.

The Extension Method

    public static IHtmlString RenderPartialKnockoutResource(this HtmlHelper HtmlHelper, string Action, string Controller, string jsonModel)
    {
        return HtmlHelper.RenderPartialKnockoutResourceWithScope(Action, Controller, jsonModel, null);
    }
    public static IHtmlString RenderPartialKnockoutResourceWithScope(this HtmlHelper HtmlHelper, string Action, string Controller, string jsonModel, string rootDomBindingElement)
    {
        Func<object, HelperResult> jsViewModelInit = (d =>
        {
            return new HelperResult(writer =>
            {
                writer.Write(@"<script src='{0}Scripts/app/Views/{1}/{2}.ViewModel.js' type='text/javascript'></script>", 
                                        HtmlHelper.GetApplicationBaseUrl(), 
                                        Controller, 
                                        Action);
            });

        });

        Func<object, HelperResult> jsKnockOutInit = (d =>
        {
            return new HelperResult(writer =>
            {
                dynamic rawModel = HtmlHelper.Raw(jsonModel);
                StringBuilder sb = new StringBuilder();
                sb.AppendLine("<script type='text/javascript'>");
                sb.AppendLine("     $(function () {");
                sb.AppendFormat("     var viewModelObj = APP.{0}.{1}ViewModel(ko.mapping.fromJS({2}));{3}", Controller, Action, rawModel, Environment.NewLine);
                if (string.IsNullOrEmpty(rootDomBindingElement))
                    sb.AppendLine("     ko.applyBindings(viewModelObj);");
                else
                    sb.AppendFormat("     ko.applyBindings(viewModelObj, document.getElementById('{0}'));{1}", rootDomBindingElement, Environment.NewLine);
                sb.AppendLine("     });");
                sb.AppendLine("</script>");
                writer.Write(sb.ToString());
            });

        });

        if (HtmlHelper.ViewContext.HttpContext.Items["js"] == null)
        {
            HtmlHelper.ViewContext.HttpContext.Items["js"] = new List<Func<object, HelperResult>>();
        }

        ((List<Func<object, HelperResult>>)HtmlHelper.ViewContext.HttpContext.Items["js"]).Add(jsViewModelInit);
        ((List<Func<object, HelperResult>>)HtmlHelper.ViewContext.HttpContext.Items["js"]).Add(jsKnockOutInit);

        return new HtmlString(String.Empty);
    }

This helper adds the resource supplied to the HttpContext to written out later on the Master/Layout view (more on this later). This helper assumes your project is setup with the following requirements:

  • Knockout.mapping plugin is referenced.
  • Javascript file naming conversion is defined for each view using the following pattern: /Scripts/app/Views/{Controller}/{Action}.ViewModel.js. The helper class will look here for its Knockout observable implementation
  • Javascript observable function name is defined for each view using the following pattern: APP.{Controller}.{Action}ViewModel

So when you call RenderPartialKnockoutResourceWithScope on each view it doesn’t actually write out the resources at that time. It simply adds them to a dictionary stored in the HttpContext. The following helper method actually writes out the registered Knockout resources:

    public static IHtmlString RenderPartialResources(this HtmlHelper HtmlHelper, string Type)
    {
        if (HtmlHelper.ViewContext.HttpContext.Items[Type] != null)
        {
            List<Func<object, HelperResult>> Resources = (List<Func<object, HelperResult>>)HtmlHelper.ViewContext.HttpContext.Items[Type];


            foreach (var Resource in Resources)
            {
                if (Resource != null) HtmlHelper.ViewContext.Writer.Write(Resource(null));
            }
        }
    }

This, in turn, is called on the Master/Layout page at the bottom so the scripts are executed by the browser in the correct order:

@Html.RenderPartialResources("js")

For each view or partial view using a knockout MVVM helper method you will see the following script rendered:

<script src='http://localhost:60030/Scripts/app/Views/Widget/UserStats.ViewModel.js' type='text/javascript'></script>
<script type='text/javascript'>
     $(function () {
     var viewModelObj = APP.Widget.UserStatsViewModel(ko.mapping.fromJS({"VisitCount":"5282","ConsecutiveDaysVisited":"23","UserScore":"+323"}));
     ko.applyBindings(viewModelObj, document.getElementById('cntrWidgetUserStats'));
     });
</script>

Notice ko.mapping.fromJS parameters object. It is the MVC model being passed to initialize the knockout view model. The UserStatsViewModel function is where you would implement your custom Knockout binding, rules, etc. Again, we are using a naming conversion to define our view’s knockout init APP.{Controller}.{Action}ViewModel

APP.Widget.UserStatsViewModel = function (model) {
    var self = model;
    self.refresh = function () {
        $.post(APP.helpers.prepareRelativeUrl("Widget/UpdateUserStats"), APP.helpers.addAntiForgeryToken({}), function (data) {
            ko.mapping.fromJS(data, {}, self);
        });
    }
    return ko.validatedObservable(self);
};

Note: The APP.Widget instantiation is performed in the ‘/Script/App/app.js’ file: APP.Widget = {}; This need to be done for each controller used.

And that is it. Easily implementing and initializing a Knockout observable without having the re-write your MVC Model.

Download it! If you would like what you have read and would like more information feel free to download there source or fork the project on GitHub.

Let me know what you think and if you have any questions.

Posted in .NET, Knockout, MVC and tagged , , , , , , .