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:
- Index – the initial view when the user goes to the list.
- Detail – the details view of the entity, used for creates and edits.
- 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 theAjaxOptions
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 correspondingAjaxOptions
, - 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.