John
John

Reputation: 7059

MVC ViewModels and url generation

I'm new to MVC and try to figure out some best practices. Just today I discovered that when I declare a controller method like so:

public class TestController : Controller
{
    public class ViewModel
    {
        public String MyValue { get; set; }
    }

    public ActionResult Index(ViewModel vm)
    {
        return View(vm);
    }
}

Then, the parameter MyValue can not only passed in with a query argument of vm.MyValue=somevalue, but also with MyValue=somevalue. I like that because then I can generate a url to that controller method with

Url.Action("index", vm)

which is more name safe than something like

Url.Action("index", new { vm = vm })

which I thought I had to write previously. Since the controller takes the very view model of the view, I can even write

@Html.HiddenFor(vm => vm.MyValue)

in my view an get the correct names.

Now here's where I'm stumbling:

The view's model and the controller method's "parameter model" will rarely be the same thing, because normally the controller will fetch additional data for the view.

If I introduce a second model, and have either the parameter model include a view model or the other way round, I lose the beautiful, simple way of have the names match as showed above:

public class TestController : Controller
{
    public class ParameterModel
    {
        public String MyValue { get; set; }
    }

    public class ViewModel
    {
        public ParameterModel ParameterModel { get; set; }
    }

    public ActionResult Index(ParameterModel pm)
    {
        var vm = new ViewModel() { ParameterModel = pm };

        return View(vm);
    }
}

Now, in the view, I had to write

@Html.HiddenFor(vm => vm.ParameterModel.MyValue)

which creates an argument string of ParameterMode.MyValue=somevalue, which is wrong.

It's as if MVC is designed to expect the view's model to be exactly what is passed into controller methods - which is often not the case.

But clearly there is way around this?

Upvotes: 1

Views: 1163

Answers (1)

Ant P
Ant P

Reputation: 25231

You've touched on one of my big gripes with the framework here. Because the view is typed to a particular model (let's call it the "display" model) and all of your tools for creating forms are based on this type, it strongly encourages you to use the same model for your parameters as you did for your display.

This is not only unwieldy but it leads to other problems - properties that were populated on GET actions won't be on form posts unless you persisted them with hidden fields, so the illusion of being able to just return the view again to display ModelState errors is immediately broken.

The fact that what you show the user is probably nothing like what you want the user to give you back (unless you are building big flat forms) is one of the major oversights of the framework.

One thought is that you could alleviate the problem by using partials for your forms:

@model MyDisplayModel

<p>@Model.SomeDisplayProperties</p>

@Html.Partial("MyForm", new MyPostModel())

Then your partial view can look like:

@model MyPostModel

@Html.TextBoxFor(m => m.SomeProperty)

Which should, I believe, generate the correct name attributes to match up to a controller action that accepts a MyPostModel.

In situations where this isn't the case, you basically have two options:

  • Flatten your GET and POST models out into one thing and accept that it's inelegant and you will have lots of nulls.
  • Create a different model for your POST actions and either try and make the properties match those in your GET model or build your HTML manually.

Upvotes: 1

Related Questions