AsyncUI MVC with Progressive Enhancement

The top concerns for any site are having basic functionality, having it perform as fast as possible, and then working for as many customer browsers as possible. Asynchronous User Interfaces are the latest technology to achieve the ultimate in performance.

Alex Maccaw:

Web developers are still stuck in the request/response mindset. I call it the ‘click and wait’ approach – where every UI interaction results in a delay before another interaction can be performed. […]  developers still insist on using the request/response model. Even the introduction of Ajax hasn’t improved the scene much, replacing blank loading states with spinners. […]

I’ve been working on this problem, specifically with a MVC JavaScript framework called Spine, and implementing what I’ve dubbed asynchronous user interfaces, or AUIs. The key to this is that interfaces should be completely non-blocking. Interactions should be resolved instantly; there should be no loading messages or spinners. Requests to the server should be decoupled from the interface.

I am using different technology to solving the problem than Alex, but the end result is the same.  My approach focuses on using Microsoft Asp.Net MVC, jQuery, and a custom javascript library I created.  This technique let’s me focus on building HTML with attributes and then the script automatically enhances those views to interact with my MVC controller action methods.

Final Output

Here is a screenshot of the final profile page for which I will show how to implement the email section.  There are 9 different areas in which you can edit something on the page, including your sign in credentials, real name, birthdate, a markdown editor for a bio, an email address, a phone number, an address, a phone number, and a school/job section.  In loading this entire page the initial load time is only 1/2 a second, plus another 4/5 second for images from another domain.  I’ll cut that down after I add bundling and more minification of the resources.

As the following screenshot shows the Undo Delete only takes 58 milliseconds post time.  But before the post began the view had already toggled to the readonly version.  I don’t know of any tools to track how long javascript ran before the post began, but to me using the button it felt instantaneous.  All the buttons feel just as fast using the code shown in this article.

Profile Undo Delete performance

MVC Adaptive Controller

To support progressive enhancement you will need to support users that do not have AJAX support or that have limited support.  It is easy to make a controller action return either JSON data for consumption by javascript and to support returning view HTML.  Here are some simple controller actions that returns a profile email address for a user as either an HTML view or as JSON.

 //UserProfileController.cs
public virtual ActionResult EmailList( Guid userID )
{
	UserProfileEmailListViewModel model = new UserProfileEmailListViewModel();
	model.Load(userID);
	if (this.IsJsonRequest())
		return Json(model, JsonRequestBehavior.AllowGet);
	else
		return View(MVC.UserProfile.Views.EmailList, model);
}
public virtual ActionResult EmailView( Guid userID, long id )
{
	UserProfileEmailViewModel model = new UserProfileEmailViewModel() { UserID = userID };
	UserProfileEmail item = UserProfileManager.GetUserProfileEmail(userID, id, model.Results);
	model.Load(item);
	if (this.IsJsonRequest())
		return Json(model, JsonRequestBehavior.AllowGet);
	else
		return View(MVC.UserProfile.Views.EmailView, model);
}
public virtual ActionResult EmailEdit( Guid userID, long id )
{
	UserProfileEmailViewModel model = new UserProfileEmailViewModel() { UserID = userID };
	UserProfileEmail item = UserProfileManager.GetUserProfileEmail(userID, id, model.Results);
	model.Load(item);
	if (this.IsJsonRequest())
		return Json(model, JsonRequestBehavior.AllowGet);
	else
		return View(MVC.UserProfile.Views.EmailEdit, model);
} //UserProfileController.cs

The key in adapting your result to what is requested is to check if the request is for JSON. You can support this by adding a simple Controller class extension method somewhere in your project or in a shared library.

public static class ControllerExtensions
{
	public static bool IsJsonRequest( this Controller controller )
	{
		foreach (string a in controller.Request.AcceptTypes)
			if (a.ToLower().Contains("json"))
				return true;
		return false;
	}
	public static bool IsAjaxRequest( this Controller controller )
	{
		return controller.Request.IsAjaxRequest();
	}
}

The create action method is slightly different in that it needs to potentially return a success flag or possibly error messages.  In the case of create and delete these action methods will rarely, if ever, be called in the case when the browser supports AJAX.  So these action methods support both JSON and the normal get HTML options, but they are primarily for the latter.

 //UserProfileController.cs
