Development Blog

 Monday, October 15, 2007

So everyone's been talking about the ALT.NET conference, Microsoft's take on MVC, and such, so I thought I'd briefly chime in with my thoughts. Photo 18 First of all, the ALT.NET conference rocked and I'm very much looking forward to the next one. Secondly, I must disclose that Scott Bellware paid me to say that ($1, signed) but I would have said it anyway.

OK, so then there's the whole MVC thing. This too was awesome and I should try and collect a signed dollar from Scott Guthrie. It's obvious he and the team working on MVC are doing their research. That said, as many of you know we're a MonoRail shop here. We've been using it heavily for about a year now and we've got a lot of code written on it. So what will the impending release of the MS MVC framework mean for us? What will it mean for MonoRail?

I can tell you that for us, it will very likely mean a week of exploration and quite possibly followed by a week or two of porting. That's right, it looked that good. Everything about it was done right or flexible enough to be made right so there really isn't any room to complain about it. MonoRail has more features at the moment, but the ones we use wouldn't be difficult to implement on top of MS MVC.

So what does this mean for MonoRail? I can't speak to that, but I will tell you what I would like for it to mean. What I'd like to see is MonoRail become more like Rails. I want to see something built on top of MS MVC that even more-so favors Convention over Configuration--including but not limited to generators and such. I want it to take it to the next level and be exactly what the community wants for a C# web platform. Dave Laribee says that "MonoRail will remain a viable option for smaller or more edgy shops." I think that's true, but I want to see it built on top of MS MVC.

Why throw away all of the framework code built into MonoRail? A few reasons:

  1. Castle has already been experimenting with throwing most of it away and starting over anyway.
  2. MS MVC will be built into the .NET Framework. This means  easier sells to big shops, and MonoRail would be just a free supplement rather than a replacement/paradigm shift, making it an easier sell.
  3. MS MVC appears to be more modular. There is no massive Controller or ginormous SmartDispatcherController. You can even start with a one method interface and implement your controller however you want.
  4. Routing.
  5. It was built with testability in mind. MonoRail is now mostly testable, but it is not nearly as clean as it could be... or as MS MVC's testability looks.
  6. We can probably all but throw away our codegenerator and use lambdas. We could reinvent this for MonoRail in .NET 3.5 but then we'd be... reinventing... it...

Whatever we decide to do when it comes out, we'll talk about our experiences so you can judge for yourself after we judge for ourselves how viable it will be to port from MonoRail to MS MVC.

by Aaron on Monday, October 15, 2007 10:59:24 PM (Pacific Standard Time, UTC-08:00)  #    Disclaimer  |  Comments [10]  |  Trackback
 Wednesday, April 11, 2007

We were on the phone today with Scott Bellware and he asked us if we could toss together a simple MonoRail project that showed the Castle.Tools.CodeGenerator stuff in action. A few other people have been asking us to do something similar for the other things we've been discussing on our blog. So, Aaron and I sat down and pair-programmed out some source that does just that. It shows several things:

  • An Action/View map is generated using Castle.Tools.CodeGenerator, and used to do view renderings, and (if desired) action redirections and the like.
  • PropertyBag wrappers are created using the IDictionaryAdapterFactory code that Lee contributed.
  • We show how we test our controllers using the test fixture code Aaron has been posting about.
  • It has a small service layer that roughly shows how we architecture our code around here.
  • It uses Windsor integration, so you can see how that's used if you've been curious.

Please keep in mind this took us about an hour to whip up and we did not actually practice full TDD with it. Enjoy!

Source

by Jacob on Wednesday, April 11, 2007 5:41:13 PM (Pacific Standard Time, UTC-08:00)  #    Disclaimer  |  Comments [13]  |  Trackback
 Wednesday, March 28, 2007

Shortly after I posted my original EleutianControllerTests, Scott Bellware, Hamilton and myself chatted about the subject, and Hamilton decided to help out by making some of the previously internal methods public. Context still isn't settable directly however (maybe we should have him change that too...), so you still have to do a little bit of magic to get your context in there. There's actually more code to do this now then when I just set it w/ reflection, but it's arguably more "correct" and doesn't involve "reflection" and you can mock more (like logging and such). Anyways, here's the code:

 

using System;
using System.Collections;
using System.Collections.Specialized;
using System.IO;
using System.ComponentModel.Design;
using System.Security.Principal;
using System.Web;

using Castle.MonoRail.Framework;
using Castle.MonoRail.Framework.Internal;
using Castle.Core.Logging;
using Castle.MonoRail.Framework.Services;

