ASP MVC Actions
Actions
- HttpGets should be idempotent. "Postback" actions aren't, so add [HttpPost] to Edits
- Use [ActionName("Illegal-Name")] to override names (non C# names, or methods that are the same except for HttpGet/Post).
[ActionName("Person-Detail")]
public ActionResult Details(int id) - To make searches bookmarkable, use the Html.BeginForm overload:
@using (Html.BeginForm("Index", "Categories", FormMethod.Get)) { - Arguments are automatically bound to RouteData.Values["x"], Request.QueryString["y"] and/or Request.Form["z"]
public ActionResult Edit(int id = 0)
{
var isValid = ModelState.IsValid; //false if id="a"
Action Results
- return View(category); //ViewResult
- return RedirectToAction("Index"); //RedirectResult
- return HttpNotFound();
- return Json(category); //Json. Yes, that's all.
- return Content("Hello world"); //plain text
//returning a date or int is automatically a ContentResult - Mark non-http public methods as [NoAction] as otherwise they'd be a ContentResult
- return PartialView(category); //@Html.Action/[ChildActionOnly]
To return simple values (such as an int) add [NoAction] attribute.
Post-Redirect-Get pattern
HttpPost actions usually redirect, with a RedirectToAction (or RedirectToRoute)
return RedirectToAction("Edit", new { id });
return RedirectToRoute(new {controller = "Person", action = "Edit", id});
To pass data across, use TempData["x"] = x.
ModelState.Remove("HasBeenPosted"); //so the model value isn't overridden
model.HasBeenPosted = true;
Partial Actions
An Html.Partial can just call a partial view/user control; Html.Action (and Html.RenderAction) calls a controller which in turn calls a partial view. In other words, Html.Action allows the view to do processing- the view calls a controller to call a view.
<div>@Html.Action("CategorySummary", "Category", Model)</div>
In CategoryController. Mark as [ChildActionOnly] if required. ControllerContext.IsChildAction can detect if within an Action.
[ChildActionOnly]
public ActionResult CategorySummary(CategoryModel category)
{
//(ControllerContext.IsChildAction)
return PartialView(category);
}
If you use dependency injection, you'll get the error A single instance of controller 'Web.Controllers.MyController' cannot be used to handle multiple requests. If a custom controller factory is in use, make sure that it creates a new instance of the controller for each request.. The DI container is supplying the same instance per request (request-scoped).
Change the DI configuration to be scoped per call- eg StructureMap is For<MyController>().AlwaysUnique();
Action Filters
[OutputCache] | Duration, VaryByParams etc |
[Authorize] | Any authenticated user - specifics with Users = "", Roles = "" |
[AllowAnonymous] | MVC 4: whitelist for logon actions when you have a global [Authorize] |
[ValidateAntiForgeryToken] | In conjunction with Html.AntiForgeryToken() |
[HandleError] | Error trapping, It goes to the default Shared/Errors.cshtml.
|
HandleErrors in GlobalFilters
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
//multiple filters are applied with ascending Order (default = -1)
//database errors
filters.Add(new HandleErrorAttribute
{
ExceptionType = typeof(System.Data.Common.DbException),
View = "DatabaseError", //-> Shared/DatabaseError.cshtml
Order = 1
});
//generic errors (default order is -1)
filters.Add(new HandleErrorAttribute { Order = 2 });
}
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
}
Error views have no controller. The model is @model System.Web.Mvc.HandleErrorInfo
Action Input Binding
You can still access Request.Form, or accept a FormCollection, but AspMVC's DefaultModelBinder is quite good at mapping the HttpPosted fields (and Route data) into simple model classes.
You can use simple arguments (which can have defaults), with UpdateModel (or TryUpdateModel) - which can take an interface (as well as inclusions/ exclusions).
[HttpPost]
public ActionResult Edit(int categoryId, string categoryName)
{
var category = new CategoryModel();
UpdateModel(category, new[] { "CategoryName", "CategoryId" });
...Or...
[HttpPost]
public ActionResult Edit(CategoryModel category)
BindAttribute
This applies to the individual parameters - or to your model class.
[HttpPost]
public ActionResult Edit(
[Bind(Include = "CategoryName,CategoryId")]
CategoryModel category)
Custom ModelBinders
- In MVC 1-2, you could add an IModelBinder via ModelBinders.Binders.Add(x)
- IModelBinder.BindModel(ControllerContext controllerContext,
ModelBindingContext bindingContext)
You can access Request.Form or bindingContext.ValueProvider.GetValue - You can inherit from the DefaultModelBinder and override CreateModel to create your models from a repository/IoC
- In MVC3, there is an IModelBinderProvider.GetBinder(type) which can be added to ModelBinderProviders.BinderProviders.Add(x).