Using ViewModel information in an ASP.NET MVC 2 Editor or Display template
Editor and Display templates are a great new feature in ASP.NET MVC 2. They allow you to define a template which will be used for any datatype you’d like, and they can be set per-controller or site-wide.
The common scenario you may have seen is to set up a jQueryUI datapicker control for all DateTime editors site-wide. It’s really easy to set up – we create a /Views/Shared/DateTime.ascx file and add the following:
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<dynamic>" %> <%= Html.TextBox("", ViewData.TemplateInfo.FormattedModelValue, new { @class = "text-box single-line" }) %> <script type="text/javascript"> $(function () { $("#<%: ViewData.TemplateInfo.GetFullHtmlFieldId(String.Empty) %>").datepicker(); }); </script>
Then we add the references to our jQueryUI js & css in our Site.master (or on pages as appropriate) and magically all our DateTime fields get datepickers.
Templates for custom objects
That’s great, but one of the more powerful uses of MVC2 templating is to create display or editor templates for your own business objects. For example, if you have a site that sells disco balls, you could create a discoball.ascx display template and use it whenever you display a discoball object (and of course, the same for editing as well).
I demonstrated an example of that in the MVC Music Store – there’s an Editor template for an Album, and it’s used in both the Edit and Create views.
The problem: DropDowns require ViewModels
Many editor templates will require a ViewModel, since your model object won’t contain all the possible values for dropdowns. For example here’s the Edit template for an Album:
The Album table holds an ArtistId and a GenreId, which link to the Artist and Genre tables. That means that our Album model knows about an ArtistId, but doesn’t have a list of all the Artists necessary to display in the dropdown. To do that, we need to introduce a ViewModel, which contains all the information our View will need:
using System.Collections.Generic; using MvcMusicStore.Models; namespace MvcMusicStore.ViewModels { public class StoreManagerViewModel { public Album Album { get; set; } public List<Artist> Artists { get; set; } public List<Genre> Genres { get; set; } } }
Note: This is a simple usage of a ViewModel. There are several usage patterns for ViewModels, and more advanced ViewModel patterns dictate that you never pass your domain entities to your view. That’s a good topic for another post, but worth mentioning in passing here.
Our StoreManagerController.Create() action can then return a ViewModel which holds an empty Album object, along with lists of Artists and Genres to be populated:
// // GET: /StoreManager/Create public ActionResult Create() { var viewModel = new StoreManagerViewModel { Album = new Album(), Genres = storeDB.Genres.ToList(), Artists = storeDB.Artists.ToList() }; return View(viewModel); }
The Problem: How do we pass additional information to a Template?
Until the RTM release of MVC 2, the above scenario wouldn’t work. You just couldn’t get at the additional ViewModel information – in this case, the Album and Genre lists – from your template files. There was no way to pass the information from your view to your template, which meant that you had to resort to stuffing the additional information into ViewData:
// // GET: /StoreManager/Create public ActionResult Create() { var album = new Album(); // NOTE: Example! Don't copy / paste / deploy! // Not the best way to do this! ViewData["Genres"] = storeDB.Genres.ToList(); ViewData["Artists"] = storeDB.Artists.ToList(); return View(album); }
But I avoid ViewData as much as possible. It’s a weakly typed collection, and I really dislike returning a strongly typed object to the View and then sneakily passing additional information via ViewData.
The Solution: Templated Helper overrides that pass additional View Data
MVC 2 RTM included a nice new feature to solve this exact problem. From the MVC 2 RTM release notes:
Templated Helpers Allow You to Specify Extra View Data
ASP.NET MVC 2 now includes new overloads of the EditorFor and DisplayFor methods. These overloads contain a parameter that accepts an anonymous object that can be used to provide extra view data. The view data provided in this parameter is merged with any existing view data that is passed to the template.
Let’s look at the implementation in MVC 2 Source Code in System.Web.Mvc.Html.TemplateHelpers.TemplateHelper():
if (additionalViewData != null) { foreach (KeyValuePair<string, object> kvp in new RouteValueDictionary(additionalViewData)) { viewData[kvp.Key] = kvp.Value; } }
That’s pretty straightforward – the common method used by both EditorTemplates and DisplayTemplates checks for additional view data and copies it to ViewData keyed with the name we provided. So, yes, it’s still using ViewData, but at least our Controller and View don’t need to know that.
Putting it all together
The View (/Views/StoreManager/Create.aspx)
We’ll use one of the EditorFor() overrides which allows passing additional view data. Note that these overloads use anonymous objects as dictionaries. This pattern is pretty common throughout the ASP.NET MVC framework, since it allows for very terse key-value pair definitions.
<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<MvcMusicStore.ViewModels.StoreManagerViewModel>" %> <asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server"> Create Album </asp:Content> <asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server"> <h2>Create</h2> <% Html.EnableClientValidation(); %> <% using (Html.BeginForm()) {%> <fieldset> <legend>Create Album</legend> <%: Html.EditorFor(model => model.Album, new { Artists = Model.Artists, Genres = Model.Genres })%> <p> <input type="submit" value="Save" /> </p> </fieldset> <% } %> <div> <%:Html.ActionLink("Back to Albums", "Index") %> </div> </asp:Content>
The Editor Template (/Views/Shared/EditorTemplates/Album.ascx)
This editor template is strongly typed to the Album, but is making use of the additional view data for the Html.DropDownList() calls.
<%@ Import Namespace="MvcMusicStore"%> <%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<MvcMusicStore.Models.Album>" %> <script src="/Scripts/MicrosoftAjax.js" type="text/javascript"></script> <script src="/Scripts/MicrosoftMvcAjax.js" type="text/javascript"></script> <script src="/Scripts/MicrosoftMvcValidation.js" type="text/javascript"></script> <p> <%: Html.LabelFor(model => model.Title)%> <%: Html.TextBoxFor(model => model.Title)%> <%: Html.ValidationMessageFor(model => model.Title)%> </p> <p> <%: Html.LabelFor(model => model.Price)%> <%: Html.TextBoxFor(model => model.Price)%> <%: Html.ValidationMessageFor(model => model.Price)%> </p> <p> <%: Html.LabelFor(model => model.AlbumArtUrl)%> <%: Html.TextBoxFor(model => model.AlbumArtUrl)%> <%: Html.ValidationMessageFor(model => model.AlbumArtUrl)%> </p> <p> <%: Html.LabelFor(model => model.Artist)%> <%: Html.DropDownList("ArtistId", new SelectList(ViewData["Artists"] as IEnumerable, "ArtistId", "Name", Model.ArtistId))%> </p> <p> <%: Html.LabelFor(model => model.Genre)%> <%: Html.DropDownList("GenreId", new SelectList(ViewData["Genres"] as IEnumerable, "GenreId", "Name", Model.GenreId))%> </p>
We can continue to use the ViewModel and Controller Action code I showed earlier (repeated here for clarity):
StoreManagerViewModel.cs:
using System.Collections.Generic; using MvcMusicStore.Models; namespace MvcMusicStore.ViewModels { public class StoreManagerViewModel { public Album Album { get; set; } public List<Artist> Artists { get; set; } public List<Genre> Genres { get; set; } } }
StoreManagerController.cs - Create():
// // GET: /StoreManager/Create public ActionResult Create() { var viewModel = new StoreManagerViewModel { Album = new Album(), Genres = storeDB.Genres.ToList(), Artists = storeDB.Artists.ToList() }; return View(viewModel); }