public virtual ActionResult EmailCreate( Guid userID )
{
	UserProfileEmailViewModel model = new UserProfileEmailViewModel();
	model.UserID = userID;
	if (this.IsJsonRequest())
		return Json(new { success = true, item = model }, JsonRequestBehavior.AllowGet);
	else //99% case
		return View(MVC.UserProfile.Views.EmailEdit, model);
}
public virtual ActionResult EmailDelete( Guid userID, long id )
{
	UserProfileEmailViewModel model = new UserProfileEmailViewModel() { UserID = userID };
	UserProfileEmail item = UserProfileManager.GetUserProfileEmail(userID, id, model.Results);
	model.Load(item);
	if (this.IsJsonRequest())
		return Json(model, JsonRequestBehavior.AllowGet);
	else
		return View(MVC.UserProfile.Views.EmailDelete, model);
} //UserProfileController.cs

All of these methods are using a class called UserProfileManager to get data.  In case you are wondering how that business layer works, you can read my previous articles on provider model.  It really isn’t pertinent to this article so I won’t mention it in this article again.

MVC Post Action Methods

When doing a post for create, edit, or delete you may be responding to an AJAX request from the AsyncUI javascript or it may be a full browser request from a client that does not support AJAX properly. In the AsycnUI case you want to return a message, if applicable, and then let the calling page determine if the user stays on the page or needs to go elsewhere. In the form post method, the client was likely just on a full edit page since they don’t support AJAX and AsyncUI, so on success you want to redirect them back to the page from before the edit. Or in the failure case (with no AsyncUI) you want to keep them on the edit page and show the error message. To support these different result options, I create helper methods in the controller to determine the correct ActionResult.

 //UserProfileController.cs
private ActionResult UserProfileEmailResult( UserProfileEmailViewModel model, ExecutionResults result )
{
	model.Results = result;
	if (this.IsAjaxRequest()) //99% case
		return Json(new { success = result.Success, message = result.ToHtmlString(), item = model }); //passing item back because it can change - share description is calculated
	else if (result.Success)
		return RedirectToAction(MVC.UserProfile.Main(SecurityContext.CurrentIdentity.Name));
	else
		return View(MVC.UserProfile.Views.EmailEdit, model);
}
private ActionResult CancelResult()
{
	if (this.IsAjaxRequest()) //1% case - cancel was not enhanced properly to revert to view mode on client side.
		return Json(new { success = true });
	else
		return RedirectToAction(MVC.UserProfile.Main(SecurityContext.CurrentIdentity.Name));
} // //UserProfileController.cs.  Note, The code here is using T4MVC to generate urls.

I can now leverage this in the save, delete, and undo delete operations.  The XCancel action methods will not typically be invoked for most users.  But these methods are here for the case when javascript is disabled and the button needs to cause a navigation somewhere.  You can accomplish this by setting urls on cancel links instead of having cancel behave like a button.  But I included the XCancel action methods because I wanted to demonstrate the FormPostActionSelector attribute also.

//UserProfileController.cs
[AcceptVerbs(HttpVerbs.Post)]
[FormPostActionSelector(ButtonName = "Save")]
public virtual ActionResult EmailEditSave( UserProfileEmailViewModel model )
{
	ExecutionResults result = new ExecutionResults();
	if (SecurityContext.CurrentUser.IsAnonymous)
	{
		result.AppendError("You must be logged in to save this.");
		return UserProfileEmailResult(model, result);
	}
	model.UserID = SecurityContext.CurrentIdentity.ID;
	UserProfileEmail item = null;
	if (model.ID != -1 && model.ID != 0)
		item = UserProfileManager.GetUserProfileEmail(model.UserID, model.ID, result);
	if (item == null)
		item = new UserProfileEmail() { UserID = model.UserID };
	if (!result.Success)
		return UserProfileEmailResult(model, result);
	item.EmailType = model.EmailType ?? string.Empty;
	item.Address = model.Address ?? string.Empty;
	item.IsPrimaryNotification = model.IsPrimaryNotification;
	item.Groups = model.Groups ?? string.Empty;
	item.Comments = model.Comments ?? string.Empty;
	item.SuggestContacts = model.SuggestContacts;
	UserProfileManager.SaveUserProfileEmail(item, result); //method populates 'result' with any validation errors
	model.Load(item);
	return UserProfileEmailResult(model, result);
}
[AcceptVerbs(HttpVerbs.Post)]
[FormPostActionSelector(ButtonName = "Cancel")]
public virtual ActionResult EmailEditCancel( UserProfileEmailViewModel model )
{
	return CancelResult();
}
[AcceptVerbs(HttpVerbs.Post)]
public virtual ActionResult EmailDeleteConfirm( Guid userID, long id )
{
	ExecutionResults result = new ExecutionResults();
	if (SecurityContext.CurrentUser.IsAnonymous)
	{
		result.AppendError("You must be logged in to delete this.");
		return UserProfileEmailResult(new UserProfileEmailViewModel() { UserID = userID, ID = id }, result);
	}

	UserProfileEmail item = UserProfileManager.GetUserProfileEmail(userID, id, result);
	UserProfileEmailViewModel model = new UserProfileEmailViewModel();
	UserProfileManager.DeleteUserProfileEmail(item, result);
	model.Load(item);
	return UserProfileEmailResult(model, result);
}
[AcceptVerbs(HttpVerbs.Post)]
[FormPostActionSelector(ButtonName = "Cancel")]
public virtual ActionResult EmailDeleteConfirmCancel( Guid userID, long id )
{
	return CancelResult();
}
[AcceptVerbs(HttpVerbs.Post)]
public virtual ActionResult EmailDeleteUndo( Guid userID, long id )
{
	ExecutionResults result = new ExecutionResults();
	if (SecurityContext.CurrentUser.IsAnonymous)
	{
		result.AppendError("You must be logged in.");
		return UserProfileEmailResult(new UserProfileEmailViewModel() { UserID = userID, ID = id }, result);
	}
	UserProfileEmail item = UserProfileManager.GetUserProfileEmail(userID, id, result);
	UserProfileEmailViewModel model = new UserProfileEmailViewModel();
	item.IsDeleted = false;
	UserProfileManager.SaveUserProfileEmail(item, result);
	model.Load(item);
	return UserProfileEmailResult(model, result);
}//UserProfileController.cs

