Sunday, 18 April 2010

Progressive Enhancement with ASP.Net MVC

With its clean model for separation of concerns, one of the things I’ve been impressed by in use of MVC with ASP.Net is the support for building AJAX enabled applications that support progressive enhancement.

This technique takes the traditional strategy of graceful degradation in web design and turns it on its head. Rather than focussing on having the best possible user interface degrade to a less rich but still workable and presentable one, the focus is on creating a functional base UI that is enhanced by the addition of CSS and JavaScript to produce an improved user experience for those that can make use of it.

Of course either method can lead to the same result, but – in addition to the use of some specific techniques such as external referencing of assets to avoid swamping the more basic client with code or data that can’t be used – the change of approach leads to a different design mindset and result that better supports the full user base.

In this post I’ll illustrate a small part of an application I’ve recently worked on, where we developed an RSS reader for a site. One function was that users should be able to view a list of feeds, and add and remove RSS sources to it – and that we needed to support both script enabled clients and those that don’t have access to such technologies.

Firstly, the basic application before enhancement:

In the domain model I have a feed class that represents an RSS Feed object. Not shown in code but also developed for the application are repository and service methods for validation and the persistence of the list of feeds to the database.


    1 public class Feed


    2 {


    3     public int Id { get; set; }


    4     public string Name { get; set; }


    5     public string Url { get; set; }


    7     public List<FeedItem> Items { get; internal set; }


    8 }




In the controller code I have a few methods, for the retrieval of the list of feeds, and to support functions for the addition to and removal of them from the list. The key points to note here is that the addition of a new feed requires a straightforward form post, and the removal is via a link to a confirmation page, that in turn uses an HTTP post to proces the deletion. No client-side script required for a functional, if basic, UI.


    1 /// <summary>


    2 /// Render feeds page default view


    3 /// </summary>


    4 /// <returns></returns>       


    5 public ActionResult List()


    6 {


    7     IPrincipal user = GetUser();


    8 


    9     return View(new IndexViewModel


   10     {


   11         Feeds = feedsService.GetFeedList(user),


   12         IsLoggedIn = (user != null && user.Identity.IsAuthenticated),


   15     });


   16 }


   17 


   18 /// <summary>


   19 /// Receives the URL to an RSS feed, verifies it, and saves to the user's account


   20 /// </summary>


   21 /// <param name="url">URL of RSS feed</param>


   22 /// <returns></returns>


   23 [AcceptVerbs(HttpVerbs.Post)]


   24 public ActionResult Add(string url)


   25 {


   26     if (url.Length >= 4 && url.Substring(0, 4) != "http")


   27         url = "http://" + url;


   28 


   29     bool success = false;


   30     int feedId = 0;


   31     string feedName = "";


   32     string message = "Feed addition was unsuccesful. Please correct the errors and try again.";


   33     if (feedsService.ValidateRSSFeed(url))


   34     {


   35         Feed feed = new Feed();


   36         feedName = feedsService.GetRSSFeedNameFromUrl(url);


   37         feed.Name = feedName;


   38         feed.Url = url;


   40         try


   41         {


   42             feedId = feedsService.AddFeed(feed, ControllerContext.HttpContext.User);


   43             success = true;


   44             message = "The RSS feed has been added to your list.";


   45         }


   46         catch (RuleException ex)


   47         {


   48             ex.CopyToModelState(ModelState);


   49         }


   50     }


   51     else


   52     {


   53         ModelState.AddModelError("Url", "The URL does not point to a valid RSS feed.");


   54     }


   55 


   56     TempData["Message"] = message;


   57     return RedirectToAction("Index");


   58 }


   59 


   60 /// <summary>


   61 /// Renders view for remove confirmation


   62 /// </summary>


   63 /// <param name="id">Id of feed for removal</param>


   64 /// <returns></returns>       


   65 public ActionResult Remove(int id)


   66 {


   67     Feed feed = feedsService.GetFeed(id, GetUser());


   68     return View("Remove", feed);


   69 }


   70 


   71 /// <summary>


   72 /// Processes feed removal


   73 /// </summary>


   74 /// <param name="contact">Feed object (created by model binding)</param>


   75 /// <returns></returns>       


   76 [AcceptVerbs(HttpVerbs.Post)]


   77 public ActionResult Remove(Feed feed)


   78 {


   79     bool success = false;


   80     string message = "Feed removal was unsuccesful. Please correct the errors and try again.";


   81     try


   82     {


   83         feedsService.RemoveFeed(feed.Id, GetUser());


   84         success = true;


   85         message = "The RSS feed has been removed from your list.";


   86     }


   87     catch (RuleException ex)


   88     {


   89         ex.CopyToModelState(ModelState);


   90     }


   91 


   92     TempData["Message"] = message;


   93     return RedirectToAction("Index");


   94 }