using NUnit.Framework;

using Rhino.Mocks;

public class EleutianControllerTests
{
  #region Constants
  private const string ApplicationPhysicalPath = "Q:\\PhysicalPath";
  #endregion

  #region Member Data
  protected string _virtualDirectory = String.Empty;
  protected MockRepository _mocks;
  protected IRailsEngineContext _context;
  protected IRequest _request;
  protected IResponse _response;
  protected IServerUtility _serverUtility;
  protected IDictionary _session;
  protected Flash _flash;
  protected IViewEngineManager _viewEngineManager;
  protected ControllerMetaDescriptor _descriptor;
  protected NameValueCollection _parameters;
  #endregion

  #region Test Setup and Teardown Methods
  [SetUp]
  public virtual void Setup()
  {
    _mocks = new MockRepository();
    _viewEngineManager = _mocks.CreateMock<IViewEngineManager>();
    _descriptor = _mocks.CreateMock<ControllerMetaDescriptor>();
    _context = _mocks.CreateMock<MockRailsEngineContext>(_viewEngineManager, _descriptor);
    _request = _mocks.DynamicMock<IRequest>();
    _response = _mocks.DynamicMock<IResponse>();
    _serverUtility = _mocks.DynamicMock<IServerUtility>();
    _session = _mocks.DynamicMock<IDictionary>();
    _flash = new Flash();
    _parameters = new NameValueCollection();
  }

  protected void InitializeController(Controller controller, string areaName, string controllerName, string actionName)
  {
    controller.InitializeControllerState(areaName, controllerName, actionName);
    controller.InitializeFieldsFromServiceProvider(_context);

    InitializeRailsContext(areaName, controllerName, actionName);
  }

  protected void InitializeRailsContext(string areaName, string controllerName, string actionName)
  {
    SetupResult.For(_context.UrlInfo).Return(
      new UrlInfo("eleutian.com", "www", _virtualDirectory, "http", 80,
                  Path.Combine(Path.Combine(areaName, controllerName), actionName), areaName, controllerName,
                  actionName, "rails"));
    SetupResult.For(_context.Server).Return(_serverUtility);
    SetupResult.For(_context.Flash).Return(_flash);
    SetupResult.For(_context.ApplicationPath).Return("/");
    SetupResult.For(_context.Request).Return(_request);
    SetupResult.For(_context.Response).Return(_response);
    SetupResult.For(_context.ApplicationPhysicalPath).Return(ApplicationPhysicalPath);
    SetupResult.For(_request.Params).Return(_parameters);
    SetupResult.For(_context.Session).Return(_session);
  }
  #endregion
}

public abstract class MockRailsEngineContext : IRailsEngineContext
{
  #region Member Data
  private IViewEngineManager _viewEngineManager;
  private IControllerDescriptorProvider _controllerDescriptorProvider;
  #endregion

  #region Properties
  public abstract void Transfer(string path, bool preserveForm);
  public abstract string RequestType { get; }
  public abstract string Url { get; }
  public abstract string UrlReferrer { get; }
  public abstract HttpContext UnderlyingContext { get; }
  public abstract NameValueCollection Params { get; }
  public abstract IDictionary Session { get; }
  public abstract IRequest Request { get; }
  public abstract IResponse Response { get; }
  public abstract ITrace Trace { get; }
  public abstract ICacheProvider Cache { get; }
  public abstract Flash Flash { get; }
  public abstract IPrincipal CurrentUser { get; set; }
  public abstract Exception LastException { get; set; }
  public abstract string ApplicationPath { get; }
  public abstract string ApplicationPhysicalPath { get; }
  public abstract UrlInfo UrlInfo { get; }
  public abstract IServerUtility Server { get; }
  public abstract IDictionary Items { get; }
  public abstract Controller CurrentController { get; set; }
  #endregion


  #region Constructors
  public MockRailsEngineContext(IViewEngineManager viewEngineManager, ControllerMetaDescriptor descriptor)
  {
    _viewEngineManager = viewEngineManager;
    _controllerDescriptorProvider = new ControllerDescriptorProviderStub(descriptor);
  }
  #endregion

