Sunday, June 1, 2014

Knockout, Mapping plug-in, Typescript and ASP.NET MVC playing nice.

MVVM

In this article I will show how you can use the MVVM pattern to the fullest using the Knockout.JS framework, with the mapping plug-in add-on. The goal of this article is to show that you can define your view model at the back-end only, and have it "mapped" dynamically at the client side without the need to code it by hand. To make the coding simpler I have decided to use Typescript and test it with JQuery, and Knockout. You can learn more about the mapping plug-in here.

Typescript

To learn more about typescript I recommend you visit this site.

Lets get the environment setup

You will need to get a few Nuget packages to get going... here are some of them:

"

Next you will create a new TypeScript file and add the following dependencies:

The ViewModel

Notice that I only define the view model properties at the back-end. You will not see the fields of the ViewModel on the client side

    public class DomainItem
    {
        public string Name { get; set; }
        public string Description { get; set; }
    }

    public class DomainViewModel
    {
        public DomainViewModel()
        {
            Items = new List();
            Status = "Loaded";
        }
        public string Name { get; set; }
        public List Items { get; set; }
        public string Status { get; set; }
    }

The Controller

    public class HomeController : Controller
    {
        //
        // GET: /Configuration/
        public ActionResult Index()
        {

            return View();
        }

        //
        // GET: /Configuration/Details/5
        public ActionResult List()
        {
            DomainViewModel vm = new DomainViewModel();
            vm.Name = "Name1";

            var list = new List();
            vm.Items.Add(new DomainItem { Name = "item 1", Description = "This is item 1" });
            vm.Items.Add(new DomainItem { Name = "item 2", Description = "This is item 2" });
            vm.Items.Add(new DomainItem { Name = "item 3", Description = "This is item 3" });

            return Json(vm, JsonRequestBehavior.AllowGet);
        }

        public ActionResult Refresh()
        {
            Random random = new Random();
            int number = random.Next(1, 50);
                        
            DomainViewModel vm = new DomainViewModel();
            vm.Name = "Name" + number;
            var list = new List();                       
            
            for (int i = 0; i < number; i++ )
            {
                vm.Items.Add(new DomainItem { Name = "item " + i, Description = "This is item " + i });
            }           

            return Json(vm, JsonRequestBehavior.AllowGet);
        }

        [HttpPost]
        public JsonResult SubmitViewModel(DomainViewModel viewModel)
        {
            viewModel.Status = "Saved...";
            return Json(viewModel);
        }
     }
  • List returns list of items, this data will be converted into a view model on the client
  • Refresh simulates a data update on the back-end and sending the updated the data to the client
  • SubmitViewModelSimulates a save operation on the view model, and updating the status field to "Saved..."

The Dynamic ViewModel base class (in Typescript)

This is the base class that holds the necessary operations to dynamically map the view model from the server into the client. It also contains some re-usable flags to indicates if we are in the process of getting data from the back-end, I use this data to notify the user when communication with the back-end occurs.

class DynamicViewModel {

    // set to the true when the data is loaded from the back-end
    isLoaded = ko.observable(false);
    
    // set to true while the data is loading from the back-end
    isLoading = ko.observable(false);

    constructor() {

    }


    // show a hidden html id
    show(id: string) {
        $(id).show();               
    }

    // does an http get to the server, take a url returns the viewmodel. Once the request
    // is done, the view model is added to this class
    httpGet(url: string, callback?: (vm) => void) {

        // keep a pointer to this view model
        var self = this;

        // update flags
        self.isLoaded(false);
        self.isLoading(true);


        // make the REST call using GET
        $.ajax(url, {
            type: "GET",
            cache: false,
        }).done((vm) =>
        {
            // map the view model data we got from the server into this viewmodel
            ko.mapping.fromJS(vm, {}, self);

            // update flags
            self.isLoaded(true);
            self.isLoading(false);

            // if there is a callback, then call it.
            if (callback !== undefined)
                return callback(vm)
        });
    }

