code blog foo - tag line bar

Leveraging the MVP Design Pattern to Perform Asynchronous Communication in ASP.NET MVC

I know what you're thinking... "Why in the world would you use the MVP design pattern in ASP.NET MVC?!!!" I have a blog post that goes into the difference between MVC, MVP, and MVVM, but let me try to summarize why.

The Problem

MVC (Model-View-Controller) is a wonderful design pattern that works great in certain situations. MVC shines when all application interaction can be funneled through a finite set of events or interaction points. For web applications, that very simple (yet so powerful) interaction point is the HTTP Request. Think about it...every interaction you have with a "plain Jane" web site is either through an HTTP/GET request, or an HTTP/POST request. ASP.NET MVC intercepts the HTTP Request via the MvcHttpHandler, and sends it through ASP.NET MVC's pipeline. So here's the problem...when DHTML and asynchronous communication via javascript come into the picture, the MVC design pattern falls apart. When DHTML is involved, the interaction point with an application is no longer funneled through an HTTP/GET or HTTP/POST...events such as button clicks, mouse hovers, keyboard input (and anything else you can think of doing with javascript and the HTML page) come into the picture.

The Easy Solution

To reiterate, DHTML with asynchronous communication does not work with the MVC design pattern because we no longer have a finite set of interaction points that can be funneled to the MVC pipeline (there are many DHMTL events and controls that can cause server-side processing). ASP.NET MVC attempts to remedy this with Ajax.BeginForm and I would recommend using this (or jQuery) for simple asynchronous interaction...here is a blog post that shows how to use Ajax.BeginForm and asynchronous jQuery. This simple approach works really well for small asynchronous tasks, but may fall short in web pages that have very complex asynchronous interactions.

When Ajax.BeginForm or $.getJSON/$.post Doesn't Cut It

Believe it or not, javascript is becoming a multi-browser, multi-OS programming platform for client side interaction. Because of this, we can "safely" implement the MVP design pattern entirely in client side javascript. So if you've tried the easy solution and it "smells bad," read on and consider using the MVP (in this case, the Supervising Controller flavor) design pattern (but in client side javascript).

Javascript MVP Implementation Walk Through

Here we go. This is what the walk through will cover:

Object Oriented Javascript

This is a very quick crash course on OO Javascript. Here is how you create a class in javascript (we are going to create a Person class in javascript):

function Person() {
}

This is how you new up a Person object:

function Person() {
}
   
//new up person class
var somePerson = new Person();

This is how you declare a constructor with parameters:

function Person(firstName, lastName) {
}

This is how you new up a Person that takes in a first and last Name:

function Person(firstName, lastName) {
}

//new up person class with parameters
var somePerson = new Person('John', 'Doe');

This is how you take those constructor parameters and associate them with properties that can be accessed....and how you new up and access those properties.

function Person(firstName, lastName) {
    //you MUST use the "this" keyword
    this.FirstName = firstName;
    this.LastName = lastName;
}

//new up person class with parameters
var somePerson = new Person('John', 'Doe');
alert(somePerson.FirstName + " " + somePerson.LastName);

This is how you add a SayHello method to our Person call...and how to call that method.

function Person(firstName, lastName) {
    //you MUST use the "this" keyword
    this.FirstName = firstName;
    this.LastName = lastName;
}

//every object in javascript has a "prototype" property
//that allows you to extend an entity...
//in this case, we are extending Person to contain
//a SayHello method
Person.prototype.SayHello = function() {
    //you MUST use the "this" keyword to access the properties
    alert(this.FirstName + ' ' + this.LastName + ' says hello!');
}

//new up person class with parameters
var somePerson = new Person('John', 'Doe');

//this is how you call the SayHello method:
//notice that when calling the method, you do not have to 
//do somePerson.prototype.SayHello()...you just have to do
//classInstance.SayHello()
somePerson.SayHello();

This is how you add a Greet method that takes in another Person object as a parameter...and how to call the Greet method:

function Person(firstName, lastName) {
    this.FirstName = firstName;
    this.LastName = lastName;
}

Person.prototype.SayHello = function() {
    alert(this.FirstName + ' ' + this.LastName + ' says hello!');
}

Person.prototype.Greet = function(otherPerson) {
    alert(this.FirstName + ' ' + this.LastName + ' says hello to ' + otherPerson.FirstName + ' ' + otherPerson.LastName + '!');
}

//new up person class with parameters
var somePerson = new Person('John', 'Doe');

//call greet passing in another person object as a parameter
//when calling this method, you would see an alert box that says: 
//"John Doe says hello to Jane Smith!"
somePerson.Greet(new Person('Jane', 'Smith'));

