ASP.NET MVC Helpers For Repopulating A Form

code, asp.net mvc 0 comments suggest edit

A common pattern when submitting a form in ASP.NET MVC is to post the form data to an action which performs some operation and then redirects to another action afterwards. The only problem is, the form data is not repopulated automatically after a redirect. Let’s look at remedying that, shall we?

When submitting form data, the ASP.NET MVC Toolkit includes a helper extension method that makes it easy to go the other direction, populating an object from the request. Check out the following simplified controller action adapted from an example ScottGu’s post on handling form data.

[ControllerAction]
public void Create()
{
  Article article = new Article();
  article.UpdateFrom(Request.Form);
  
  repository.Persist(article);
 
  RedirectToAction("List");
}

UpdateFrom() is an extension method on the type object that will attempt to populate each property of the object with a corresponding value from the posted form.

The example above doesn’t address what should happen if the data passed to product is incomplete or incorrect.

In that situation, I may want to redirect back to the action that renders the form for creating a new product. But now I’m responsible for populating the form fields because the posted values are lost after a redirect.

One option is to use the TempData dictionary, which is intended for these scenarios in which you need to persist data for the next request, but not afterwards.

[ControllerAction]
public void Create()
{
  Article article = new Article();
  article.UpdateFrom(Request.Form);
  
  //Pretend this was extensive validation
  if(article.IsValid)
  {
    repository.Persist(article);
    RedirectToAction("List");
  }
  else
  {
    TempData["message"] = "Please fill in all fields!";
    TempData["title"] = article.Title;
    TempData["body"] = article.Body;
    //... and so on and so on...

    //Takes us to a form for creating a product.
     RedirectToAction("New");
  }
}

That’ll work nicely for object with a small number of properties, but when you have a lot of properties to store in TempData, it gets a bit cumbersome. We have an extension method for populating an object from a form, why not do something similar for populating TempData from an object?

I put together a series of helper extension methods that help simplify the above case. Using these extension methods, you can reduce the above code to…

[ControllerAction]
public void Create()
{
  Article article = new Article();
  article.UpdateFrom(Request.Form);
  
  //Pretend this was extensive validation
  if(article.IsValid)
  {
    repository.Persist(article);
    RedirectToAction("List");
  }
  else
  {
    TempData["Message"] = "Please supply all fields.";
    TempData.PopulateFrom(article);

    //Takes us to a form for creating a product.
     RedirectToAction("New");
  }
}

PopulateFrom is an extension method on TempDataDictionary that populates the dictionary collection using whatever you pass into it. I wrote some overloads so you can populate it directly from an object, the form collection, or from a dictionary. I will show the code for the extension methods at the end.

This gets us part of the way, but we still need to populate the form field values in the view. Here is the relevant code snippet from the New view.

<% using(Html.Form("Article", "Create")) { %>
    
<span class="error">
    <%= ViewContext.TempData.SafeGet("Message") %>
</span>

<label for="title">Title: </label> 
<%= Html.TextBox("title"
  , Html.Encode(ViewContext.TempData.SafeGet("title"))) %>

<label for="body">Body: </label>
<%= Html.TextArea("body"
  , Html.Encode(ViewContext.TempData.SafeGet("body"))) %>


<%= Html.SubmitButton() %>
<% } %>

When retrieving a value from the TempData dictionary, if the key you specify doesn’t exist, it throws an exception. So I added a SafeGet extension method to make it cleaner to extract values from TempData.

Note: We plan on making TempData a direct property of ViewPage in the future, so you don’t have to go through ViewContext.

Here is the code for my temp data extensions…

public static class TempDataExtensions
{
  public static void PopulateFrom(this TempDataDictionary tempData, object o)
  {
    foreach (PropertyValue property in o.GetProperties())
    {
      tempData[property.Name] = property.Value;
    }
  }

