Tuesday, March 5, 2013

ASP.NET MVC & Areas

Areas are a very handy way to segregate you application when you have alot of controllers that are very specific to a behavior or when you need to separate areas of your application. A common scenario, for example, is when you need an Admin section. The tricky thing about areas is handling the routing. In this example we will have an Admin area and a section for regular users, which should look normal to you. You can have controllers with the exact same name in your areas/non-areas and there will be no conflict. Let's see how to do this.

When creating a new MVC project we get the Home Controller right out of the box...

public class HomeController : Controller
{
   public ActionResult Index()
   {
      return View();
   }
}

We will assume that you have added an Area to your project, Admin, and added a Home Controller. The Home Controller will more than likely look quite similar.

In order to get things to work properly, we need to add namespacing in two areas, RouteConfig.cs and AdminAreaRegistration.cs. First let's have a go with RouteConfig.cs. Let's assume our project name is GiftIdeas...

public class RouteConfig
{
   public static void RegisterRoutes(RouteCollection routes)
   {
      routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

      routes.MapRoute(
         name: "Default",
         url: "{controller}/{action}/{id}",
         defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
         namespaces: new string[] { "GiftIdeas.Controllers" }
      );
   }
}

The important piece is the line namespaces: new string[] { "GiftIdeas.Controllers" }. All this is basically doing is saying, for the routes defined here, look in this namespace GiftIdeas.Controllers.

The other adjustment we need to make is very similar. By default, when you add an area to your project, a file called *YourAreaName*AreaRegistration.cs will be added. This is where we need to add some code, actually, very similar code...

public class AdminAreaRegistration : AreaRegistration
{
   public override string AreaName
   {
      get
      {
         return "Admin";
      }
   }

   public override void RegisterArea(AreaRegistrationContext context)
   {
      context.MapRoute(
         "Admin_default",
         "Admin/{controller}/{action}/{id}",
         new { action = "Index", id = UrlParameter.Optional },
         new string[] { "GiftIdeas.Areas.Controllers" }
      );
   }
}

The other piece of the puzzle is that now that we have an area in place, how do we deal with navigating the links within the application? For this, we have to go into _Layout.cshtml in the Views/Shared folder. Of course, this is if you want to show links across the top of your application. The change that needs to be made is in the nav section of the body. When you fire up a MVC application, this is what that part normally looks like:

<nav>
   <ul id="menu"
      <li>@Html.ActionLink("Home", "Index", "Home")</li>
      <li>@Html.ActionLink("About", "About", "Home")</li>
      <li>@Html.ActionLink("Contact", "Contact", "Home")</li>
   </ul>
</nav>

We have to add another parameter in order for all the routes to hook up and work, for each action link, it is the same...

<nav>
   <ul id="menu"
      <li>@Html.ActionLink("Home", "Index", "Home", new { area = "" }, null)</li>
      <li>@Html.ActionLink("About", "About", "Home" new { area = "" }, null)</li>
      <li>@Html.ActionLink("Contact", "Contact", "Home" new { area = "" }, null)</li>
      <li>@Html.ActionLink("Admin", "Index", "Home", new { area = "admin" }, null)</li>
   </ul>
</nav>

With this in place, you should now be able to have controllers with the same name in different parts of you application.
A common scenario is that we pass an id to views. When you are working with views in an area it does look a little different. Let's take a look at how we do that. For example, let's say we want to render a view for a GiftIdeaViewModel. The view in the Admin area would look like this:
@model GiftIdeaViewModel

<div>
   @Html.HiddenFor(giftIdeaViewModel => giftIdeaViewModel.GiftIdeaId)
</div>
<br />
<div>
   Name: @Html.DisplayFor(giftIdeaViewModel => giftIdeaViewModel.Name)
</div>
<br />
<div>
   @Html.ActionLink("Edit", "Edit", "GiftIdea", new { area = "admin", id = @Model.GiftIdeaId }, null) |
   @Html.ActionLink("Details", "Details", "GiftIdea", new { area = "admin", id = @Model.GiftIdeaId }, null) |
   @Html.AcitonLink("Delete", "Delete", "GiftIdea", new { area = "admin", id = @Model.GiftIdeaId }, null) |
</div>

Just add the id in the anonymous object and you are good to go. About the null parameter, per the MSDN documentation, the null parameter is required only because the ActionLink method overloads that have a routeValues parameter also have an htmlAttributes parameter. However, this parameter is not required in order to be able to link between areas.