So this C# Person class:

public class Person
{
    public Person(string firstName, string lastName) 
    {
        FirstName = firstName;
        LastName = lastName;
    }

    public string FirstName { get; set; }
    public string LastName { get; set; }
    public void SayHello() { }
    public void Greet(Person otherPerson) { }
}

Looks like the following in javascript:

function Person(firstName, lastName) {
    this.FirstName = firstName;
    this.LastName = lastName;
}

Person.prototype.SayHello = function() { }

Person.prototype.Greet = function(otherPerson) { }

Crystal clear? Wonderful! Let's move on.

Creating your Presenter in Javascript

For this example, consider the following HTML page. The page will be used to retrieve and save a person:

//controller action for retrieving the initial view
[HttpGet]
public ActionResult Index(Guid? personId)
{
    ViewData["PersonId"] = personId ?? Guid.NewGuid();
    return View();
}
<div>
    <span id="spanStatus" style="color: Green"></span><br />
    <input type="hidden" id="personId" value='<%= ViewData["PersonId"].ToString() %>' />
    First Name:<br />
    <input id="firstName" type="text" /><br />
    Last Name:<br />
    <input id="lastName" type="text" /><br />
    <input type="submit" value="save" id="buttonSave" />
</div>

The presenter for this view will need a Load method for initializing the page (and getting things going), a GetPerson method for retrieving a person, and a SavePerson method for saving a person. Here is the starting point for the presenter:

//PersonPresenter
function PersonPresenter() {

}

//Load method which will get called when the html page is loaded
PersonPresenter.prototype.Load = function () {

}

//Request to retrieve a person
PersonPresenter.prototype.GetPerson = function () {

}

//Request to save a person
PersonPresenter.prototype.SavePerson = function () {

}