  public static void PopulateFrom(this TempDataDictionary tempData
    , NameValueCollection nameValueCollection)
  {
    foreach (string key in nameValueCollection.Keys)
      tempData[key] = nameValueCollection[key];
  }

  public static void PopulateFrom(this TempDataDictionary tempData
    , IDictionary<string, object> dictionary)
  {
    foreach (string key in dictionary.Keys)
      tempData[key] = dictionary[key];
  }

  public static string SafeGet(this TempDataDictionary tempData, string key)
  { 
    object value;
    if (!tempData.TryGetValue(key, out value))
      return string.Empty;
    return value.ToString();
  }
}

These methods rely on some object extension methods I wrote based on the work that Eilon Lipton did with using C# 3.0 Anonymous Types as Dictionaries.

public static class ObjectHelpers
{
  public static IDictionary<string, object> ToDictionary(this object o)
  {
    Dictionary<string, object> properties = new Dictionary<string, object>();

    foreach (PropertyValue property in o.GetProperties())
    {
      properties.Add(property.Name, property.Value);
    }
    return properties;
  }


  internal static IEnumerable<PropertyValue> GetProperties(this object o)
  {
    if (o != null)
    {
      PropertyDescriptorCollection props = TypeDescriptor.GetProperties(o);
      foreach (PropertyDescriptor prop in props)
      {
        object val = prop.GetValue(o);
        if (val != null)
        {
          yield return new PropertyValue { Name = prop.Name, Value = val };
        }
      }
    }
  }
}

internal sealed class PropertyValue
{
  public string Name { get; set; }
  public object Value { get; set; }
}

The ToDictionary method makes it easy to convert an anonymous typed object to a dictionary.

Using these extension methods should help reduce some of the chore of handling form submissions with the CTP version ASP.NET MVC. In future versions of the framework, I hope we can make some of these common scenarios more streamlined.

And before I forget, I have a simple solution for download that includes full source code for the extension methods I wrote as well as a trivially simple application that demonstrates using this code.

Tags: ASP.NET MVC , TempData , Form Handling

Found a typo or error? Suggest an edit! If accepted, your contribution is listed automatically here.

Comments

avatar

