Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ViewModel with SelectList binding in ASP.NET MVC2

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.

like image 747
Rebecca Avatar asked Feb 25 '10 10:02

Rebecca


1 Answers

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.

like image 87
stun Avatar answered Sep 24 '22 01:09

stun