MVC and Entity Framework Html.DisplayNameFor with Composite ViewModel

Dan P picture Dan P · Apr 12, 2013 · Viewed 23.2k times · Source

I’m fairly comfortable with MVVM using WPF/Silverlight but this is my first attempt at an MVC Web Application…just an fyi for my background.

I’ve created a controller called TestSitesController which was auto generated from from the “Site” model class in my Entity Framework Model (the template that generates the read/write actions and views). The only thing I modified was in 3 spots there was a default parameter of Guid id = null for some methods. I just got rid of the “ = null” all works fine. Here is an example of what I changed

public ActionResult Delete(Guid id = null)
{
    //....
}

This was changed to

public ActionResult Delete(Guid id)
{
    //....
}

The Site model is nothing special SiteId, Abbreviation, and DisplayName…trying to keep it as simple as possible for this question. Ok so I run the website and goto htpp://.../TestSites/ and everything works perfectly.

I noticed that all of my views (Create, Delete, Details and Edit) are using an @model MVCWeb.MyEntities.Site which I’m perfectly ok with for now; but in the Index.cshtml view I noticed it was using an

@model IEnumerable<MVCWeb.MyEntities.Site>

This works fine for the generated template but I would like to use a “Composite View Model” and maybe this is where I’m trying to mix in my MVVM knowledge but would to stick with that if all possible. In my mind a Composite View Model is just a Model that is specific towards a view that is composed of 1 or more Entity Models along with additional properties like SelectedSiteId, etc.

So I created a very simple ViewModel called TestSitesViewModel

public class TestSitesViewModel
{
    //Eventually this will be added to a base ViewModel to get rid
    //of the ViewBag dependencies
    [Display(Name = "Web Page Title")]
    public string WebPageTitle;

    public IEnumerable<MVCWeb.MyEntities.Site> Sites;

    //Other Entities, IEnumberables, or properties go here
    //Not important for this example
}

Then in my controller I added an action method called IndexWithViewModel

public ActionResult IndexWithViewModel()
{
    var vm = new TestSitesViewModel();
    vm.WebPageTitle = "Sites With Composite View Model";
    vm.Sites = db.Sites.ToList();

    return View(vm);
}

I then made a copy of the Index.cshtml and named it IndexWithModel.cshtml to match my new ActionResult method name. I changed the top line of

@model IEnumerable<MVCWeb.MyEntities.Site>

To

@model MVCWeb.Models.TestSitesViewModel

I added this before the table section to test for the DisplayNameFor and the DisplayFor

@Html.DisplayNameFor(model => model.WebPageTitle)
&nbsp;
@Html.DisplayFor(model => model.WebPageTitle)
<br />

Changed the

@foreach (var item in Model) {

To

@foreach (var item in Model.Sites) {

And commented out the tr section that contains all of the table headers for now. Everything works perfectly except that the @Html.DisplayNameFor is displaying the variable name of “WebPageTitle” instead of using “Web Page Title” as denoted in the Display attribute of the Data Annotation in

[Display(Name = "Web Page Title")]
public string WebPageTitle;

Also if I comment back in the tr section that contains the table header information. I can not for the life of me figure out what to put in I’ve tried model.Sites.Abbreviation, model.Sites[0].Abbreviation, and various other combinations but get errors.

<tr>
    <th>
        @Html.DisplayNameFor(model => model.Abbreviation)
    </th>
    <th>
        @Html.DisplayNameFor(model => model.DisplayName)
    </th>
    <th></th>
</tr>

So what should I use there? I'm not quite sure why model.Sites.Abbreviation doesn't work as Sites is the exact same type that was used as the model in the original Index.cshtml

Answer

Dan P picture Dan P · Apr 12, 2013

I finally figured this out after messing around for several hours.

1st problem in order to use the Display arrtibute in the Data Anotations a get and set acessor methods must be added even if they are only the default ones. I'm assuming this is because they must only work on properties and not public data members of a class. Here is code to get the

@Html.DisplayNameFor(model => model.WebPageTitle)

to work correctly

public class TestSitesViewModel
{
    //Eventually this will be added to a base ViewModel to get rid of
    //the ViewBag dependencies
    [Display(Name = "Web Page Title")]
    public string WebPageTitle { get; set; }

    //PUBLIC DATA MEMBER WON'T WORK
    //IT NEEDS TO BE PROPERTY AS DECLARED ABOVE
    //public string WebPageTitle;

    public IEnumerable<MVCWeb.MyEntities.Site> Sites;

    //Other Entities, IEnumberables, or properties go here
    //Not important for this example
}

2nd part of problem was accessing the Metadata in an IEnumerable variable. I declared temporary variable called headerMetadata and used it instead of trying to access the properties through the IEnumerable

@{var headerMetadata = Model.Sites.FirstOrDefault();}
<tr>
    <th>            
        @Html.DisplayNameFor(model => headerMetadata.Abbreviation)
    </th>
    <th>
        @Html.DisplayNameFor(model => headerMetadata.DisplayName)
    </th>
    ...

This does beg the question of when does headerMetadata variable go out of scope? Is it available for the entire view or is it limited to the html table tags? I suppose that's another question for another date.