25 responses

  1. Avatar for Sergio Pereira
    Sergio Pereira December 21st, 2007

    Phil, this is an interesting solution but it still looks a little bit clunky to me.
    In this scenario, my initial reaction would be to put the actual Article instance in the TempData. My New() action could eventually become:
    [ControllerAction]
    public void New()
    {
    Article article = null;
    try{ article = TempData["article"];}catch{/*ugh!*/}
    if(article == null) article = new Article();
    ViewData["article"] = article;
    RenderView("New");
    }
    And the view:
    <%/* base class --> ViewPage<Article> */%>
    <% using(Html.Form("Article", "Create")) { %>
    <span class="error">
    <%= ViewContext.TempData.SafeGet("Message") %>
    </span>
    <label for="title">Title: </label>
    <%= Html.TextBox("title"
    , Html.Encode(ViewData.Title) %>
    <label for="body">Body: </label>
    <%= Html.TextArea("body"
    , Html.Encode(ViewData.Body) %>
    <%= Html.SubmitButton() %>
    <% } %>
    Do you see anything that could prevent this from working?

  2. Avatar for Jeremy Gray
    Jeremy Gray December 21st, 2007

    You guys really need to back off on the whole generation of dictionaries from anonymous types routine. A large number of people are going to read all of these posts and start to head down a path towards death by reflection overhead. Use dictionaries as dictionaries. If you really find constructing them to be a chore, it is incredibly easy to create a very nice-looking fluent interface that builds dictionaries of lists and/or objects and/or other dictionaries, ripe for use in semi-structured data scenarios, and all with minimal syntactic and mental complexity as well as with none of the perf issues.

  3. Avatar for Sergio Pereira
    Sergio Pereira December 21st, 2007

    For the sake of completeness, I tried it out and t worked with minor tweaks:
    [ControllerAction]
    public void New()
    {
    Article article = TempData.Get<Article>("article") ??
    new Article { Title = "", Body = "" };
    RenderView("New", article);
    //The New page inherits from ViewPage<Article>
    }
    // Get<T> is an extension I created
    [ControllerAction]
    public void Create()
    {
    Article article = new Article();
    article.UpdateFrom(Request.Form);
    //Pretend this was extensive validation
    if (article.IsValid)
    {
    repository.Persist(article);
    RedirectToAction("List");
    }
    else
    {
    TempData["Message"] = "Please supply all fields.";
    TempData["article"] = article;
    //Takes us to a form for creating a product.
    RedirectToAction("New");
    }
    }
    The "New" view (inherits from ViewPage<Article>:
    <% using(Html.Form("Create", "Articles")) { %>
    <span class="error">
    <%= ViewContext.TempData.SafeGet("Message") %>
    </span>
    <label for="title">Title: </label>
    <%= Html.TextBox("title"
    , Html.Encode(ViewData.Title)) %>
    <label for="body">Body: </label>
    <%= Html.TextArea("body"
    , Html.Encode(ViewData.Body)) %>
    <%= Html.SubmitButton() %>
    <% } %>

  4. Avatar for Haacked
    Haacked December 21st, 2007

    @Sergio Nice! I like that appreach even better than mine.

  5. Avatar for Haacked
    Haacked December 21st, 2007

    @Sergio I remember now why I didn't put the object in TempData. TempData uses the Session to store values temporarily. If you're using a State Server or database as the session store, you need to mark your object as Serializable.
    Easy enough to do with Article. I think I just have a tendency to not put objects in session as a holdover from my VB6 days. ;) I like putting primitives and loading from primitives.

  6. Avatar for Rick Strahl
    Rick Strahl December 21st, 2007

    Phil,
    This looks so familiar its scary. I had something like this in an old framework. But there were a number of problems with this. First is the convention issue. It requires that your fields and controls are named the same which often is not the case (ie. only if you got front to end control). One thing at the very least that should be supported is 'prefixing' for control names(ie. txtName, chkEntered etc.). that can *optionally* be stripped.
    The other thing is this starts falling down quickly if you have multiple objects that might have overlapping field names especially if you only want to update a few properties on several large objects. I suppose you can fix that up after the fact but you may run into conversion problems if types don't match.
    Lots of little things to worry about with this particular approach.

  7. Avatar for Sergio Pereira
    Sergio Pereira December 21st, 2007

    Another alternative would be to call RenderView("New", article) in the Create action instead of RedirectToAction("New").
    That way you don't have to deal with TempData and, since you did not persist anything yet, you won't be affected by refreshes/reposts of the previous request. Only the browser's URL will change.

  8. Avatar for Haacked
    Haacked December 21st, 2007

    @Rick For the simple cases, this is a nice approach. I don't think having your field names match the property names is such a bad convention. Hungarian is so played! ;) Where it really breaks down is when you need 3 inputs for one property. We're working on ways to do that. For now, this is a nice way for the simple case.
    @Sergio - So you you're saying you want "postbacks" in MVC? ;) It's an interesting approach, but I think the URL representing the resource you're looking at (ala REST) is kind of an important detail that I'd hate to simply give up.
    If we did this, we really have 2 URLs that represent the same thing, the input form. Why even have the original URL then?
    Perhaps /article/create renders the form when it's a GET request, but is also the action that does the creation when it is a POST.
    It's certainly an approach one could take, but even so I think we still need to figure out ways to better deal with the original case because some developers will want to keep things more REST-like.

  9. Avatar for tgmdbm
    tgmdbm December 22nd, 2007

    I've expanded your method to account for non primitives, and arrays or lists of objects/primitives.
    This code is obviously slower because of the calls to GetType(). If someone can suggest a better way of doing this I'm all ears. Let me know if you find this useful.
    [Apologies, this probably won't fit on the screen.]
    <pre>
    public static void PopulateFrom(this IDictionary<string, object> store, object o, string prefix)
    {
    Type t = o.GetType();
    if( string.IsNullOrEmpty( prefix ) )
    prefix = t.Name;
    if( t.IsPrimitive )
    store[prefix] = o;
    else
    {
    prefix = prefix + ".";
    foreach( PropertyValue property in o.GetProperties() )
    {
    if( property.Value.GetType().IsPrimitive )
    store[prefix + property.Name] = property.Value;
    else if( property.Value is IEnumerable )
    {
    int index = 0;
    foreach( object subObject in property.Value as IEnumerable )
    {
    PopulateFrom( store, subObject, string.Format( "{0}{1}[{2}]", prefix, property.Name, index++ ) );
    }
    }
    else
    PopulateFrom( store, property.Value, string.Format( "{0}{1}", prefix, property.Name ) );
    }
    }
    }
    </pre>

  10. Avatar for Member Blogs
    Member Blogs December 23rd, 2007

    .NET Free Training: C# and VB.NET for Kids (That's what I said, for Kids!) Hunting down bad try..catch

  11. Avatar for Julian Birch
    Julian Birch December 23rd, 2007

    I'm not convinced I like this anonymous types as dictionaries thing either, to be honest. It strikes me as a clever trick designed to get around the lack of a decent hashtable syntax in C#. Even if you were convinced that this was the way forward, it would be significantly more elegant to have coded an implicit cast from an anonymous type to an IDictionary<string, object> into the compiler. This implementation wouldn't require reflection.
    What's particularly bad is that I think that the library implementation right now precludes fixing that up later...

  12. Avatar for Daniel Jin
    Daniel Jin December 24th, 2007

    Definitely do away with hungarian. You needed that in webforms because I often have to extract txtName to a variable string "name", therefore I can't name my actual control just "name", but now with the automatic 2 way binding, there's no need.
    I'd like to take the convention even further and have the helpers to automatically match up the values in the TempData without you having to explicitly specify. (like monorail). I also happen to like the monorail's convention of naming your control user.name for the "name" property of item "user" in Flash. kinda like an INamingContainer based not on control hierarchy but object hierarchy. (unfortunately, this convention breaks down on elements that failed post binding.)
    regarding the over-reliance of these anonymous types, that also concerns me a little, seems that MS is really trying to take this a bit too far, and it because we can't use hash.

  13. Avatar for Sergio Pereira
    Sergio Pereira December 24th, 2007

    I'm in the same boat regarding the anonymous types hackery. I'm sure I'll not be replicating that pattern in my own APIs. Tell those guys in the C# team to give us hash literals :)

  14. Avatar for rarouš
    rarouš December 25th, 2007

    Hi, I have testing issue with TempData. In test runner it's null. How can I inject mock of TempDataDictionary?
    Thanx

  15. Avatar for Brian Lowry
    Brian Lowry December 26th, 2007

    I concur with removing hungarian notion in the sense of using "txt" to represent a textbox and "chk" to represent a checkbox. On the other hand, I am a big fan of naming server controls/ UI controls by a similar prefix. I read a post about it and it made a lot of sense (with code-behind) anyhow. The author suggested using "ui" or "ux" ("ui" is a bit too cluttered in Intellisense) before all IDs as a way for developers to know that a given variable in the code-behind is not just a variable, but a control. While this convention may be useless in MVC, it still would be nice to have a way to map control IDs to properties in the UpdateFrom function without too much reflection/magic.
    Just my .02

  16. Avatar for Andy
    Andy December 30th, 2007

    When might an extended RedirectToAction arrive to allow easier passing of a typed object to an action for this validation scenario?

    [ControllerAction]
    public void New(Article article)
    {
    RenderView("New", article ?? new Article());
    }
    [ControllerAction]
    public void Create([Deserialize] Article article)
    {
    if(article.IsValid)
    {
    article.SubmitChanges();
    RedirectToAction("List");
    }
    else
    {
    TempData["Message"] = "Please check your submission.";
    RedirectToAction<ArticleController>(c => c.New(article));
    }
    }

  17. Avatar for Eric
    Eric December 31st, 2007

    Won't there be an awful lot of duplicated code between the Create/Update actions? Back in the pre-ASP.NET days (classic ASP and Perl/CGI), we used to have a single form/page for Create/Update which shared a lot of the request-loading and validation logic. I guess I should dig in and see how that changes with the MVC architecture...

  18. Avatar for Elias
    Elias January 2nd, 2008

    Is anyone else worried that since we've had extension methods as part of the language not a blog post goes by without some scenario or other being 'solved' through their use? In fact you could say the same for the anonymous types as dictionary trick as well.
    Does the saying 'When you're a hammer everything looks like a nail' apply a bit here?
    Perhaps I'm being overly pessimistic but I'm worried that this framework is in danger of losing a coherent architecture and sense of overall direction in a favour of a load of syntactic fluff and 'trivially simple' examples that look good in a blog post but are essentially useless in a real world application.
    I personally am not interested in the 'write a blog application in 15 minutes' type stuff, I want to see how this framework helps me to write scalable, maintainable, testable real world code, and I want evidence that some thought has indeed gone into making this the real focus of the framework rather than it just being a toy.
    Sorry to rant, I'm really on board with what you guys are trying to do here, I just really want it to be brilliant.

  19. Avatar for Mike
    Mike January 7th, 2008

    So how about the edit page. On first load it should get the field values from the ViewData, after submitting, if there was a validation error, it should not reload the field values from ViewData. I'd appreciate if that was also 'streamlined'.
    kthx!

  20. Avatar for mike
    mike January 7th, 2008

    One more thing. Since the TempData seems to store objects, you'd have to do some casting if you store more than a simple string in it. I like to have a message object with a messagetype enum (error, warning, confirmation) and the actual message. In TempData I would liek to store more than 1 message under one key (like an array). Id always have to cast that when I get it out right?
    Thanks!

  21. Avatar for K. Scott Allen
    K. Scott Allen January 22nd, 2008

    I did a couple user group presentations this month on the new ASP.NET MVC framework. This post is a follow-up...

  22. Avatar for BusinessRx Reading List
    BusinessRx Reading List January 22nd, 2008

    I did a couple user group presentations this month on the new ASP.NET MVC framework. This post is a follow-up

  23. Avatar for Pablo Blamirez
    Pablo Blamirez January 22nd, 2008

    Hi,
    In my opinion Andys approach looks like a winner to me. Very similar to a pattern I use with Monorail.
    I have changed the code Andy supplied to remove the extended RedirectToAction.
    Now I haven't tested the code below so sorry about that but I don't see any major issues.
    I'd love to hear Phils feedback on Andys/this suggestion
    [ControllerAction]
    public void New()
    {
    RenderNew(new Article());
    }
    private void RenderNew(Article article)
    {
    RenderView("New", article);
    }

    [ControllerAction]
    public void Create([Deserialize] Article article)
    {
    if(article.IsValid)
    {
    article.SubmitChanges();
    RedirectToAction("List");
    }
    else
    {
    TempData["Message"] = "Please check your submission.";
    RenderNew(article));
    }
    }
    Loving your work fellas!

  24. Avatar for Mike
    Mike July 6th, 2008

    So what when article contains a property Message? How do handle collisions? Such an extension method should perhaps add keys like Object.Property

  25. Avatar for Kedar
    Kedar August 8th, 2008

    Hi Phil,
    This is really helpfull. But I'm trying to do the same using Html Button class.

    <%= Html.Button<optionscontroller>(x => x.Calculate(), "Options", "Calculate")%>

    But it's working as it is Get request. Is there any work around for this?