18

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.

Brettski
  • 19,351
  • 15
  • 74
  • 97
  • Hi, i have the same issue on create action. Can you please take a look http://stackoverflow.com/questions/2494940/custom-viewmodel-with-mvc-2-strongly-typed-html-helpers-return-null-object-on-cre – Barbaros Alp Mar 23 '10 at 12:02

5 Answers5

19

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.

Haacked
  • 58,045
  • 14
  • 90
  • 114
  • 1
    There is nothing better than getting your answer from the PM of a project. Thank you Phil, that worked great. I used your first example, which was a simple and straight forward as I hoped. – Brettski Jan 31 '10 at 21:03
  • I am following the same example but getting exception on updating the model: http://stackoverflow.com/questions/2377065/how-to-update-using-mvc2-rc2 – Picflight Mar 06 '10 at 08:54
  • Hi Phil, i have the same issue with Create action. I am using a custom view model for the create form. When i try to Create an object it doesnt get binded from the form. So i took a look to source and found that "Category.Title" instead of "Title". How can i fix it. Thank you – Barbaros Alp Mar 22 '10 at 18:36
2

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" })%>
Craig Gjerdingen
  • 1,844
  • 1
  • 21
  • 21
1

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.

Arnis Lapsa
  • 45,880
  • 29
  • 115
  • 195
  • Thank you, that does seem to do the trick. The issue I am having is that why add the strongly typed helpers to the scaffolding if you need to jump through a hoop to use them. I have to be missing something here. – Brettski Jan 29 '10 at 15:04
  • @Brettski I've had some difficulties with them too (http://stackoverflow.com/questions/2093216/asp-net-mvc2-strongly-typed-htmlhelper-indexes). You aren't missing anything - they are just not mature enough, that's all. At least - that's how i see it. – Arnis Lapsa Jan 29 '10 at 15:08
1

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);
   }
  }
Gabriele Petrioli
  • 191,379
  • 34
  • 261
  • 317
0

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

public ActionResult Edit(Dinner Dinner, int DinnerID)
{
   ...
}
Gabriele Petrioli
  • 191,379
  • 34
  • 261
  • 317
jamaz
  • 16