views:

3325

answers:

4

I am trying to implement an Edit ViewModel for my Linq2SQL entity called Product. It has a foreign key linked to a list of brands.

Currently I am populating the brand list via ViewData and using DropDownListFor, thus:

<div class="editor-field">
    <%= Html.DropDownListFor(model => model.BrandId, (SelectList)ViewData["Brands"])%>
    <%= Html.ValidationMessageFor(model => model.BrandId) %>
</div>

Now I want to refactor the view to use a strongly typed ViewModel and Html.EditorForModel():

<% using (Html.BeginForm()) {%>
    <%= Html.ValidationSummary(true) %>

    <fieldset>
        <legend>Fields</legend>

        <%=Html.EditorForModel() %>

        <p>
            <input type="submit" value="Save" />
        </p>
    </fieldset>

<% } %>

In my Edit ViewModel, I have the following:

public class EditProductViewModel
{
    [HiddenInput]
    public int ProductId { get; set; }

    [Required()]
    [StringLength(200)]
    public string Name { get; set; }

    [Required()]
    [DataType(DataType.Html)]
    public string Description { get; set; }

    public IEnumerable<SelectListItem> Brands { get; set; }

    public int BrandId { get; set; }

    public EditProductViewModel(Product product, IEnumerable<SelectListItem> brands)
    {
        this.ProductId = product.ProductId;
        this.Name = product.Name;
        this.Description = product.Description;
        this.Brands = brands;
        this.BrandId = product.BrandId;
    }
}

The controller is setup like so:

public ActionResult Edit(int id)
{
    BrandRepository br = new BrandRepository();

    Product p = _ProductRepository.Get(id);
    IEnumerable<SelectListItem> brands = br.GetAll().ToList().ToSelectListItems(p.BrandId);

    EditProductViewModel model = new EditProductViewModel(p, brands);

    return View("Edit", model);
}

The ProductId, Name and Description display correctly in the generated view, but the select list does not. The brand list definitely contains data.

If I do the following in my view, the SelectList is visible:

<% using (Html.BeginForm()) {%>
    <%= Html.ValidationSummary(true) %>

    <fieldset>
        <legend>Fields</legend>

        <%=Html.EditorForModel() %>

        <div class="editor-label">
            <%= Html.LabelFor(model => model.BrandId) %>
        </div>
        <div class="editor-field">
            <%= Html.DropDownListFor(model => model.BrandId, Model.Brands)%>
            <%= Html.ValidationMessageFor(model => model.BrandId) %>
        </div>
        <p>
            <input type="submit" value="Save" />
        </p>
    </fieldset>

<% } %>

What am I doing wrong? Does EditorForModel() not generically support the SelectList? Am I missing some kind of DataAnnotation?

I can't seem to find any examples of SelectList usage in ViewModels that help. I'm truly stumped. This answer seems to be close, but hasn't helped.

A: 

Have a look at this article here. The section on UI Helper Templating Support might help.

Phil
I had already looked at this. It uses EditorFor() rather than EditorForModel(). My question is specifically whether EditorForModel can understand and output a drop down list automatically.
Junto
I have tried it and it works for EditForModel(). EditForModel, does an EditFor for each property in your model. if you create a BrandID.ascx in shared/EditorTemplates, it will auomatically use that for BrandID. My guess is you cant specify a select list using data annotations, you would prob need to extend them to do this.
Phil
I've tried your suggestion already, but I couldn't get the data into the select list from the ViewModel without using ViewData["Brands"] and I'm trying to do away with magic strings.
Junto
Phil, do you have a rough example of what BrandId.ascx, EditViewModel and controller would look like?
Junto
A: 

You should build that lookup INTO your ViewModel. Then create a Builder object that builds the ViewModel and populates that lookup.

After all, that's what your ViewModel is for: to provide a model specifically for your view.

Nexxas
A: 

A new attribute DropDownList for your property BrandId may be helpful. Have a look at the Extending ASP.NET MVC 2 Templates article. But this approach uses ViewData as selectlist's items source.

