Friday, June 17, 2011

The Big Four (things I've learned so far at Headspring's ASP.NET MVC Boot Camp)

At this point I am more than halfway through taking the Headspring ASP.NET MVC Boot Camp. Today is the last day. So far I have learned of numerous neat tricks such as placing [AcceptVerbs(HttpVerbs.Post)] before an Action and placing a constructor on a Controller to painlessly instantiate a repository via a little help from StructureMap (custom model binding). There are four big things I'm learning that so far have really made the class worth my investment in it. They are:




  1. Extensibility

    Wouldn't it be neat if you could roll your own ActionResult? Well, you can. (This was a great learning exercise Jimmy Bogard!) The MVC framework is remarkably extensible. In experimenting last night I made a silly new MVC app in which one could navigate to /Home/Digit/88 and get back HTML which simply contained a random number between zero and eighty-eight. HomeController.cs looked like so:




    using System;

    using System.Web.Mvc;

    using MvcApplication.Helpers;

     

    namespace MvcApplication.Controllers

    {

        public class HomeController : DefaultController

        {

            public ActionResult Index()

            {

                ViewBag.Message = "Welcome to ASP.NET MVC!";

                return View();

            }

     

            public RandomNumber Digit(string id)

            {

                return Foo(Convert.ToInt32(id));

            }

        }

    }


    So what is "DefaultController" anyways? Why are we inheriting from it instead of Controller? Well, we are still inheriting from Controller as it turns out. We inherit from DefaultController which in turn inherits from Controller allowing us to extend Controller and override Controller's methods if desired. DefaultController is a new class we have built. It reads like...



    using System.Web.Mvc;

    using MvcApplication.Helpers;

     

    namespace MvcApplication.Controllers

    {

        public class DefaultController : Controller

        {

            protected RandomNumber<T> Foo<T>(T topEnd)

            {

                return new RandomNumber<T>

                {

                    rangeToRandomizeIn = topEnd

                };

            }

        }

    }


    The remaining piece holds the extension of ActionResult. I have it in a new file called RandomNumber.cs in a new folder called Helpers.



    using System;

    using System.Web.Mvc;

     

    namespace MvcApplication.Helpers

    {

        public class RandomNumber<T> : ActionResult

        {

            public T rangeToRandomizeIn { get; set; }

     

            public override void ExecuteResult(ControllerContext context)

            {

                Random random = new Random();

                Int32 range = Convert.ToInt32(rangeToRandomizeIn);

                var contentResult = new ContentResult();

                contentResult.Content = random.Next(0,range).ToString();

                contentResult.ContentType = "text/html";

                contentResult.ExecuteResult(context);

            }

        }

    }


  2. Filters

    Hey, in the example above, what happens if one goes to /Home/Digit/Wonk instead of /Home/Digit/88 wherein the id cannot be converted from string to Int32? We can find out by adding <customErrors mode="On" /> to Web.config and then testing. We should see, in a box that appears...



    • FormatException was unhandled by user code
    • Input string was not in a correct format.
    • Troubleshooting tips:
      • Make sure your method arguments are in the right format.
      • When converting a string to DateTime, parse the string to take the date before putting each variable into the DateTime object.
      • Get general help for this exception.
      • Search for more Help Online...
    • Actions:
      • View Detail...
      • Enable editing
      • Copy exception detail to the clipboard

    So, let's refactor the example above to account for the exception. Let's put...


    public static void RegisterGlobalFilters(GlobalFilterCollection filters)

    {

        filters.Add(new HandleErrorAttribute());

    }

    ...in Global.asax.cs if it is not already there, and let's put...

    [HandleError(View = "FormatError", ExceptionType = typeof(FormatException))]

    ...immediately above the Digit Action in the Home Controller. Finally, let's create a FormatError View that just contains some text that alerts a user that he/she did not enter an integer. Run the app and get the error where applicable. Yay! So, why this aside...


    In attempting to run the code in an Action, C#'s process may:
    1. pass through an Authorize filter before the Action
    2. pass through a Result filter after the Action
    3. pass through an Exception filter if something goes wonk

    These may be used to abstract away a lot of if/then sanity checking that might otherwise muck up Controllers.


    (note that FormatException is just applicable in this case and that NullReferenceException or a different exception might be applicable in other cases/stage exceptions to get the exception type)

    Use filters as Attributes on Actions, Controllers, base classes that extend Controllers, and Global. Add an Order parameter to an Attribute to specify the order in which filters fire off.


  3. Razor

    ASP.NET MVC 3 uses a new view engine called Razor which is... pretty sharp. It offers new markup. An example:


    @using (Html.BeginForm("Save", "Foo"))

    {

        <h1>ViewBag.ShoutingText</h1>

        <text>Complete to update Foo:</text>

        <div>

            @Html.LabelFor(m => m.Foo)

            @Html.TextBoxFor(m => m.Foo)

            @Html.HiddenFor(m => m.Id)

        </div>

        <input type="submit" value="Save" />

    }

    OK, so what does this do? The at symbol opens Razor syntax. Everything nested inside the curly braces is Razor markup with one exception. The only place we break out of Razor is in using the text tag to show copy instead of interpreting the copy as Razor markup, as, otherwise, everything is interpreted as either Razor markup or HTML markup which Razor is smart enough to know isn't of Razor. This blob of code will make a form. Some form controls may be made with Razor syntax and some may not. Something interesting here is the ViewBag which uses C# 4.0's dynamic feature to allow coders to assign whatever parameters they wish to it. ShoutingText isn't a parameter of ViewBag until we define it in advance of calling the View (a .cshtml file) that will contain the markup above. We do so like so:


    public ActionResult Edit()

    {

        ViewBag.ShoutingText = "Update Foo!";

        return View();

    }


  4. Routing

    This is an example of customizing the routes found in Global.asax.cs. Herein, I create an exception to the last route with the first route. i.e. if one goes to http://www.example.com/ww1/Woodrow%20Wilson/ ASP.NET will not attempt to find a ww1 Controller as is the norm, it will find the Ally Controller, the Show Action (View is not available as an Action name given the nature of MVC) by default without an Action being specified, and Woodrow Wilson as the id


    public static void RegisterRoutes(RouteCollection routes)

    {

        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

     

        routes.MapRoute(

            "Allies",

            "ww1/{id}/{action}",

            new { controller = "Ally", action = "Show" }

        );

     

        routes.MapRoute(

            "Default",

            "{controller}/{action}/{id}",

            new { controller = "Home", action = "Index", id = "" }

        );

    }


    This was a new insight for me. I did not know I could approach custom routing in such a manner previously or I would have approached building a CMS in MVC very differently. One thing I certainly would have done would have been to abstract administration into an Area (good call Justin Pope). To add an area, right click on the MVC project and select "Add Area," then put AreaRegistration.RegisterAllAreas(); in the Application_Start method of Global.asax.cs. If you drag existing Controllers to your Area you will have to update the namespace with Resharper. The area will have a SecurityAreaRegistration.cs file where its route logic is tucked away independent of those in Global.asax.cs. An example:



    public override void RegisterArea(AreaRegistrationContext context)

    {

        context.MapRoute(

            "Administration_default",

            "Administration/{controller}/{action}/{id}",

            new { action = "Index", id = UrlParameter.Optional }

        );

    }


    Other routing-related syntax includes:
    • RedirectToRoute("Default", new { controller = "Thing", action = "Update" });
    • @Html.RouteLink("Default", "Click Me", new { controller = "Thing", action = "Index" })
    • @Html.ActionLink("Axe", "Delete", new { area = "Administration", controller = "Thing" })




 
You sure are silly for a guy with glasses.



My job with Essilor at framesdirect.com previously gave me the opportunity to work on a Greenfield MVC application and the opportunity for me to serve as de facto architect. I wish I had known then what I know now.

No comments:

Post a Comment