Skip navigation

Category Archives: ASP.Net MVC

If, for whatever reason (I’m not going into this discussion), you’d like to inject your dependencies at the method level instead of using your constructor, like so:


public ActionResult Post(Whatever whatever, IMyDependency dependency)
{
	// use dependency here
}

Just create these two classes:

public class DependencyModelBinderProvider : IModelBinderProvider
{
	public IModelBinder GetBinder(Type modelType)
	{
		return modelType.IsAbstract ? new DependencyModelBinder() : null;
	}
}

public class DependencyModelBinder : IModelBinder
{
	object IModelBinder.BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
	{
		return DependencyResolver.Current.GetService(bindingContext.ModelType);
	}
}

And, of course, at bootstrap/app start time:

ModelBinderProviders.BinderProviders.Add(new DependencyModelBinderProvider());

Voilà, DI at method level in 2 minutes!

I was almost going crazy before I found out that when you use a PreApplicationStartMethodAttribute for bootstrapping your hosting environment you must point to a type that resides in the same assembly that is being decorated!

Pointing to types in a different assembly will just blow up with a YSOD saying ‘The method specified by the PreApplicationStartMethodAttribute on assembly ‘assembly name’ cannot be resolved. Type: ‘type name’, MethodName: ‘Initialize’. Verify that the type is public and the method is public and static (Shared in Visual Basic).’.

Just though I’d mention it here, there’s not to much info on this…

There’s quite some annoyence about how ASP.Net routing handles trailing slashes. I agree, it would have been nice if one could enforce whether or not to use a trailing slash.

I’ve always taken good care of my trailing slashes. Almost every link comes from my custom UrlHelper or extension methods (for convenience – e.g. Model.Product.GetThumbnailUrl()). Anyway, that doesn’t mean it stopped bugging me! For one, external links (SEO) are not under your control, I don’t really know how serious this duplicate content issue is, but I’m not willing to take a chance on it. Another unsolved annoyence is when you add content pages. You always have to think twice about trailing slashes in every little link you include.

This week, I thought it was about time to go for a more generic solution. I started by googling for it and almost ended up using the new IIS7 URL Rewrite Module just like Scott suggested in his post. The main reason I didn’t was because I hate managing things in two places. Especially when their so closely related.

And then I thought: Why not let the system do what you ask it to do? How hard can it be to make the system respect you route mappings? If I map a route with url "{category}/" or "{category}/{product}" I want to make sure category urls end with a slash and products don’t. That simple.

It seemed to me that tuning ASP.Net MVC’s UrlRoutingModule was the most abvious thing to do, so here’s what I’ve done:

public class UrlRoutingModule : System.Web.Routing.UrlRoutingModule
{
	private const string SLASH = "/";

	public override void PostResolveRequestCache(HttpContextBase context)
	{
		var routeData = RouteCollection.GetRouteData(context);

		var redirecturi = GetRedirectUri(routeData, context.Request.Url);

		if (redirecturi != null)
		{
			context.Response.RedirectPermanently(redirecturi);
			return;
		}

		base.PostResolveRequestCache(context);
	}

	private static Uri GetRedirectUri(RouteData routeData, Uri requestedUri)
	{
		if (routeData == null) return null;

		var route = routeData.Route as Route;
		if (route == null) return null;

		var requestedSegments = GetSegmentCount(requestedUri.AbsolutePath);
		var mappedSegements = GetSegmentCount(route.Url);

		if (requestedSegments == mappedSegements)
		{
			var slashRequired = route.Url.EndsWith(SLASH);

			if (slashRequired && !requestedUri.AbsolutePath.EndsWith(SLASH))
				return requestedUri.Append(SLASH);

			if (!slashRequired && requestedUri.AbsolutePath.EndsWith(SLASH))
				return requestedUri.TrimPathEnd(SLASH[0]);
		}
		else if (!requestedUri.AbsolutePath.EndsWith(SLASH)) // requestedSegments < mappedSegements
		{
			return requestedUri.Append(SLASH);
		}

		return null;
	}

	private static int GetSegmentCount(string path)
	{
		return path.Split(SLASH.ToCharArray(), StringSplitOptions.RemoveEmptyEntries).Length;
	}
}