SergeanT
+2  A: 

Junto,

the Html.EditorForModel() method isn't smart enough to match BrandId with the Brands select list.

First, you can't use the shortcut EditorForModel() method.
You have to create your own HTML template like this.

<% using (Html.BeginForm()) { %>

    <div style="display:none"><%= Html.AntiForgeryToken() %></div>

    <table>
        <tr>
            <td><%= Html.LabelFor(m => m.Name) %></td>
            <td><%= Html.EditorFor(m => m.Name) %></td>
        </tr>

        <tr>
            <td><%= Html.LabelFor(m => m.Description) %></td>
            <td><%= Html.EditorFor(m => m.Description) %></td>
        </tr>

        <tr>
            <td><%= Html.LabelFor(m => m.BrandId) %></td>
            <td><%= Html.EditorFor(m => m.BrandId) %></td>
        </tr>
    </table>
<% } %>



Second, you need to change your Action method.

[ImportModelStateFromTempData]
public ActionResult Edit(int id)
{
    BrandRepository br = new BrandRepository();

    Product p = _ProductRepository.Get(id);
    ViewData["BrandId"] = br.GetAll().ToList().ToSelectListItems(p.BrandId);

    EditProductViewModel model = new EditProductViewModel(p);

    return View("Edit", model);
}



Third, you need to update your EditProductViewModel class.

public class EditProductViewModel
{
    [Required]
    [StringLength(200)]
    public string Name { get; set; }

    [Required()]
    [DataType(DataType.Html)]
    public string Description { get; set; }

    [Required] // this foreign key *should* be required
    public int BrandId { get; set; }

    public EditProductViewModel(Product product)
    {
        this.Name = product.Name;
        this.Description = product.Description;
        this.BrandId = product.BrandId;
    }
}

By now, you are probably saying: Dude, where is my [ProductId] property?".
Short Answer: You don't need it!

The HTML rendered by your view already points to "Edit" action method with an appropriate "ProductId" as shown below.

<form action="/Product/Edit/123" method="post">
    ...
</form>

This is your HTTP POST action method and it accepts 2 parameters.
The "id" comes from the <form> tag's action attribute.

[HttpPost, ValidateAntiForgeryToken, ExportModelStateToTempData]
public ActionResult Edit(int id, EditProductViewModel model)
{
    Product p = _ProductRepository.Get(id);

    // make sure the product exists
    // otherwise **redirect** to [NotFound] view because this is a HTTP POST method
    if (p == null)
        return RedirectToAction("NotFound", new { id = id });

    if (ModelState.IsValid)
    {
        TryUpdateModel<Product>(p);
        _ProductRepository.UpdateProduct( p );
    }

    return RedirectToAction("Edit", new { id = id });
}

The ExportModelStateToTempData and ImportModelStateFromTempData are very useful.
Those attributes are used for PRG (Post Redirect Get) pattern.

Read this Use PRG Pattern for Data Modification section in this blog post by Kazi Manzur Rashid.
http://weblogs.asp.net/rashid/archive/2009/04/01/asp-net-mvc-best-practices-part-1.aspx




Okay, this data bind code is not my favorite way of doing things.

TryUpdateModel<Product>( p );

My favorite way of doing it is to have a separate interface for pure data binding.

public interface IProductModel
{
    public string Name {get; set;}
    public string Description {get; set;}
    public int BrandId {get; set;}
}

public partial class Product : IProductModel
{
}

public partial class EditProductViewModel : IProductModel
{
}

And this is how I will update my data binding code.

TryUpdateModel<IProductModel>( p );

What this helps is it makes it simple for me to data bind my model objects from post back data. Additionally, it makes it more secure because you are only binding the data that you want to bind for. Nothing more, nothing less.

Let me know if you have any question.

stun
Thanks Stun. Best answer by far. In summary, EdutorForModel() isn't smart enough, and I still have to use ViewData and magic strings to get the SelectList across. Very useful walkthrough. Thank you.
Junto