Pretty straight forward (again, if you aren't familiar with Model-View-Presenter I would recommend reading my blog post that goes into more details). Now that we have a presenter, we need to inject a view (the view will also be implemented in javascript). The view will get passed into the presenter's constructor:

function PersonPresenter(view) {
    this._view = view;
}

Here is what the html page looks like so far:

<script type="text/javascript">
    //PersonPresenter
    function PersonPresenter(view) {
        this._view = view;
    }

    //Load method which will get called when the html page is loaded
    PersonPresenter.prototype.Load = function () {
        
    }

    //Request to retrieve a person
    PersonPresenter.prototype.GetPerson = function () {
        
    }

    //Request to save a person
    PersonPresenter.prototype.SavePerson = function () {
        
    }
</script>

<body>
    <script type="text/javascript">
        //declare an instance of the view
        var _view;
        
        //declare an instance of the presenter
        var _presenter;

        //jQuery $() syntax delineates that the Initialize() method will get called when the form loads
        $(Initialize);

        function Initialize() {
            //new up the view (the view will be a JSON object)
            _view = { }

            //new up the presenter, injecting the view into it
            _presenter = new PersonPresenter(_view);

            //load the view after everything is wired up
            _presenter.Load();
        }
    </script>
    <div>
        <span id="spanStatus" style="color: Green"></span><br />
        <input type="hidden" id="personId" value='<%= ViewData["PersonId"].ToString() %>' />
        First Name:<br />
        <input id="firstName" type="text" /><br />
        Last Name:<br />
        <input id="lastName" type="text" /><br />
        <input type="submit" value="save" id="buttonSave" />
    </div>
</body>

The Controller Actions

When the html page has been rendered, the Load() method will get called on the PersonPresenter. The following controller action services up this view and populates ViewData["PersonId"]. The person id will be used to load our Person asynchronously. Here is the controller action that gets everything going:

[HttpGet]
public ActionResult Index(Guid? personId)
{
    ViewData["PersonId"] = personId ?? Guid.NewGuid();
    return View();
}

And here is the controller action to retrieve the actual person data:

[HttpGet]
public ActionResult Get(Guid personId)
{
    Person person = //retrieves some person from a repository
    
    //return the person object as a json serialization (ASP.NET MVC 2.0 version)
    return Json(person, JsonRequestBehavior.AllowGet);

    //return if you are using MVC 1.0, this is what the return statement would look like
    return Json(person);
}

And here is the controller action for saving a person:

[HttpPost]
public ActionResult Save(Guid personId, string firstName, string lastName)
{
    //validate the person server side
    //save data to the database and return an empty result to signify that everything went well
    return new EmptyResult();
}

The Presenter

When the page gets loaded, we can then load the person asynchronously via the presenter. This is the fully implemented presenter (read the comments carefully to get a feel for what's going on):

<!-- controller actions -->
<% string getPersonUri = Url.RouteUrl(new { controller = "Person", action = "Get" }); %>
<% string savePersonUri = Url.RouteUrl(new { controller = "Person", action = "Save" }); %>

<script type="text/javascript">
    //person presenter class declaration
    function PersonPresenter(view) {
        this._view = view;
    }

    //method gets called when the page loads
    PersonPresenter.prototype.Load = function () {
        //when the page loads get the person asynchronously
        this.GetPerson();
    }

    //request to retrieve a person asynchronously
    PersonPresenter.prototype.GetPerson = function () {
        //reference the view locally so that it can be used in the $.getJSON closure
        var view = this._view;

        //get the person id from the view reference
        var personId = this._view.GetPersonId();

        //call $.getJSON to asynchronously return the person
        //when the function returns, the presenter tells the view to populate the first name and last name
        $.getJSON(
            //uri for controller action 
            '<%= getPersonUri %>',
            //parameter for controller action
            { personId: personId }, 
            //delegate to call when controller action returns successfully
            function (data) {
                //the presenter tells the view to set the first name
                view.SetFirstName(data.FirstName);
                //the presenter tells the view to set the last name
                view.SetLastName(data.LastName);
            });
    }

    //request to save a person asynchronously
    PersonPresenter.prototype.SavePerson = function () {
        //reference the view locally so that it can be used in the $.post closure
        var view = this._view;
        var personId = view.GetPersonId();
        var firstName = view.GetFirstName();
        var lastName = view.GetLastName();

        //if the first name or last name is empty, then notify that the view
        //of these errors and do not save.
        if(firstName == '' || lastName == '') {
            view.NotifyStatus('First name and last name are both required.');
            return;
        }

        //if everything is valid,
        //call $.post to asynchronously save the person
        //when the function returns, the presenter tells the view that the save was successful
        $.post(
               //uri for controller action to save person
               '<%= savePersonUri %>',
               //create a list of parameters to pass to the save person uri
               { 
                 //get the person id from the view
                 personId: personId, 

                 //get the first name from the view
                 firstName: firstName, 

                 //get the last name from the view
                 lastName: lastName  
               },
               //delegate to call when the controller action returns successfully
               function () {
                   //the presenter tells the view to notify the user of a successful save
                   view.NotifyStatus('Save successful!');
               });
    }
</script>

The View

This is the view that gets injected into the PersonPresenter above.

<body>
    <script type="text/javascript">
        //declare variable to represent the view
        var _view;
        
        //declare a variable to represent the presenter
        var _presenter;

        //when the html is completely rendered, call the initialize method (jQuery's equivalent of the onload event)
        $(Initialize);

        //method that gets called when the page loads
        function Initialize() {
        
            //the view is a json object that contains all of the methods that are called by the presenter
            _view = {
                //GetPersonId finds the hidden element called personId and returns the value
                GetPersonId: function () { return $("#personId").val(); },

                //SetFirstName finds the input text box called firstName and sets the value to the value passed in
                SetFirstName: function (value) { $("#firstName").val(value); },
                
                //GetFirstName finds the input text box called firstName and returns the value of the text box 
                GetFirstName: function (value) { return $("#firstName").val(); },

		//SetLastName finds the input text box called lastName and sets the value to the value passed in
                SetLastName: function (value) { $("#lastName").val(value); },
                
                //GetLastName finds the input text box called lastName and returns the value of the text box 
                GetLastName: function (value) { return $("#lastName").val(); },

                //NotifySaveSuccessful sets the html of a span called spanStatus to Save Successful!
                NotifyStatus: function (message) { $("#spanStatus").html(message); }
            };

            //new up the presenter and inject the view into it.
            _presenter = new PersonPresenter(_view);

            //wire up the save button to call the SavePerson method
            $("#buttonSave").click(function () { _presenter.SavePerson(); });

            //call the Load method on the presenter to start things off
            _presenter.Load();
        }
    </script>
    <div>
        <span id="spanStatus" style="color: Green"></span><br />
        <input type="hidden" id="personId" value='<%= ViewData["PersonId"].ToString() %>' />
        First Name:<br />
        <input id="firstName" type="text" /><br />
        Last Name:<br />
        <input id="lastName" type="text" /><br />
        <input type="submit" value="save" id="buttonSave" />
    </div>
</body>

Done.

That's it! We have implemented the MVP design pattern in ASP.NET MVC. Here is a final summary of what will occur when the user browses to the page.


Written: 5/17/2010