FormPostActionSelector

Some of the code above used a custom action name selector called FormPostActionSelector. This lets you just put a button on a form in your view and then let the controller action method intercept which button it handles. If you only have one button (like Save), then this is not necassary. But if you have two or more buttons on a form then it let’s you have a post action for each of the form’s buttons. I have this in a shared library and it is available within NuGet package candor.web.mvc.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;

namespace XQuiSoft.Xulgent.MvcWebApplication.Filters
{
	///
/// Matches a controller action method to a post from another action where the attributed action is the name of a button on the form.
	///
	///
	/// Variation from source article: http://blog.ashmind.com/2010/03/15/multiple-submit-buttons-with-asp-net-mvc-final-solution/
	///
	public class FormPostActionSelectorAttribute : ActionNameSelectorAttribute
	{
		///
/// Gets or sets the name of the action method that must match.
		/// Or leave empty to match on a combination of the method name
		/// and button name.
		///
		public string FormActionName { get; set; }
		///
/// Gets or sets the name of the button (form field name) that
		/// must post in order to match the action method.
		///
		public string ButtonName { get; set; }
		///
/// Creates a new instance.
		///
		public FormPostActionSelectorAttribute() : base() { }
		///
/// Determines if the current request matches this method.
		///
		///
		///
		///
		///
		public override bool IsValidName( ControllerContext controllerContext,
			string actionName, System.Reflection.MethodInfo methodInfo )
		{
			if (!string.IsNullOrEmpty(ButtonName))
			{
				if (controllerContext.RequestContext.HttpContext.Request[ButtonName] != ButtonName)
					return false;
				if (!string.IsNullOrEmpty(FormActionName))
				{
					if (actionName == FormActionName && actionName.Equals(methodInfo.Name, StringComparison.InvariantCultureIgnoreCase))
						return true;

					if (methodInfo.Name == ButtonName && actionName == FormActionName)
						return true;
					else
						return false;
				}
				else if (ButtonName.StartsWith(actionName))
					return methodInfo.Name == ButtonName || methodInfo.Name == actionName + ButtonName;
				else
					return methodInfo.Name == actionName + ButtonName;
			}
			else if (!string.IsNullOrEmpty(FormActionName))
			{
				if (actionName.Equals(methodInfo.Name, StringComparison.InvariantCultureIgnoreCase))
					return true;

				if (!actionName.Equals(FormActionName, StringComparison.InvariantCultureIgnoreCase))
					return false;

				return controllerContext.RequestContext.HttpContext.Request[methodInfo.Name] != null;
			}
			return false;
		}
	}
}

Asp.Net MVC View Models