My view code marks-up the HTML for the display of the list, the links for removal and the form for adding a new RSS feed to the list.


    1 <ul id="feed-list">


    2 


    3 <% foreach (var item in Model.Feeds) { %>


    4 


    5     <li>


    6         <span><%= Html.Encode(item.Id) %></span>


    7         <%= Html.ActionLink(item.Name, "Details", new { id = item.Id })%>


    8 


    9         <%if (Model.IsAdmin || !item.Required) { %>


   10             [<%= Html.ActionLink("Remove", "Remove", new { id = item.Id })%>]


   11         <% } %>


   12     </li>


   13 


   14 <% } %>


   15 


   16 </ul>


   17 


   18 <% if (Model.IsLoggedIn) { %>


   19 


   20     <% using (Html.BeginForm("Add", "Feeds", FormMethod.Post, new { id = "form-add-feed"}))


   21       {%>


   22 


   23         <fieldset>


   24             <legend>Add new feed to your list</legend>


   25             <p>


   26                 <label for="form-add-feed-url">Feed URL:</label>


   27                 <span>


   28                     <%= Html.TextBox("Url", "", new { id = "form-add-feed-url" })%>


   29                 </span>


   30             </p>


   31             <p>       


   32                 <input type="submit" value="Add Feed" />                   


   33             </p>


   34         </fieldset>     


   35 


   36     <% } %>


   37 


   38 <% } %>




So far very straightforward, but the interface requires a number of full page post backs to function, and hence for a better user experience we can progressively enhance the application with AJAX.

Firstly, for the addition of the feeds to the list we can hijack the form post using some jquery. The key point to note about this and the other progressive enhancement techniques described is that if the client doesn’t support them, they are simply ignored and the base functionality remains untouched.

Within this function we can read the URL from the form, and construct an HTTP post response from client side code. When the response comes back from the post, we can process it and display either a success message (and carry out additional processing such as adding the new feed to the HTML list) or error response as appropriate. Finally, and importantly, we return a false response such that the default form submission process is cancelled.


    1 $(document).ready(function() {


    2     hookUpAddFeedFormAjaxPost();


    3 });


    4 


    5 function hookUpAddFeedFormAjaxPost() {


    6     $("#form-add-feed").submit(function() {


    7 


    8         //Get feed details


    9         var url;


   10         url = $("#form-add-feed-url").val();


   11 


   12         //Post to controller action for adding feed


   13         $.post("/Feeds/Add/", { url: url },


   14             function(data) {


   15 


   16                 if (data.Success) {


   17 


   18                     //Success... show message


   19                     $("#ajax-message p.message").text(data.Message);


   20                     $("#ajax-message").show();


   21 


   22                     //Add feed to list (if not already there)


   23                     var id, name;


   24                     name = data.Data.split("|")[0];


   25                     id = data.Data.split("|")[1];


   26 


   27                     if ($("#feed-list li span:contains(\"" + name + "\")").length == 0) {


   28                         $("#feed-list").append("<li><a href=\"/Feeds/View/" + id +


   29                             "\" class=\"feed\">" + name +


   30                             "</a> [<a href=\"/Feeds/Remove/" + id +


   31                             "\">Remove</a>]</li>");


   34                     }


   35 


   36                     $("#form-add-feed-url").val("");


   37 


   38                 } else {


   39 


   40                     //Failed... show errors


   41                     $("#ajax-error span.validation-summary-errors").text(data.Message);


   42                     for (var i = 0; i < data.Errors.length; i++)


   43                         $("#ajax-error ul").append("<li>" + data.Errors[i] + "</li>");


   44                     $("#ajax-error").show();


   45                 }


   46 


   47             }, "json");


   48 


   49         return false;


   50     });


   51 }




In order to hook all this up we need to make some changes to our controller code, but actually very little - which is where the support of this approach from the ASP.Net MVC framework comes in. In the controller we can examine a property of the Request object to determine if the action method is being called from an AJAX request, and take appropriate steps. Here we want to by-pass the usual RedirectToAction at the end of the method, and instead format and return a JSON response.


   56     if (Request.IsAjaxRequest())


   57         return CreateAjaxResponse(success, message, ModelState, feedName + "|" + feedId);


   58     else


   59     {


   60         TempData["Message"] = message;


   61         return RedirectToAction("Index");


   62     }




The JSON response itself is contructed using this helper function:


    1 /// <summary>


    2 /// Private helper method to format a JSON respone for AJAX requests


    3 /// </summary>


    4 /// <param name="success">Flag indicating success or failure of the method</param>


    5 /// <param name="message">Message</param>


    6 /// <param name="modelState">ModelStateDictionary containing error details on failure</param>


    7 /// <param name="data">Any further specific information required by callback</param>


    8 /// <returns></returns>


    9 private JsonResult CreateAjaxResponse(bool success, string message,


   10     ModelStateDictionary modelState, string data)


   11 {


   12     JsonResult json = new JsonResult();


   13     IList<string> errors = new List<string>();


   14     foreach (var item in modelState.Values)


   15         foreach (ModelError me in item.Errors)


   16             errors.Add(me.ErrorMessage);


   17     json.Data = new


   18     {


   19         Success = success,


   20         Message = message,


   21         Errors = errors,


   22         Data = data


   23     };


   24     return json;


   25 }




