August 2009 - Posts

Enabling Windsor Integration in MonoRail

I recently wanted to take on old MonoRail application and update it to use Windsor for dependency injection (DI).  The application stated as a sort of prototype and slowing grew into a decent sized application.  There's a couple of places that I want to add some unit tests and I could really benefit from DI.  So I sat down to hook Windsor into the application.

There's already documentation on how to integrate Windsor into MonoRail.  After all, they kind of "grew up" together.  However, the documentation is a little old and MonoRail has gone through a number of changes (some breaking) and improvements.  The "using MonoRail" site (a sort of wiki-like site) had some information on integrating Windsor into MonoRail which was more up-to-date than the Castle docs, but still left out an important section about ViewComponents.  So I thought I'd bring everything I learned into a single blog post.  I plan to take much of this and use it to update the Castle docs as well as the "using" site.

Requirements

You'll need to reference a few extra assemblies in your MonoRail application:

  • Castle.DynamicProxy2.dll
  • Castle.MicroKernel.dll
  • Castle.Windor.dll
  • Castle.MonoRail.WindsorExtension.dll

Custom Container

The first thing is to create a sub-class of the Windsor container for your MonoRail application.  This specialized version of the container will add the following capabilities:

  1. It will add a new facility which will automatically configure our Controllers for a transient lifestyle (one instance per request).  Windsor's default lifestyle is Singleton (imagine having only a single instance for each controller – yikes!!).
  2. It will automatically register all of our controllers for us.  This is optional as you can individually register each controller, but I find the automatic registration much easier.
  3. It will automatically register all of our ViewComponents.  Like controllers, this could be done individually, but this is easier.

Our custom container is really quite simple.  We'll create WebAppContainer.cs in our App_Code directory.  Our constructor will call the base class constructor that will initialize the container from the config file (web.config):

using Castle.Core.Resource;
using Castle.Windsor.Configuration.Interpreters;
using Castle.MonoRail.WindsorExtension;
using Castle.Windsor;
using Castle.MicroKernel.Registration;
using Castle.MonoRail.Framework;
using Castle.Core;
 
public class WebAppContainer : WindsorContainer
{
    public WebAppContainer()
        : base(new XmlInterpreter(new ConfigResource()))
    {
    }
 
    public void Init()
    {
    }
}

Now let's work on our 3 items above.  We'll plug all of this into our Init() method which we'll call later when we initialize our container.

First, we add a facility for MonoRail integration.  This facility is part of MonoRail:

AddFacility("rails", new MonoRailFacility());

Next, we register all of our controllers (yes, in one statement).  My controllers are in a separate assembly so this is how my registration looks:

Register(
    AllTypes.Of<IController>()
    .FromAssemblyNamed("YourAssemblyName.Controllers"));

If your controllers are directly in your web application, you can change "FromAssemblyNamed(…)" to "FromAssembly(Assembly.GetExecutingAssembly())".

Now we also need to register our ViewComponents.  This registration will also make sure that the ViewComponents are set up with a transient lifestyle:

Register(
    AllTypes.Of<ViewComponent>()
        .FromAssemblyNamed("YourAssemblyName.Controllers")
        .Configure(cr => cr.Named(cr.ServiceType.Name).LifeStyle.Is(LifestyleType.Transient))
    );

That's it for the container!  Our complete, customized container looks like this:

using Castle.Core.Resource;
using Castle.Windsor.Configuration.Interpreters;
using Castle.MonoRail.WindsorExtension;
using Castle.Windsor;
using Castle.MicroKernel.Registration;
using Castle.MonoRail.Framework;
using Castle.Core;
 
public class WebAppContainer : WindsorContainer
{
    public WebAppContainer()
        : base(new XmlInterpreter(new ConfigResource()))
    {
    }
 
    public void Init()
    {
        AddFacility("rails", new MonoRailFacility());
 
        Register(
            AllTypes.Of<IController>()
            .FromAssemblyNamed("YourAssemblyName.Controllers"));
 
        Register(
            AllTypes.Of<ViewComponent>()
                .FromAssemblyNamed("YourAssemblyName.Controllers")
                .Configure(cr => cr.Named(cr.ServiceType.Name).LifeStyle.Is(LifestyleType.Transient))
            );
    }
}

Integrating Our Container

Now we need to hook this container into our web application.  We need to plug this into our custom HttpApplication (usually, your GlobalApplication.cs).  First, we need to add the IContainerAccessor interface:

