Using the ASP.NET MVC ModelBinder attribute - Second part

Edit on GitHub

Just after the ASP.NET MVC preview 5 was released, I made a quick attempt to using the ModelBinder attribute. In short, a ModelBinder allows you to use complex objects as action method parameters, instead of just basic types like strings and integers. While my aproach was correct, it did not really cover the whole picture. So here it is: the full picture.

First of all, what are these model binders all about? By default, an action method would look like this:

[code:c#]

public ActionResult Edit(int personId) {
    // ... fetch Person and do stuff
}

[/code]

Now wouldn't it be nice to pass this Person object completely as a parameter, rather than obliging the controller's action method to process an id? Think of this:

[code:c#]

public ActionResult Edit(Person person) {
    // ... do stuff
}

[/code]

Some advantages I see:

  • More testable code!
  • Easy to work with!
  • Some sort of viewstate-thing which just passes a complete object back and forth. Yes, I know, ViewState is BAD! But I recently had a question about how to manage concurrency, and using a version id as an hidden HTML field or a complete object as a hidden HTML field should not be that bad, no?

Just one thing to do: implementing a ModelBinder which converts HTML serialized Persons into real Persons (well, objects, not "real" real persons...)

How to implement it...

Utility functions

No comments, just two utility functions which serialize and deserialize an object to a string an vice-versa.

[code:c#]

using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;

namespace ModelBinderDemo.Util
{
    public static class Serializer
    {
        public static string Serialize(object subject)
        {
            MemoryStream ms = new MemoryStream();
            BinaryFormatter bf = new BinaryFormatter();
            bf.Serialize(ms, subject);

            return Convert.ToBase64String(ms.ToArray());
        }

        public static object Deserialize(string subject)
        {
            MemoryStream ms = new MemoryStream(Convert.FromBase64String(subject));
            BinaryFormatter bf = new BinaryFormatter();
            return bf.Deserialize(ms);
        }
    }
}

[/code]

Creating a ModelBinder

The ModelBinder itself should be quite simple to do. Just create a class which inherits DefaultModelBinder and have it ocnvert a string into an object. Beware! The passed in value might also be an array of strings, so make sure to verify that in code.

[code:c#]

using System;
using System.Globalization;
using System.Web.Mvc;
using ModelBinderDemo.Models;
using ModelBinderDemo.Util;

namespace ModelBinderDemo.Binders
{
    public class PersonBinder : DefaultModelBinder
    {
        protected override object ConvertType(CultureInfo culture, object value, Type destinationType)
        {
            // Only accept Person objects for conversion
            if (destinationType != typeof(Person))
            {
                return base.ConvertType(culture, value, destinationType);
            }

            // Get the serialized Person that is being passed in.
            string serializedPerson = value as string;
            if (serializedPerson == null && value is string[])
            {
                serializedPerson = ((string[])value)[0];
            }

            // Convert to Person
            return Serializer.Deserialize(serializedPerson);
        }
    }
}

[/code]

In order to use this ModelBinder, you'll have to register it in Global.asax.cs:

[code:c#]

// Register model binders
ModelBinders.Binders.Add(typeof(Person), new PersonBinder());

[/code]

Great View!

Nothing strange in the View: just a HTML form which generates a table to edit a Person's details and a submit button.

[code:c#]

<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" AutoEventWireup="true" CodeBehind="Index.aspx.cs" Inherits="ModelBinderDemo.Views.Home.Index" %>
<%@ Import Namespace="ModelBinderDemo.Models" %>

<asp:Content ID="indexContent" ContentPlaceHolderID="MainContent" runat="server">
    <h2>Edit person</h2>
    <% using (Html.Form("Home", "Index", FormMethod.Post)) { %>
        <%=Html.Hidden("person", ViewData["Person"]) %>
        <%=Html.ValidationSummary()%>
        <table border="0" cellpadding="2" cellspacing="0">
            <tr>
                <td>Name:</td>
                <td><%=Html.TextBox("Name", ViewData.Model.Name)%></td>
            </tr>
            <tr>
                <td>E-mail:</td>
                <td><%=Html.TextBox("Email", ViewData.Model.Email)%></td>
            </tr>
            <tr>
                <td>&nbsp;</td>
                <td><%=Html.SubmitButton("saveButton", "Save")%></td>
            </tr>
        </table>
    <% } %>
</asp:Content>

[/code]

Wait! One thing to notice here! The <%=Html.Hidden("person", ViewData["Person"]) %> actually renders a hidden HTML field, containing a serialized version of my Person. Which might look like this:

[code:c#]

<input Length="340" id="person" name="person" type="hidden" value="AAEAAAD/////AQAAAAAAAAAMAgAAAEZNb2RlbEJpbmRlckRlbW8
sIFZlcnNpb249MS4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj1udWxsBQEAAAAdTW9kZWxCaW5kZXJEZW1vLk1v
ZGVscy5QZXJzb24DAAAAEzxJZD5rX19CYWNraW5nRmllbGQVPE5hbWU+a19fQmFja2luZ0ZpZWxkFjxFbWFpbD5rX19CYWNraW5nRmllb
GQAAQEIAgAAAAEAAAAGAwAAAAdNYWFydGVuBgQAAAAabWFhcnRlbkBtYWFydGVuYmFsbGlhdXcuYmUL" />

[/code]

Creating the action method

All preparations are done, it's time for some action (method)! Just accept a HTTP POST, accept a Person object in a variable named person, and Bob's your uncle! The person variable will contain a real Person instance, which has been converted from AAEAAD.... into a real instance. Thank you, ModelBinder!

[code:c#]

[AcceptVerbs("POST")]
public ActionResult Index(Person person, FormCollection form)
{
    if (string.IsNullOrEmpty(person.Name))
    {
        ViewData.ModelState.AddModelError("Name", person.Name, "Plese enter a name.");
    }

    if (string.IsNullOrEmpty(person.Email))
    {
        ViewData.ModelState.AddModelError("Name", person.Name, "Plese enter a name.");
    }

    return View("Index", person);
}

[/code]

Make sure to download the full source and see it in action! ModelBinderDemo2.zip (239.87 kb)

kick it on DotNetKicks.com

This is an imported post. It was imported from my old blog using an automated tool and may contain formatting errors and/or broken images.

Leave a Comment

avatar

11 responses

  1. Avatar for tasarım
    tasarım October 3rd, 2008

    Actually i like it. Do you use MVCContrib?

  2. Avatar for maartenba
    maartenba October 3rd, 2008

    I use MvcContrib for personal projects, blog posts are always based (well, mostly) on a "standard" ASP.NET MVC template.

  3. Avatar for Chris
    Chris October 3rd, 2008

    Hey, did my comment get killed? I posted yesterday. Funnily enough notifications about this comments posted to blog were emailed to me.

    I'm familiar with the IModelBinder stuff in MVC. I downloaded your source and the code doesn't make sense to me. Changing the value of the name for example and then posting, doesn't update the person model. I'm not following why you would need to serialize a Person instance to the client side, you already did that when you displayed a form with the fields prepopulated with the person values. I had written an AutoBinder thingy some time back and made a post about it, just an easy way of mapping up field values to property values, check it out: http://panteravb.com/blog/p...

  4. Avatar for tasarım
    tasarım October 4th, 2008

    Ok. I do have question on MVC and models. I want to build an multilingual website so i need to get the localized version of the objects from db. This is not a problem only there are multible objects like menuitems and person and footer etc.. How can i bind them to 1 view with modelbinder? What is best practise for it? Can i bind multiple models?

  5. Avatar for maartenba
    maartenba October 5th, 2008

    Chris, the idea here is that you et the original Person object (in the hidden field) + the modified fiels that can be applied on the model. Reason for this is that you might want to have the Person object the way it was at the time a view was generated, for example for concurrency reasons.

    Tasarim, I suggest posting your question on the ASP.NET MVC forum (forums.asp.net), as it is not really a modelbinder issue. If you want to, you can also send me an email containing some more details and I'll see if I can help you out.

  6. Avatar for Andy
    Andy October 5th, 2008

    For concurrency and versions, surely an ETag (last-modified) approach with MVC, rather than serializing the object to the client would be better.

  7. Avatar for Chris
    Chris October 5th, 2008

    I guess since the demo doesn't demonstrate how the original model serialized, is updated from form values, i wasn't able to see that in action.

    But that gets me thinkin now about concurrency. The hidden field thingy seems worse.

    Let's say Jack and Jill sit down at 8:00am to edit the same Person, and Jill gets up to get a cup of coffee. While she's gone, Jack changes some details on person and saves it to the database.

    Jill returns at 8:05am(with her cup of coffee), she has a stale version of person, makes some changes, and saves to the database. She stepped on the changes that Jack made. In terms of concurrency, doing it this way actually hides the fact the the problem exists, right? If the last person to save is going to be the winner so to speak, why waste time with the serializing in the first place?

    If concurrency is really the issue, you will want to NOT save Jill's changes, rather you'll want to let her know that her changes cannot be saved at this time because user Jack made changes while she was gone. Follow me?

  8. Avatar for tgmdbm
    tgmdbm October 6th, 2008

    Check the second call to AddModelError. The perils of cut and paste. *wink*

    The only problem that I can see with this approach is a malicious user can see all serializable properties of the Person object and you don't necessarily want that.

  9. Avatar for maartenba
    maartenba October 6th, 2008

    @Chris, Andy --> Indeed, this is not the best way, but it is one of the many options that can solve a problem.

    @tgmdbm --> There's one thing you might want to consider: encryption using the machinekey

  10. Avatar for Petar Petrov
    Petar Petrov October 30th, 2008

    Hi.
    I have a suggestion and a question. You should dispose the memory stream used in de/Serialization. Something like this :

    public static string Serialize(object subject)
    {
    byte[] data = null;

    using (MemoryStream ms = new MemoryStream())
    {
    BinaryFormatter bf = new BinaryFormatter();
    bf.Serialize(ms, subject);
    data = ms.ToArray();
    }

    return Convert.ToBase64String(data);
    }

    public static object Deserialize(string subject)
    {
    using (MemoryStream ms = new MemoryStream(Convert.FromBase64String(subject)))
    {
    BinaryFormatter bf = new BinaryFormatter();
    return bf.Deserialize(ms);
    }
    }

    I've seen that DefaultModelBinder implements IModelBinder. Is it possible to have a generic version IModelBinder<T> like for example ViewPage ? In this case ConvertType will simply return T and we don't need the check destinationType != typeof(Person). In fact there will be no destinationType argument in ConvertType. I don't like the object value parameter either and the cast as string, as string[]. I will be very happy to see string[] values as parameter.

  11. Avatar for maartenba
    maartenba October 30th, 2008

    What about an abstract base class ModelBinderBase<T> which you can overload?