This post has been migrated from www.experimentsincode.com, we apologise if some of the images or content is missing

Sitecore allows you to add controllers to an item, this is specified in the Controller and Action fields of the Sitecore item:
However this limits you to a single action on the controller, in this post I will look at one solution to allow multiple actions. Firstly we have to decide how we are going to determine which action is going to be used. For this solution I want the action to be part of the URL, similar to the standard .NET MVC. For example in Sitecore I have an item at /sitecore/content/home/test2, this is accessible at http://demo.com/test2. The methods will be added to the end of this URL, e.g. http://demo.com/test2/method1, however I don't want it to stop putting items beneath the test2 item. So my basic rules will be:
  1. Try to find an item using the standard item resolver
  2. If no item is found find the parent and check if it has a controller
  3. If it has a controller use the last part of the URL as the action
    1. So the first part of the problem is to create a pipeline processor that will look for a possible parent item with a controller if the standard item resolver doesn't find an item:
          public class DynamicController : HttpRequestProcessor
          {
              public override void Process(HttpRequestArgs args)
              {
                  if (Context.Item == null && Context.Database != null)
                  {
                      //split up the original item path
                      string[] parts = args.Url.ItemPath.Split('/');
      
                      //find the possible parent
                      string newPath = parts.Take(parts.Length - 1).Aggregate((x, y) => x + "/" + y);
                      var item = Context.Database.GetItem(newPath);
      
                      if (item != null)
                      {
                          //if there is an action then redirect to it
                          if (item.Paths.FullPath.StartsWith(Context.Site.RootPath) 
                              && !string.IsNullOrEmpty(item["__Controller"])
                              )
                          {
                              Context.Item = item;
                          }
                      }
                  }
              }
          }
      
      
      This processor needs to be placed just after the Sitecore.Pipelines.HttpRequest.ItemResolver in the HttpBeginRequest pipeline:
      <configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
        <sitecore>
          <pipelines>
            <httpRequestBegin>
              <processor patch:after="processor[@type='Sitecore.Pipelines.HttpRequest.ItemResolver, Sitecore.Kernel']" type="Demo.Pipelines.HttpRequestBegin.DynamicController, Demo" />
            </httpRequestBegin>
          </pipelines>
        </sitecore>
      </configuration>
      
      Now that we have a way to change the context of the item we need to update how a controller gets created for an item, this means overriding the Sitecore.Mvc.Pipelines.Request.CreateController.CreateItemController, Sitecore.Mvc class which is called as part of the mvc.createController pipeline:
          public class CreateItemController : CreateControllerProcessor
          {
      
              public override void Process(CreateControllerArgs args)
              {
                  Assert.ArgumentNotNull((object)args, "args");
                  if (args.Result != null)
                      return;
                  args.Result = this.CreateController(args);
              }
      
              protected virtual IController CreateController(CreateControllerArgs args)
              {
                  Debug.ArgumentNotNull((object)args, "args");
                  Item obj = args.PageContext.Item;
                  if (obj == null)
                      return (IController)null;
                  else
                      return this.CreateControllerFromItem(obj, args);
              }
      
      
              protected virtual IController CreateControllerFromItem(Item item, CreateControllerArgs args)
              {
                  string controller = string.Empty;
                  string action = string.Empty;
      
                 
                  if (!string.IsNullOrEmpty(item["__Controller"]))
                  {
                      controller = item["__Controller"];
      
                      string original = Sitecore.Context.Request.ItemPath;
                      //Index is alway the default action
                      action = "Index";
      
                      //if there is an action specified on the item use it
                      if (!string.IsNullOrEmpty(item["__Controller Action"]))
                      {
                          action = item["__Controller Action"];
                      }
                      //if not action defined on the item then it is a dynamic controller
                      else if (original.ToLowerInvariant() != item.Paths.FullPath.ToLowerInvariant())
                      {
                          var parts = original.Split('/');
                          //get the last part of the URL to find out which action to use
                          action = parts.Last();
                      }
      
                      //set the appropriate route values
                      RequestContext requestContext = PageContext.Current.RequestContext;
                      requestContext.RouteData.Values["controller"] = controller;
                      requestContext.RouteData.Values["action"] = action;
         
                      return new ControllerLocator().GetController(controller, action);
      
                  }
               
                  return null;
                  
              }
      
          }
      
      And the config:
      <configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
        <sitecore>
          <pipelines>
            <mvc.createController>
              <processor type="Sitecore.Mvc.Pipelines.Request.CreateController.CreateItemController, Sitecore.Mvc">
                <patch:attribute name="type">Demo.Pipelines.MvcRequestBegin.CreateItemController, Demo</patch:attribute>
              </processor>
            </mvc.createController>
          </pipelines>
        </sitecore> 
      </configuration>
      
      To test this I have the following controller, which defines a few methods:
          public class ItemTestController : SitecoreController
          {
      
              public override ActionResult Index()
              {
                  return View();
              }
      
              public ActionResult Method1()
              {
                  return View();
              }
              public ActionResult Method2()
              {
                  return View();
              }
          }
      
      Within Sitecore I configure my item to use the controller but I don't specify an action:
      Each method has the appropriate view. If I request the actual item the index view gets returned:
      If I had method1 to the end of the URL I can call the Method1 action:
      This solution allows for more flexibility in your controllers, one problem I haven't worked out is what happens when an action is missing on the controller, at the moment you get the exception:
      Could not invoke action method: notamethod. Controller name: ItemTest. Controller type: Demo.Controllers.ItemTestController
      
      It would be better if this returned a 404. Another problem occurs if the content editor places an item called method1 beneath the test2 item, this will cause the method2 item to be returned rather than my action.
      comments powered by Disqus