public class GlobalApplication : HttpApplication, IContainerAccessor

Now add a static class variable to old our container:

private static WebAppContainer container;

Now we need to create the container when our application first starts and dispose of it when the application ends.  This is easy by hooking into the Application_OnStart and Application_OnEnd events.  The OnStart event is where we call the container's Init() method.

public void Application_OnStart()
{
    container = new WebAppContainer();
    container.Init();
}
 
public void Application_OnEnd()
{
    container.Dispose();
}

Finally, we need to implement the IContainerAccessor.Container property to return our custom container:

public IWindsorContainer Container
{
    get { return container; }
}

We're almost there!

Web.config Changes

Now that we're using Windsor, we need to configure our web.config.  Add the following section handler declaration in the <configSections> node:

<configSections>
    ..
    <section name="castle"
             type="Castle.Windsor.Configuration.AppDomain.CastleSectionHandler, Castle.Windsor" />
    ..
</configSections>

And add a "useWindsorIntegration" attribute to your existing <monorail> node, setting its value to "true":

<monorail useWindsorIntegration="true">

Since you probably don't have any components to register (since you haven't been using Windsor until now), you can add an empty <castle> section.  When you're ready to configure your components to enable DI and loose coupling, see the Windsor Configuration Reference at the Castle Project site.

That's It!

You're done.  We added a new, 33-line WebAppContainer.cs, added about 6-8 lines to GlobalApplication.cs and added a few lines to web.config.  Now we've got the full power of Castle's dependency injection to allow us to build loosely-couple, easily testable controllers and view components.

Technorati Tags: ,,,
Posted by PSteele | 1 comment(s)
Filed under: , ,

ASP.NET MVC + MVC Contrib + Unit Testing

One of the key benefits of the MVC (Model View Controller) pattern is a separation of concerns that leads to better testability.  Microsoft recognizes this and will automatically create a separate MS Test project when creating a new ASP.NET MVC solution.  While this gives you a nice head start, there's room for improvement.  While actions in the MVC pattern are simply methods on a class that can easily be called by MSTest (or any unit testing framework), most web applications have interactions with supporting objects such a Request (query string, form parameters, etc…), Response (cookies, content type, headers, etc…), Session, and more.  In a live environment, these objects come as a result of the HTTP request being processed by IIS.  In a test environment, you're isolating just your controllers and actions and you don't have IIS and an entire HTTP pipeline.

We can use mocking to provide "pretend" implementations of all of these objects, but there's a lot to mock.  This is where the MVC Contrib project on CodePlex can really come in handy!

UPDATE: The code for this entire project is available in ZIP format from my Google Code page or you can do an SVN checkout of http://patricksteele.googlecode.com/svn/trunk/UnitTestingAspNetMVC.

MVC Contrib Test Helper

The MVC Contrib project gets a lot of praise for the many benefits it brings when developing for ASP.NET MVC – numerous UI helpers, Model Binders, Controller factories, etc…  But it also contains a TestHelper library that makes unit testing your controllers much easier.  By utilizing Rhino.Mocks, the MVC Contrib TestHelper can create and initialize your controller with mocked instances of:

  • HttpRequest
  • HttpResponse
  • HttpSession
  • Form
  • HttpContext
  • and more!

In this article, I'll utilize the MVC Contrib TestHelper library to fully unit test a simple ASP.NET MVC controller.  As with many small demos, it's totally contrived, but helps illustrate the principals.

Scenario

We're building an ASP.NET MVC project that has to accept submissions from speakers.  We'll be doing this in a "wizard-like" fashion.  The prospective speaker will first enter their personal information (first name and last name).  The next step will have them enter their submission information.  There will be a final review point and finally, the actual submission.  Since some speakers want to submit multiple talks, we'll re-display the speakers personal information if they start the wizard up again after submitting a talk.

Design

In this article, we're only going to deal with the first step: collecting the speakers personal information (first name and last name).  We'll save this information in the Session object and pull it back out if the speaker returns to the beginning of the wizard after submitting a talk.  Both the speakers first name and last name are required – the user can not continue if they aren't both filled in.  The action on the controller is the "Speaker" action.  The next step in the wizard is the "SessionDetails" action.