  #region Methods
  public abstract void AddService(Type serviceType, object serviceInstance);
  public abstract void AddService(Type serviceType, object serviceInstance, bool promote);
  public abstract void AddService(Type serviceType, ServiceCreatorCallback callback);
  public abstract void AddService(Type serviceType, ServiceCreatorCallback callback, bool promote);
  public abstract void RemoveService(Type serviceType);
  public abstract void RemoveService(Type serviceType, bool promote);
  public object GetService(Type serviceType)
  {
    if (typeof(IViewEngineManager).Equals(serviceType))
    {
      return _viewEngineManager;
    }
    else if (typeof(IControllerDescriptorProvider).Equals(serviceType))
    {
      return _controllerDescriptorProvider;
    }
    else if (typeof(ILoggerFactory).Equals(serviceType))
    {
      return null;
    }
    else if (typeof(IUrlBuilder).Equals(serviceType))
    {
      return new DefaultUrlBuilder();
    }

    return null;
  }
  #endregion

  #region Classes
  private class ControllerDescriptorProviderStub : IControllerDescriptorProvider
  {
    private ControllerMetaDescriptor _descriptor;

    public ControllerDescriptorProviderStub(ControllerMetaDescriptor descriptor)
    {
      _descriptor = descriptor;
    }

    public ControllerMetaDescriptor BuildDescriptor(Controller controller)
    {
      return _descriptor;
    }

    public ControllerMetaDescriptor BuildDescriptor(Type controllerType)
    {
      return _descriptor;
    }

    public void Service(IServiceProvider provider)
    {
    }
  }
  #endregion
}

Source
by Aaron on Wednesday, March 28, 2007 2:51:30 PM (Pacific Standard Time, UTC-08:00)  #    Disclaimer  |  Comments [6]  |  Trackback
 Tuesday, March 27, 2007

Update: Hamilton committed my patch, so if you're running off trunk you won't need to do this anymore. Thanks Hamilton!

Currently MonoRail uses ResourceSets wrapped up in an implementation of IResource to provide Resources to views. Unfortunately, ResourceSets do not cascade when it comes to resource resolution.  ResourceManager.GetResourceSet does have a flag called tryParents, but all that does is try less and less specific cultures until it finds a match. In other words, if you have two resource files:
Foo.ko.resx: defines A, B
Foo.resx: defines A, B, C

If you locale is ko-KR, calling RseourceManager.GetResourceSet will yield a ResourceSet that maps to Foo.ko.resx, so asking for B will work, but asking for C will not. A ResourceSet only knows about itself.

ResourceManagers on the other hand, are perfectly capable of handling this cascade, which is quite necessary in at least our localized app, as we do not want to have to define strings in all languages for everything. So with ResourceManagers, asking for A, B, and C all behave as expected, preferring the most specified culture and cascading down as necessary.

Below is an implementation of an IResourceFactory that spits out wrapped ResourceManagers instead of ResourceSets.

To use it you'll need to add this to your web.config:

<monorail>
  <services>
    <service id="ResourceFactory" type="Eleutian.Shared.MonoRail.ResourceManagerFactory, Eleutian.Shared" />
  </services>
</monorail>

Source

by Aaron on Tuesday, March 27, 2007 4:13:12 AM (Pacific Standard Time, UTC-08:00)  #    Disclaimer  |  Comments [0]  |  Trackback
 Thursday, March 22, 2007

In a previous post I talked about how we use automatically generated interfaces to wrap our PropertyBag's. You can also use this technique in your ViewComponents to clean up that code quite a lot. We used to have code like this:

public class SomeComponent : ViewComponent
{
  public override void Initialize()
  {
    base.Initialize();
    User user = (User)this.ComponentParams["User"];
    float value = this.Service.CalculateThatValue(user);
    this.ComponentParams["SomeCalculatedValue"] = value;
  }
}

Now we can make an interface:

public interface ISomeComponentView
{
  User User { get; set; }
  float SomeCalculatedValue { get; set; }
}

And ask the code we wrote for the PropertyBag to give us a hand in cleaning things up:

public class SomeComponent : ViewComponent
{
  public override void Initialize()
  {
    base.Initialize();
    ISomeComponentView view = this.ViewFactory.ResolveView<ISomeComponentView>(this.ComponentParams);
    float value = this.Service.CalculateThatValue(view.User);
    view.SomeCalculatedValue = value;
  }
}

Much better! If you're looking for an implementation of that ViewFactory service, Lee Henson tossed one together shortly after our post. He's also added some other, pretty nitfy features since then. Like the ability to specify a prefix with an attribute for the generated dictionary keys. As he mentions in his post, you can use it for anything (and probably should) that uses strings as keys into an IDictionary (Session, Flash, etc..). I'm planning on checking it in to CastleContrib soon, until then, grab it off the list. Thanks Lee!

by Jacob on Thursday, March 22, 2007 9:21:27 AM (Pacific Standard Time, UTC-08:00)  #    Disclaimer  |  Comments [0]  |  Trackback
 Friday, March 16, 2007

