Reputation: 7095
I'm having trouble getting the default model binder to work as expected for the following data structure:
json:
{
template: "path/to/template",
slides: [{
index: 0,
context: {
foo: "bar"
}
}, {
index: 1,
context: {
foo: 'bar!',
table: [
['Price', 'Revenue', 'Profit'],
[$5', 100, 20],
['$10', 320, 4],
['$7', 50, 2]
]
}
}]
}
IPresentationData
model:
public interface IPresentationData
{
public string Template { get; set; }
public ICollection<SlideData> Slides { get; set; }
}
ISlideData
model:
public interface ISlideData
{
public int Index { get; set; }
public IContext Context { get; set; }
}
IContext
model:
public interface IContext : IDictionary<string, dynamic>
{
}
The default model binder works fine with the exception of SlideData.Context
, which can be an arbitrarily large/deep object.
For example, json.context.table
is deserialized into:
{
'table[0][0]': 'Price',
'table[0][1]': 'Revenue',
...
}
Instead of:
{
'table': [
[...]
]
}
I'm thinking I need to override System.Web.Mvc.DefaultModelBinder#BindModel
for just the SlideData.Context
property, but I'm not really sure where to start.
Any help here would be much appreciated.
Upvotes: 2
Views: 1330
Reputation: 7095
It turns out what was needed was an IModelBinder
that supports ExpandoObject
.
Here's the code:
In HttpApplication#Application_Start
:
ModelBinders.Binders.Add(typeof(Context), new DynamicDictionaryModelBinder());
ModelBinders.Binders.Add(typeof(ExpandoObject), new DynamicDictionaryModelBinder());
Custom binder:
public class DynamicDictionaryModelBinder : DefaultModelBinder
{
public override object BindModel(
ControllerContext controllerContext,
ModelBindingContext bindingContext)
{
var model = bindingContext.Model;
var modelType = bindingContext.ModelType;
if (model == null)
{
model = this.CreateModel(controllerContext, bindingContext, modelType);
}
var dictionaryBindingContext = new ModelBindingContext()
{
ModelMetadata =
ModelMetadataProviders.Current
.GetMetadataForType(() => model, modelType),
ModelName = bindingContext.ModelName,
ModelState = bindingContext.ModelState,
PropertyFilter = bindingContext.PropertyFilter,
ValueProvider = bindingContext.ValueProvider
};
return this.UpdateDynamicDictionary(controllerContext, dictionaryBindingContext);
}
private static KeyValuePair<string, object> CreateEntryForModel(
ControllerContext controllerContext,
ModelBindingContext bindingContext,
Type valueType,
IModelBinder valueBinder,
string modelName,
string modelKey)
{
var valueBindingContext = new ModelBindingContext()
{
ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, valueType),
ModelName = modelName,
ModelState = bindingContext.ModelState,
PropertyFilter = bindingContext.PropertyFilter,
ValueProvider = bindingContext.ValueProvider
};
var thisValue = valueBinder.BindModel(controllerContext, valueBindingContext);
return new KeyValuePair<string, object>(modelKey, thisValue);
}
private object UpdateDynamicDictionary(
ControllerContext controllerContext,
ModelBindingContext bindingContext)
{
var modelList = new List<KeyValuePair<string, object>>();
var enumerableValueProvider = bindingContext.ValueProvider as IEnumerableValueProvider;
if (enumerableValueProvider != null)
{
var keys = enumerableValueProvider.GetKeysFromPrefix(bindingContext.ModelName);
var groups = keys.GroupBy((k) => k.Key.Split('[')[0]);
foreach (var group in groups)
{
if (group.Count() > 1)
{
var valueType = typeof(ICollection<ExpandoObject>);
modelList.Add(
CreateEntryForModel(
controllerContext,
bindingContext,
valueType,
Binders.GetBinder(valueType),
bindingContext.ModelName + '.' + group.Key,
group.Key));
}
else
{
var item = group.Single();
var value = bindingContext.ValueProvider.GetValue(item.Value);
var valueType = value != null && value.RawValue != null ?
typeof(object) : typeof(ExpandoObject);
modelList.Add(
CreateEntryForModel(
controllerContext,
bindingContext,
valueType,
Binders.GetBinder(valueType),
item.Value,
item.Key));
}
}
}
var dictionary = (IDictionary<string, object>)bindingContext.Model;
foreach (var kvp in modelList)
{
dictionary[kvp.Key] = kvp.Value;
}
return dictionary;
}
}
The json was already being serialized properly by the built in IValueProvider
stack, it was just a matter of providing the proper types to Binder#GetBinder(Type)
as well as the proper accessor. For example ICollection<ExpandoObject>
+ revenue
vs object
+ revenue[0][0]
.
Upvotes: 4