    // makes a post call to the server, and updates the viewmodel response
    httpPost(url: string, onSuccess?: (vm) => void) {
        
        // keep a pointer to this viewmodel
        var self = this;

        // update flags
        self.isLoaded(false);
        self.isLoading(true);
        
        // make a POST call to the server
        $.ajax({
            url: url,
            type: 'post',
            // pass this viewmodel
            data: ko.mapping.toJSON(self), 
            contentType: 'application/json',
            success: function (vm) {
                // update flags
                self.isLoaded(true);
                self.isLoading(false);

                // update the view model 
                ko.mapping.fromJS(vm, {}, self);

                // if there is a callback, call it
                if (onSuccess !== undefined)
                    return onSuccess(vm);
            }
        });
    }
}
  • ko.mapping.fromJS(vm, {}, self); this is the code that dynamically expends the view model, and adds the observable items based on the JSON coming back from the back-end
  • ko.mapping.toJSON(self) is used to serialize the view model to JSON so it can be sent to the back-end (it is used on the post)
  • Making making a post to the server, to send the view model, the code ko.mapping.toJSON(self) the magic
  • Notice that I had to use self, that's because the this keyword changes scope even with Typescript
  • the url will be passed from the child class

Child ViewModel

class DomainViewModel extends DynamicViewModel {
    initUrl: string;
    postUrl: string;
    refreshUrl: string;


    constructor(initUrl: string, refreshUrl: string, postUrl: string) { 
        super();  
        this.initUrl = initUrl;
        this.postUrl = postUrl;
        this.refreshUrl = refreshUrl;        
    }

    // Initialize the view model for the first time.
    initializeAction() {

        super.httpGet(this.initUrl, (data) =>
        {
            // apply the binding
            ko.applyBindings(this);
            
            // show the window
            super.show("#Main");
        });
    }


    // Update the data on the UI
    updateAction() {

        super.httpGet(this.refreshUrl);
    }

    submitAction() {

        super.httpPost(this.postUrl);
    }
}

Notice that the DomainViewModel deals mostly with commands and doesn't actually define the data in the view model. This is because the base class will handle injecting the data in when doing a GET or a Post

The View


@{
    ViewBag.Title = "Index";
}

@section scripts
{
    
    <script type="text/javascript">
        $(document).ready(function () {

            // get some url configuration for the view model to do its work
            var initializeUrl = "@Url.Action("List")";
            var refreshUrl = "@Url.Action("Refresh")";
            var submitUrl = "@Url.Action("SubmitViewModel")";

            var domainViewModel = new DomainViewModel(initializeUrl, refreshUrl, submitUrl);

            // initialize the view model
            domainViewModel.initializeAction();
        });

    </script>

    @*typed scripted geneated*@
    <script src="~/Scripts/TypeScript/ViewModel.js"></script>
}

<h2>Configuration Controller</h2>
<div id="Main" class="part" style="display : none">
    <div class="part">
        <h3>Binding to a selection box</h3>
        <select data-bind="options: Items,
                       optionsText: 'Name',
                       optionsCaption: 'Choose...'" size="5" multiple="true"></select>
    </div>

    <div class="part">
        <h3>Binding to a text box</h3>
        <input type="text" data-bind="value: Name" />
    </div>

    <div class="part scrollDiv">
        <h3>Binding to a table</h3>
        <table class="table">
            <thead>
                <tr><th>First name</th><th>Last name</th></tr>
            </thead>
            <tbody data-bind="foreach: Items">
                <tr>
                    <td data-bind="text: Name"></td>
                    <td data-bind="text: Description"></td>
                </tr>
            </tbody>
        </table>
    </div>

    <div class="part">
        <label data-bind="text: Status"></label>
    </div>
    <div class="part">
        <button data-bind="click: updateAction">Refresh data</button>
    </div>

    <div class="part">
        <button data-bind="click: submitAction">Submit data</button>
    </div>


    <div class="part">
        <label data-bind="if:isLoading">Loading from server...</label>
        <label data-bind="if:isLoaded"> Loading from server... done</label>        
    </div>

</div>

No need to code the ViewModel by hand in Javascript anymore

So thanks to the mapping plug-in for knockout, you don't need to worry about typing all the view model observable by hand anymore. You can use the base class view model and use it to do most of the work for you. This code is not production quality code, and only used to show the use of a dynamic view model using the knockout mapper plug-in

.

The full code

Click here for the full source code (compiled with Visual Studio 2013 Update 2)

2 comments:

Anonymous said...

Nice article.
But,
you didn't use the power of Typescript here since when you use mapping plugin - fromJson you get object. No code completion and type safety will be provided.

If you find a way to deal with that, let us know :)

Anonymous said...

Hi, great article, very helpfull.

Just a question about it, I saw you are mapping the DynamicViewModel with JS data type, and by the other hand the get method from MVC returns data as JSON.

¿Does the mapper works with "incopatible" data types alike?