Monday, December 13, 2010

Status Message Handling in MVC

Here's a quick method I use to handle status messages in MVC. It uses an extension method. This means when you use Intellisense on TempData or ViewData, this method will appear as TempData.AddStatusMessage or ViewData.AddStatusMessage.

The problem to solve here is after we save a user, we want to display something like 'User Saved', but since good MVC design practices dictate using the PRG pattern (Post/Redirect/Get), GET requests do not lend themselves nicely to status messages and thus we have a problem.

Remember that we cannot simply set a label from a code behind as may be done in Asp.Net Web Forms. We need another way to accomplish this in the scope of MVC.

I go not recommend using a parameter as a status message as it gives a direct attack vector into your application from a security standpoint. A user can do something along the lines of
/Users/Index?status=

Yes - you can help prevent this by encoding the message before displaying, but why even open up the possibility when there are other ways to accomplish this.

So let's have these requirements:
1. Any code in a controller can add a status message (pages cannot add status messages via this mechanism in my design since the data for the view should already be formed. anything else can use conditional view logic on the page)
2. Status messages can be for the current request (for example, when a post request fails and needs to redisplay data immediately without a redirect)
3. Status messages can be for the next request (IE the GET portion of the Post/Redirect/Get pattern, for ex. 'User Saved Successfully'

To use this code, simply put the following code in your Site.Master (or any page the results should be rendered to)


<%= ViewData.GetMessageSummary()%><br />
<%= TempData.GetMessageSummary()%><br />



To add a message to tempdata, so it will show up on the next request only (as this is how TempData works)

TempData.AddStatusMessage("User Saved Successfully");


To add a message that is displayed when the current request is rendered

ViewData.AddStatusMessage("Please fix the errors below");


The code is as follows

public static class ViewDataExtensions
{
/// <summary>
/// Gets the status messages from TempData and ViewData.
/// When this is called, the status messages are cleared.
/// </summary>
/// <param name="viewData"></param>
/// <param name="message"></param>
public static string GetMessageSummary(this IDictionary<string, object> viewData)
{

//</div>",
//<label style="background-color: <%: ViewData["JobStatusMessageColor"] %>">
//<%: ViewData["StatusMessage"]%></label>

List<string> messageItems = (List<string>)viewData["StatusMessages"];
if (messageItems != null)
{
StringBuilder sb = new StringBuilder();
sb.Append("<br><div class=\"status-message\">\r\n<ul>");

foreach (string item in messageItems)
{
sb.Append(string.Format("<li>{0}</li>\r\n", item));
}
sb.Append("</ul>\r\n</div>");
return sb.ToString();
}
//Im assuming internally this is thread safe since its ViewData or TempData being used. it could be false but going with that for now : )
viewData.Remove("StatusMessages");
return "";
}


/// <summary>
/// Adds a status message to ViewData, meant to be displayed on the _current_ request
/// </summary>
/// <param name="viewData">Since this is an extension method, this operates on TempData.AddStatusMessage</param>
/// <param name="message">The message to be added to the collection</param>
public static void AddStatusMessage(this ViewDataDictionary viewData, string message)
{
AddStatusMessageToDictionary(viewData, message);
}

/// <summary>
/// Adds a status message to TempDate, meant to be displayed on the very _next_ request.
/// </summary>
/// <param name="viewData">Since this is an extension method, this operates on TempData.AddStatusMessage</param>
/// <param name="message">The message to be added to the collection</param>
public static void AddStatusMessage(this TempDataDictionary viewData , string message)
{
AddStatusMessageToDictionary(viewData, message);
}


/// <summary>
/// Adds a message to be displayed to the user. This works with ViewData and TempData
/// </summary>
/// <param name="viewData"></param>
/// <param name="message"></param>
private static void AddStatusMessageToDictionary(this IDictionary<string, object> viewData, string message)
{
List<string> messageItems;
if (viewData.ContainsKey("StatusMessages"))
{

messageItems = (List<string>)viewData["StatusMessages"];
}
else
{
messageItems = new List<string>(5);
viewData["StatusMessages"] = messageItems;
}

messageItems.Add(message);

}


}

No comments:

Post a Comment