Last week, Aaron and I were hanging out with a bunch of the Microsoft MVP's that were up in Seattle for the MVP Summit. We had an absolute blast and got to meet some excellent developers. It was a great opportunity to get a sense for how others in our situation were handling similar problems and to share things that we have all learned along the way. We had a few beers at the Party with Palermo event and the following day a few of the guys dropped by the Eleutian Seattle offices and we threw together a podcast with Scott Hanselman. It's a very quick introduction to MonoRail for those that aren't familiar. We go on to discuss testability and IoC in the context of the Castle project. It was a great chance to expose MonoRail and the concepts behind it to a wider audience.

In no particular order, here are some blogs if you're interested in some good reading on various areas of software development:

  • Scott Bellware - Development practices, C# 3.0, Ruby, and more. His company, Dovetail, is looking into starting development using MonoRail. They are hiring.
  • Jeremy Miller - Agile, especially TDD, IoC, etc...  Jeremy wrote StructureMap, an IoC framework. So, he definitely knows the why behind a lot of the agile practices.
  • Scott Hanselman - Anything and everything development and technology. I always find something new here. I'm sure you will also.

I'm glad to see some of the news that came out of the summit. I'm sure one of us will be back to expound on them once we've done a little more research and tinkering.

by Jacob on Friday, March 16, 2007 10:56:57 PM (Pacific Standard Time, UTC-08:00)  #    Disclaimer  |  Comments [0]  |  Trackback
 Sunday, February 25, 2007

Roy Osherove brings up several good points with regards to the preached design methodology of old and the new testable design some of us have come to know and love lately.

I think that TDD really helps to eliminate a large number of these issues. If you test everything yourself, then inherently your API is testable. Unfortunately, as Jonathan Cogley points out, TDD is not yet mainstream.

A nice side effect of having a testable API or application is that often times your code is also extensible. The thing that blocks it from being fully testable and extensible are those darn sealed and internal keywords, and of course, not making things virtual that really should be. Even open source projects are guilty of not having testable or extensible enough APIs. Yes, Castle MonoRail provides AbstractMRTestCase, but that actually uses ASP.NET and is way more heavy than I like.

