Using Modals with Durandal and ASP .NET MVC

Recently I added Durandal to a portion of our Centare Client Dashboard application to make the experience more modern when managing work items for our various projects.  My decision to use Durandal was based on the fact that our existing implementation used KnockoutJS for client side interaction and Durandal relies on Knockout for its view models.

The application is pretty straightforward.  The user selects a project from a list and see’s work items in various views (product backlog, current and past iteration/sprint).  The items are displayed in a tabular list with each row being clickable.  When clicked a modal dialog is presented enabling the user to make changes to the work item.

Because changes need to be reflected in the list, I knew that I would have to use Knockout so that certain fields in the view model representing each line could contain observables.  While this sounded great in theory, in ended up falling apart.

Modals in Durandal are created using composition based on the name of the view model backing the modal, this also dictates which view is used for the HTML.  The key with a list like this is to create a POVM (Plain Old View Model), that is one that is not defined through RequireJS syntax.  Below shows how I got around this:

define(['knockout', 'plugins/http', 'durandal/app', 'jquery', 'jquery.ui'], function(ko, http, app, $) {
    function ctor() {
        var that = this;
        that.workItems = ko.observableArray([]);

        this.compositionComplete = function() {
            that.workItems([]);

            http.get(GetBacklogListUrl()).then(function (result) {
                var canEdit = result.CanEdit;

                that.workItems(response.WorkItems.map(function (v) {
                    return new WorkItemViewModel(v, canEdit);
                }));
            });
        };

       function WorkItemViewModel(json, canEdit) {
            this.canEdit = canEdit;
            this.ID = json.ID;
            this.IsBug = json.IsBug;
            this.Attachments = json.Attachments;
            this.Effort = json.Effort;
            this.State = json.State;
            this.Tags = json.Tags;
            this.Description = json.Description;
            this.AcceptanceCriteria = json.AcceptanceCriteria;
            this.StepsToReproduce = json.StepsToReproduce;
            this.History = json.History;

            this.Title = ko.observable(json.Title);
            this.BusinessValue = ko.observable(json.BusinessValue);
        }
    };

    return new ctor();
});

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

We are able to declare a local JavaScript class which, since its inside the RequireJS module it can use the injected libraries for the view model.  The idea here is to keep your view model definition for the line items as simple as possible, you can open the modal by doing the following:

        this.view = function (data) {
            app.showDialog('/Scripts/app/viewmodels/project/workitem.detail.js', data).then(function (result) {
                if (result != null) {
                    var item = _.find(that.workItems(), function(v) {
                        return v.ID == result.id;
                    });

                    item.Title(result.title());
                    item.BusinessValue(result.businessValue());
                }
            });
        };

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

Based on convention, this code will attempt to grab HTML from /Scripts/app/views/project/workitem.detail.js/, so we will create a custom Route rule in ASP .NET so load a specific view for this path, below is that route:

            routes.MapRoute(
                name: "Dialog View",
                url: "Scripts/app/views/project/workitem.detail.js/",
                defaults: new { controller = "WorkItem", action = "Detail" });

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

You notice the call to .then, in Durandal modal dialogs are handled as promises, meaning they will allow you to pass data back on close.  This is very useful when creating applications and helps us easily update the source entry after a save operation.

This is where you will use a full blown RequireJS module to represent the work items actual data, the view model being loaded is workitem.detail.js as indicated by the first parameter to app.showDialog.

To close the modal, simply call close on the dialog plugin and pass a reference to the view model itself, this will allow Durandal to determine which dialog to close.  Here is an example:

define(['knockout', 'plugins/dialog', 'plugins/http', 'durandal/app', 'jquery', 'bootstrap.wysiwyg', 'jquery.hotkeys'],
    function (ko, dialog, http, app, $) {
    var ctor = function () {
        var that = this;
        
        this.activate = function(ctx) {
        };

        this.attached = function(view, parent) {
        };

        this.saveItem = function () {
            var formData = {
                ID: that.id,
                Title: that.title(),
                BusinessValue: that.businessValue(),
                Description: that.description(),
                AcceptanceCriteria: that.acceptanceCriteria(),
                StepsToReproduce: that.stepsToReproduce(),
                IsBug: that.isBug
            };

            http.post(GetWorkItemSaveUrl(), formData).then(function(result) {
                that.isSaving(false);
                dialog.close(that, that);
            }, function(error) {
                app.showMessage(error.statusText, "Uh oh..");
                that.isSaving(false);
            });
        };

        this.closeWindow = function () {
            dialog.close(that, null);
        };
    };

    return ctor;
});

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

Its pretty straightforward and works even if you have layered modal dialogs (one on top of the other).  Notice that when the user simply closes the window without saving, I pass null otherwise I pass the the view model itself, it is then handled above in then.

I realize that you might be curious what activate, attached, and compositionComplete are; put briefly they are Durandal View Model lifecycle events and they will not be discussed here, you can find more information here: http://durandaljs.com/documentation/Interacting-with-the-DOM.html

Otherwise, look for them in a future entry

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s