views:

2017

answers:

4

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.

+1  A: 

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 L.
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
@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 L.
+15  A: 

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
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
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
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
A: 

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

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

jamaz
A: 

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); } }

brian leiby