How can I extend DataAnnotationsModelMetadata
I have a custom DataAnnotationsModelMetadataProvider
one and I would like to return an object obtained from ModelMetadata
in order to have additional properties in my razor templates.
So far, my custom provider is only overriding the function CreateMetadata
:
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 action (in fact it only adds AdditionalValues
)
But since almost all of my attributes basically add attributes to the html elements that I generate in my razor template, I would like the ModelMetadata
view to contain the dictionnary attributes that I want to add (and some other stuff).
I need a class that inherits from DataAnnotationsModelMetadata
.
I can't just call the function base.CreateMetadata
as it won't be correctly applied to my derived class.
I was thinking about making a copy of the public properties DataAnnotationsModelMetadata
returned by the function base.CreateMetadata
into my derived class, but I might lose information to make it look unsafe.
Another way I thought was to copy / paste the base code CreateMetadata
and add my logic, but that seems ugly ... (and I only have mvc3 sources, so it could have been changed in mvc4)
Also thought about inheriting from ViewDataDictionnary
so that I can provide my own metadata class instead of the default one, but I don't know how. (also I admit that I didn't know much about this particular issue)
I have looked through a lot of articles about DataAnnotations
and suppliers but could not find something similar to what I am trying to do.
So what are my options? In what direction can I look to get closer to what I want to do?
EDIT:
I looked at this question (pretty similar): Can I get a "copy constructor" in C # that copies from a derived class?
but what it does is copy the properties and I wanted to avoid that. The last answer in this post has something about PopulateMetadata
, but I cannot find this feature in the underlying provider ...
source to share
After some thinking, I actually found a solution that doesn't need to be used MvcExtensions
and copies all the information contained in the base class to the derived one.
Since DataAnnotationsModelMetadata
its base classes do nothing, but initialize some private or protected values, there is no risk of side effects for this.
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 the base class fields in your derived, and you can't just use inheritance because you're in the same situation I was in, 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));
}
}
source to share
I suggest you take a look at MvcExtensions
: http://mvcextensions.github.io/
One of the main parts is exactly what you are doing - extensive configuration / use of model metadata. You may find many answers there, or just accept this as a "turnkey" solution.
source to share