Here is our base class for Controller Tests. This allows you to unit test nearly every aspect of your controller, without ever hitting ASP.NET. Notice the lovely and necessary use of reflection? In order to mock the context, you'll want to call InitializeController and pass it the controller you're testing, but be sure to do it in your _mocks.Unordered block (we almost ALWAYS use mocks.Unordered... it makes tests more readable and ensures that you're tests aren't too tightly coupled with your implementation, but that's the subject for another post.)

  public class EleutianControllerTests
  {
    #region Constants
    private const string ApplicationPhysicalPath = "Q:\\PhysicalPath";
    #endregion

    #region Member Data
    protected string _virtualDirectory = String.Empty;
    protected MockRepository _mocks;
    protected IControllerServices _controllerServices;
    protected IRailsEngineContext _context;
    protected IRequest _request;
    protected IResponse _response;
    protected IServerUtility _serverUtility;
    protected IDictionary _session;
    protected Flash _flash;
    protected NameValueCollection _parameters;
    #endregion

    #region Test Setup and Teardown Methods
    [SetUp]
    public virtual void Setup()
    {
      _mocks = new MockRepository();
      _context = _mocks.CreateMock<IRailsEngineContext>();
      _request = _mocks.DynamicMock<IRequest>();
      _response = _mocks.DynamicMock<IResponse>();
      _serverUtility = _mocks.DynamicMock<IServerUtility>();
      _session = _mocks.DynamicMock<IDictionary>();
      _flash = new Flash();
      _parameters = new NameValueCollection();
    }

    protected void InitializeController(Controller controller, string areaName, string controllerName, string actionName)
    {
      BindingFlags bindingFlags = BindingFlags.FlattenHierarchy | BindingFlags.Instance | BindingFlags.NonPublic |
                                  BindingFlags.InvokeMethod;

      MethodInfo method = controller.GetType().GetMethod("InitializeControllerState", bindingFlags);
      method.Invoke(controller, new object[] { areaName, controllerName, actionName });

      FieldInfo field = controller.GetType().GetField("context", bindingFlags);
      field.SetValue(controller, _context);

      field = controller.GetType().GetField("serviceProvider", bindingFlags);
      field.SetValue(controller, _context);

      InitializeRailsContext(areaName, controllerName, actionName);
    }

    protected void InitializeRailsContext(string areaName, string controllerName, string actionName)
    {
      Expect.Call(_context.UrlInfo).Return(
        new UrlInfo("eleutian.com", "www", _virtualDirectory, "http", 80,
                    Path.Combine(Path.Combine(areaName, controllerName), actionName), areaName, controllerName,
                    actionName, "rails")).Repeat.Any();
      Expect.Call(_context.GetService(typeof(IUrlBuilder))).Return(new DefaultUrlBuilder()).Repeat.Any();
      Expect.Call(_context.Server).Return(_serverUtility).Repeat.Any();
      Expect.Call(_context.Flash).Return(_flash).Repeat.Any();
      Expect.Call(_context.ApplicationPath).Return("/").Repeat.Any();
      Expect.Call(_context.Request).Return(_request).Repeat.Any();
      Expect.Call(_context.Response).Return(_response).Repeat.Any();
      Expect.Call(_context.ApplicationPhysicalPath).Return(ApplicationPhysicalPath).Repeat.Any();
      Expect.Call(_request.Params).Return(_parameters).Repeat.Any();
      Expect.Call(_context.Session).Return(_session).Repeat.Any();
    }
    #endregion
  }

Oh, and back to the original point of the post. Please, please don't mark things internal or sealed unless you have a very good reason to do it. And no, I don't think that "We can't afford to support it" is a good enough reason. Also, "We're afraid the user might break something" is DEFINITELY not a good enough reason. We know what we're getting into when we extend APIs... we're all developers here.

by Aaron on Sunday, February 25, 2007 4:16:47 PM (Pacific Standard Time, UTC-08:00)  #    Disclaimer  |  Comments [4]  |  Trackback
 Tuesday, February 20, 2007

I spent yesterday and today refactoring the code for the Controller Action/View map generator tool. I made some improvements, wrote some tests and I'm pleased to announce the first release. I saw Brian had implemented his as an MsBuild task and I liked that idea, so that's how ours runs now. First, things aren't as "drop in and go" as I would like because it's still evolving in and around our, already large, code base. It's my hope that over time the tool evolves as peoples needs change, etc... we can add some more configuration, that sort of thing. I'm more than willing to hear suggestions and such. Anway, here we go...

So in order to get yourself up and running, you'll need to do a few things. First, you'll need to add the task to your MsBuild. Simply open your csproj and add the following:

<UsingTask TaskName="Eleutian.Tools.CodeGenerator.MsBuild.GenerateMonoRailSiteTreeTask" AssemblyFile="Eleutian.Tools.CodeGenerator.dll" />
<ItemGroup>
  <ViewSources Include="../YourViewsDirectory/**/*.brail">
    <InProject>false</InProject>
  </ViewSources>
</ItemGroup>
<ItemGroup>
  <ControllerSources Include="Controllers/**/*.cs">
    <InProject>false</InProject>
  </ControllerSources>
</ItemGroup>
<Target Name="BeforeBuild" Inputs="@(Compile)" Outputs="$(ProjectDir)\SiteMap.generated.cs">
  <GenerateMonoRailSiteTreeTask File="SiteMap.generated.cs" Namespace="YourNamespace.SiteMap" ControllerSources="@(ControllerSources)" ViewSources="@(ViewSources)">
  </GenerateMonoRailSiteTreeTask>
</Target>

You'll notice a few things in here. ViewSources is just the collection of view files (be it .vm or .brail, etc..). It will use the paths of those files to add the View nodes into the map. ControllerSources is the C# source files for your controllers only, specifying this helps speed up parsing. I'll get to why in a bit. And for the Sources attribute on the task you should specify all the sources in the assembly, which @(Compile) works great for.

With the task added, you should be able to build and have a SiteMap.generated.cs file, now you'll need to tweak your "base" Controller class. If you're like us, you have a Controller class that you made yourself that all of your Controller's inherit from. In that class you'll need to add some code like the following:

private ICodeGeneratorServices _services;

public ICodeGeneratorServices CodeGeneratorServices
{
  get { return _services; }
}

public RootAreaNode Site
{
  get { return new RootAreaNode(this.ControllerServices); }
}

protected virtual void PerformGeneratedInitialize()
{
  _services = new DefaultCodeGeneratorServices(
    new DefaultControllerReferenceFactory(),
    new AspDotNetRedirectService()
  );
  _services.Controller = this;
}

First is the member variable, _services. You can do this however you like, but it's consumed by the ControllerAction/ViewReference classes to do various likes. CodeGeneratorServices is a property the partial controller code uses to create the MyActions and MyViews node instances. Site is just an easy way to get the top, root level node. PerformGeneratorInitialize is overridden in the partial controller classes to add the MyViews and MyActions nodes to the PropertyBag so they are available in the views. So there you go. If I'm not missing anything, that should do the trick. Let me know if I did and I'll append to this post.

Oh, the reason for specifying the ControllerSources is to cut down on the number of classes/types we visit when generating the internal "tree" that the source code is generated from. At first glance it seems like you should just be able to parse the ControllerSources only. We coudln't really do that because we needed to gather information on the other types in the assembly so we'd be able to use them in the arguments of our actions, etc... 

I have to stress that the code is relatively new, having undergone a major refactor to isolate it from the rest of our project. I make no promises that I won't have to upload a new copy, with a fix. :)

Source and Binaries

by Jacob on Tuesday, February 20, 2007 6:36:46 PM (Pacific Standard Time, UTC-08:00)  #    Disclaimer  |  Comments [4]  |  Trackback
 Tuesday, January 30, 2007

I figured I might as well continue on the string literal kick I started with my last post and talk about another situation where we've eliminated string literals. Take the following code from inside a MonoRail action method:

PropertyBag["User"] = ourUser;
PropertyBag["TwoStates"] = new string[] { "WA", "CA" };
PropertyBag["IsAdmin"] = true;

String literals, used as keys into dictionaries carry a cetain degree of code smell (for us) and we try to avoid them. It reminds me too much code that does something similar, but in the opposite direction:

int id = Int32.Parse(this.Params["id"])

Thankfully, this kind of code is eliminated when using MonoRail and its SmartDispatchController. We found ourselves thinking of how the same code would look in a SWF application. We would be populating views, only those views would be interfaces that the various forms/controls implemented. Well, this is exactly what we wanted in MonoRail, to wrap the PropertyBag with an interface!

After a few hours playing with Reflection.Emit, I had a code generator that would take an interface:

public interface IEditMyProfileView
{
  string Name { get; set; }
  DateTime Birthday { get; set; }
  IList<TimeZone> TimeZones { get; set; }
}

And produce a class like the following:

public class EditMyProfileViewPropertyBagManipulator : IEditMyProfileView {
  private IDictionary _bag;

  public EditMyProfileViewPropertyBagManipulator(IDictionary bag) {
    _bag = bag;
  }

  public string Name {
    get { return (string)_bag["Name"]; }
    set { _bag["Name"] = value; }
  }
  public DateTime Birthday {
    get { return (DateTime)_bag["Birthday"]; }
    set { _bag["Birthday"] = value; }
  }
  public IList<TimeZone> TimeZones {
    get { return (IList<TimeZone>)_bag["TimeZones"]; }
    set { _bag["TimeZones"] = value; }
  }
}

This has the advantage of keeping our interactions with the PropertyBag as type safe as they can be. Changing the interface, breaks the build. Where as changing the type of a value inserted into the PropertyBag will (hopefully) only break tests if even that. This is another example of us trying to turn run-time failures into compile-time failures.

This kind of thing also makes tests more elegant. Instead of:

Assert.Equals("Jacob", PropertyBag["Name"]);

In our controller tests, we can continue to leverage the use of RhinoMocks to ensure the views are properly initialized:

using (_mocks.Unordered()) {
  _view.Name = "Jacob";
}
_mocks.ReplayAll();
_controller.Action();
_mocks.Verify(_view);

All we've done here is created a mock from the view interface, rather than emitting the wrapper class. Again, another added benefit is if the view changes, compiling the tests will also break. The sooner things break after a change the better, and the build is pretty soon. Although, with ReSharper, it's nice to see red squiggles appear as that's even sooner.

by Jacob on Tuesday, January 30, 2007 8:31:07 PM (Pacific Standard Time, UTC-08:00)  #    Disclaimer  |  Comments [1]  |  Trackback
 Monday, January 29, 2007

I was trying to think of a way to introduce this post, and I realize that here at Eleutian, we hate strings. We don't hate all strings, it's mostly string literals that we hate. Not even all string literals. But most of them. A good portion of the work we do to make our code more solid involves removing string literals. Anybody familiar with MonoRail has seen lines similar to the following:

RenderView("Home", "Action");
<a href="${siteRoot}/Home/Action.rails">...</a>

${UrlHelper.LinkTo("Home", "Action")}

And so on, see all those string literals that refer to things that aren't really string literals? At first glance, we can get rid of the Controller string by doing something with typeof(HomeController).Name and chopping off the Controller suffix. That'd be an ugly static method call everywhere and still leaves "Action" to be dealt with. We want things to break if HomeController changes names and we want to know where HomeController is referenced.

One of our rules is to turn potential run-time errors into potential compile time errors if at all possible.

It's funny, but the compiler is our first test, and a good one at that. We found ourselves using those controller names, area names, and action names in a variety of situations - redirections, view rendering, generating url's in the views, etc... Anytime I see this I'm annoyed:

Redirect("AnotherAction.rails?parameter=34");

or even worse:

Dictionary query = new Dictionary();
query["parameter"] = 34;
Redirect("AnotherAction.rails", query);

You'll notice that our life would be much easier if methods were first-class objects. They aren't in C# 2.0... or even in 3.0 (without lambda functions) and so this is where we are.

Aaron and I talked things over, complaining on end about how frustrating things were. Our problem boiled down to turning the controller action's into something we could reference, so we decided to try a little code generation magic. We wanted to be able to write code like the following:

MyActions.Action().Render()

Site.AdministrativeArea.Home.Action(24, "Jacob").Redirect()

And so on. In order to do this we have a tool that runs as a pre-build step that does the following:

  • Walks the source tree, parsing *.cs files, looking for controllers. We have to parse source, we can't use reflection because the same source files we're parsing will be using this generated code.
  • Look for public methods (the actions) on the controller.
  • Generates a class (HomeControllerNode) that has a method, with the same prototype as the action, that returns a ControllerActionReference:
public partial class HomeControllerNode {        
  private IControllerServices _services;

  public HomeControllerNode(IControllerServices services) {
    this._services = services;
  }

  public virtual HomeControllerViewsNode Views {
    get {
      return new HomeControllerViewsNode(this._services);
    }
  }

  [System.Diagnostics.DebuggerNonUserCodeAttribute()]
  public virtual ControllerActionReference SomeAction() {
    return this._services.ControllerReferenceFactory.CreateActionReference(this._services.Controller, typeof(HomeController),
      "Home", "Administrative", "SomeAction", new ActionArgument[0]);
  }

  [System.Diagnostics.DebuggerNonUserCodeAttribute()]
  public virtual ControllerActionReference AnotherAction(string name) {
    return this._services.ControllerReferenceFactory.CreateActionReference(this._services.Controller, typeof(HomeController), 
      "Home", "Administrative", "AnotherAction", new ActionArgument[] {
        new ActionArgument("name", typeof(string), name)});    }
  }
}
  • Generates a stub class for area's with properties for each controller in that area (they return HomeControllerNode instances) We introduce a default root area that all top level controllers and other areas are children of:
