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
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 Tutorial
- Building your Presenter in Javascript
- Communicating with your MVC Controllers via the Presenter
- Wiring up your MVC View to use the Presenter
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.
- The user browses to the page.
- The Index controller action gets called and the ViewData["PersonId"] gets set.
- The html page gets rendered and the ViewData["PersonId"] is written to the page in a hidden variable.
- When the page is done loading, jQuery will call the Initialize method we created.
- The Initialize method "new's up" a view and presenter and maps UI components to the persenter methods (in this case, the SavePerson method will get called when the user clicks the save button).
- After everything is instantiated, the Load method is called on the presenter.
- The Load method calls GetPerson to get things going.
- The GetPerson method retrieves the person id from the view and asynchronously calls the GetPerson controller action
- When the action returns, the JSON result's properties are applied to the view via the presenter.
- *user clicks the save method*
- The SavePerson method on the presenter gets executed.
- The id, first name and last name are validated.
- If the person is invalid, the presenter tells the view to notify the user of the errors.
- If the person is valid, the SavePerson controller action is called asynchronously passing in the Person data provided by the view.
- Once the controller action returns, the presenter tells the view to notify the user of a successful save.
Written: 5/17/2010