A few months ago I blogged about using Model metadata to render a dynamic UI in MVC 2. The scenario in the post was that we might have a view model where the questions are conditionally displayed and therefore a dynamic UI is needed. To recap the previous post, the solution was to use a custom attribute called [QuestionId] in conjunction with an “ApplicableQuestions” collection to identify whether each question should be displayed. This allowed me to have a view model that looked like this: 1: [UIHint("ScalarQuestion")]
2: [DisplayName("First Name")]
3: [QuestionId("NB0021")]
4: public string FirstName { get; set; }
5:
6: [UIHint("ScalarQuestion")]
7: [DisplayName("Last Name")]
8: [QuestionId("NB0022")]
9: public string LastName { get; set; }
10:
11: [UIHint("ScalarQuestion")]
12: [QuestionId("NB0023")]
13: public int Age { get; set; }
14:
15: public IEnumerable<string> ApplicableQuestions { get; set; }
At the same time, I was able to avoid repetitive IF statements for every single question in my view:
1: <%: Html.EditorFor(m => m.FirstName, new { applicableQuestions = Model.ApplicableQuestions })%>
2: <%: Html.EditorFor(m => m.LastName, new { applicableQuestions = Model.ApplicableQuestions })%>
3: <%: Html.EditorFor(m => m.Age, new { applicableQuestions = Model.ApplicableQuestions })%>
by creating an Editor Template called “ScalarQuestion” that encapsulated the IF statement:
1: <%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>
2: <%@ Import Namespace="DynamicQuestions.Models" %>
3: <%@ Import Namespace="System.Linq" %>
4: <%
5: var applicableQuestions = this.ViewData["applicableQuestions"] as IEnumerable<string>;
6: var questionAttr = this.ViewData.ModelMetadata.ContainerType.GetProperty(this.ViewData.ModelMetadata.PropertyName).GetCustomAttributes(typeof(QuestionIdAttribute), true) as QuestionIdAttribute[];
7: string questionId = null;
8: if (questionAttr.Length > 0)
9: {
10: questionId = questionAttr[0].Id;
11: }
12: if (questionId != null && applicableQuestions.Contains(questionId)) { %>
13: <div>
14: <%: Html.Label("") %>
15: <%: Html.TextBox("", this.Model)%>
16: </div>
17: <% } %>
You might want to go back and read the full post in order to get the full context.
MVC 3 offers a couple of new features that make this scenario more elegant to implement. The first step is to use the new [AdditionalMetadata] attribute which, so far, appears to be an under appreciated new feature of MVC 3. With this attribute, I don’t need my custom [QuestionId] attribute anymore - now I can just write my view model like this:
1: [UIHint("ScalarQuestion")]
2: [DisplayName("First Name")]
3: [AdditionalMetadata("QuestionId", "NB0021")]
4: public string FirstName { get; set; }
5:
6: [UIHint("ScalarQuestion")]
7: [DisplayName("Last Name")]
8: [AdditionalMetadata("QuestionId", "NB0022")]
9: public string LastName { get; set; }
10:
11: [UIHint("ScalarQuestion")]
12: [AdditionalMetadata("QuestionId", "NB0023")]
13: public int Age { get; set; }
Thus far, the documentation seems to be pretty sparse on the AdditionalMetadata attribute. It’s buried in the Other New Features section of the MVC 3 home page and, after showing the attribute on a view model property, it just says, “This metadata is made available to any display or editor template when a product view model is rendered. It is up to you to interpret the metadata information.” But what exactly does it look like for me to “interpret the metadata information”? Well, it turns out it makes the view much easier to work with. Here is the re-implemented ScalarQuestion template updated for MVC 3 and Razor:
1: @{
2: object questionId;
3: ViewData.ModelMetadata.AdditionalValues.TryGetValue("QuestionId", out questionId);
4: if (ViewBag.applicableQuestions.Contains((string)questionId)) {
5: <div>
6: @Html.LabelFor(m => m)
7: @Html.TextBoxFor(m => m)
8: </div>
9: }
10: }
So we’ve gone from 17 lines of code (in the MVC 2 version) to about 7-8 lines of code here. The first thing to notice is that in MVC 3 we now have a property called “AdditionalValues” that hangs off of the ModelMetadata property. This is automatically populated by any [AdditionalMetadata] attributes on the property. There is no more need for me to explicitly write Reflection code to GetCustomAttributes() and then check to see if those attributes were present. I can just call TryGetValue() on the dictionary to see if they were present. Secondly, the “applicableQuestions” anonymous type that I passed in from the calling view – in MVC 3 I now have a dynamic ViewBag property where I can just “dot into” the applicableQuestions with a nicer syntax than dictionary square bracket syntax. And there’s no problems calling the Contains() method on this dynamic object because at runtime the DLR has resolved that it is a generic List<string>.
At this point you might be saying that, yes the view got much nicer than the MVC 2 version, but my view model got slightly worse. In the previous version I had a nice [QuestionId] attribute but now, with the [AdditionalMetadata] attribute, I have to type the string “QuestionId” for every single property and hope that I don’t make a typo. Well, the good news is that it’s easy to create your own attributes that can participate in the metadata’s additional values. The key is that the attribute must implement that IMetadataAware interface and populate the AdditionalValues dictionary in the OnMetadataCreated() method:
1: public class QuestionIdAttribute : Attribute, IMetadataAware
2: {
3: public string Id { get; set; }
4:
5: public QuestionIdAttribute(string id)
6: {
7: this.Id = id;
8: }
9:
10: public void OnMetadataCreated(ModelMetadata metadata)
11: {
12: metadata.AdditionalValues["QuestionId"] = this.Id;
13: }
14: }
This now allows me to encapuslate my “QuestionId” string in just one place and get back to my original attribute which can be used like this: [QuestionId(“NB0021”)].
The [AdditionalMetadata] attribute is a powerful and under-appreciated new feature of MVC 3. Combined with the dynamic ViewBag property, you can do some really interesting things with your applications with less code and ceremony.