ASP.NET MVC ViewData.Eval() method

While digging deeper into MVC Views, I stumbled on this method – ViewData.Eval(). Found it interesting and researched and played with some code around this method.

This method gives the user a way to search through the ViewData’s object graph. So in order to get the data, you can either do

   1:  <%= ViewData["Message"] %>

or

   1:  <%= ViewData.Eval("Message") %>

Either line gives the same answer. The key passed, “Message” is case-insensitive => “Message” = “message” = “mEssage” and so on.

You can walk through the graph using the ‘.’ (dot) syntax. So you can do

   1:  ViewData["employee"] = new Employee{ Name = "ScottGu"};
   2:   
   3:  <%= ViewData.Eval("Employee.Name") %>

MVC recursively loops through the graph looking for the whole key first and then removing one token at a time. So in the above case, it looks for the whole string ‘Employee.Name’ first, which returns a null. Then it tries to search the graph for ‘Employee’, which it does find and then it looks for ‘Name’ in the Employee graph.

It first checks in the ViewData’s dictionary and then tries to find it in the ViewData.Model’s properties. To test this I created a class called Software to represent my model.

   1:  public class Software
   2:  {
   3:      public string Name { get; set; }
   4:      public string Version { get; set; }
   5:      public string Company { get; set; }
   6:  }

In the HomeController’s Index action, I added:

   1:  ViewData["software"] = new Software
   2:                             {
   3:                                 Name = "MVC",
   4:                                 Version = "2",
   5:                                 Company = "MSFT",
   6:                             };
   7:  ViewData["software.name"] = "test";

And accordingly, in the View, the code looks like:

   1:  <%= ViewData.Eval("software.name") %>

When you run the app, you’ll see ‘test’ getting rendered for the above line instead of ‘MVC’, which, in this case, also maps to ‘software.name’ in the ViewData’s object graph. Remove line 7 from the above code and you’ll see ‘MVC’ gets rendered. This proves the first point. Although it’s very rare to hit this situation, it is good to know.

You can also use Eval to fill-in values for controls in an Edit screen. Here's a very interesting read regarding this from Stephen Walther. In this article, Stephen says “When you need to repopulate the form data in an edit form, displaying both valid and invalid values, use ViewData.Eval() to retrieve the values from both the view data dictionary and the view data Model.

It’s interesting to know that there is no support for finding indexed items in an array through the Eval method. So if your controller has something like below:

   1:  List<Software> softwares = new List<Software>();
   2:  softwares.Add(new Software
   3:                    {
   4:                        Name = "ASP.NET MVC",
   5:                        Version = "2",
   6:                        Company = "MSFT"
   7:                    });
   8:   
   9:  softwares.Add(new Software
  10:                    {
  11:                        Name = "C#",
  12:                        Version = "4.0",
  13:                        Company = "MSFT",
  14:                    });
  15:   
  16:  ViewData["Softwares"] = softwares;
  17:   
  18:  // and your view said the following
  19:  <%= ViewData.Eval("softwares[0].name") %>
  20:  // this returns null

I did a search to see if Eval could handle indexes and I hit one interesting link. According to Phil (sounds like According to Jim) Haack, Eval does not support index syntax and he suggests to try:

   1:  <%= ViewData.Eval("softwares.0.name") %>

This did not work for me. I even looked at the source code and there’s nothing in there that can catch the ‘.0’ and enumerate through the ‘softwares’ collection and pull out the first item (index = 0). And also, this syntax will not come instinctively to either a C# or a VB.net developer.

I decided to see if I could add that functionality to the Eval method. Since MVC is Open Source, you have the ability to look and modify (at your own risk) the code. (See my article Custom 404 when no route matches, where I’ve modified the MVC source code before.)

