Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 924
Brettski Avatar asked Jan 29 '10 05:01

Brettski


People also ask

What is strongly typed HTML helpers in MVC?

The Strongly-Typed HTML helper (i.e., NumericTextBox) takes lambda as a parameter that tells the helper, which element of the model to be used in the typed view. The Strongly typed views are used for rendering specific types of model objects, instead of using the general ViewData structure.

Can I use ViewModel in MVC?

In ASP.NET MVC, ViewModel is a class that contains the fields which are represented in the strongly-typed view. It is used to pass data from controller to strongly-typed view.

What is templated HTML helpers in MVC?

The Html. EditorFor templated helper also renders validation logic that enforces constraints that are built into the data model or that you can add as System. ComponentModel. DataAnnotations attributes to a partial class that is associated with the data model.

What is the difference between TextBox and TextBoxFor in MVC?

IMO the main difference is that Textbox is not strongly typed. TextboxFor take a lambda as a parameter that tell the helper the with element of the model to use in a typed view. You can do the same things with both, but you should use typed views and TextboxFor when possible.


3 Answers

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.

like image 122
Haacked Avatar answered Oct 26 '22 23:10

Haacked


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" })%>
like image 24
Craig Gjerdingen Avatar answered Oct 27 '22 00:10

Craig Gjerdingen


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.

like image 30
Arnis Lapsa Avatar answered Oct 26 '22 22:10

Arnis Lapsa