Binding complex user template values to your Model with Asp.Net MVC
Posted on: 2013-03-26
If you have simple class, even if this one contain underlying classes, Asp.Net MVC takes care of creating all html input for you and takes care to rebound everything into objects once the form is submitted back. However, if you have a custom control (called template in Asp.Net MVC) which let you add sub objects with multiple properties to your main object, you will have to handle it manually.
First of all, in this type of scenario, you should use UIHint
or Html Helper to generate the control itself.
public class MyObject {
public int MyObjectId{get;set;}
[UIHint("MyTemplate")]
public MyInnerObject ParticipationsRepartitionActif { get; set; }
}
public class MyInnerObject {
public int Uid{get;set;}
public MyItems Items{get;set;}
}
public class MyItems {
public int Id{get;set;}
public DateTime Date1{get;set;}
public DateTime Date2{get;set;}
public string Comment{get;set;}
}
The UIHint
will generate the correct html output for the user. Let say that this UIHint lets you select two dates and a comment. When you click a button, a row is added to a grid which let the user enter dates and a comment. The problem is how to send back these 3 information to the server in a way that the user could enter several lines in the same time.
The solution is to send back a collection of property in a format that Asp.Net MVC will be able to map back to your model class.
The first thing to do is to create an hidden field to your template with the name of the property that your model has (or view model in the case you are binding a view model to the view). This will bound the property to your template with an hidden field. This hidden field will be the transportation channel to send a collection of information that your grid contains.
The second thing is to populate this hidden field. You will need to set in your save button a Javascript that will loop through all lines and transform all html inputs into a serialized Json object.
$(document).on('click', '#mySaveButton', function (e) {
//1) Transform here all grid lines into the hidden field
//2) Be sure you do not prevent default action or return false because we want to server to post normally.
});
This Json object will be in a string format where we will insert it into the hidden field. Once saved, the hidden field is sent has other normal html field and Asp.Net MVC will deserialize it correctly.
function SetHiddenField() {
var obj = {};
obj.Items = [];
$('myControlThatHasAListOfItems').each(function () {
var myObjectToSerialize = {};
myObjectToSerialize.ID = $(this)...;
myObjectToSerialize.Date1 = $(this)...;
myObjectToSerialize.Date2 = $(this)...;
myObjectToSerialize.Comment = $(this)...;
obj.Items.push(myObjectToSerialize);
});
$('#MyHiddenField').val(JSON.stringify(obj));
}
Of course, do not create a Javascript function hard coded like this one. You should create it in a way where you could have multiple of this custom template control in the same page without conflicting or having to edit this code in the future. In this example, the hidden field should be called "Items" and will be bound with the hidden field. Asp.Net MVC will deserialize the Json when it will be ready to bound Items. Since all properties' name match the binding is done by magic. From here, you can use on your controller the model and save everything.
Complex binding
In the case the object contains a type that Asp.Net MVC cannot handle for deserializing, you will have to create a model binder. This is done by creating in the Global.Asax.cs
an entry for a model binding:
ModelBinders.Binders.Add(typeof(YourComplexClass<bool>), new YourComplexClassModelBinder());
And the binder. The following code have to be modified to fulfill your need.
public class YourComplexClassModelBinder:IModelBinder {
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) {
var valueResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
var modelState = new ModelState { Value = valueResult };
YourComplexClass<bool> actualValue = null;
if (!string.IsNullOrWhiteSpace(valueResult.AttemptedValue)) {
var settings = new JsonSerializerSettings();
settings.Converters.Add(new JSonComplexConverterForYourComplexClass());
try { actualValue = JsonConvert.DeserializeObject<YourComplexClass<bool>>(valueResult.AttemptedValue, settings); }
catch (FormatException e) { modelState.Errors.Add(e); }
}
bindingContext.ModelState.Add(bindingContext.ModelName, modelState);
return actualValue;
}
public class JSonComplexConverterForYourComplexClass : JsonConverter {
public override bool CanConvert(Type objectType) {
return (objectType == typeof(I_XYZ<bool>));
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) {
return serializer.Deserialize<XYZ>(reader);
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) {
throw new NotImplementedException();
}
}
}
This is often the case of Interface that need to be deserialized into a concrete class. In the above example, I_XYZ
was the interface but when it's the time to deserialize, Asp.Net MVC doesn't know to which type to deserialize. This is why, we provide a model binder for the class that contains the property that has I_XYZ
interface and provide to Json.Net library how to read the Json for deserialization.