Click here to Skip to main content
15,893,368 members
Articles / Web Development

Provide Html.LabelFor Value for Model Fields from Database at Runtime

Rate me:
Please Sign up or sign in to vote.
5.00/5 (2 votes)
7 Nov 2012CPOL5 min read 31.4K   266   6  
Using ModelMetadataProvider to Set Display Name Metadata for the Model Fields at Runtime

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: 

C#
[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.

C#
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.

C#
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

C#
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:

  1. Generalize DisplayNameMetadataProvider so that it does not depend on the typed database context
  2. Wrap it by a class library in order to make it more reusable
  3. 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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
Iran (Islamic Republic of) Iran (Islamic Republic of)
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
-- There are no messages in this forum --