Master Details with Dialog in ASP.Net MVC and Unobstrusive Ajax

Today’s post explains a user interface that is not uncommon. The user interface consists of a list of items, and if the user clicks a link in the row of data for an item, a dialog window appears so the user can edit the details of the item. When the user saves their changes, the table updates itself to reflect the new changes.

I’m going to cover an approach you can use with ASP.Net MVC to enable this, and explain the aspects required for the Controller and Views that make it all hang together. I’m not going into detail about where the data comes from or what the data is. The idea here is to concentrate on the pattern – you can work out what the data for the list and details screens, respectively, is.

Controller

We’ll set up the controller first. The controller requires at least four methods:

Index method

This handles the initial interaction of the user with the screen. It’s job is to retrieve the data from whatever is the data source to display and display the “Index” View.

public ActionResult Index(){
  MyListModel model = new MyListModel{
    Data= myDataStore.GetDataAsList()             
  };

  return View(model);
}

The Record - Get - Method

This action returns a partial view via Ajax of the record to be edited. I will cover in a moment what is done on the client, but this method builds the edit View Model for the entity by first retrieving it from the data store. Note the use of the null able parameter to indicate if a new record should be created.

Also of interest is the use of the OutputCache action filter to ensure that the Ajax GET request is not cached on the client.

[OutputCache(NoStore = true, VaryByParam = "None", Duration = 0)]
public PartialViewResult MyEntity(int? id){
  DetailViewModel model = new DetailViewModel ();

  if (id.HasValue){
    model.EntityDetail = myDataStore.GetData(id.Value);
  }
  else{
    model.EntityDetail = new MyEntityDetail();
  }
  
  return PartialView("Detail", model);
}

The Record - Post- Method

This method does the update based on a POSTed model. Note how it the method has same name as the previous method, but is decorated with a [HttpPost] attribute and takes a populated entity model as a parameter. If the ModelState is valid, then it is prudent to clear the model state so that posted values are not re-displayed when rendering the view. Instead, the model is refreshed from the data store.

[HttpPost]
public PartialViewResult MyEntity(DetailViewMode model){
  if(ModelState.IsValid){
     ModelState.Clear();   
     model.EntityDetail = myDataStore.UpdateMyEntity(model.EntityDetail);
     ViewBag.Message = "Details Successfully Updated";  
  }

  return PartialView("Detail", model);
}

The partial list refresh

Once the record has been updated, the list of records needs to refresh itself. To do this, it need to make an Ajax call to the server to return a PartialView of just the table contents. That's what this method does. Note again the use of the [OutputCache] filter to prevent Ajax request caching.

[OutputCache(NoStore = true, VaryByParam = "None", Duration = 0)]
public PartialViewResult EntityList(){
  MyListModel model = new MyListModel{
    Data= myDataStore.GetDataAsList()             
  };
  return PartialView("List", model);
}

Views

Okay, so that's the controller covered, now lets consider the views. I'm going to use 3 views:

  1. Index - the initial view when the user goes to the list.
  2. Detail - the details view of the entity, used for creates and edits.
  3. List - the partical view of the table of entitites.

The Index View

This is very simple. It simply renders the partial list view and all the client side script that makes it all work. Aside from the comments in the script, the keys points are the hidden div for the placements of the Ajax retrieved content.

@model MyListModel
@{
    ViewBag.Title = "List of Entities";
    Layout = "~/Views/Shared/_MainLayout.cshtml";
    //render the partial view
    Html.RenderPartial("List", Model);
}

<!-- container for ajax loaded dialog content -->
<div id="ajax-content" style="display:none">

</div>


<script type="text/javascript">
//<![CDATA[
    //this function gets called when the ajax request to GET a entity detail has completed.
    //The GET request ends with the returned HTML being placed in the div with id='ajax-content'
    //A modal dialog is created. The title for the dialog is extracted from the contained fieldset legend.
    //On close, the dialog is destroyed and the content of the ajax-content div are removed
    //On Save the contained form is submitted
    function loadEntity(xhr, status) {
        $('#ajax-content').dialog({
            modal: true,
            width:600,
            title: $('#ajax-content legend').text(),
            buttons: {
                "Save": function () {
                    //submit the form
                    $('#ajax-content form').submit();
                },
                "Close": function () {
                    //remove the content, destroy the dialog
                    $(this).dialog('destroy').html('');
                }
            }
        });
    }

    //this function gets called when the contained form has been submitted via ajax
    //this results in the content INSIDE the ajax-content being replaced, however ajax-content, which is wrapped in
    //a dialog, is not replaced and so remain visible.
    //The title of the dialog is refreshed based on the contained legend.
    //The last thing this function does is makes the table 'entity-list' reload itself from the appropriate Url which points to the "EntityList" action on the controller
    function entityDetailsUpdated(responseText, status, xhr) {
        $('#ajax-content').dialog("option", "title", $('#ajax-content legend').text());          
        $('#entity-list').load( '@Url.Action("EntityList")');
    }
