Views in separate assemblies in ASP.NET MVC
Asked Answered
S

4

54

I'm trying to create a webapplication where I want to be able to plug-in separate assemblies. I'm using MVC preview 4 combined with Unity for dependency injection, which I use to create the controllers from my plugin assemblies. I'm using WebForms (default aspx) as my view engine.

If I want to use a view, I'm stuck on the ones that are defined in the core project, because of the dynamic compiling of the ASPX part. I'm looking for a proper way to enclose ASPX files in a different assembly, without having to go through the whole deployment step. Am I missing something obvious? Or should I resort to creating my views programmatically?


Update: I changed the accepted answer. Even though Dale's answer is very thorough, I went for the solution with a different virtual path provider. It works like a charm, and takes only about 20 lines in code altogether I think.

Schroth answered 21/8, 2008 at 12:37 Comment(4)
Does using the virtual path provider still work with the routing in mvc.net ?Mandarin
@jmcd: it looks like it does.Larrikin
Is there any sample code you could post that allowed you to accomplish this?Gonorrhea
The project I tried it on was ditched, so I don't have example code at ready. However, the spark view engine allows this as well (sparkviewengine.com). There's a sample in there called modules I think, which separates areas in different assemblies.Schroth
H
15

Essentially this is the same issue as people had with WebForms and trying to compile their UserControl ASCX files into a DLL. I found this http://www.codeproject.com/KB/aspnet/ASP2UserControlLibrary.aspx that might work for you too.

Hermia answered 29/8, 2008 at 20:34 Comment(0)
A
30

It took me way too long to get this working properly from the various partial samples, so here's the full code needed to get views from a Views folder in a shared library structured the same as a regular Views folder but with everything set to build as embedded resources. It will only use the embedded file if the usual file does not exist.

The first line of Application_Start:

HostingEnvironment.RegisterVirtualPathProvider(new EmbeddedViewPathProvider());

The VirtualPathProvider

   public class EmbeddedVirtualFile : VirtualFile
{
    public EmbeddedVirtualFile(string virtualPath)
        : base(virtualPath)
    {
    }

    internal static string GetResourceName(string virtualPath)
    {
        if (!virtualPath.Contains("/Views/"))
        {
            return null;
        }



        var resourcename = virtualPath
            .Substring(virtualPath.IndexOf("Views/"))
            .Replace("Views/", "OrangeGuava.Common.Views.")
            .Replace("/", ".");

        return resourcename;

    }


    public override Stream Open()
    {
        Assembly assembly = Assembly.GetExecutingAssembly();


        var resourcename = GetResourceName(this.VirtualPath);
        return assembly.GetManifestResourceStream(resourcename);
    }




}

public class EmbeddedViewPathProvider : VirtualPathProvider
{


    private bool ResourceFileExists(string virtualPath)
    {

        Assembly assembly = Assembly.GetExecutingAssembly();


        var resourcename = EmbeddedVirtualFile.GetResourceName(virtualPath);
        var result = resourcename != null && assembly.GetManifestResourceNames().Contains(resourcename);
        return result;
    }

    public override bool FileExists(string virtualPath)
    {
        return base.FileExists(virtualPath) || ResourceFileExists(virtualPath);
    }


    public override VirtualFile GetFile(string virtualPath)
    {

        if (!base.FileExists(virtualPath))
        {
            return new EmbeddedVirtualFile(virtualPath);
        }
        else
        {
            return base.GetFile(virtualPath);
        }

    }

}

The final step to get it working is that the root Web.Config must contain the right settings to parse strongly typed MVC views, as the one in the views folder won't be used:

<pages
    validateRequest="false"
    pageParserFilterType="System.Web.Mvc.ViewTypeParserFilter, System.Web.Mvc, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"
    pageBaseType="System.Web.Mvc.ViewPage, System.Web.Mvc, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"
    userControlBaseType="System.Web.Mvc.ViewUserControl, System.Web.Mvc, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35">
  <controls>
    <add assembly="System.Web.Mvc, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" namespace="System.Web.Mvc" tagPrefix="mvc" />
  </controls>
