How can I extend DataAnnotationsModelMetadata

2019-05-15 02:54发布

I have a custom DataAnnotationsModelMetadataProvider and I'd like to return an object derived from ModelMetadata so that I can have extra properties in my razor templates.

So far my custom provider only overrides the CreateMetadata function:

protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName)
{
    var modelMetadata = base.CreateMetadata(attributes, containerType, modelAccessor, modelType, propertyName);

    ModelMetadataAttribute mma;            
    foreach (Attribute a in attributes)
    {
        mma = a as ModelMetadataAttribute;
        if (mma != null)
            mma.Process(modelMetadata);                
    }

    return modelMetadata;
}        

so that every attribute derived from ModelMetadataAttribute can do some custom actions (actually it only adds AdditionalValues)

But since nearly all of my attributes mainly add attributes to the html elements I generate in my razor template, I'd like the ModelMetadata in the view to contain a dictionnary of the attributes I want to add (and some other things).

So I need a class that inherits from DataAnnotationsModelMetadata.

I can't just call the base.CreateMetadata function since it won't cast properly to my derived class.

I thought about making a copy of public properties of DataAnnotationsModelMetadata returned by the base.CreateMetadata function into my derived class, but I can loose information so it doesn't seem safe.

Another way I thought of is to copy/paste the base code of CreateMetadata and add my logic, but it seems ugly... (and I only have the mvc3 sources, so it might have changed in mvc4)

Also thought of inheriting from ViewDataDictionnary so that I can provide my custom metadata class instead of the standard one, but I don't have a clue about how to do this. (also I admit I didn't dig much on that particular question)

I looked at a lot of articles about DataAnnotations and providers but couldn't find something similar to what I'm trying to do.

So what are my options here? In which direction could I search to get closer to what I want to do?

EDIT:

I looked at this question (quite similar): Can I achieve a 'copy constructor' in C# that copies from a derived class? but what it does is a copy of properties and I wanted to avoid that. In the last answer in this post there's something about PopulateMetadata but I can't find that function in the base provider...

2条回答
孤傲高冷的网名
2楼-- · 2019-05-15 03:03

After some thinking, I actually found a solution that doesn't need to use MvcExtensions and that copies all the information contained in the base class to the derived one.

Since DataAnnotationsModelMetadata and its base classes don't do anything but initialize some private or protected values, there is no risk of having side effects to do that.

public class ArtifyModelMetaDataProvider : DataAnnotationsModelMetadataProvider
{
    private static List<Tuple<FieldInfo, FieldInfo>> _fieldsMap;

    static ArtifyModelMetaDataProvider()
    {
        _fieldsMap = new List<Tuple<FieldInfo, FieldInfo>>();
        foreach (FieldInfo customFI in GetAllFields(typeof(ArtifyModelMetadata)))
            foreach (FieldInfo baseFI in GetAllFields(typeof(DataAnnotationsModelMetadata)))
                if (customFI.Name == baseFI.Name)
                    _fieldsMap.Add(new Tuple<FieldInfo, FieldInfo>(customFI, baseFI));
    }

    private static List<FieldInfo> GetAllFields(Type t)
    {
        List<FieldInfo> res = new List<FieldInfo>();

        while (t != null)
        {
            foreach (FieldInfo fi in t.GetFields(BindingFlags.NonPublic | BindingFlags.Instance))
                if (!fi.IsLiteral)
                    res.Add(fi);
            t = t.BaseType;
        }

        return res;
    }

    private static void CopyToCustomMetadata(ModelMetadata baseMetadata, ArtifyModelMetadata customMetadata)
    {
        foreach (Tuple<FieldInfo, FieldInfo> t in _fieldsMap)
            t.Item1.SetValue(customMetadata, t.Item2.GetValue(baseMetadata));
    }

    protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName)
    {
        ArtifyModelMetadata modelMetadata = new ArtifyModelMetadata(this, containerType, modelAccessor, modelType, propertyName);
        CopyToCustomMetadata(base.CreateMetadata(attributes, containerType, modelAccessor, modelType, propertyName), modelMetadata);

        ModelMetadataAttribute mma;
        Dictionary<string, string> htmlAttributes;
        object tmp;
        foreach (Attribute a in attributes)
        {
            mma = a as ModelMetadataAttribute;
            if (mma != null)
            {
                mma.Process(modelMetadata);
                htmlAttributes = mma.GetAdditionnalHtmlAttributes();

                if (htmlAttributes != null)
                {
                    foreach (KeyValuePair<string, string> p in htmlAttributes)
                    {
                        tmp = null;
                        tmp = modelMetadata.AdditionnalHtmlAttributes.TryGetValue(p.Key, out tmp);
                        if (tmp == null)
                            modelMetadata.AdditionnalHtmlAttributes.Add(p.Key, p.Value);
                        else
                            modelMetadata.AdditionnalHtmlAttributes[p.Key] = tmp.ToString() + " " + p.Value;
                    }
                }
            }
            if (mma is TooltipAttribute)
                modelMetadata.HasToolTip = true;
        }

        return modelMetadata;
    }
}

public class ArtifyModelMetadata : DataAnnotationsModelMetadata
{

    public bool HasToolTip { get; internal set; }

    public Dictionary<string, object> AdditionnalHtmlAttributes { get; private set; }

    public ArtifyModelMetadata(DataAnnotationsModelMetadataProvider provider, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName)
        : base(provider, containerType, modelAccessor, modelType, propertyName, null)
    {
        AdditionnalHtmlAttributes = new Dictionary<string, object>();
    }
}

And if you want a generic solution to get base class fields in your derived one, and you can't just use inheritance because you're in the same kind of situation I was, just use this class:

public abstract class GenericBaseCopy<Src, Dst> where Dst : Src
{
    private static List<Tuple<FieldInfo, FieldInfo>> _fieldsMap;

    static GenericBaseCopy()
    {
        _fieldsMap = new List<Tuple<FieldInfo, FieldInfo>>();
        foreach (FieldInfo customFI in GetAllFields(typeof(Dst)))
            foreach (FieldInfo baseFI in GetAllFields(typeof(Src)))
                if (customFI.Name == baseFI.Name)
                    _fieldsMap.Add(new Tuple<FieldInfo, FieldInfo>(customFI, baseFI));
    }

    private static List<FieldInfo> GetAllFields(Type t)
    {
        List<FieldInfo> res = new List<FieldInfo>();

        while (t != null)
        {
            foreach (FieldInfo fi in t.GetFields(BindingFlags.NonPublic | BindingFlags.Instance))
                if (!fi.IsLiteral)
                    res.Add(fi);
            t = t.BaseType;
        }

        return res;
    }

    public static void Copy(Src baseClassInstance, Dst dstClassInstance)
    {
        foreach (Tuple<FieldInfo, FieldInfo> t in _fieldsMap)
            t.Item1.SetValue(dstClassInstance, t.Item2.GetValue(baseClassInstance));
    }
}
查看更多
倾城 Initia
3楼-- · 2019-05-15 03:07

I suggest you to take a look at MvcExtensions: http://mvcextensions.github.io/

One of it's main part is exactly what you are doing - extensive model metadata configuration/ usage. You may find a lot of answers there or simply take it as a "ready to use" solution.

查看更多
登录 后发表回答