Something to consider when creating a view model class that is used as both a means to bind a view and to return as JSON is that you may need more values in your view file than you want serialized as part of your JSON data.  You can accomplish property hiding in your JSON data with the ScriptIgnoreAttribute.  For instance, I have a model class that I use for an email address that has a property with business layer validation messages on it.  I use it on the server side view to render a validation block.  But in the client side view template I don’t use that mechanism for returning error messages since the server side property holding those messages is bloated when serialized to JSON. Another thing you’ll notice about the controller method above is the Load method. This is just my personal preference that the view model knows how to load itself from the core business objects that it represents. You could just as easily put that Load logic in your controller. I prefer it in a Load method so that if I ever reuse my view model elsewhere in the controller or in other controllers I only have to maintain that mapping logic in one place.

	public class UserProfileEmailViewModel
	{
		public UserProfileEmailViewModel()
		{
			Results = new ExecutionResults();
		}

		[ScriptIgnore]
		public ExecutionResults Results { get; set; }
		public long ID { get; set; }
		public Guid UserID { get; set; }
		[DisplayName("Type")]
		public string EmailType { get; set; }
		[DisplayName("Email")]
		public string Address { get; set; }
		public bool IsValidated { get; set; }
		[DisplayName("Receive notifications at this email address")]
		public bool IsPrimaryNotification { get; set; }
		public bool IsDeleteable { get; set; }
		[DisplayName("Share With")]
		public string Groups { get; set; }
		public bool IsDeleted { get; set; }
		[DisplayName("Notes")]
		public string Comments { get; set; }
		[DisplayName("Let others find me by this email address")]
		public bool SuggestContacts { get; set; }
		public string ShareDescription
		{
			get
			{
				if (UserID.Equals(SecurityContext.CurrentIdentity.ID))
					return SharingManager.GetUserVisibilityDescription(UserID, Groups);
				else
					return SharingManager.GetPublicVisibilityDescription(UserID, Groups);
			}
		}
		public bool IsViewingUser
		{
			get { return UserID.Equals(SecurityContext.CurrentUser.Identity.ID); }
		}

		public void Load(UserProfileEmail source)
		{
			if (source != null)
			{
				ID = source.ID;
				UserID = source.UserID;
				EmailType = string.IsNullOrWhiteSpace(source.EmailType) ? null : source.EmailType; //want null on view model for empty value - useful in JSON payload as null
				Address = source.Address;
				IsValidated = source.IsValidated;
				IsPrimaryNotification = source.IsPrimaryNotification;
				IsDeleteable = !source.IsValidated || !source.IsPrimaryNotification;
				Groups = IsViewingUser ? source.Groups : "";
				IsDeleted = source.IsDeleted;
				Comments = string.IsNullOrWhiteSpace(source.Comments) ? null : source.Comments; //want null on view model for empty value - useful in JSON payload as null
				SuggestContacts = source.SuggestContacts;
			}
		}
	}

Asp.Net MVC Razor Views

‘EmailList’ Razor View

On my profile page where I want a list of email addresses that are editable I just render a partial of my email list. First the view code, then I’ll explain it.

@model XQuiSoft.Xulgent.MvcWebApplication.Models.UserProfile.UserProfileEmailListViewModel</pre>
<div class="user-profile-email-list inline-editable-host" id="user-profile-email-list">data-json-url="@Url.Action(MVC.UserProfile.EmailList(Model.UserID))"
 data-json-var="UserProfileEmailListJSON"
 data-template="UserProfileEmailView" }>
 @foreach (var item in Model.Items)
 {
 Html.RenderPartial(MVC.UserProfile.Views.EmailView, item);
 }</div>
<pre>
@if (Model.IsViewingUser)
{
<script id="UserProfileEmailListJSON" type="text/javascript">// <![CDATA[
	var UserProfileEmailListJSON = @Html.Raw(new System.Web.Script.Serialization.JavaScriptSerializer().Serialize(Model))
// ]]></script>
<script id="UserProfileEmailEdit" type="text/x-jQuery-tmpl">// <![CDATA[
	@{Html.RenderPartial(MVC.UserProfile.Views.EmailEditTemplate, Model);}
// ]]></script>
<script id="UserProfileEmailView" type="text/x-jQuery-tmpl">// <![CDATA[
	@{Html.RenderPartial(MVC.UserProfile.Views.EmailViewTemplate, Model);}
// ]]></script>
<script id="UserProfileEmailDeleteUndo" type="text/x-jQuery-tmpl">// <![CDATA[
	@{Html.RenderPartial(MVC.UserProfile.Views.EmailDeleteUndoTemplate, Model);}
// ]]></script>
}

This particular view can only be used server side to render the initial list, as you can see with the @model declaration on the first line. There is no client side template for the list as a whole. That isn’t to say you couldn’t do that, but I choose not to for my project. In the next lines notice how a bunch of data attributes are only output if the current viewing user is the owner of the profile. These attributes support the editing, so there is no point in putting them on the page for a visitor of the profile. There is also a section below that defines the client side templates, and it is only output for the owner of the profile.

