Question

I am currently experimenting with dynamically loaded areas with ASP.NET MVC 3 RC. I've seen it written in many places that this is not what areas are intended for, and (at least pre-MVC 2) not possible, say here for example.

But still! It should be possible to get it to work, right? I've created a solution, added a MVC 3 project, added an area and some content. All is working well. Now I created a new class library project (in the same solution), added a reference to it from the MVC-project, and started moving over the area-related parts to the library. Changed the output-directory of the library project to the area-folder of the MVC-project, and made sure the Views and their web.config are copied to the output-folder.

After reading so much about how you couldn't have external areas, it was a little surprising that this worked. No problem at all really! The problem starts when I remove the reference between the projects, and instead load the library in code. (Before calling AreaRegistration.RegisterAllAreas().) Now it doesn't work. At all.

I've been poking around a bit in the source for MVC 3, and the problem seems to be with BuildManager.GetReferencedAssemblies() which is used to get the assemblies to look for implementations of AreaRegistration.

Now, I'm not 100% sure about this, but it seems as if this method only looks at assemblies that were present/referenced at compile-time, can someone confirm if this is in fact so?

I have debugged through this, and that method-call does indeed not find the assembly I loaded just before the call to it. It might be because of something else that I've missed perhaps.. Any ideas?

Was it helpful?

Solution

The way things work is a bit complicated.

GetReferencedAssemblies includes referenced assemblies, not loaded assemblies. This includes:

  • all assemblies referenced in you application's web.config (such as System.Web.Mvc)
  • everything inherited from root web.config, which includes things like System, System.Web and others that you do not have to add yourself. (You can take a look at the list here: C:\Windows\Microsoft.Net\Framework\v4.0.30319\web.config).
    It also contains a special * item, which:
  • includes everything in your site's bin folder

So now take your app v1 (everything in a single app). Everything works because the application code gets compiled into the bin folder which gets automatically included. Also, all of the area views etc are in the application itself so they are accessible.

Now in app v2 (different project with a proj-to-proj reference and a custom build task that copies the views to the right location in your main app) everything still works, because by default a proj-to-proj references means that the class library binary gets copied to your app's bin folder. So by the above rules, the area code still gets loaded correctly. The fact that you've set the library's output path to be some location within your main app's Areas folder does not actually make a difference - you just end up with two copies of the binary.

Now in app v3 (no proj-proj ref, area library assembly loaded manually) your library assembly gets loaded too late. By the time your code runs the set of referenced assemblies has already been locked and can no longer be changed.

There is a way to run code and add items to the list of registered assemblies: you can do it using the AddReferencedAssembly method which must be invoked from a PreApplicationStartMethodAttribute method.

Of course you still have to deal with how you manage your view files. The way you currently have it set up is pretty much the same as having the views in the main application (since they effectively get copied into the right location).

OTHER TIPS

1 - Seperate you Mvc Areas into differrent Mvc Projects to be compiled into their own seperate assemblies

2 - Add this to your AssemblyInfo.cs class, to call a method when the application is loaded

[assembly: PreApplicationStartMethod(typeof(PluginAreaBootstrapper), "Init")]

3 - Here's what the Init method looks like when it's invoked during the load

public class PluginAreaBootstrapper
{
    public static readonly List<Assembly> PluginAssemblies = new List<Assembly>();

    public static List<string> PluginNames()
    {
        return PluginAssemblies.Select(
            pluginAssembly => pluginAssembly.GetName().Name)
            .ToList();
    }

    public static void Init()
    {
        var fullPluginPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Areas");

        foreach (var file in Directory.EnumerateFiles(fullPluginPath, "*Plugin*.dll"))
            PluginAssemblies.Add(Assembly.LoadFile(file));

        PluginAssemblies.ForEach(BuildManager.AddReferencedAssembly);
    }
}

4 - Add a custom RazorViewEngine