public partial class AdministrativeAreaNode {        
  private IControllerServices _services;
        
  private HomeControllerNode _home;

  public AdministrativeAreaNode(IControllerServices services) {
    this._services = services;
    this._home = new HomeControllerNode(this._services);
  }
        
  public virtual HomeControllerNode Home {
    get {
      return this._home;
    }
  }
}
  • Walks the Views subdirectory looking for *.brail files mapping them onto their controllers.
  • Generates a class (HomeControllerViewsNode) with methods for each view that return a ControllerViewReference.
public partial class HomeControllerViewsNode {        
  private IControllerServices _services;

  public HomeControllerViewsNode(IControllerServices services) {
    this._services = services;
  }

  [System.Diagnostics.DebuggerNonUserCodeAttribute()]         
  public virtual ControllerViewReference SomeAction {
    get {
      return this._services.ControllerReferenceFactory.CreateViewReference(this._services.Controller, typeof(HomeController),
        "Home", "Administrative", "SomeAction");
    }
  }
}
  • Generates a partial class for the controller class itself with two properties - MyViews and MyActions that return the ControllerNode and ControllerViewsNode instances for that controller.
public partial class HomeController {
  public virtual HomeControllerNode MyActions {
    get {
      return new HomeControllerNode(this.ControllerServices);
    }
  }
        