For the owner of the profile we are also outputting a javascript variable populated with the initial JSON data used to populate the list. However, in the section above you also see the initial view templates are output. The view template is rendered out on the server side initially so that something appears before any javascript runs. This is important for progressive enhancement, which then supports both users without javascript enabled and for search engines. So why output the JSON data? This is to make a second pass at these elements to make them editable via AsyncUI. But you don’t have to output the JSON as a variable. You can just load the JSON after the page loads via AJAX. The data attributes in this view support both options.

Data Attributes

  • data-json-url. This option let’s you define the url where the JSON will be loaded for the list.
  • data-json-var. This lets you define the name of the (global scope) variable holding the JSON for this list. This option does pollute your global scope, so the data-json-url option may be preferred for the idealistic approach. However, the variable option is faster given that another request to the server via AJAX is not needed to prep the list items for edit.
  • data-template. This is the initial template name to use for the initial rendering of each JSON object in the list defined in one of the previous options. You should typically set this to the name of your view template. But if you want all your items shown as editable on page load, you can put that template name here instead.

‘EmailView’ Razor view

As you can see on the list I render out the initial view of each item on the server side. So I need a Razor view for just viewing the data. This code is standard server side MVC Razor. The only thing special about it is that there is a class on the top level element called ‘inline-editable’ that the javascript library looks for.

@model XQuiSoft.Xulgent.MvcWebApplication.Models.UserProfile.UserProfileEmailViewModel
@if (!Model.IsDeleted)
{ // deleted items are only shown via templates.  For users without javascript they will see a warning before delete instead of the undo delete.
<span class="inline-editable user-profile-email">
	@if(Model.IsViewingUser){
		if (Model.IsDeleteable){
	
		}
	<a class="inline-editable-button" title="click to edit" href="@Url.Action(MVC.UserProfile.EmailEdit(Model.UserID, Model.ID))" data-template="UserProfileEmailEdit">
		
		<span class="user-profile-email-address">@Model.Address</span>
	</a>
	}
	else
	{
		<span class="user-profile-email-address">@Model.Address</span>
	}
	@if (!string.IsNullOrEmpty(Model.EmailType))
	{
	<span class="user-profile-item-type">(@Model.EmailType)</span>
	}
	@if(Model.IsViewingUser){
	<span class="user-profile-item-share-description" title="@Model.ShareDescription">Visible to: @Model.Groups</span>
	}
	@if (!string.IsNullOrEmpty(Model.Comments))
	{
	<span class="user-profile-email-comment notice">@Model.Comments</span>
	}
</span>
}

This view has two buttons on it. The delete button posts a delete to the server immediately and then toggles to the delete undo view.  The edit button just toggles to the edit view and does not send anything to the server.  Edit, Delete, View, and Undo delete are NOT built in behaviors of the script.  You are just setting up those behaviors by the attributes you put on the buttons.  The script follows the same steps for the above as it does for the edit view buttons below.

‘EmailEditTemplate’ view

If the user wants to make an edit, we switch over to the edit template on the client side. This is without making a request back to the server. So this view switches practically immediately, or at least as fast as the javascript engine can make the rendering change. No latency or other server processing delay is involved. The only ‘server side’ processed portion of rendering this view is at the time that the view is sent to the browser on the initial page request, which is just to build the form tag posting to the correct controller action, and building textarea and input tags. The data put in those inputs is done when the template is rendered.