Using the information above, we have 5 test scenarios to cover:

  1. If we run the "Speaker" action with nothing in the Session: The result should be a ViewModel which has no speaker first name/last name and we should return a "View" result to ASP.NET MVC.
  2. If we run the "Speaker" action with a first name/last name in the Session: The result should be a ViewModel which as the first name/last name from the Session and should return a "View" result to ASP.NET MVC.
  3. If we run the "Speaker" action and only send in a first name: We should save the first name to the Session, but we'll also add an error for the missing last name to the ModelState and return a "Redirect" result so ASP.NET MVC will go back to the "Speaker" action and give the user the chance to enter their missing last name.
  4. Do the same test as above, but for the last name.
  5. If we run the "Speaker" action and send both a first name and a last name: The first and last name should be saved to the Session object and a "Redirect" result should be returned that tells ASP.NET MVC to go to the "SessionDetails" action.

Set Up

Start up a new ASP.NET MVC project and make sure you select the option to create unit tests.  The default ASP.NET MVC project comes with a Home controller.  For simplicity's sake, we'll add our actions to that controller and add our tests to the HomeControllerTests class.

First, let's create a class to maintain our speaker information.  Sure, it's only two pieces of information, but this is a sample.  In the real world, we'd probably have more than this and therefore, we'll use a class:

public class SpeakerInfo
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

Test #1

We start by writing our failing test (red), we'll throw together enough production code to get the test to pass (green), then we'll implement our actual logic and make sure our tests still pass (refactor).

[TestMethod]
public void Speaker_WithoutSessionData_Returns_EmptyModel()
{
    var controller = new HomeController();
 
    var result = (ViewResult)controller.Speaker();
    var info = (SpeakerInfo)result.ViewData.Model;
 
    Assert.IsNull(info.FirstName);
    Assert.IsNull(info.LastName);
}

All we did here was create our controller and call the Speaker method.  This of course won't even compile so let's create a simple "Speaker" action on our home controller to get it to compile:

public ActionResult Speaker()
{
    return View();
}

Ok, now we compile and run the test.  We get a failure (red) because we don't have any view model.  Let's change our Speaker method so we have a passing unit test:

public ActionResult Speaker()
{
    return View(new SpeakerInfo());
}

Run the test again and we're green!  Now it's time to refactor into real code.

