Add new Property to MVC ModelMetadata

7.5k views Asked by At

I'm seeing alternative approaches to this such as AdditionalValues, but I'm wondering if it's possible to end up in a scenario where you can add a new property to the ModelMetadata object available in template views.

For example you could have:

@ViewData.ModelMetadata.MvvmBound

I want to use this in Editor and Display templates to add MVVM attributes to the HTML elements being rendered.

I'm completely lost, but here are my efforts so far:

public class MyModelMetaDataProvider : DataAnnotationsModelMetadataProvider
{
    protected new MyModelMetaData CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName)
    {
        ModelMetadata modelMetaData = base.CreateMetadata(attributes, containerType, modelAccessor, modelType, propertyName);
        MyModelMetaData myMetaData = (MyModelMetaData)modelMetaData;

        return myMetaData;
    }
}

public class MyModelMetaData: ModelMetadata
{
    public MyModelMetaData(ModelMetadataProvider provider, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName)
        : base(provider, containerType, modelAccessor, modelType, propertyName)
    {

    }

    public int MyProperty { get; set; }
}  

Then in global asax I use:

ModelMetadataProviders.Current =new MyModelMetaDataProvider();

The problem is if I try to use MyProperty in the view it isn't recognized, which I anticipated because VS isn't aware of the custom ModelMetadata class.

I'm not even sure this is possible?

1

There are 1 answers

0
Daniel J.G. On BEST ANSWER

There is a dictionary AdditionalValues in the ModelMetadata class. Ref here

You can add values to that dictionary using the AdditionalMetadata attribute, as in:

public class DummyModel
{
    [AdditionalMetadata("MvvmBound", true)]
    public string PropertyA{ get; set; }

    public string PropertyB{ get; set; }
}

And you could check those in a string editor template, for example:

@{
    bool isBound = false;
    if(ViewData.ModelMetadata.AdditionalValues.ContainsKey("MvvmBound"))
    {
        isBound = (bool)ViewData.ModelMetadata.AdditionalValues["MvvmBound"];    
    }
}

//use isBound to set some attributes in the html element

You may also want to check those attributes in the editor template for DummyModel, which you can do retrieving the metadata for individual properties as in:

@ModelMetadata.FromLambdaExpression(x => x.PropertyA, ViewData).AdditionalValues["MvvmBound"] 

There are some downsides of this code as it is. The approach requires to pass the key name to the AdditionalValues dictionary and you also need to cast the value from object to the proper type. You could create your own extension method for retrieving that value from the metadata without the need to manually provide the key name and manually perform a casting:

public static class ModelMetadataExtensions
{
    public static bool IsMvvmBound(this ModelMetadata metadata)
    {
        if (!metadata.AdditionalValues.ContainsKey("MvvmBound")) return false;
        return (bool)metadata.AdditionalValues["MvvmBound"];
    }
}

Which would allow you a cleaner code in the editor templates. For example in an editor template for the DummyModel above you could do something like (remember to add the namespace of the extension method to the Web.config file inside the /Views folder):

<h3>WholeModel Is MvvmBound?: @ViewData.ModelMetadata.IsMvvmBound()</h3>
<h3>PropertyA Is MvvmBound?: @ModelMetadata.FromLambdaExpression(x => x.PropertyA, ViewData).IsMvvmBound()</h3>
<h3>PropertyB Is MvvmBound?: @ModelMetadata.FromLambdaExpression(x => x.PropertyB, ViewData).IsMvvmBound()</h3>

You could pair that extension method with your custom attribute extending IMetadataAware so you don't need to hardcode the name of the key for the metadata value every time you use it on a property:

public class IsMvvmBoundAttribute : Attribute, IMetadataAware 
{
    public IsMvvmBoundAttribute()
    {
        this.IsMvvmBound = true;
    }

    public bool IsMvvmBound { get; set; }

    //This is the single method of IMetadataAware, and what allows us to add the values into the AdditionalValues dictionary
    public void OnMetadataCreated(ModelMetadata metadata)
    {
        metadata.AdditionalValues.Add("MvvmBound", this.IsMvvmBound);
    }
}

public class DummyModel
{
    //[AdditionalMetadata("MvvmBound", true)] <- replaced with the new attribute
    [IsMvvmBoundAttribute]
    public string PropertyA{ get; set; }

    public string PropertyB{ get; set; }
}

Because ModelMetadata.AdditionalValues is a Dictionary<string,object>, you are not limited to storing a simple value per key. You can follow the code above to create you own class holding all the properties that you need, an attribute that you can use to set each of those values and saves an instance of that class to the AdditionalValues dictionary and an extension method that extracts the instance of that class from the dictionary.

As you can see, you can go pretty far without the need for your own ModelMetadata class or extending the ModelMetadataProvider.

Hope it helps!