<span class="inline-editable user-profile-email">
@using (Html.BeginForm(MVC.UserProfile.EmailEdit()))
{

		Only enter email accounts under your control. Otherwise the owner of the email address
		could potentially compromise your account

	<input id="ID" type="hidden" name="ID" value="${ID}" />
	<input id="UserID" type="hidden" name="UserID" value="${UserID}" />
</span></pre>
<div class="property"><label for="Address">Email</label>:
 @Html.TextBox("Address", "${Address}", new { style = "width:300px;" })
 <span class="input-tip">Examples: alias@live.com, alias@gmail.com, alias@sample-company.com</span></div>
<div class="property"><label for="EmailType">Type</label>:
 @Html.TextBox("EmailType", "${EmailType}", new { @class = "autocomplete", style = "width:300px;", data_autocomplete_url = @Url.Action(MVC.ContactMethod.GetEmailTypes()), data_autocomplete_delay = 600 })
 <span class="input-tip">The type of email address (Personal, Work, or add a new one)</span></div>
<div class="property"><label for="Groups">Share With</label>:
 @Html.TextBox("Groups", "${Groups}", new { @class = "group-listbuilder listbuilder", style = "width:300px;", data_autocomplete_url = @Url.Action(MVC.Sharing.FindPermissableGroups()), data_autocomplete_itemtemplate = "PermissableMemberSummary" })
 <span class="input-tip">Enter 'family', 'friends', 'coworkers', or any custom group
 name such as 'college friends' or 'neighbors'. Hit enter after each group name you
 type or click on one from the list. </span></div>
<div class="property"><input id="SuggestContacts" type="checkbox" checked="checked" name="SuggestContacts" value="true" />
 <input type="hidden" name="SuggestContacts" value="false" />
 <label for="SuggestContacts">Let others find me by this email address</label></div>
<div class="property"><input id="IsPrimaryNotification" type="checkbox" checked="checked" name="IsPrimaryNotification" value="true" />
 <input type="hidden" name="IsPrimaryNotification" value="false" />
 <label for="IsPrimaryNotification">Receive notifications at this email address</label></div>
<div class="property"><label for="Comments">Notes</label>:
 <textarea class="resizable" id="Comments" style="width: 425px;" cols="20" name="Comments" rows="2">${Comments}</textarea>
 <span class="input-tip" style="width: 400px;">An explanation of when those that you
 share the address with should use it.</span></div>
<div><input class="inline-editable-button link-button ui-state-default ui-corner-all link-button-confirm left-float" id="Save" type="submit" name="Save" value="Save" data-template="UserProfileEmailView" />
 <input class="inline-editable-button link-button ui-state-default ui-corner-all link-button-cancel right-float" id="Cancel" type="submit" name="Cancel" value="Cancel" data-template="UserProfileEmailView" data-url="" /></div>
<pre><span class="inline-editable user-profile-email">
}


</span>

Just like the previous view there is a class on the top level element called ‘inline-editable’ that the javascript library looks for. Additionally since this view has buttons on it, it also has an attribute on each button called “data-template” which defines which view to toggle to when the button is pressed. But if the action posted to fails, the view is switched back to the edit view with any error messages from the server or updated values from the server automatically. A few of the other inputs also have special classes and data attributes on them so that they are progressively enhanced into jQuery controls after rendering of the template. I might discuss those in another article.  I won’t now since they as are not required for making AsyncUI work.

‘EmailEdit’ View

There is another view for email edit when the server needs to render it.  If you look back at the ‘EmailView’ razor view you’ll see that the buttons have an href attribute.  These buttons are hyperlinks that are enhanced with the jQuery button widget to look like a button.  If script is disabled, when the user clicks on the button/link they will navigate to the edit view.  If script is enabled, the script changes the href to “#” and attached a click handler that does the steps mentioned above instead (AsyncUI).

When the edit view is shown as rendered by the server then we render it slightly differently and using Razor syntax instead of template syntax.

@model XQuiSoft.Xulgent.MvcWebApplication.Models.UserProfile.UserProfileEmailViewModel
<span class="inline-editable user-profile-email">
@using (Html.BeginForm(MVC.UserProfile.EmailEdit()))
{

		Only enter email accounts under your control. Otherwise the owner of the email address
		could potentially compromise your account

	@Html.HiddenFor(model => model.ID)
	@Html.HiddenFor(model => model.UserID)
</span></pre>
<div class="property">@Html.LabelFor(model => model.Address):
 @Html.TextBoxFor(model => model.Address, new { style = "width:300px;" })
 <span class="input-tip">Examples: alias@live.com, alias@gmail.com, alias@sample-company.com</span></div>
<div class="property">@Html.LabelFor(model => model.EmailType):
 @Html.TextBoxFor(model => model.EmailType, new { @class = "autocomplete", style = "width:300px;", data_autocomplete_url = @Url.Action(MVC.ContactMethod.GetEmailTypes()), data_autocomplete_delay = 600 })
 <span class="input-tip">The type of email address (Personal, Work, or add a new one)</span></div>
<div class="property">@Html.LabelFor(model => model.Groups):
 @Html.TextBoxFor(model => model.Groups, new { @class = "group-listbuilder listbuilder", style = "width:300px;", data_autocomplete_url = @Url.Action(MVC.Sharing.FindPermissableGroups()), data_autocomplete_itemtemplate = "<strong>${Name}</strong>: ${ContactCount}:${MemberCount}<em>${MemberSummary}</em>" })
 <span class="input-tip">Enter 'family', 'friends', 'coworkers', or any custom group
 name such as 'college friends' or 'neighbors'. Hit enter after each group name you
 type or click on one from the list. </span></div>
<div class="property">@Html.CheckBoxFor(model => model.SuggestContacts)
 @Html.LabelFor(model => model.SuggestContacts)</div>
<div class="property">@Html.CheckBoxFor(model => model.IsPrimaryNotification)
 @Html.LabelFor(model => model.IsPrimaryNotification)</div>
<div class="property">@Html.LabelFor(model => model.Comments):
 @Html.TextAreaFor(model => model.Comments, new { @class = "resizable", style = "width:425px;" })
 <span class="input-tip" style="width: 400px;">An explanation of when those that you
 share the address with should use it.</span></div>
<div><input class="link-button ui-state-default ui-corner-all link-button-confirm left-float" id="Save" type="submit" name="Save" value="Save" />
 <input class="link-button ui-state-default ui-corner-all link-button-cancel right-float" id="Cancel" type="submit" name="Cancel" value="Cancel" />
 @Html.DisplayFor(model => model.Results)</div>
<pre><span class="inline-editable user-profile-email">

}
</span>