//]]>
</script>


The List View

This is simply a table that shows whatever properties of the entity you want, but with one of the properties being rendered as an Ajax.ActionLink. This little Microsoft extension simply leverages the jQuery Ajax stack in an unobtrusive way. You can see that I have used the AjaxOptions class to define that callback function to call when the Ajax call is complete and the element to put the returned content in, both of which are defined in the Index View.

Note the use of the additional link in the footer of the table to create a new record. In this case, no id is passed to the controller.

@model MyListModel
@{
    AjaxOptions options = new AjaxOptions
    { 
        //"loadEntity" is a javascript method defined on the Index View
        OnComplete = "loadEntity",
        //"ajax-content" is a div defined on the Index View
        UpdateTargetId = "ajax-content"
    };
}

<!-- Important to give this an Id so that it can be told to reload itself when the 
the entity is updated -->
<table id="entity-list">
  <thead>
    <tr>
      <th>
        Name
      </th>
          .........            
    </tr>
  </thead>
<tbody>
@foreach (var entity in Model) {           
    <tr>
       <td>
            @Ajax.ActionLink(entity.Name, "MyEntity", new { id = entity.EntityID }, options)
       </td>
        ......
     </tr>
 }
  </tbody>
<tfoot>
  <tr>
    <td colspan="4">
        @Ajax.ActionLink("New Entity", "MyEntity", options)
    </td>
  </tr>
</tfoot>
</table>


The Details View

This is the edit form, and it will display in a modal dialog all going well. The key points are

  • The use of an id for the form, so that it can be submitted when the Save button is clicked.

  • the use of the Ajax.BeginForm to apply unobtrusive Ajax enabling of the form and the corresponding AjaxOptions,
  • the use of a hidden legend in the fieldset, the text of which you might recall is extracted when and used as the dialog title,
  • the setting of unobtrusive validations using script, because Ajax loaded content does not have this happen automagically
@model DetailViewModel
@{
  AjaxOptions options = new AjaxOptions{
    //always post
    HttpMethod = "post",
    //replace the existing content in the "ajax-content" element
    InsertionMode = InsertionMode.Replace,
    //defined in the Index view
    UpdateTargetId = "ajax-content",
    //the callback, defined in the Index view
    OnSuccess = "entityDetailsUpdated"        
  };    
}

@using (Ajax.BeginForm("MyEntity", null, options, new { id = "entitydetails" }))
{
  <fieldset>
    <legend style="display:none">Entity Details - @Model.Name</legend>
    @if (ViewBag.Message != null){ 
       <span class="update-message">@ViewBag.Message</span>
    }      
    @Html.ValidationSummary(true)
    <ol class="formFields">
      <li>
         @Html.EditorFor(m => m.EntityDetail.Name)
      </li>
      .......
    </ol>
    @Html.HiddenFor(m => m.EntityDetail.EntityID)
  </fieldset>  
    
  <script type="text/javascript">
    $(function () {
       $.validator.unobtrusive.parse('#entitydetails');            
     });
  </script>
}

Conclusion

This post has demonstrated how to achieve the user interface pattern for master-Details views with dialogs using ASP.Net MVC, jQuery and unobtrusive Ajax. The same effects can be created using explicit script of course, but the unobtrusive Ajax extensions are a time saver and make your views more succinct.


kick it on DotNetKicks.com

About these ads
This entry was posted in .Net, Ajax, ASP.Net MVC, jQuery and tagged , , . Bookmark the permalink.

3 Responses to Master Details with Dialog in ASP.Net MVC and Unobstrusive Ajax

  1. zahidadeel says:

    Nice Article. thanks for sharing such valuable information. Do you plan to share demo project for this post. I have also blogged about creating master detail form in asp.net mvc 3 and used your post for client validating dynamically added fields. it does not use ajax rather it uses steve sanderson’s BeginCollectionHelper and jquery templates for creating multiple records for detail elements.

  2. Xavi Rodà says:

    Hi,

    could you please share this example in a project download. I’m having problems probably with scripts included in the project and i can’t make it run properly.

    • xhalent says:

      @Xavi,
      I hosting on wordpress atm but when I get some time I will be moving to a more downloadable location. Sorry for any inconvience. Prehaps you can share what problems you’ve experienced to date?

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