开发者

Using ViewModel Pattern with MVC 2 Strongly Typed HTML Helpers

I am working with ASP.NET MVC2 RC and can't figure out how to get the HTML helper, TextBoxfor to work with a ViewModel pattern. When used on an edit page the data is not saved when UpdateModel() is called in the controller. I have taken the following code examples from the NerdDinner application.

Edit.aspx

<%@ Language="C#" Inherits="System.Web.Mvc.ViewUserControl<NerdDinner.Models.DinnerFormViewModel>" %>
...
<p>
    // This works when saving in controller (MVC 1)
    <label for="Title">Dinner Title:</label>
    <%= Html.TextBox("Title", Model.Dinner.Title) %>
    <%= Html.ValidationMessage("Title", "*") %>
</p>
<p>
    // This does not work when saving in the controller (MVC 2)
    <label for="Title">Dinner Title:</label>
    <%= Html.TextBoxFor(model => model.Dinner.Title) %>
    <%= Html.ValidationMessageFor(model=> model.Dinner.Title) %>
</p>

DinnerController

// POST: /Dinners/Edit/5

[HttpPost, Authorize]
public ActionResult Edit(int id, FormCollection collection) {

    Dinner dinner = dinnerRepository.GetDinner(id);

    if (!dinner.IsHostedBy(User.Identity.Name))
        return View("InvalidOwner");

    try {
        UpdateModel(dinner);

        dinnerRepository.Save();

        return RedirectToAction("Details", new { id=dinner.DinnerID });
    }
    catch {
        ModelState.AddModelErrors(dinner.GetRuleViolations());

        return View(new DinnerFormViewModel(dinner));
 开发者_开发百科   }
}

When the original helper style is used (Http.TextBox) the UpdateModel(dinner) call works as expected and the new values are saved.

When the new (MVC2) helper style is used (Http.TextBoxFor) the UpdateModel(dinner) call does not update the values. Yes, the current values are loaded into the edit page on load.

Is there something else which I need to add to the controller code for it to work? The new helper works fine if I am just using a model and not a ViewModel pattern.

Thank you.


The issue here is your Edit form is using strongly typed helpers against a DinnerFormViewModel type, but you're calling UpdateModel on a Dinner type.

When you use the strongly typed helpers against the type, the helpers create form fields assuming that's the type you're posting to. When the types don't match up, there's a problem.

However, this is very easy to fix. You can provide a prefix to UpdateModel which indicates that you weren't trying to edit the whole model, you were trying to edit a property of the model, in this case a Dinner.

UpdateModel(dinner, "Dinner");

The other approach is to call UpdateModel on the actual ViewModel.

var viewModel = new DinnerFormViewModel();
viewModel.Dinner = repository.GetDinner(id);
UpdateModel(viewModel);

I think the first approach is much better.


On Page 90 in the "Wrox Professional ASP.NET MVC 2" book the code is listed as:

if (TryUpdateModel(dinner)) {
     dinnerRepository.Save();

     redirectToAction("Details", new { id=dinner.DinnerID });

But it should read:

if (TryUpdateModel(dinner, "Dinner")) {
     dinnerRepository.Save();

     redirectToAction("Details", new { id=dinner.DinnerID });

This method overload will try to update the specified model [Dinner], rather than the default [ViewModel], using the values from the controller's value provider. Basically all it does is add a prefix to all your values when looking them up in the provider.

So when the Model is looking to update its' Title property, it will look for Dinner.Title, instead of just Title in the controller's value provider.

While debugging, take a look in the Edit ActionResult method and inspect the FormCollection input param. When you dig down into it's entry array, you'll find Keys that all start with the prefix of the property object you referenced in your View, in your case the Edit View, like this:

<%: Html.TextBoxFor(model => model.Dinner.Title, new {size=50, @class="prettyForm" })%>


I'm not 100% sure, but it seems that strongly typed helper creates ids/names "Dinner.Title" instead of just "Title" and therefore - UpdateModel can't bind it.

Unfortunately - i haven't used UpdateModel method myself so i don't know the solution.

Could you add html that gets rendered for both approaches?


Playing around with reflector ATM.

This is what i found:

protected internal bool TryUpdateModel<TModel>(TModel model, string prefix, string[] includeProperties, string[] excludeProperties, IValueProvider valueProvider) where TModel: class
{
    if (model == null)
    {
        throw new ArgumentNullException("model");
    }
    if (valueProvider == null)
    {
        throw new ArgumentNullException("valueProvider");
    }
    Predicate<string> predicate = delegate (string propertyName) {
        return BindAttribute.IsPropertyAllowed(propertyName, base.includeProperties, base.excludeProperties);
    };
    IModelBinder binder = this.Binders.GetBinder(typeof(TModel));
    ModelBindingContext context2 = new ModelBindingContext();
    context2.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(delegate {
        return base.model;
    }, typeof(TModel));
    context2.ModelName = prefix;
    context2.ModelState = this.ModelState;
    context2.PropertyFilter = predicate;
    context2.ValueProvider = valueProvider;
    ModelBindingContext bindingContext = context2;
    binder.BindModel(base.ControllerContext, bindingContext);
    return this.ModelState.IsValid;
}

Parameters
- model The model instance to update.
- prefix The prefix to use when looking up values in the value provider.


So - you can try to use UpdateModel<T>(T model, string prefix) overload and pass "Dinner" or "Dinner." as prefix argument.


Maybe a simpler way to put this is as follows. If you are cutting and pasting code from the wrox download for the NerDDinner tutorial, you'll find there are some errors. Using a suggestion from above, I modified the example from 1-53.txt to get this to work. The change follows:

 //
  // POST: /Dinners/Edit/2
  [HttpPost]
  public ActionResult Edit(int id, FormCollection formValues)
  {
   // Retrieve existing dinner
   Dinner dinner = dinnerRepository.GetDinner(id);
   DinnerFormViewModel viewModel = new DinnerFormViewModel(dinner);

   if (TryUpdateModel(viewModel))
   {
    // Persist changes back to database
    dinnerRepository.Save();
    // Perform HTTP redirect to details page for the saved Dinner
    return RedirectToAction("Details", new { id = dinner.DinnerID });
   }
   else
   {
    return View(viewModel);
   }
  }


A simpler way is using the prefix as parameter name, just do like this:

public ActionResult Edit(Dinner Dinner, int DinnerID)
{
   ...
}
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