Notice this edit view does not have the data attributes. If this view is shown we already know script is disabled on the client so the data attributes are not needed. The buttons on this edit view post directly to the MVC controller  EmailEditSave action method synchronously.  So we fallback to the ‘click and wait’ model when script is disabled.  Look back at the UserProfileEmailResult and this is where the RedirectToAction occurs on a succesful edit, and on a failed edit the EmailEdit view is returned again with the values that were just posted.

‘EmailViewTemplate’ view

After a user completes editing an item and they cancel or save, we need to toggle back over to the edit view.  This toggle should be immediate using a template and with the data on the form (in the save case).  We can’t use the ‘EmailView’ view because that is a server side rendering.  This template initially is rendered on the server as part of the ‘EmailList’ view.  This is why all the @Html.Raw Razor syntax blocks are in the file.  But that is just to get the template to the client.  Once on the client those do not exist are are not part of the rendering of the template client side.  Html.Raw is needed because otherwise the file would not be valid Razor syntax.

@Html.Raw("{{if IsDeleted}}")
@Html.Raw("{{tmpl '#UserProfileEmailDeleteUndo'}}")
@Html.Raw("{{else}}")
<span class="inline-editable user-profile-email">
	@Html.Raw("{{if IsViewingUser}}")
	@Html.Raw("{{if IsDeleteable}}")
	
	@Html.Raw("{{/if}}")
	<a class="inline-editable-button" title="click to edit" href="#" data-template="UserProfileEmailEdit">
		
		<span class="user-profile-email-address">${Address}</span>
	</a>
	@Html.Raw("{{else}}")
		<span class="user-profile-email-address">${Address}</span>
	@Html.Raw("{{/if}}")
	@Html.Raw("{{if EmailType}}")
	<span class="user-profile-item-type">(${EmailType})</span>
	@Html.Raw("{{/if}}")
	@Html.Raw("{{if IsViewingUser}}")
	<span class="user-profile-item-share-description" title="${ShareDescription}">Visible to: ${Groups}</span>
	@Html.Raw("{{/if}}")
	@Html.Raw("{{if Comments}}")
	<span class="user-profile-email-comment notice">${Comments}</span>
	@Html.Raw("{{else}}")
	

	@Html.Raw("{{/if}}")
</span>
@Html.Raw("{{/if}}")

This template also handles the case when the item is deleted by rendering that as sub-template as needed.  Structurally this is the same as the ‘EmailEditTemplate’ view.  This looks the same as the ‘EmailView’ view, except that this is in template syntax (inside the Razor syntax), instead of binding to a .net view model class.  Another difference from ‘EmailView’ view is that we don’t set the href attribute on the anchor tag buttons.  We know if the template is able to render then script is enabled and the subsequent enhancement to change the anchor tag to a button will also work.

As shown in the following screenshot, MVC renders the view code above as valid template syntax.

‘EmailDeleteUndoTemplate’ View

The undo delete template only applies to AsyncUI, at least in my example.  If you delete an email without script enabled you get prompted if you are sure, and then you can’t undo delete.  But if you then load the page with script enabled you’ll see the items that can be restored with the undo template (deleted section of view template).  This template is relatively short and simple.  It has one button to restore the item, and that button works the same as the buttons on the other client side template views.  Just like the others it also defines the post url via the ‘data-url’ attribute and the template to switch to on click with the ‘data-template’ attribute.

<span class="inline-editable user-profile-email delete-undo">
	@Html.Raw("{{if IsViewingUser}}")
	<a class="inline-editable-button" title="click to undo the delete" href="#" data-url="@Url.Action(MVC.UserProfile.EmailDeleteUndo())" data-template="UserProfileEmailView">
		
		Email address
			<span class="user-profile-email-address">${Address}</span>
			is deleted.  Click to recover.
		
	</a>
	

	@Html.Raw("{{/if}}")
