[ Draft ]

šŸ§© Presentation Patterns (MVC)

back

Microsoft ASP.NET MVC has a few peculiarities in it, that might be worth knowing about. Here are some tips for overcoming these challenges.

Contents

Controller

In an ASP.NET MVC application a Controller has a lot of responsibilities, but in this architecture most of the responsibility is delegated to Presenters. The responsibilities that are left for the MVC Controllers are the URL routing, the HTTP verbs, redirections, setting up infrastructural context and miscellaneous MVC quirks.

The Controller may use multiple Presenters and ViewModels, since it is about multiple screens.

Entity names put in Controller are plural by convention. So CustomersController not CustomerController.

Post-Redirect-Get

This is a quirk intrinsic to ASP.NET MVC. We must conform to the Post-Redirect-Get pattern to make sure the page navigation works as expected.

At the end of a post action, you must call RedirectToAction() to redirect to a Get action.

Before you do so, you must store the ViewModel in the TempData dictionary. In the Get Action that you redirect to, you have to check if the ViewModel is in the TempData dictionary. If the ViewModel exist in the TempData, you must use that ViewModel, otherwise you must create a new ViewModel.

Here is simplified pseudo-code in which the pattern is applied.

public ActionResult Edit(int id)
{
    object viewModel;
    if (!TempData.TryGetValue(TempDataKeys.ViewModel, out viewModel))
    {
      // TODO: Call Presenter
    }
    return View(viewModel);
}

[HttpPost]
public ActionResult Edit(EditViewModel viewModel)
{
    // TODO: Call Presenter
    TempData[TempDataKeys.ViewModel] = viewModel2;
    return RedirectToAction(ActionNames.Details);
}

There might be an exception to the rule to always RedirectToAction at the end of a Post. When you would redirect to a page that you can never go to directly, you might return View() instead, because there is no Get method. This may be the case for a NotFoundViewModel or a DeleteConfirmedViewModel.

< TODO:

- Mention that return View in case of validation messages is the way to go, because otherwise MVC will not remember un-mappable wrong input values, like Guids and dates entered as strings. (In one case this lead to the browser asking for resending postdata upon clicking the back button, so check whether this is actually a good idea.)
- Not using return View() in a post action makes old values not be remembered. >

Considerations

If you do not conform to the Post-Redirect-Get pattern in MVC, you may get to see ugly URLs. When you hit the back button, you might go to an unexpected page, or get an error. You may see original values that you changed re-appear in the user interface. You may also see that MVC keeps complaining about validation errors, that you already resolved. So conform to the Post-Redirect-Get pattern to stay out of trouble.

ValidationMessages in ModelState

For the architecture to integrate well with MVC, you have to make MVC aware that there are validation messages, after you have gotten a ViewModel from a Presenter. If you do not do this, you will get strange application navigation in case of validation errors.

You do this in an MVC HTTP GET action method.

The way we do it here is as follows:

if (viewModel.ValidationMessages.Any())
{
    ModelState.AddModelError(
        ControllerHelper.DEFAULT_ERROR_KEY,
        ControllerHelper.GENERIC_ERROR_MESSAGE);
}

In theory we could communicate all validation messages to MVC instead of just communicating a single generic error message. In theory MVC could be used to color the right input fields red automatically, but in practice this breaks easily without an obvious explanation. So instead we manage it ourselves. If we want a validation summary, we simply render all the validation messages from the ViewModel ourselves and not use the Html.ValidationSummary() method at all. If we want to change the appearance of input fields if they have validation errors, then the ViewModel should give the information that the appearance of the field should be different. Our View's content is totally managed by the ViewModel.

Polymorphic RedirectToAction / View()

A Presenter action method may return different types of ViewModels.

This means that in the MVC Controller action methods, the Presenter returns object and you should do polymorphic type checks to determine which View to go to.

Here is simplified code for how you can do this in a Post method:

var editViewModel = viewModel as EditViewModel;
if (editViewModel != null)
{
    return RedirectToAction(ActionNames.Edit, new { id = editViewModel.Question.ID });
}

var detailsViewModel = viewModel as DetailsViewModel;
if (detailsViewModel != null)
{
    return RedirectToAction(ActionNames.Details, new { id = viewModel.Question.ID });
}

At the end throw the following exception (from JJ.Framework.Exceptions):

throw new UnexpectedTypeException(() => viewModel);

To prevent repeating this code for each Controller action, you could program a generalized method that returns the right ActionResult depending on the ViewModel type. Do consider the performance penalty that it may impose and it is worth saying that such a method is not very easy code.

For Loops for Lists in HTTP Postdata

An alternative to for posting collections is using for loops.

@Html.TextBoxFor(x => x.MyItem.MyProperty)

@for (int i = 0; i < Model.MyItem.MyCollection.Count; i++)
{
    @Html.TextBoxFor(x => x.MyItem.MyCollection[i].MyProperty)
}

This solution only works if the expressions you pass to the Html helpers contain the full path to a ViewModel property (or hack the HtmlHelper.ViewData.TemplateInfo.HtmlFieldPrefix) and therefore it does not work if you want to split up your View code into partials.

Return URLs

The ret parameter is the following value encoded: /Menu/Index
That is the URL you will go back to after you log in.

The Login action can Redirect to the ret URL like this:

[HttpPost]
public ActionResult Login(... string ret = null)
{
    ...
    return Redirect(ret);
    ...
}

ASSIGN DIFFERENT RET FOR FULL PAGE LOAD OR AJAX CALL.

< TODO: Incorporate this: Ret parameters can be done with new { ret = Request.RawUrl } for full load, and for AJAX this works: { ret = Url.Action(ActionNames.Index) } if you always make sure you have an Index action in your Controller, which is advisable. >

Back Buttons

There is a pitfall in builing back buttons. If you mix back buttons being handled at the server side, compared to window.history.back() at the client-side, you run the risk that the back button at one point keeps flipping back and foreward between pages.

TODO

< TODO: Mention ModelState.ClearErrors. >

< TODO: Mention: Using Request.UrlReferrer in Http Get actions crashes. Use Request.RawUrl. >

back