  public virtual HomeControllerViewsNode MyViews {
    get {
      return new HomeControllerViewsNode(this.ControllerServices);
    }
  }
        
  protected override void PerformGeneratedInitialize() {
    base.PerformGeneratedInitialize();
    this.PropertyBag["MyViews"] = this.MyViews;
    this.PropertyBag["MyActions"] = this.MyActions;
  }
}

In the generated code, we pass a reference to an IControllerServices implementation down through the hierarchy, which provides the ControllerReferenceFactory that creates the ControllerActionReference and ControllerViewReference objects. Our EleutianController class has a property on it - Site, that returns the RootAreaNode so the top of the site can be reached from anywhere. This is also always placed into the controller's PropertyBag, along with MyActions and MyViews. The PerformGeneratedInitialize method, that's called in our base class so that each controller can add it's own MyViews and MyActions, which don't live in the base class.

Our ControllerActionReference class has Redirect and Transfer methods as well as a Url property. So now we can do the following:

<a href="${Site.HomeController.Action(200, true).Url}">...</a>

ControllerViewReference has a Render method that does the appropriate RenderView call on the controller. Now we've accomplished a lot of things:

  1. No more string literals. If we rename something, the build fails. Our views will fail when they're opened and not when links are followed, it is pretty easy to test that a view compiles.
  2. All URLs for redirections/transfers are always well formed and include necessary parameters with proper type checking.
  3. Provided ourselves with Intellisense when building URLs in tests and controllers. (Sexy? Yes.)