</span>



‘EmailDelete’ View

The delete view is only used when script is disabled.  It prompts the user if they are sure that they want to delete the item.  If so it posts to the EmailDeleteConfirm action.  This is the same action method as the client side view template delete button.  Look back at the EmailDeleteConfirm controller action method to see how it handles both at the same time.

@model XQuiSoft.Xulgent.MvcWebApplication.Models.UserProfile.UserProfileEmailViewModel
<span class="inline-editable user-profile-email">
@using (Html.BeginForm(MVC.UserProfile.EmailDeleteConfirm()))
{

		Are you sure you want to delete this email address from your profile?

	@Html.HiddenFor(model => model.ID)
	@Html.HiddenFor(model => model.UserID)
	<span class="user-profile-email-address">@Model.Address</span>
	@if (!string.IsNullOrEmpty(Model.EmailType)) {
	<span class="user-profile-item-type">(@Model.EmailType)</span>
	}
</span></pre>
<div><input class="link-button ui-state-default ui-corner-all link-button-confirm left-float" id="Confirm" type="submit" name="Confirm" value="Confirm" />
 <input class="link-button ui-state-default ui-corner-all link-button-cancel right-float" id="Cancel" type="submit" name="Cancel" value="Cancel" />
 @Html.DisplayFor(model => model.Results)</div>
<pre><span class="inline-editable user-profile-email">
}
</span>

Custom Command Views

You can define other views to be handled by this AsycnUI script by just defining the necessary controller action methods and then setting the “data-url” and “data-template” attributes on the view buttons. It should be able to handle any custom scenario that you can think of.

The Script

When one of the ‘inline-editable-button’ classed HTML elements is pressed it basically does the following operations in order.

  1. Gets the JSON object that represents this item (which was loaded at page load from data-json-var or data-json-url and stored)
  2. Updates the JSON object from the form values (by element name), if applicable.  It supports drilling down to sub-properties and into arrays.
  3. Looks for a template name in the ‘data-template’ attribute and replaces the current ‘inline-editable’ element with an instance of that template bound to the updated JSON object.  This is why you see the change back to the other view immediately.
  4. If “data-url” is not explicitely set to an empty string (as in cancel button) it posts to the url defined by the form and includes the button name as a form variable as well (for the FormPostActionSelector to pick up).  You can override the post url per button with a “data-url” attribute on the button (not shown above).  The button can also define a “url-method” attribute if it wants to use something other than the ‘post’ verb (not shown above).  “url-method” is more commonly used for buttons in the non-form (read-only) views.
    • On post success if the item was passed back it replaces the template again with the new model.  This is in place for the times when some properties are calculated on the server from other inputs.  The server side also has the option of returning the name of the template to instantiate if the one defined on the button needs to be overridden due to some special case.
    • On a post business rule failure the view can toggle back to the edit view and show the error message in your placeholder for errors.  Note the span with class ‘msg’ in the template above.

I plan on sharing this script via GitHub soon.  For now you can find it on jsFiddle.  If you have any ideas for a name on Github, please let me know.  Update July 26,2012: This script is now available on Github, the repo is named jquery-auto-async. I moved parts of the jsFiddle that were a modification of form2js on github into my own fork.  You will need my fork of form2js as a dependency.

You will also need jQuery, jQueryUI, jQuery Validate, and jQuery Templates jsRender templating engine (Update August 2013).  If you want to use the ‘listbuilder’ widget for one of your inputs you can find that on my fork of jQueryUI and find discussion on inclusion of it within jqueryUI on the jQueryUI development wiki.

The templates in this article currently use the now deprecated jQuery Templates library.  You could just as easily use any other templating solution with a couple line modification to the jquery-auto-async script.  I am still using this template solution for now since the jQuery team is working on a replacement.  It really has little bearing on the functionality of this sample code, so I didn’t take the time to update it to another template library yet.
Update August 15,2013: The jQuery team seems to have given up on a templating engine. I changed the dependency to use jsRender instead.

Styles

In my project I am using the themeroller style of my choice, plus the following structural styles.

.inline-editable .ui-icon { display:inline-block;}
.inline-editable a {text-decoration:none;}
a.inline-editable-button span {font-weight:bold;color:Black;}
span.delete-undo {background-color:#DDDDDD; border:1px solid #BBBBBB; display:inline-block;width:100%;}

About the Author

Michael Lang

Co-Founder and CTO of Watchdog Creative, business development, technology vision, and more since 2013, Developer, and Mentor since 1999. See the about page for more details.

3 Comments