Note: If your route ends with optional segments, urls that don’t include them always end up with a trailing slash.

Another note: The Append and TrimPathEnd are Uri extension methods:

public static class UriExtensions
{
	public static Uri Append(this Uri uri, string value)
	{
		return new Uri(GetAbsoluteUriWithoutQuery(uri) + value + uri.Query);
	}

	public static Uri TrimPathEnd(this Uri uri, params char[] trimChars)
	{
		return new Uri(GetAbsoluteUriWithoutQuery(uri).TrimEnd(trimChars) + uri.Query);
	}

	private static string GetAbsoluteUriWithoutQuery(Uri uri)
	{
		var ret = uri.AbsoluteUri;
		if (uri.Query.Length > 0) ret = ret.Substring(0, ret.Length - uri.Query.Length);
		return ret;
	}
}

One more note:: I’m fully aware that this module alone does not solve the entire trailing slash problem, but minor additional (possibly project-specific) things like a custom UrlHelper (what I did/had to do, I spare you the details) or solutions like this method are easy enough to make it complete.

I just read Jimmy Bogard’s post on how we do MVC ViewModels and I must say, I digg the approach. Personally, I find it a shame that the ViewData dictionary is so damn prominent.

In the comments there was a little bit of discussion on how to handle the ViewModel for the master page. Since there wasn’t much of elaboration on the topic, I thought I’d mention how I’m doing it for quite a while now, and I’m still ok with it :-).

I chose to use a ViewModel base class for the master page with the following in mind:

  • The base class only contains information that makes sense for every view page that is using the particular master page.
  • Just like Jimmy explains in his post, I also have very specific requirements. I’m not only talking about XHTML et. al., but things like unique titles, etc.
  • I’ve got my app split up in independent modules. The only thing they’ve got in common, is a base class for initialization and the fact that their views reside in the same project/directory structure on disk. This means that when I’m working in module X, I know what my master page and its (ViewModel-) requirements are and can subclass ViewModel as I see fit.

Before I continue: I certainly do not put ALL view model data of the master page in the base class, don’t get me wrong. There is quite some information that’s rendered by the master page, but that information is not coming from its model. Most things come from composed RenderActions (not partials!). Only view data that can be/needs to be provided by distinct action methods is defined in the base class. Every master page in my app (typically, one per module) inherits from ViewMasterPage<ViewModel> where ViewModel looks something like this:

/// <summary>

/// Represents the model that contains information for the view.
/// </summary>
public class ViewModel
{
	private readonly HtmlHeader header = new HtmlHeader();

	/// <summary>
	/// Represents the information for the (X)HTML head element.
	/// </summary>
	public HtmlHeader HtmlHeader { get { return header; } }
}

I guess it’s no surprise that to you that HtmlHeader looks like this (in my case):

public class HtmlHeader
{
	public string Title { get; set; }
	public string MetaDescription { get; set; }
	public string MetaKeywords { get; set; }
}

My master page itself uses the ViewModel as follows:

...
<head>
	<title><%= Html.Encode(Model.HtmlHeader.Title ?? Resources.Master.DefaultPageTitle) %></title>
	<meta name="description" content="<%= Html.Encode(Model.HtmlHeader.MetaDescription ?? Resources.Master.DefaultMetaDescription) %>" />

	<meta name="keywords" content="<%= Html.Encode(Model.HtmlHeader.MetaKeywords ?? Resources.Master.DefaultMetaKeywords) %>" />
	...
</head>
...

This model only contains information that is to be supplied by action methods. When an end user requests product information, the action method must provide the title, META keywords etc. When a user requests a content page, the ViewContent(string slug) method is responsible for getting the content and map its title, META description, author, copyright, etc. to the ContentViewModel.

But how can we be sure?

Ok so far so good, we’ve organized ourselves a bit, but how can we make sure we return the proper type of view model? Due to the loose coupling between the M, V and C and the way ControllerBase and Controller are designed, it practically requires a rewrite (encapsulation if we’re lucky) of the existing hierarchy…

But then again, if you’re doing TDD, you should be fine (’the_view_model_should_…’). If you’re not, the very first HTTP GET will blow up, so you’ll know something’s wrong soon enough either way. So, why go through all this hassle?