public class PluginRazorViewEngine : RazorViewEngine
{
    public PluginRazorViewEngine()
    {
        AreaMasterLocationFormats = new[]
        {
            "~/Areas/{2}/Views/{1}/{0}.cshtml",
            "~/Areas/{2}/Views/{1}/{0}.vbhtml",
            "~/Areas/{2}/Views/Shared/{0}.cshtml",
            "~/Areas/{2}/Views/Shared/{0}.vbhtml"
        };

        AreaPartialViewLocationFormats = new[]
        {
            "~/Areas/{2}/Views/{1}/{0}.cshtml",
            "~/Areas/{2}/Views/{1}/{0}.vbhtml",
            "~/Areas/{2}/Views/Shared/{0}.cshtml",
            "~/Areas/{2}/Views/Shared/{0}.vbhtml"
        };

        var areaViewAndPartialViewLocationFormats = new List<string>
        {
            "~/Areas/{2}/Views/{1}/{0}.cshtml",
            "~/Areas/{2}/Views/{1}/{0}.vbhtml",
            "~/Areas/{2}/Views/Shared/{0}.cshtml",
            "~/Areas/{2}/Views/Shared/{0}.vbhtml"
        };

        var partialViewLocationFormats = new List<string>
        {
            "~/Views/{1}/{0}.cshtml",
            "~/Views/{1}/{0}.vbhtml",
            "~/Views/Shared/{0}.cshtml",
            "~/Views/Shared/{0}.vbhtml"
        };

        var masterLocationFormats = new List<string>
        {
            "~/Views/{1}/{0}.cshtml",
            "~/Views/{1}/{0}.vbhtml",
            "~/Views/Shared/{0}.cshtml",
            "~/Views/Shared/{0}.vbhtml"
        };

        foreach (var plugin in PluginAreaBootstrapper.PluginNames())
        {
            masterLocationFormats.Add(
                "~/Areas/" + plugin + "/Views/{1}/{0}.cshtml");
            masterLocationFormats.Add(
                "~/Areas/" + plugin + "/Views/{1}/{0}.vbhtml");
            masterLocationFormats.Add(
                "~/Areas/" + plugin + "/Views/Shared/{1}/{0}.cshtml");
            masterLocationFormats.Add(
                "~/Areas/" + plugin + "/Views/Shared/{1}/{0}.vbhtml");

            partialViewLocationFormats.Add(
                "~/Areas/" + plugin + "/Views/{1}/{0}.cshtml");
            partialViewLocationFormats.Add(
                "~/Areas/" + plugin + "/Views/{1}/{0}.vbhtml");
            partialViewLocationFormats.Add(
                "~/Areas/" + plugin + "/Views/Shared/{0}.cshtml");
            partialViewLocationFormats.Add(
                "~/Areas/" + plugin + "/Views/Shared/{0}.vbhtml");

            areaViewAndPartialViewLocationFormats.Add(
                "~/Areas/" + plugin + "/Views/{1}/{0}.cshtml");
            areaViewAndPartialViewLocationFormats.Add(
                "~/Areas/" + plugin + "/Views/{1}/{0}.vbhtml");
            areaViewAndPartialViewLocationFormats.Add(
                "~/Areas/" + plugin + "/Areas/{2}/Views/{1}/{0}.cshtml");
            areaViewAndPartialViewLocationFormats.Add(
                "~/Areas/" + plugin + "/Areas/{2}/Views/{1}/{0}.vbhtml");
            areaViewAndPartialViewLocationFormats.Add(
                "~/Areas/" + plugin + "/Areas/{2}/Views/Shared/{0}.cshtml");
            areaViewAndPartialViewLocationFormats.Add(
                "~/Areas/" + plugin + "/Areas/{2}/Views/Shared/{0}.vbhtml");
        }

        ViewLocationFormats = partialViewLocationFormats.ToArray();
        MasterLocationFormats = masterLocationFormats.ToArray();
        PartialViewLocationFormats = partialViewLocationFormats.ToArray();
        AreaPartialViewLocationFormats = areaViewAndPartialViewLocationFormats.ToArray();
        AreaViewLocationFormats = areaViewAndPartialViewLocationFormats.ToArray();
    }
}

5 - Register your Areas from your different Mvc (Area) Projects

namespace MvcApplication8.Web.MyPlugin1
{
    public class MyPlugin1AreaRegistration : AreaRegistration
    {
        public override string AreaName
        {
            get { return "MyPlugin1"; }
        }

        public override void RegisterArea(AreaRegistrationContext context)
        {
            context.MapRoute(
                "MyPlugin1_default",
                "MyPlugin1/{controller}/{action}/{id}",
                new {action = "Index", id = UrlParameter.Optional}
                );
        }
    }
}

Sourcecode and additional references can can be found here:http://blog.longle.io/2012/03/29/building-a-composite-mvc3-application-with-pluggable-areas

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top