In a similar manner we can intercept the click of the remove link to disable the normal function and instead provide a client side method of confirming the deletion and constructing the HTTP post to process it. Again, if scripting is not supported, the hyperlink will just function as a normal anchor link to the confirmation form. But if it is, our client-side code will be executed and the rich AJAX UI provided instead.

Thursday, 15 April 2010

View Models and Form Models in ASP.Net MVC - A Second Look

Following my previous post looking at options on the use of view models within ASP.Net MVC, where I came down on the side of using a simpler approach passing domain models to views directly, in this blog post I'll outline the approach I'm taking with more recent project - where I've changed my approach to create view models and form models that wrap the domain model and can include additional fields and options that may be required in the particular view.

The argument for using custom view models is that they can be tailored precisely to each strongly typed view, providing a cleaner approach for display and updating data from views. The downside though is that there is some additional work involved. However, after further reading on the subject - would recommend both Steve Michelotti's post on MVC view model patterns and Jorit Salverda's maintainable MVC series - on balance now I've been using them more and more on projects, it seems the extra steps are worthwhile.

To illustrate this approach let's take again a simple example, where we have a domain model that represents a user. Each user has a user name and a role. The application using them needs to present a form for the update of a user's details, with the option to send them a welcome email.

The following code illustrates the code for the domain model.


    1 public class User


    2 {


    3     public string Username { get; set; }


    4     public Role Role { get; set; }


    5 }




And then I've created two further models in the UI layer - a view model that contains all the data for presentation on the form (the user object itself, and a SelectList for the various role options the administrator of the application can choose from. The second form model is created via model binding following the form post, where the user object is extracted and updated, and additional information also passed in a strongly typed manner is processed.


    1 public class UserViewModel


    2 {


    3     public User User { get; set; }


    4     public SelectList Roles { get; set; }


    5 }


    6 


    7 public class UserFormModel


    8 {


    9     public User User { get; set; }


   10     public bool SendEmail { get; set; }


   11 }




The controller code illustrates how the view models are created and information from the form model extracted.


    1 public ActionResult Add()


    2 {


    3     return View(new UserViewModel


    4     {


    5         Roles = new SelectList(usersService.GetRoleList(), "Id", "Name"),


    6     });


    7 }


    8 


    9 public ActionResult Edit(int id)


   10 {


   11     User user = usersService.GetUserById(id);


   12     return View(new UserViewModel


   13     {


   14         User = user,


   15         Roles = new SelectList(usersService.GetRoleList(), "Id", "Name", user.Role.Id),


   16     });


   17 }


   18 


   19 [AcceptVerbs(HttpVerbs.Post)]


   20 public ActionResult Edit(UserFormModel userFormModel)


   21 {


   22     if (ModelState.IsValid)


   23     {


   24         try


   25         {


   26             usersService.SaveUser(userFormModel.User, userFormModel.ResetPassword, GetUser());


   27         }


   28         catch (RuleException ex)


   29         {


   30             ex.CopyToModelState(ModelState);


   31         }


   32     }


   33     if (ModelState.IsValid)


   34     {


   35         TempData["Message"] = "User account " + (userFormModel.User.Id == 0 ? "created" : "updated") + ".";


   36         return RedirectToAction("Index");


   37     }


   38     else


   39     {


   40         return View((userFormModel.User.Id == 0 ? "Add" : "Edit"), new UserViewModel


   41         {


   42             User = userFormModel.User,


   43             Roles = new SelectList(usersService.GetRoleList(), "Id", "Name", userFormModel.User.Role.Id),


   44         });


   45     }


   46 }




And finally this strongly typed view code demonstrates how the form fields are named to support model binding following the post.


    1 <% using (Html.BeginForm()) {%>


    2 


    3     <fieldset>


    4         <h2>Account Details</h2>


    5         <p>


    6             <label for="User.Username">User name:</label>


    7             <span class="field">


    8                 <%= Html.TextBox("User.Username", Model.User.Username)%>


    9             </span>


   10         </p>


   11         <p>


   12             <label for="User.Role.Id">Role:</label>


   13             <span class="field">


   14                 <%= Html.DropDownList("User.Role.Id", Model.Roles)%>


   15             </span>


   16         </p>


   17         <p>


   18             <label for="SendEmail">Reset Password:</label>


   19             <span class="field">


   20                 <%= Html.CheckBox("SendEmail", false)%>


   21             </span>


   22         </p> 


   23         <p>


   24             <input type="submit" value="Save" class="indent" />                             


   25         </p>                           


   26         <%= Html.Hidden("User.Id",Model.User.Id) %>


   27     </fieldset>


   28 


   29 <% } %>




Without this approach it would be necesssary to pass the additional data to the view - the list of roles - in a loosely typed manner in the ViewData. And the additional information required from the form post - the flag to send the welcome email - would need to be passed as an extra paramenter to the controller action responding to the form post.

With hindsight I think this is clearly a cleaner approach and am using it for the MVC projects I'm working on now. In particular the method of creating a custom class for both the form display and the form post, provides a very elegent approach in my opinion.