We are developing an application where the end-user schema is dynamic (we have a good business case for this - it is not something that can be handled easily by a static model).
I have used the .NET DynamicObject class to allow these dynamic schema objects to be addressed easily from code, and expected this to just work with the MVC model metadata. However the MVC metadata support seems to be hamstrung in that it only deals with meta-data defined per type - not per object which will be the case here.
Even when I dug down and tried implementing our own ModelMetadataProvider, it seems that the neccessary information is simply not passing in - the GetMetadataForProperty method is particularly problematic. Effectively I need to access the parent or container object for the property, but all that is passed in is the type.
The above is called mainly from the FromStringExpression method in the ModelMetadata class. This method actually DOES have the container (at least in this case) but does not pass it through. This branch is executed when it finds the view data about the expression stored (cached?) in the ViewData. If that fails it falls back to looking it up via the ModelMetadata object - which ironically might work for me. Whats particularly irritating is that the FromStringExpression method is static, so I can't easily override its behavior.
In desperation I have considered trying to traverse the modelAccessor expression, but this seems like a kludge at best and extremely fragile.
I have searched extensively for a solution to this. Many point to Brad Wilson's talk (http://channel9.msdn.com/Series/mvcConf/mvcConf-2011-Brad-Wilson-Advanced-MVC-3) on non-class models, however if you look at the actual code presented, you will see that it TOO is bound to the TYPE and not the object - in other words not terribly useful. Others have pointed to the http://fluentvalidation.codeplex.com/, but that only seems to apply to the validation side, and I suspect suffers from the same problem (bound to type rather than object) as the above.
For example, I may have a dictionary object that contains a series of field objects. This looks something like (very cut down/simplified example):
public class Entity : DynamicObject, ICustomTypeDescriptor
{
public Guid ID { get; set; }
public Dictionary<string, EntityProp> Props { get; set; }
... DynamicObject and ICustomTypeDescriptor implementation to expose Props as dynamic properties against this Entity ...
}
public class EntityProp
{
public string Name { get; set; }
public object Value { get; set; }
public Type Type { get; set; }
public bool IsRequired { get; set; }
}
This might be passed to a view as its view-mode (or part of it), and in my view I'd like to use:
@Html.EditorForModel()
Has anyone found a way around this?
I've identified two possible alternative approaches, but both have significant drawbacks:
Maybe creating a custom ModelMetadataProvider:
public class CustomViewModelMetadataProvider : DataAnnotationsModelMetadataProvider
{
public override IEnumerable<ModelMetadata> GetMetadataForProperties(object container, Type containerType)
{
if (containerType == null)
{
throw new ArgumentNullException("containerType");
}
return GetMetadataForPropertiesImpl(container, containerType);
}
private IEnumerable<ModelMetadata> GetMetadataForPropertiesImpl(object container, Type containerType)
{
var propertiesMetadata = new List<ModelMetadata>();
foreach (EntityProp eprop in ((Entity)container).Props.Values)
{
Func<object> modelAccessor = () => eprop;
propertiesMetadata.add(GetMetadataForProperty(modelAccessor, containerType, eprop.Name));
}
return propertiesMetadata; // List returned instead of yielding, hoping not be needed to re-call this method more than once
}
public override ModelMetadata GetMetadataForProperty(Func<object> modelAccessor, Type containerType, string propertyName) {
if (containerType == null) {
throw new ArgumentNullException("containerType");
}
if (String.IsNullOrEmpty(propertyName)) {
throw new ArgumentException(MvcResources.Common_NullOrEmpty, "propertyName");
}
return CreateMetadata(null, containerType, modelAccessor, modelAccessor().Type, propertyName);
}
protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName)
{
EntityProp eprop = modelAccessor();
DataAnnotationsModelMetadata result;
if (propertyName == null)
{
// You have the main object
return base.CreateMetadata(attributes, containerType, modelAccessor, modelType, propertyName);
}
else
{
// You have here the property object
result = new DataAnnotationsModelMetadata(this, containerType, () => eprop.Value, modelType, propertyName, null);
result.IsRequired = eprop.IsRequired;
}
return result;
}
}
Finally, setup your custom provider in Global.asax.cs
:
protected void Application_Start()
{
//...
ModelMetadataProviders.Current = new CustomViewModelMetadataProvider();
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With