Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dynamic Model (non-class) Metadata Provider in MVC

Tags:

c#

asp.net-mvc

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:

  • Abandon using the MVC ModelMetadata for this, and instead build a view-model that directly contains the necessary metadata, along with the templates needed to display these more complex view-model objects. Means however that I'm then having to treat these objects differently to 'normal' objects, defeating the purpose somewhat and increasing the amount of view templates we need to build. This is the approach I am leaning towards now - more or less abandoning integrating with the MVC ModelMetadata stuff
  • Generate a unique key for each templated property, and use this for the property name rather than the display name - that would allow the ModelMetadataProvider to find the metadata that related to the property without needing a reference to its parent. However this would lead to a fairly ugly situation when debugging, and again seems like a large scale kludge. I have now tried a simplified version of this, and it seems to work, but does have some undesirable behavior, such as needing to use an meaningless property name if I want to explicitly bind to elements of the model.
  • In the ModelMetadataProvider when returning a collection of ModelMetadata objects for contained properties, record the container in the ModelMetadataProvider that is associated with those returned properties. I have tried this, but this returned collection of property metadata is ignored in this case, and the FromStringExpression method goes directly to the GetMetadataForProperty method instead.
like image 483
Linus Dillon Avatar asked Dec 16 '13 01:12

Linus Dillon


1 Answers

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();
}
like image 114
Guillermo Gutiérrez Avatar answered Oct 11 '22 14:10

Guillermo Gutiérrez