4

I use a DataContractJsonSerializer to create a JsonResult for my model data when sending data to the client. My model represents data to be displayed in a data table, and I wished to change the name of the model's properties in the JSON only so that less verbose property names are sent over the wire for each data table row. Now, I'm attempting to send the data table cell values via JSON to the server's controller action method. The names of the fields being sent back are still the short names, and the model binding doesn't seem to like that. What can I do to get model binding working and preserve the ability to sent alternate property names via JSON?

Model:

[DataContract()]
public class UsageListModel {

    [DataMember(Name = "results")]
    public IEnumerable<UsageModel> Usages { get; set; }
}

[DataContract()]
public class UsageModel {

    [DataMember(Name = "job")]
    public string JobId { get; set; }

    [DataMember(Name = "dt")]
    public DateTime UsageDate { get; set; }

    [DataMember(Name = "qty")]
    public int Quantity { get; set; }

    [DataMember(Name = "uom")]
    public string UnitOfMeasure { get; set; }

    [DataMember(Name = "nts")]
    public string Notes { get; set; }
}
Community
  • 1
  • 1
Matt Hamsmith
  • 3,955
  • 1
  • 27
  • 42
  • What does your controller action look like? Also, have you checked the request being sent up in the Browser dev tools and see what the form-data is you send up? – Nope May 10 '12 at 07:53
  • @François - Yep, I see all the data in Firebug going from the browser to the server, using the short property names. As for my controller action, see my comment on your answer-comment below. – Matt Hamsmith May 21 '12 at 21:37

3 Answers3

2

It's not as elegant but I usually do this by just making an intermediary class (I refer to it as a ViewModel) that has those shortname properties and can be translated back and forth between it and the actual Model. Although it seems like busy work, the ViewModel can be useful beyond this stint - for example you can use it to easily cache client-side info if the need arises, or serialize/deserialize exactly what's going to/from the client in tests.

Chris Moschini
  • 36,764
  • 19
  • 160
  • 190
0

I'm still in disbelief that MVC doesn't offer some easier method to bind using custom attributes (or even the .NET data-contract attributes). Given that it doesn't... your best bet is to implement your own IModelBinder. Use reflection to get the DataMember names of the properties, and look for those values in the binding context.

Here's a great reference on model binding: http://msdn.microsoft.com/en-us/magazine/hh781022.aspx

A good general approach to maintaining custom binders: http://lostechies.com/jimmybogard/2009/03/18/a-better-model-binder/

EDIT

Generic model binder that handles a defined type. To add this to your application, add this line in global.asax:

ModelBinders.Binders.Add(typeof(UsageModel), new CustomModelBinder<UsageModel>());

And the binder:

public class CustomModelBinder<T> : IModelBinder
{
    public override bool IsMatch(Type t)
    {
        return t == typeof(T);
    }

    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        Type t = typeof(T);
        var entity = (bindingContext.Model ?? Activator.CreateInstance(t));

        // Cycle through the properties and assign values.
        foreach (PropertyInfo p in t.GetProperties())
        {
            string sourceKey;

            // this is what you'd do if you wanted to bind to the property name
            // string sourceKey = p.Name;

            // TODO bind sourceKey to the name in attribute DataMember

            Type propertyType = p.PropertyType;

            // now try to get the value from the context ...
            ValueProviderResult valueResult = bindingContext.ValueProvider.GetValue(sourceKey);
            if (valueResult != null)
            {
                bindingContext.ModelState.SetModelValue(sourceKey, valueResult);
                p.SetValue(entity, valueResult.ConvertTo(propertyType), null);
            }
        }
        return entity;
    }
}
McGarnagle
  • 101,349
  • 31
  • 229
  • 260
  • Well, this pointed me in the proper direction, but I'm having difficulty figuring out how to implement the minimal amount of code to make this work. At first blush, it looked to me like creating a custom model binder class that inherits from DefaultModelBinder is the way to go. After a lot of searching, it also looks like overriding DefaultModelBinder.GetTypeDescriptor would do the trick (where I can substitute in the DataMember names for property names), but now I'm stuck on how to accomplish this. Or am I going the wrong way - should I just implement IModelBinder? – Matt Hamsmith May 03 '12 at 19:04
  • I think you'll want to implement IModelBinder to handle your custom type(s), because you'd want to leave binding of other models untouched... I'll update. – McGarnagle May 03 '12 at 20:31
  • I couldn't get this to work. It seems the array indices weren't handled, nor subobjects. I could see that the bindingContext contained values (ModelName = "UsageInformation") for "UsageInformation.results[0].job", "UsageInformation.results[0].nts", etc. but I couldn't figure out a general way to put that all together in the sourceKey variable. Also, it seems like this solution left out any model validation. – Matt Hamsmith May 04 '12 at 19:52
  • I'm eager to see if anyone tries to claim the bounty you put out. – Matt Hamsmith May 07 '12 at 21:51
  • 1
    Sorry to be not construcive. However, building a serious and reliable ModelBinder for json objects based on the datacontract serializer is not an easy task. Expecially if you want to support all data annotations. It might take very far from the software you are implemnting. Moreover Mvc4 WebApi uses the DataContratSerializer in its model binder and have serious problems handling dates...Microsoft promised to ship the Json.Net serializer with the final release to solve them. So, in my opinion wasting a lot of effort into the implemntation of a DataContract model binder is not worth the result. – Francesco Abbruzzese May 08 '12 at 16:39
  • 1
    Not knowing the OP's design I would always try to stick with what works. When we create ViewModels we make them suit the view and the names of the properties send by the client to the controller action will always match the names of the properties in the model (not the names in the annotations) . We use a completely separate model (a DTO) to send the data from the controller to the service layer. This way we separate out any dependancies, ensure individual requirements in design between the models/DTOs don't clash and avoid any binding issues. (So far anyway) – Nope May 10 '12 at 08:04
  • @François - My controller action basically takes in an id value, queries the data store for the appropriate data (hydrated into data models), and then maps the data model values to the view model. So, I think in essence I am doing exactly what you state. I just get annoyed, even in my view models, of looking at property names such as "nm" instead of "Name". Until this gets resolved, I've just learned to live with property names that look silly on this particular view model. – Matt Hamsmith May 21 '12 at 21:40
0

I stumbled across a potential answer to this question randomly while browsing this other question.

I never realized this until now, but apparently you can add attributes to method parameters. Let's take a simple example:

public ActionResult SomeMethod(string val) {
    return View(val);
}

If you call this URL -- /MyController/SomeMethod?val=mytestval -- then you'll get back "mytestval" in the model, right? So now you can write this:

public ActionResult SomeMethod([Bind(Prefix="alias")] string val) {
    return View(val);
}

Now this URL will produce the same result: /MyController/SomeMethod?alias=mytestval.

Anyway, I'm still not sure if that will answer your question, but I thought it was very interesting.

Community
  • 1
  • 1
McGarnagle
  • 101,349
  • 31
  • 229
  • 260
  • That is pretty interesting, and I can see how it might solve the renaming of the top-level property name on the action method parameter. I'm not sure there is a way to use this to rename all of the sub-objects' properties, however. I have yet to use the MVC4 Beta - I wonder if this entire issue is solved in that version of MVC, as @Francesco Abbruzzesse hinted at. – Matt Hamsmith Jun 06 '12 at 19:15