[ Draft ]
Microsoft ASP.NET MVC
has a few peculiarities in it, that might be worth knowing about. Here are some tips for overcoming these challenges.
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
.
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. >
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.
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
.
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.
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.
URLs
indicate what page to go back to when you are done in another page.Login
screen, so it knows what page to go back to after you Login
.URLs
are encoded into a URL
parameter, called ret
e.g.:
http://www.mysite.com/Login?
ret=%2FMenu%2FIndex
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.
For full page loads, the ret
parameter must be set to:
Request.RawUrl
For AJAX
calls the ret
parameter must be set to:
Url.Action(ActionNames.Index)
The ret
parameter is set in a Controller
action method, when you return the ActionResult
. Example:
EXAMPLE WORKS FOR FULL PAGE LOAD ONLY!!!
return RedirectToAction(
ActionNames.Login,
ControllerNames.Account,
new { ret = Request.RawUrl });
URL
should always be optional, otherwise you could never serparately debug a View
.Do not use RefferrerUrl
, because that only works for HttpPost
, not HttpGet
. Use Request.RawUrl instead.
URLs
. If you pass the same return URL
along multiple HTTP
requests, only one action has to forget to pass along the return URL
and a back or close button is broken and you will find out very late that it is, because it is not an obvious thing to test. The same error-proneness is there for return actions with return actions with return actions, or with bread-crumb like structures with multiple return actions built in.< 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. >
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: Mention ModelState.ClearErrors. >
< TODO: Mention: Using Request.UrlReferrer in Http Get actions crashes. Use Request.RawUrl. >