This does not mean there’s nothing that can’t be done! But before I (try to) do, I’m going to address my SRP issue! I currently investigating things like commands in stead of controllers and ActionControllers and how to fit in the ViewModel (maybe).

I’m just done updating my expression routing extensions class: First I’ve added support for extraction default values from Nullable parameters (oh boy, how could I miss that one!). Second, and more interestingly maybe, I’ve added support for automatically extracting constraints.

Let’s say you’ve got this route:

routes.MapRoute<ProductsController>(
	"brands/{title}-{brandId}/{pageIndex}",
	c => c.Brands(0, 0),
	cultureNames,
	null,
	new { brandId = @"\d+", pageIndex = @"\d+" });

The two regex values are there to ensure that request for ~/brands/somebrand-1/foo or ~/brands/somebrand-bar/1 are never even forwarded to the action method. It is part of the contract and does not make sense. (I have setup my routing in such a way that the final fallback/wildcard route maps to a content management module that renders a ‘page not found’ if the slug cannot be found in the database.)

If you take a look at the action method Brands(int, int?) you’ll see that it is perfectly possible to deduct these constraints from the expression. I decided to add support for this as follows:

  • Explicit constraints (passed to the MapRoute method), always win
  • If the type of the parameter is IConvertible (including nullable types), I automatically add a constraint
  • Strings parameters are ignored

Just like the GetActionDefaults, I’ve added a GetActionConstraints, that parses the method call as follows:

private static RouteValueDictionary GetActionConstraints(MethodCallExpression call, RouteValueDictionary constraints)
{
	if (constraints == null) constraints = new RouteValueDictionary(); 

	foreach (var parameter in call.Method.GetParameters())
	{
		// if there's an explicit constraint, keep it, if it's a string, just ignore it
		if(constraints.ContainsKey(parameter.Name) || parameter.ParameterType == typeof(string)) continue;

		var converter = TypeDescriptor.GetConverter(parameter.ParameterType);
		if(converter != null && converter.CanConvertFrom(typeof(string)))
		{
			constraints.Add(parameter.Name, new ConversionTestConstraint(converter));
		} 

		return constraints;
	}
}