I landed in the ViewDataDictionary.cs class (the other class with a very similar name takes a generic parameter – ignore that). In this class, there is an internal class named ‘ViewDataEvaluator’ which defines the Eval method. The first thing I did was to move this into a .cs file of its own, just to add more clarity. Well, it does not make sense for me to go through all the lines of code in this class, but the method that caught my attention was the GetIndexedPropertyValue. It’s default implementation looks like this:

   1:  private static object GetIndexedPropertyValue(object indexableObject, string key) {
   2:      Type indexableType = indexableObject.GetType();
   3:   
   4:      ViewDataDictionary vdd = indexableObject as ViewDataDictionary;
   5:      if (vdd != null) {
   6:          return vdd[key];
   7:      }
   8:   
   9:      MethodInfo containsKeyMethod = indexableType.GetMethod("ContainsKey", 
  10:                  BindingFlags.Public | BindingFlags.Instance, 
  11:                  null, 
  12:                  new Type[] { typeof(string) }, 
  13:                  null);
  14:      if (containsKeyMethod != null) {
  15:          if (!(bool)containsKeyMethod.Invoke(indexableObject, new object[] { key })) {
  16:              return null;
  17:          }
  18:      }
  19:   
  20:      PropertyInfo info = indexableType.GetProperty("Item", 
  21:                  BindingFlags.Public | BindingFlags.Instance, 
  22:                  null, 
  23:                  null, 
  24:                  new Type[] { typeof(string) }, 
  25:                  null);
  26:      if (info != null) {
  27:          return info.GetValue(indexableObject, new object[] { key });
  28:      }
  29:   
  30:      PropertyInfo objectInfo = indexableType.GetProperty("Item", 
  31:                  BindingFlags.Public | BindingFlags.Instance, 
  32:                  null, 
  33:                  null, 
  34:                  new Type[] { typeof(object) }, 
  35:                  null);
  36:      if (objectInfo != null) {
  37:          return objectInfo.GetValue(indexableObject, new object[] { key });
  38:      }
  39:      return null;
  40:  }

With a little hassle, I was (partially) able to make the Eval method support indexing. Replace the below code in place of the code between lines 4 through 7 of the above snippet.

   1:  ViewDataDictionary vdd = indexableObject as ViewDataDictionary;
   2:  if (vdd != null)
   3:  {
   4:      // the below code helps to decode keys like: softwares[0].name
   5:      // there's no in-built support for indexing in the Eval method
   6:      // this 'detour' checks for indexes in keys 
   7:      // and returns the appropriate object value
   8:      // it is worth mentioning that the indexing
   9:      // is done only on the first token 
  10:      // – ‘softwares’ token as per this example
  11:   
  12:      // check if the key has '[' and it does not have any other tokens 
  13:      // i.e., key = softwares[0] and not like key = softwares[0].name
  14:      if (key.Contains("[") && !key.Contains("."))
  15:      {
  16:          // get the actual key (softwares, in this case)
  17:          string actualKey = key.Substring(0, key.IndexOf("["));
  18:   
  19:          // store the index value
  20:          int index = int.Parse(key.Replace(actualKey, "").Replace("[", "").Replace("]", ""));
  21:   
  22:          // get the collection of vdd[actualKey]
  23:          var objectList = vdd[actualKey];
  24:   
  25:          if(objectList != null)
  26:          {
  27:              // make sure object can be enumerated
  28:              if(objectList is IEnumerable)
  29:              {
  30:                  int counter = 0;
  31:                  // create a dummy return item object
  32:                  // this avoids checking if index is out of bounds of objectList
  33:                  // if index > number of items in objectList
  34:                  // then return null be default
  35:                  object returnItem = null;
  36:   
  37:                  foreach (var item in (IEnumerable)objectList)
  38:                  {
  39:                      // return the item that matches the index
  40:                      if(index == counter)
  41:                      {
  42:                          returnItem = item;
  43:                          break;
  44:                      }
  45:                      // increment the counter to check 
  46:                      // against the next index
  47:                      counter++;
  48:                  }
  49:                  return returnItem;
  50:              }
  51:          }
  52:      }
  53:      else
  54:      {
  55:          return vdd[key];
  56:      }
  57:  }

I believe, I’ve added enough comments to make the code easy to understand and so I will not go through it line by line. But there’s one thing I’d like to repeat. This code works only if the first token needs to be indexed. In our case, this means we’ll be able to retrieve the value for ‘softwares[0].name’. It will not work for something like ‘softwares[1].previousversions[0].name’ (if previousversions was some collection with a property called ‘name’ in the ‘Software’ class).

All coding/testing for this article has been done on VS2008 and MVC1. I wanted to test this on VS2010 with MVC2, but then again, Phil Haack says, ‘running ASP.NET MVC 2 Beta on VS10 Beta 2 is not supported’. I also did go through the Release Notes and I did not see any changes for the ViewData.Eval method. So hopefully the above article holds good for MVC 2 as well. Happy to be proven wrong.

Published Thursday, November 26, 2009 12:39 AM by nmarun
Filed under: ,

Comments

No Comments

Leave a Comment

(required) 
(required) 
(optional)
(required)