Speaking of testing, in our controller tests we have a custom IControllerReferenceFactory that returns mock ControllerActionReference instances (via RhinoMocks) So now redirections, transfers and view renderings are just mocked method calls:

using (_mocks.Unordered())
{
  _controller.MyViews.AnotherView.Render();
}

_mocks.ReplayAll();
_controller.SomeActionThatRendersAnotherView();
_mocks.VerifyAll();

Which is much cleaner than string comparisons on _controller.SelectedViewName, especially if a redirect with parameters is expected in an action. This has simplified our life and given us incredible peace of mind, well worth the time to implement - which was relatively simple. Whew.

by Jacob on Monday, January 29, 2007 6:45:08 PM (Pacific Standard Time, UTC-08:00)  #    Disclaimer  |  Comments [1]  |  Trackback
 Saturday, January 27, 2007

Previously I mentioned that we switched to MonoRail for all of our new features. Well, MonoRail doesn't make page localization any easier than Web Forms does.  As a matter of fact, it actually breaks an important part of ASP.NET's localization (something I should probably submit a patch for, heh).  Basically, the DefaultResourceFactory in MonoRail creates IResources that wrap ResourceSets, which will not resolve resources to their parent resource. In other words, if you have Res.ko-KR.resx which defines String1 and Res.resx which defines String1 and String2, then calling GetString on a ResourceSet when your locale is ko-KR with String1 will work, but String2 will fail.  Calling GetString with any other locale, will return the String2 from Res.resx. To get around this, we created an IResourceFactory that returns IResources that wrap ResourceManagers instead of ResourceSets, which are capable of properly traversing the resource tree.  I'll probably submit this code soon. 

Anyways, in order to localize with MonoRail, you still need to create your resx file, add an attribute to your controller to map that resx file to something in your view's property bag, and create, name and initialize each string in your resx file.   

What we did was create a new generator like the one mentioned in my previous post that converts mybrail files to brail files.  All it does is convert this: 

${g.Test: Global Text} <br />
${v.ViewTest: View Text} <br />
${c.ControllerTest: Controller Text} <br />



To this: 

${g.Test} <br />
${c.Localization_ViewTest} <br />
${c.ControllerTest} <br /> 

Now a few things happened here:

  1. A file called Global.resx was created.
  2. A file called Test.resx was created (Test is the name of the controller)
  3. A resource called Test was added to Global.resx and initialized with the value "Global Text"
  4. A resource called ControllerTest was added to the Test.resx and initialized with the value "Controller Text"
  5. A resource called Localization_ViewTest (Localization is the name of the view) was added to Test.resx and initialized with the value "View Text"
  6. v.ViewTest was converted to c.Localization_ViewTest

Essentially, what you end up with, is one resx for your global strings, and one resx per controller, with view specific strings being stored in the controller resx with ViewName_ prefixed to them.  There's still one piece missing to the puzzle, which is how do the views know what g and c are? Do we attribute each controller with a g resource and a c resource? No... that would be too much work for us. Instead what we do is define a new IControllerDescriptorProvider that wraps a DefaultControllerDescriptorProvider and adds appropriate ResourceDescriptors to the Resources collection of the descriptor built by the DefaultControllerDescriptorProvider.   

There is some room for improvement, like gracefully handling the case where you define and give a value for a resource twice in the same file or two seperate files, e.g. ${g.Test: Hi} ${g.Test: Hello}.  In this case, g.Test will be Hello.  In the case where they're in seperate files, it'll depend on which one was saved last (ew).  A decent way around this would be to detect this happening and pop a message box if you try to do this.

by Aaron on Saturday, January 27, 2007 2:05:36 PM (Pacific Standard Time, UTC-08:00)  #    Disclaimer  |  Comments [0]  |  Trackback