18

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

Dan P
  • 1,939
  • 3
  • 17
  • 30

2 Answers2

23

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.

Dan P
  • 1,939
  • 3
  • 17
  • 30
  • 1
    The getter/setter are required because [fields and properties are handled differently by Reflection](http://stackoverflow.com/a/653799/957950). The scope of `headerMetadata` is the remainder of entire block it is declared within, just like the rest of C#, so either the for-loop or the rest of the view. – brichins Sep 30 '15 at 19:08
1

I'll take a stab with my newly learned MVVM ASP.NET MVC experience.

Instead of calling @Html.DisplayNameFor(model => model.WebPageTitle), try using @Html.DisplayFor(model => model.WebPageTitle). You appear to be setting the WebPageTitle value in your IndexWithViewModel controller which should show up in your view.

Look into shared templates for ASP.NET MVC. You can set up custom templates or partials for DisplayFor/EditorFor helpers at /Views/Shared/EditorTemplates/Site.cshtml and /Views/Shared/DisplayTemplates/Site.cshtml. ASP.NET MVC will automatically pick up on those templates when rendering your Site object with DisplayFor and EditorFor.

Good luck!

Community
  • 1
  • 1
Brett
  • 316
  • 2
  • 9
  • Instead of calling @Html.DisplayNameFor(model => model.WebPageTitle), try using @Html.DisplayFor(model => model.WebPageTitle). I am using both. The DisplayNameFor should give me the friendly label used in the [Display(Name = "Web Page Title")] Data Anotation. The DisplayFor is working properly and is giving me the value of model.WebPageTitle. This code is working fine except that the DisplayNameFor is giving me back the variable name "WebPageTitle" and not "Web Page Title". I can't get the DisplayNameFor to work at all for the IEnumerable of Sites at all – Dan P Apr 12 '13 at 18:20
  • Ah gotcha. I usually end up using a label helper with a custom display for this type of thing. `@Html.LabelFor(model => model.WebPageTitle, "Web Page Title")` – Brett Apr 12 '13 at 18:26
  • That works for the model.WebPateTitle if I type out "Web Page Title" every time I use LabelFor but I still don't know how to get it to use the value declared with the Data Anotation in [Display(Name = "Web Page Title")]. I'd like to be able to change this easily in one spot...it's the whole point of using the Display attribute and Data Anotations. Any idea how to get that to work? Again this also apply's to the IEnumerable of Sites. It's just funny that it works with generated code but not when I put it in a composite view model. – Dan P Apr 12 '13 at 18:42
  • Also as for the share templates and partial views I'm trying to keep it as simple as possible while learning and figured easiest way for now would be to put two models into one "Composite ViewModel" this practice is pretty common in the MVVM world. – Dan P Apr 12 '13 at 18:44
  • Hmm... Yeah, that's a bit strange. Not sure why those aren't rendering correctly. Another option is to use Razor's sections/layouts. I've used those in the past for populating meta elements and other things that have model specific values, but need to show in the layout. – Brett Apr 12 '13 at 18:52
  • 2
    Update...when I added a get; set; default accessor methods the problem for @Html.DisplayNameFor(model => model.WebPageTitle) worked correctly. I'll do some more playing around with the IEnumerable property to see if I can't get that to work. I changed [Required, Display(Name = "Web Page Title")] public string WebPageTitle; to [Required, Display(Name = "Web Page Title")] public string WebPageTitle { get; set; } That fixed the DisplayNameFor problem for that. – Dan P Apr 12 '13 at 19:57