The method adds a ConversionTestConstrating that takes the specific converter (we need this, since we can’t be sure we will be able to determine the type (or converter) the moment the constraint is called.

The ConversionTestConstraint is somewhat ugly, I admit, but in my defense: I have to catch a general Exception because most TypeConverters are implemented badly (in my opinion). The conversion will rarely fail, so I guess it’s not the end of the world, but still.

public class ConversionTestConstraint : IRouteConstraint
{
	private readonly TypeConverter converter;

	public ConversionTestConstraint(TypeConverter converter)
	{
		this.converter = converter;
	}

	public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
	{
		if (!values.ContainsKey(parameterName)) return true;

		try
		{
			converter.ConvertFrom(values[parameterName]);
			return true;
		}
		catch (Exception) // WTF?! ConvertFrom throws System.Exception with FormatException as InnerException
		{
			return false;
		}
	}
}

Now I can simply put the following, with the same result:

routes.MapRoute<ProductsController>(
	"brands/{title}-{brandId}/{pageIndex}",
	c => c.Brands(0, 0),
	cultureNames);

Update: I’ve added automatic constraints to this.

When you’re working with ASP.Net MVC, routing is a very important thing to get just right. Unfortunately, the default routing options are pretty fragile. By default you have to resort to ‘magic strings’, anonymous types and/or string-based dictionaries etc. Depending on your requirements, this kind of routing can become quite the PITA.

Looking at my personal requirements: I have about 120 routes to map (including localization and a fall-back to managed content in the database). One day I got so fed up with the default routing options, that I decided to go for my own MapRoute() extension method.

Here’s just a fraction of my current RegisterRoutes() method (I’ve removed localization, for simplicity):

routes.MapRoute<ProductsController>("search/{query}/{pageIndex}", c => c.Search("", 0));
routes.MapRoute<ProductsController>("new/{pageIndex}",            c => c.NewProducts(0));
routes.MapRoute<ProductsController>("soon/{pageIndex}",           c => c.ComingProducts(0));

Before my changes it looked like this (again, without localization; with localization, there would be 3 more per route):

routes.MapRoute("Search", "search/{pageIndex}", new { controller = "Products",	action = "Search", pageIndex = 0 });
routes.MapRoute("newproducts", "new/{pageIndex}", new { controller = "Products", action = "NewProducts", pageIndex = 0 });
routes.MapRoute("soonproducts", "soon/{pageIndex}", new { controller = "Products", action = "ComingProducts", pageIndex = 0 });

Notice the difference?

Getting rid of magic strings was not my only motivation, though. I have chopped-up my application in different modules. The two main modules are the actual customer front-end and the administrative product/category/brand/news/foo/bar management pages. As it happens, both have a ProductsController class… If you go for default routing, you’ll run in to issues: ASP.Net MVC won’t be able to distinguish the one class from the other and throw you a nice exception telling you to specify the namespaces to disambiguate the controllers. The default solution would be specifying the namespace using magic strings, yet again…

Here’s the extension method (the most extended overload):

public static void MapRoute<T>(this RouteCollection routes, string url, Expression<Func<T, ActionResult>> action, RouteValueDictionary defaults, RouteValueDictionary constraints)
	where T : Controller
{
	if (routes == null) throw new ArgumentNullException("routes");
	if (url == null) throw new ArgumentNullException("url");
	if (action == null) throw new ArgumentNullException("action");

	var methodCall = action.Body as MethodCallExpression;
	if (methodCall == null) throw new ArgumentException(string.Format("The action '{0}' does not represent a method call.", action), "action");

	// merge action defaults with explicit defaults
	defaults = GetActionDefaults(methodCall, defaults);

	// verify constraints
	if(constraints != null && constraints.Any(c => !(c.Value is IRouteConstraint || c.Value is string)))
		throw new ArgumentException("The constraints dictionary contains illegal elements. Constraint values must be strings (regex) or IRouteConstraint implementations.", "constraints");

	var route = new Route(url, new MvcRouteHandler())
	{
		Defaults = new RouteValueDictionary(defaults),
		Constraints = constraints,
		DataTokens = new RouteValueDictionary { { NamespacesKey, new[] { typeof(T).Namespace } } }
	};

	routes.Add(url, route);
}

Here’s the GetActionDefaults method that extracts the default values from the method signature:

private static RouteValueDictionary GetActionDefaults(MethodCallExpression call, RouteValueDictionary defaults)
{
	var controllerName = call.Method.DeclaringType.Name;
	if (controllerName.EndsWith(ControllerSuffix, StringComparison.OrdinalIgnoreCase))
		controllerName = controllerName.Remove(controllerName.Length - ControllerSuffix.Length, ControllerSuffix.Length);

	defaults.Add(ControllerKey, controllerName);

	var attributes = call.Method.GetCustomAttributes(typeof(ActionNameAttribute), true);
	defaults.Add(ActionKey, attributes.Length == 1 ? ((ActionNameAttribute) attributes[0]).Name : call.Method.Name);

	var parameters = call.Method.GetParameters();
	for (var i = 0; i < parameters.Length; i++)
	{
		var expression = call.Arguments[i] as ConstantExpression;
		if (expression != null) defaults.Add(parameters[i].Name, expression.Value);
	}

	return defaults;
}

Result:

  • Who needed that route name anyway?
  • No more magic string (except fot the url)
  • Namespaces are registered automatically, modules/areas no longer require special treatment
  • Defaults are resolved from the action expression (additional defaults are still suppported)
  • Constraints are checked as soon as the route is mapped (as good as it gets, I suppose)
  • If you’re happy with ‘http://domain.com/{cultureName}/…’ url’s, you’re good to go as well, just pass along an array of supported culture names:
public static void MapRoute<T>(this RouteCollection routes, string url, Expression<Func<T, ActionResult>> action, string[] cultureNames, RouteValueDictionary defaults, RouteValueDictionary constraints)
	where T : Controller
{
	routes.MapRoute(url, action, new RouteValueDictionary(defaults), constraints);

	foreach (var cultureName in cultureNames)
	{
		var d = new RouteValueDictionary(defaults) {{"lang", cultureName}};
		routes.MapRoute(string.Format("{0}/{1}", cultureName, url), action, d, constraints);
	}
}

Man, since about a week now, I’m forced at using ASP.Net WebForms. It’s been so long, that I almost forgot what a terrible PITA WebForms really is.

I can only hope I get them to move to something else real soon…

WebForms is a lie. It’s abstraction wrapped in deception covered in lie sauce presented on a plate full of diversion and sleight of hand. Nothing you do with Webforms has anything to do with the web – you let it do the work for you.

I couldn’t agree more with Rob’s post.

As you might know, I run a web shop written in ASP.Net MVC. The move to ASP.Net MVC is fairly recent (time frame of preview 4), the shop ran for a few years on ‘classic’ ASP.Net (am I glad that’s finally over), but provided basically the same functionality as the ported version now.

There are some improvements though (besides the obvious: a far better implementation). One of the most visible changes is the support for more SEO-friendly urls. Since the shop is available in three languages I decided to finally provide decent localized urls as well. My requirements were as follows:

  • Each domain has a default culture (.be defaults to ‘nl’, .fr to ‘fr’, etc.), but supports all others as well
  • When browsing the site with the default culture no culture url segment is required
  • If the culture url segment specifies the default culture, the request is redirected to a url without culture segment
  • Urls with a culture segment should look like ‘http://domain.com/[culture-name]/[content-url]‘

There are various ways of getting this done, but the most obvious one is a custom ActionFilterAttribute (+ optional controller base class, if you don’t like putting the attribute over and over again). An action filter alone won’t cut it, however. We’ll need to setup appropriate routes first…

Routing

In order to register localized routes as convenient as possible, I’ve created a little helper class with extension methods that provides a ‘MapLocalizedRoute‘ equivalent for each standard ‘MapRoute‘ overload.

/// <summary>
/// Provides extension methods to register localized routes.
/// </summary>
public static class RouteCollectionExtensions
{
	public static void MapLocalizedRoute(this RouteCollection routes, string name, string url)
	{
		MapLocalizedRoute(routes, name, url, null, null);
	}

	public static void MapLocalizedRoute(this RouteCollection routes, string name, string url, object defaults)
	{
		MapLocalizedRoute(routes, name, url, defaults, null);
	}

	public static void MapLocalizedRoute(this RouteCollection routes, string name, string url, string[] namespaces)
	{
		MapLocalizedRoute(routes, name, url, null, null, namespaces);
	}

	public static void MapLocalizedRoute(this RouteCollection routes, string name, string url, object defaults, object constraints)
	{
		MapLocalizedRoute(routes, name, url, defaults, constraints, null);
	}

	public static void MapLocalizedRoute(this RouteCollection routes, string name, string url, object defaults, string[] namespaces)
	{
		MapLocalizedRoute(routes, name, url, defaults, null, namespaces);
	}

	public static void MapLocalizedRoute(this RouteCollection routes, string name, string url, object defaults, object constraints, string[] namespaces)
	{
		foreach (var cultureName in Locale.SupportedCultureNames)
		{
			var route = routes.MapRoute(
				string.Format("{0}-{1}", name, cultureName),
				string.Format("{0}/{1}", cultureName, url),
				defaults,
				constraints,
				namespaces);

			route.Defaults.Add(Locale.CultureSegmentName, cultureName);
		}
	}
}

MapLocalizedRoute will simply map a route for every supported culture name (as set in the Locale class). Because the culture name is a literal in the url, we need to add a ‘culture’ value to the default values of the route. Doing so gives you more control over your routing and avoids the possibility of handling unsupported or malformed culture names. With these extension methods we can do the following in our Application_Start:


Locale.DefaultCultureName = "nl";
Locale.SupportedCultureNames = new[] {"nl", "en", "fr"};

// ...

routes.MapLocalizedRoute("about", "about/", new { controller = "Root", action = "About" });
routes.MapLocalizedRoute("terms", "terms/", new { controller = "Root", action = "Terms" });
routes.MapLocalizedRoute("faq", "faq/", new { controller = "Root", action = "Faq" });

// ...

routes.MapLocalizedRoute("brands", "brands/{title}-{brandId}/{pageIndex}",
	new { controller = "Products", action = "Brands", pageIndex = "0" });
routes.MapLocalizedRoute("cat", "{name}-{categoryId}/{pageIndex}",
	new { controller = "Products", action = "Category", pageIndex = "0" });

// map non-localized routes here

In case you’re wondering why default culture routes are not mapped in MapLocalizedRoute and why the listing above mentions that you should map non-localized route after the localized ones. This is simply because we need to add the most common route patterns first.

Localize

Now let’s have a look at the LocalizeAttribute. For every localized url the OnActionExecuting will make sure the CurrentCulture and CurrentUICulture of the current thread are set as requested. If the requested culture is the default one, the filter will redirect the user to the default culture url (without culture segment).

Note that we don’t have to verify if the requested culture name is a supported one; the MapLocalizedRoute will make sure that only the supported languages are mapped.

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class LocalizeAttribute : ActionFilterAttribute
{
	private static readonly Regex removeCultureSegmentRegex = new Regex(@"^/[a-zA-Z\-]+", RegexOptions.Compiled);

	public override void OnActionExecuting(ActionExecutingContext filterContext)
	{
		if (!filterContext.RouteData.Values.ContainsKey(Locale.CultureSegmentName))
		{
			SetCurrentCultures(Locale.DefaultCultureName);
			return;
		}

		var culture = filterContext.RouteData.Values[Locale.CultureSegmentName].ToString();

		if (culture == Locale.DefaultCultureName)
		{
			RedirectToDefaultCultureUrl(filterContext);
			return;
		}

		SetCurrentCultures(culture);
	}

	private static void SetCurrentCultures(string cultureName)
	{
		Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture(cultureName);
		Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo(cultureName);
	}

	private static void RedirectToDefaultCultureUrl(RequestContext filterContext)
	{
		filterContext.HttpContext.Response.Redirect(removeCultureSegmentRegex.Replace(filterContext.HttpContext.Request.RawUrl, string.Empty), true);
	}
}

Finally, there just theLocale class that configures the default and supported cultures.

/// <summary>
/// Holds the default and supported culture names.
/// </summary>
public static class Locale
{
	static Locale()
	{
		DefaultCultureName = "en";
		SupportedCultureNames = new[] {"en", "nl", "fr"};
	}

	internal const string CultureSegmentName = "culture";

	public static string DefaultCultureName { get; set;}
	public static string[] SupportedCultureNames { get; set;}
}

About a month ago, I was asked to come up with something to support modules in ASP.NET MVC. Basically, they wanted to be able to deploy a certain module on one machine but not another, be it customer-specific, a different version or variation. All this with the same core/base application and setup.

After some experimenting, I came up with a mechanism where a module consists of one assembly (well there could be more, but only one actually represents the module). This assembly must be present in the bin directory of the ASP.NET MVC application and the corresponding views should reside in the ~/<AssemblyName>/Views/ directory. That’s it, no special base-classes or configuration, just a simple convention.

For some reasons we have changed this principle a bit by introducing some DI/IoC (using Windsor) to make the thing configurable. Instead of just dropping the necessary dll’s in the bin directory, they must be explicitly configured. (the following discusses that version)

The implementation of this is drop-dead simple. When your application starts, just make sure you do something like this:

MvcModule.Initialize(HttpContext.Current.Server.MapPath("~/App_Data/modules.config"));
// Any other (default) route mapping you wish...

The MvcModule.Initialize method will make sure

  • our MvcModuleControllerFactory is configured (more on that in just a sec.)
  • our windsor container is initialized
  • all loaded modules get their routes mapped

Here’s the code of the MvcModule class:

/// <summary>
/// Represents an ASP.Net MVC Module.
/// </summary>

public abstract class MvcModule
{
   private static List<MvcModule> loadedModules;

   private static List<MvcModule> LoadedModules
   {
      get
      {
         if (loadedModules == null)
            LoadModules();
         return loadedModules;
      }
   }

   private static void LoadModules()
   {
      loadedModules = new List<MvcModule>(Container.ResolveAll<MvcModule>());
   }

   /// <summary>
   /// Gets the IoC container for all MVC modules and controllers.
   /// </summary>

   public static WindsorContainer Container { get; private set; }

   /// <summary>
   /// Initiazes the MVC Modules and controllers using an xml configuration file.
   /// </summary>
   /// <param name="xmlFile">The full path of the xml configuration file.</param>
   public static void Initialize(string xmlFile)
   {
      if (string.IsNullOrEmpty(xmlFile)) throw new ArgumentNullException("xmlFile");

      Initialize(new WindsorContainer(xmlFile));
   }

   /// <summary>

   /// Initiazes the MVC Modules and controllers using a windsor container.
   /// </summary>
   /// <param name="container">The container holding module and controller information.</param>
   public static void Initialize(WindsorContainer container)
   {
      if (container == null) throw new ArgumentNullException("container");
      Container = container;
      ControllerBuilder.Current.SetControllerFactory(new ModuleControllerFactory());
      foreach (var module in LoadedModules)
      {
         module.RegisterRoutes(RouteTable.Routes);
      }
   }

   /// <summary>
   /// Gets the friendly name of the module.
   /// </summary>

   public abstract string FriendlyName { get; }

   /// <summary>
   /// Registers the routes for the module controllers.
   /// </summary>
   /// <param name="routes">The RouteCollection which needs to have the module routes registered.</param>
   public abstract void RegisterRoutes(RouteCollection routes);
}

I guess this is self-explanatory. By the way, the non-windsor version did something like this

private static void LoadModules()
{
   loadedModules = new List<MvcModule>();

   foreach (Assembly a in System.Web.Compilation.BuildManager.GetReferencedAssemblies())
   foreach (Type type in a.GetTypes())
   {
      if (!type.Equals(typeof(MvcModule)) && (typeof (MvcModule).IsAssignableFrom(type)))
         loadedModules.Add((MvcModule) Activator.CreateInstance(type));
   }
}

The MvcModuleControllerFactory creates controllers based on the windows container configuration and makes sure our views directory name convention works:

/// <summary>

/// Creates module controllers for ASP.Net MVC requests.
/// </summary>
public class ModuleControllerFactory : IControllerFactory
{
   private static readonly Dictionary<string, string[]> locationFormats = new Dictionary<string, string[]>();

   protected virtual IController CreateController(RequestContext requestContext, string controllerName)
   {
      var ret = (IController)MvcModule.Container.Resolve(controllerName.ToLower() + "controller");// base.CreateController(requestContext, controllerName);
      Debug.Assert(ret != null, string.Format("The controller with name '{0}' could not be resolved from the container.", controllerName));
      SetViewLocationFormats(ret);
      return ret;
   }

   protected virtual void DisposeController(IController controller)
   {
      var disposable = controller as IDisposable;
      if (disposable != null) disposable.Dispose();

      MvcModule.Container.Release(controller);
   }

   IController IControllerFactory.CreateController(RequestContext context, string controllerName)
   {
      return CreateController(context, controllerName);
   }

   void IControllerFactory.DisposeController(IController controller)
   {
      DisposeController(controller);
   }

   private static void SetViewLocationFormats(IController controller)
   {
      var c = controller as Controller;
      if (c != null)
      {
         var assemblyName = controller.GetType().Assembly.GetName().Name;
         ((ViewLocator)((WebFormViewEngine)c.ViewEngine).ViewLocator).ViewLocationFormats = GetViewLocationFormats(assemblyName);
      }
   }

   private static string[] GetViewLocationFormats(string assemblyName)
   {
      if (locationFormats.ContainsKey(assemblyName)) return locationFormats[assemblyName];

      var formats = new string[8];

      // Modules
      formats[0] = "~/" + assemblyName + "/Views/{1}/{0}.aspx";
      formats[1] = "~/" + assemblyName + "/Views/{1}/{0}.ascx";
      formats[2] = "~/" + assemblyName + "/Views/Shared/{0}.aspx";
      formats[3] = "~/" + assemblyName + "/Views/Shared/{0}.ascx";

      // These are the defaults
      formats[4] = "~/Views/{1}/{0}.aspx";
      formats[5] = "~/Views/{1}/{0}.ascx";
      formats[6] = "~/Views/Shared/{0}.aspx";
      formats[7] = "~/Views/Shared/{0}.ascx";

      // cache them
      locationFormats.Add(assemblyName, formats);
      return formats;
   }
}

What do you think, does this make sense?

Follow

Get every new post delivered to your Inbox.