</pages>

A couple of additional steps are required to get it working with Mono. First, you need to implement GetDirectory, since all files in the views folder get loaded when the app starts rather than as needed:

public override VirtualDirectory GetDirectory(string virtualDir)
    {
        Log.LogInfo("GetDirectory - " + virtualDir);
        var b = base.GetDirectory(virtualDir);
        return new EmbeddedVirtualDirectory(virtualDir, b);
    }

public class EmbeddedVirtualDirectory : VirtualDirectory
{
    private VirtualDirectory FileDir { get; set; } 

    public EmbeddedVirtualDirectory(string virtualPath, VirtualDirectory filedir)
        : base(virtualPath)
    {
        FileDir = filedir;
    }

    public override System.Collections.IEnumerable Children
    {
        get { return FileDir.Children; }
    }

    public override System.Collections.IEnumerable Directories
    {
        get { return FileDir.Directories; }
    }

    public override System.Collections.IEnumerable Files
    {
        get {

            if (!VirtualPath.Contains("/Views/") || VirtualPath.EndsWith("/Views/"))
            {
                return FileDir.Files;
            }

            var fl = new List<VirtualFile>();

            foreach (VirtualFile f in FileDir.Files)
            {
                fl.Add(f);
            }


            var resourcename = VirtualPath.Substring(VirtualPath.IndexOf("Views/"))
.Replace("Views/", "OrangeGuava.Common.Views.")
.Replace("/", ".");

            Assembly assembly = Assembly.GetExecutingAssembly();

            var rfl = assembly.GetManifestResourceNames()
                .Where(s => s.StartsWith(resourcename))
                .Select(s => VirtualPath + s.Replace(resourcename, ""))
                .Select(s => new EmbeddedVirtualFile(s));
            fl.AddRange(rfl);

            return fl;
        }
    }
}

Finally, strongly typed views will almost but not quite work perfectly. Model will be treated as an untyped object, so to get strong typing back you need to start your shared views with something like

<% var Model2 = Model as IEnumerable<AppModel>;  %>
Assai answered 1/1, 2011 at 10:59 Comment(1)
It is also important to remember to set build action to "Embedded resource" for the view. The folder for the view is also should exist in web project. In my case, I put view in assembly project's folder Views/Shared/Dynamic.cshtml and have to create Views/Shared in web project which referenced the assembly.Electrodynamics
H
15

Essentially this is the same issue as people had with WebForms and trying to compile their UserControl ASCX files into a DLL. I found this http://www.codeproject.com/KB/aspnet/ASP2UserControlLibrary.aspx that might work for you too.

Hermia answered 29/8, 2008 at 20:34 Comment(0)
F
13
protected void Application_Start()
{
    WebFormViewEngine engine = new WebFormViewEngine();

    engine.ViewLocationFormats = new[] { "~/bin/Views/{1}/{0}.aspx", "~/Views/Shared/{0}.aspx" };
    engine.PartialViewLocationFormats = engine.ViewLocationFormats;

    ViewEngines.Engines.Clear();
    ViewEngines.Engines.Add(engine);

    RegisterRoutes(RouteTable.Routes);
}

Set the 'Copy to output' property of your view to 'Copy always'

Fi answered 3/10, 2009 at 20:29 Comment(1)
Instead of creating a WebFormviewEngine you can do the same by getting the RazorViewEngine like this: RazorViewEngine engine = ViewEngines.Engines.OfType<RazorViewEngine>().First(); and then just concat the additional paths to the mentioned properties.Vine
S
2

An addition to all you who are still looking for the holy grail: I've come a bit closer to finding it, if you're not too attached to the webforms viewengine.

I've recently tried out the Spark viewengine. Other than being totally awesome and I wouldn't go back to webforms even if I was threathened, it also provides some very nice hooks for modularity of an application. The example in their docs is using Windsor as an IoC container, but I can't imagine it to be a lot harder if you want to take another approach.

Schroth answered 21/3, 2010 at 22:40 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.