Introduction
During the development of my first MVC application I implemented a method to store the text of the labels for each field of entity framework model of the database, so that I could easily change the labels without any compilation and any single field has a unified label in all the data entry forms and grid column headers. This approach is very useful especially in the database first development method when it's very hard and time-consuming task to define a DisplayName attribute for all the fields.
To do this I derived a new from DataAnnotationsModelMetadataProvider
class, which – as its name suggests – is responsible for providing data annotations for model at runtime. I have named this class DisplayNameMetadataProvider
, and it uses a database table to find corresponding label for each model field.
Background
As far as I know there are 2 other ways to define label texts for model fields; a traditional method and "Compile-time DisplayName attribute" method. In traditional method, a text is assigned as the label for each model field in the HTML pages. Although this is the easiest method to use, it is the hardest to maintain when a single model field appears in more than one page; you should be aware of what pages the field has appeared into and change its label in all of them.
In "Compile-time DisplayName attribute" method, we assign a DisplayName attribute to each field of our model and then use Html.LabelFor in our HTML pages to show it. This method is very useful when we use Code First approach in the development time and create model classes manually by coding; this way we easily add DisplayName
attribute to our field without any hesitation. Although we have to compile the project each time we make changes to this attribute but it's not that annoying.
However when you don't create the classes by coding, either in code first or in database first approach, using this method is somehow tricky and have some drawbacks. In these occasions, you have to use the "partiality" of the model classes generated by code generator and MetadataTypeAttribute
to assign DisplayName to the fields.
For instance, suppose you have a model class named "Student
" which has "FName
" and "LName
" fields; to add DisplayName
attribute to these fields you have to add the following code to file:
[MetadataTypeAttribute(typeof(Student.StudentMetadata))]
public partial class Student
{
internal sealed class StudentMetadata
{
[DisplayName("First Name")]
public string FName {get; set;}
[DisplayName("Last Name")]
public string LName {get; set;}
}
}
As you can see, this approach is very time consuming and also very hard to maintain since by almost any change made to model the corresponding classes should get updated too.
How does DisplayNameMetadataProvider works?
I'm not going to go into details of how ModelMetadataProvider and its to gather metadata information for each field in the model. If you want to study more about this subject I suggest this article.
subclasses work, but in short I should say that these classes are used by MVC I chose DataAnnotationsModelMetadataProvider
as the base class because all the abstract methods of ModelMetadataProvider
and its descendant AssociatedMetadataProvider
are implemented by it, and all I had to do was overriding CreateMetadata
method.
When DataAnnotationsModelMetadataProvider
or one of its descendants is set as default model metadata provider for MVC, each time a model field is going to be used for the first time a call to CreateMetadata
is made. In return the metadata for the field is returned as an instance of ModelMetadata
class which has a property for each standard DataAnnotation
attributes. The following piece of code shows the CreateMetadata
method of DisplayNameMetadataProvider
.
protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName)
{
ModelMetadata metadata = base.CreateMetadata(attributes, containerType, modelAccessor, modelType, propertyName);
if (containerType == null)
return metadata;
TypeDisplayNameData displayNameMetaData = null;
if (attributes.OfType<DisplayNameAttribute>().Count() == 0)
{
displayNameMetaData = GetTypeDisplayNameData(containerType);
if (displayNameMetaData != null)
{
string displayNameValue = String.Empty;
if (displayNameMetaData.DisplayNames.TryGetValue(propertyName, out displayNameValue))
metadata.DisplayName = displayNameValue;
}
}
return metadata;
}
In the first line, this method calls its original counterpart and gets the default metadata for the model field specified by propertyName
parameter. Then checks weather DisplayName
attribute is assigned to it at compile time or not. If not, it calls GetTypeDisplayNameData
to get all the display names assigned to the container class of the field in the database, then looks through these names to find the display name for the field. If it can find a display name for the field, assigns it to DisplayName property of result. If CreateMetadata
could not find any display name for the given field, the default result from DataAnnotationsModelMetadataProvider
will get returned.GetTypeDisplayNameData method
As mentioned before, GetTypeDisplayNameData
is responsible for providing available display names for each model class. When called, this method first looks in a dictionary named FetchedDisplayNameData to check if display names for the given class have been fetched before. If they have been fetched, GetTypeDisplayNameData
return the display names without accessing the database. But if they have not, this method makes a call to FetchTypeDisplayNameData
which makes the actual access to the database, reads all the display names assigned to the given class and returns them. The result returned by FetchTypeDisplayNameData
is added to FetchedDisplayNameData
dictionary for next uses before returned to CreateMetadata
.
The code for FetchTypeDisplayNameData
method is shown below. I should point out that in first 4 lines of this method the method check if the given class is in my entity framework data model represented by Models.ContactsEntities
or not, and only when it is in the data model the method will try to fetch the display names for the class from database.
private SortedDictionary<string, string> FetchTypeDisplayNameData(Type containerType)
{
Models.ContactsEntities context = new Models.ContactsEntities();
System.Data.Metadata.Edm.MetadataWorkspace workspace = ((System.Data.Entity.Infrastructure.IObjectContextAdapter)context).ObjectContext.MetadataWorkspace;
System.Collections.ObjectModel.ReadOnlyCollection<System.Data.Metadata.Edm.GlobalItem> items = workspace.GetItems(System.Data.Metadata.Edm.DataSpace.OSpace);
if (items.OfType<System.Data.Metadata.Edm.EntityType>().Any(x => x.FullName == containerType.FullName))
{
var displayNames =
from displayName in context.FieldDisplayNames
where displayName.Namespace == containerType.Namespace
&& displayName.ClassName == containerType.Name
select displayName;
if (displayNames.Count() == 0)
return null;
else
{
var fieldDisplayNameDictionary = displayNames.ToDictionary<TestWebApp.Models.FieldDisplayName, string, string>(x => x.FieldName, x => x.DisplayName);
return new SortedDictionary<string, string>(fieldDisplayNameDictionary);
}
}
else
return null;
}
Database structure for DisplayNameMetadataProvider
As you can see in FetchTypeDisplayNameData
method, the display names are stored in a table named FieldDisplayNames
. The structure of this table is shown as an entity framework model in diagram below.
Using the code
A simple project which uses this provider is attached to this article. This project has only one entity, Contact, which stores some information about user's contacts. As you can see, there are four views working with this entity and all of them use Html.LabelFor
method to show label for the fields of it. You can change the label of Contact entity fields in FieldDisplayName table; you should remember that as MVC creates metadata for each field only one time you have to restart the web server after each change.
To use DisplayNameMetadataProvider
you have to add it to your project and change Models.ContactsEntities
to your own database context. Then you have to add FieldDisplayName table to your database as well. Finally you should tell MVC to use this provider as its default provider; to do this you have to add the following line to the end of Application_Start
method in Global.asax:
ModelMetadataProviders.Current = new DisplayNameMetadataProvider();
Possible Improvements
There are some possible improvements can be made to DisplayNameMetadataProvider
, to make it more general and usable, some of them are:
- Generalize
DisplayNameMetadataProvider
so that it does not depend on the typed database context - Wrap it by a class library in order to make it more reusable
DisplayNameMetadataProvider
has a great potential for getting used in localized applications; By applying some simple changes to it and its data structure, DisplayNameMetadataProvider
can be easily used in this kind of applications