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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With