We're going to store the SpeakerInfo in the ASP.NET Session object so it's available later if the speaker decides to submit another talk (they won't have to re-enter their personal information).  Likewise, if we look in the Session and don't see a saved SpeakerInfo, we provide an empty one.  Let's put all of this into a property:

private SpeakerInfo SpeakerInfo
{
    get
    {
        var info = Session[SessionKeys.SpeakerInfoKey] as SpeakerInfo;
        if (info == null)
        {
            info = new SpeakerInfo();
        }
 
        return info;
    }
    set
    {
        Session[SessionKeys.SpeakerInfoKey] = value;
    }
}

NOTE: You'll notice that the code references an object calls "SessionKeys".  I don't like magic strings so I usually create a simple static class that has const fields for all of my session keys.  This has the added benefit of giving me some intellisense during development:

public static class SessionKeys
{
    public const string SpeakerInfoKey = "SI";
}

Now we refactor our original Speaker implementation to use the SpeakerInfo property instead of always creating a new SpeakerInfo object:

public ActionResult Speaker()
{
    return View(this.SpeakerInfo);
}

Now re-run the test and what do we get?  Red (an error)!  Why?  Because there is no Session object!  We're not running under ASP.NET so we'll have to mock out the Session object.  MVC Contrib TestHelper to the rescue!

Since we'll be using this controller throughout all of these tests, let's create a single method (DRY) to create the controller and mock out the supporting ASP.NET objects:

private static HomeController CreateController()
{
    TestControllerBuilder builder = new TestControllerBuilder();
    return builder.CreateController<HomeController>();
}

That's it!  The HomeController returned by this method has mocked implementations of all of the major ASP.NET objects (Session, Request, Response, etc…).  Let's change our unit test to use the CreateController method:

[TestMethod]
public void Speaker_WithoutSessionData_Returns_EmptyModel()
{
    var controller = CreateController();
 
    var result = (ViewResult)controller.Speaker();
    var info = (SpeakerInfo)result.ViewData.Model;
 
    Assert.IsNull(info.FirstName);
    Assert.IsNull(info.LastName);
}

Re-run the test and now we're green!  Let's move to the next test.

Test #2

This test makes sure that if the Session does contain a SpeakerInfo object, we return that SpeakerInfo object.  This handles the case where a speaker submits one talk and then starts over again to submit another talk – their personal information will be re-displayed.  Let's write our test.  Remember, since we've got a fully mocked Session object, we can access it like a regular object and place things in it directly:

[TestMethod]
public void Speaker_WithSessionData_Returns_PopulatedModel()
{
    var controller = CreateController();
    controller.Session[SessionKeys.SpeakerInfoKey] = new SpeakerInfo {FirstName = "Bob", LastName = "Smith"};
 
    var result = (ViewResult)controller.Speaker();
    var info = (SpeakerInfo)result.ViewData.Model;
 
    Assert.AreEqual("Bob", info.FirstName);
    Assert.AreEqual("Smith", info.LastName);
}

If we run this test, we're already green.  That's because our SpeakerInfo property we implemented earlier already had the logic for both saving and retrieving a SpeakerInfo instance from the Session.

Test #3

This tests make sure we return back to the Speaker action and display errors if the user only entered their first name.  Let's start with our test:

[TestMethod]
public void Data_Posted_Without_LastName_Returns_Error()
{
    var controller = CreateController();
    var result = (RedirectToRouteResult)controller.Speaker("jim", "");
    var info = (SpeakerInfo)controller.Session[SessionKeys.SpeakerInfoKey];
 
    Assert.AreEqual("jim", info.FirstName);
    Assert.AreEqual("", info.LastName);
    Assert.AreEqual(1, controller.ModelState.Count);
    Assert.IsTrue(controller.ModelState.ContainsKey("lastName"));
    result.AssertActionRedirect().ToAction<HomeController>(c => c.Speaker());
}

Take a look at that last line.  That's an extension method in the MVC Contrib TestHelper library.  By utilizing lambdas, it gives us a very nice way of asserting that the RedirectToRouteResult is going to a specific controller and action.  Without it you'd need to write code like:

Assert.AreEqual("Speaker", result.RouteValues["action"]);
Assert.AreEqual("Home", result.RouteValues["controller"]);

Magic strings?  Yuck!  No refactor support?  Double-yuck!  That AssertActionRedirect extension methods kicks butt!

We now have code that won't compile because we don't have a "SpeakerInfo" method that accepts a first name and last name.  Let's get our code compiling by implementing the bare-minimum production code:

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Speaker(string firstName, string lastName)
{
    return null;
}

NOTE: We added the AcceptVerbs attribute on the method since we only want this method called via a <form> POST.  Also notice that we're taking advantage of the ASP.NET binder and we don't need to dig around the Request.QueryString or Request.Form variables – as long as the Request data coming in contains a parameter called "firstName" and one called "lastName", it will populate those parameters when calling our method.

Now we're compiling – but we're also seeing red in our unit test.  That's because our expectation is that we'll save the information we're given (first name) in the Session object.  But even if we implement that, our other expectation is that we're going to add a model error to the ModelState property for the missing last name.  Let's implement those behaviors now.

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Speaker(string firstName, string lastName)
{
    this.SpeakerInfo = new SpeakerInfo { FirstName = firstName, LastName = lastName };
    if (String.IsNullOrEmpty(lastName))
    {
        ModelState.AddModelError("lastName", "Last Name is Reqiured.");
    }
 
    return this.RedirectToAction(c => c.Speaker());
}

Run out tests and we're green!  We've now written 3 of our 5 unit tests and all of them pass.

Test #4

This one is the same as the last one, but handles the case where the first name is not entered by the user.  Remember, test first!

[TestMethod]
public void Data_Posted_Without_FirstName_Returns_Error()
{
    var controller = CreateController();
    var result = (RedirectToRouteResult)controller.Speaker("", "jones");
    var info = (SpeakerInfo)controller.Session[SessionKeys.SpeakerInfoKey];
 
    Assert.AreEqual("", info.FirstName);
    Assert.AreEqual("jones", info.LastName);
    Assert.AreEqual(1, controller.ModelState.Count);
    Assert.IsTrue(controller.ModelState.ContainsKey("firstName"));
    result.AssertActionRedirect().ToAction<HomeController>(c => c.Speaker());
}

The code compiles fine but when we run it, we'll see red.  We haven't implemented any logic to check for a blank first name.  Let's do that now to our existing Speaker(string,string) method:

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Speaker(string firstName, string lastName)
{
    this.SpeakerInfo = new SpeakerInfo { FirstName = firstName, LastName = lastName };
    if (String.IsNullOrEmpty(lastName))
    {
        ModelState.AddModelError("lastName", "Last Name is Reqiured.");
    }
    if (String.IsNullOrEmpty(firstName))
    {
        ModelState.AddModelError("firstName", "First Name is Reqiured.");
    }
 
    return this.RedirectToAction(c => c.Speaker());
}

Our implementation is complete and now we've got a passing test.

But looking this over, it seems we're missing a test.  What if both first name AND last name are empty?  One of the nice things about test-driven-development is that things like this (a missing test) can come out during the process.  Let's make sure our code will handle this by writing a unit tests.  Granted, we can all probably tell from reading the code (all 6 lines of it – not including braces!) that the code will handle it, but we still need to write the test to make sure no one breaks this expectation in the future if they modify the code.

Our test case will look very similar to #3 and #4, except both first and last name should be blank and we'll be expecting two errors in ModelState:

[TestMethod]
public void Data_Posted_Blank_Returns_Error()
{
    var controller = CreateController();
    var result = (RedirectToRouteResult)controller.Speaker("", "");
    var info = (SpeakerInfo)controller.Session[SessionKeys.SpeakerInfoKey];
 
    Assert.AreEqual("", info.FirstName);
    Assert.AreEqual("", info.LastName);
    Assert.AreEqual(2, controller.ModelState.Count);
    Assert.IsTrue(controller.ModelState.ContainsKey("firstName"));
    Assert.IsTrue(controller.ModelState.ContainsKey("lastName"));
    result.AssertActionRedirect().ToAction<HomeController>(c => c.Speaker());
}

Run the test: Green!  Only one more test to go!

Test #5

This test ensures that if the speaker provides both their first name and last name, we'll save that info into the Session object and re-direct them to the next phase of the submission wizard – the "SessionDetails" method:

[TestMethod]
public void Data_Posted_To_Speaker_Saves_To_Session_and_Redirects()
{
    var controller = CreateController();
 
    var result = (RedirectToRouteResult)controller.Speaker("jon", "jones");
    result.AssertActionRedirect().ToAction<HomeController>(c => c.SessionDetails());
 
    var info = (SpeakerInfo)controller.Session[SessionKeys.SpeakerInfoKey];
    Assert.AreEqual("jon", info.FirstName);
    Assert.AreEqual("jones", info.LastName);
}

Compiling this fails as we don't have a SessionDetails() method yet.  Let's add one to our controller so we can run the test:

public ActionResult SessionDetails()
{
    return View();
}

Compile and run the test.  Red!  Remember the last line of Speaker(string,string) is always re-directing back to Speaker.  We haven't had a need for it to do anything else until now.  So now it's time to refactor the Speaker(string,string) method that that we'll only redirect back to Speaker if there's any errors.  Otherwise, we'll re-direct to the SessionDetails action:

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Speaker(string firstName, string lastName)
{
    this.SpeakerInfo = new SpeakerInfo { FirstName = firstName, LastName = lastName };
    if (String.IsNullOrEmpty(firstName))
    {
        ModelState.AddModelError("firstName", "First Name is Reqiured.");
    }
    if (String.IsNullOrEmpty(lastName))
    {
        ModelState.AddModelError("lastName", "Last Name is Reqiured.");
    }
 
    if (ModelState.Count != 0)
    {
        return this.RedirectToAction(c => c.Speaker());
    }
 
    return this.RedirectToAction(c => c.SessionDetails());
}

Compile and run the test.  Green!  Now re-run all of the tests.  Green!

Conclusion

If you've stuck around this long – Thank You!  This has been a long post, but we started with no code and ended up developing an ASP.NET MVC Controller action and a full set of automated unit tests.  We've taken advantage of the MVC Contrib project to help make the tests easier to write.  We have complete code-coverage in our tests of all of the expectations on the "Speaker" method.  We can move forward developing the code and we have confidence that further refactorings won't break our code since we have our unit tests to verify everything!

Posted by PSteele | 5 comment(s)

Know Your Environment!

This is probably one of the most embarrassing things I've admitted to in public (well, maybe not – but close).  I really had to think about whether I wanted to post this.  The mentor in me said "You need to post this.  Others may run into this situation and this will help them."  But the rest of me was saying "You can't admit to that!" The mentor in me won out and I'm posting this in the hopes it may save someone else a few hours of headaches.

So I developed a small web site using ASP.NET MVC for a client.  Everything installed fine.  There were a few parameters in global.asax.cs they could tweak if they needed to.  So after some initial testing, they tweaked said parameters and when viewing the website, they still got the old data.  Hmmm…  Ok, so I recycled the app pool that the site was using.  Still seeing old data.  Ok, let's just do an iisreset.  Still seeing old data!  So I play around for an hour or so checking out permissions and finally get frustrated enough that I just reboot the machine.

After a reboot, I viewed the site and WAS STILL SEEING THE OLD DATA!  Whoa!  I was blown away.  How can I get IIS to pick up the new values?  They don't even exist in the global.asax.cs file any more – where could they be coming from?

After a couple more frustrating hours, I suspected that there was a problem in one of my DLL's – or something weird like that.  As soon as I looked at the bin directory and saw the list of files, it hit me:

ASP.NET MVC is a totally compiled environment.  You can't just change a .cs file that is sitting on your website and let the worker process recompile the website.

I had tweaked code-behind .cs files so often in webforms it was just natural to do it.  Need to do a quick change on the client's machine?  No problem -- just pull up the code-behind in Notepad and make the tweak.  On the next view of the website it's all re-compiled and you're good to go.  But MVC is different.  The code-behind stuff is all compiled into a DLL and placed in the bin directory.  No matter what change you do to the code-behind files (like global.asax.cs!), the site will continue to use the compiled DLL.

Needless to say, I did a quick refactor so the parameters the client may want to change are now in web.config.  A change in that still causes the worker process to re-cycle and pick up the changes.

So make sure you don't carry-over any old habits from webforms into your MVC development.  Your sanity will thank you!

Technorati Tags: ,,
Posted by PSteele | 3 comment(s)
Filed under: ,

Lansing Day of .NET 2009

Last Saturday (August 1st) was Lansing's Day of .NET.  The guys organizing this did a great job and I had a really fun time.  I gave my "Intro to ASP.NET MVC" presentation.  I got some great questions during the presentations as well as good feedback.  I did have a few questions that I wanted to follow up on:

Using WebForms Controls in ASP.NET MVC

I mentioned that it should be possible to re-use ASP.NET Webforms controls if they don't rely on ViewState.  I've never done it before, but knowing how web forms controls do their rendering, I thought it would be quite possible to re-use WebForms controls as simply generators of HTML.  I did a few Google searches and found out it's incredibly easy.

All you need to do is register the assembly that contains the user control.  In this example, I registered the System.Web assembly and the System.Web.UI.WebControls namespace so I could access the WebForms <asp:Table> control.  I used the tag prefix of "wc" to indicate a WebForms control:

<%@ Register Assembly="System.Web" Namespace="System.Web.UI.WebControls" TagPrefix="wc" %>

Now using the controls in the System.Web.UI.WebControls namespace is as easy as it was in WebForms.  Here's a sample for one of my MVC views:

    <wc:Table runat="server" id="wfpan">
        <wc:TableRow>
            <wc:TableHeaderCell>Hdg1</wc:TableHeaderCell>
            <wc:TableHeaderCell>Hdg2</wc:TableHeaderCell>
        </wc:TableRow>
        <wc:TableRow>
            <wc:TableCell>Cell Data</wc:TableCell>
            <wc:TableCell>More Cell Data</wc:TableCell>
        </wc:TableRow>
    </wc:Table>

Again, if the control relies on ViewState for any behaviors, this probably won't work too well.  But you can at least utilize it as a simple HTML generator.

Eliminating "magic strings"

Many people who first see ASP.NET MVC code notice redirect code like this:

return RedirectToAction("Index");

This redirects to the "Index" method of the current controller.   The problem is that if you rename your Index method, this string will not get updated (well, this assumes you're not using ReSharper which would fix the string too – but that's a different story).  If you hate magic strings as much as I do, check out the MvcContrib project on CodePlex.  They've got some extension methods that let you use lamdas to express your redirects:

return this.RedirectToAction(c => c.Index());

If you need to redirect to a different controller, you can specify a controller type in the call:

return this.RedirectToAction<AccountController>(c => c.LogOn());

Very cool!  No more magic strings!  For more information, see the MvcContrib project and this blog post.

Sample Project Code

Finally, I've had a few people asking for the sample code I wrote during the presentation.  Since I didn't get into as much detail as I wanted during the presentation, I'll ZIP up a reference project I used to create the presentation and make that available for download.  It should be up by end of day today.

Technorati Tags: ,,
Posted by PSteele | 2 comment(s)
More Posts