Using custom VirtualPathProvider to load embedded resource Partial Views
Asked Answered
P

3

37

I wrote custom VirtualFile and VirtualPathProvider implementations that are successfully obtaining embedded resources that are Partial Views.

However, when I attempt to render them it produces this error:

The view at '~/Succeed.Web/Succeed.Web.Controls.SImporter._SImporter.cshtml' must derive from WebViewPage, or WebViewPage<TModel>.

When rendering the partial view, inside of a regular View, it looks like the following:

Html.RenderPartial("~/Succeed.Web/Succeed.Web.Controls.SImporter._SImporter.cshtml");

What is causing it to believe this isn't a partial view?

EDIT: I Posted my code for both the virtual file & virtual file provider implementations for anyone who stumbles upon this looking for solution on getting that component working. This question will also serve well for those based upon the question title.

ere is the VirtualFile implementation for reference:

public class SVirtualFile : VirtualFile
{
    private string m_path;

    public SVirtualFile(string virtualPath)
        : base(virtualPath)
    {
        m_path = VirtualPathUtility.ToAppRelative(virtualPath);
    }

    public override System.IO.Stream Open()
    {
        var parts = m_path.Split('/');
        var assemblyName = parts[1];
        var resourceName = parts[2];

        assemblyName = Path.Combine(HttpRuntime.BinDirectory, assemblyName);
        var assembly = System.Reflection.Assembly.LoadFile(assemblyName + ".dll");

        if (assembly != null)
        {
            return assembly.GetManifestResourceStream(resourceName);
        }
        return null;
    }
}

VirtualPathProvider:

public class SVirtualPathProvider : VirtualPathProvider
{
    public SVirtualPathProvider() 
    { 

    }

    private bool IsEmbeddedResourcePath(string virtualPath)
    {
        var checkPath = VirtualPathUtility.ToAppRelative(virtualPath);
        return checkPath.StartsWith("~/Succeed.Web/", StringComparison.InvariantCultureIgnoreCase);
    }

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

    public override VirtualFile GetFile(string virtualPath)
    {
        if (IsEmbeddedResourcePath(virtualPath))
        {
            return new SVirtualFile(virtualPath);
        }
        else
        {
            return base.GetFile(virtualPath);
        }
    }

    public override CacheDependency GetCacheDependency( string virtualPath, IEnumerable virtualPathDependencies, DateTime utcStart)
    {
        if (IsEmbeddedResourcePath(virtualPath))
        {
            return null;
        }
        else
        {
            return base.GetCacheDependency(virtualPath, virtualPathDependencies, utcStart);
        }
    }
}

And of course, don't forget to register this new provider in the Global.asax file of your project in the Application_Start() event

System.Web.Hosting.HostingEnvironment.RegisterVirtualPathProvider(new SVirtualPathProvider());
Pluvious answered 12/10, 2011 at 20:37 Comment(0)
S
45

Because now you are serving your views from some unknown location there is no longer the ~/Views/web.config file which applies and indicates the base class for your razor views (<pages pageBaseType="System.Web.Mvc.WebViewPage">). So you could add an @inherits directive at the top of each embedded view to indicate the base class.

@inherits System.Web.Mvc.WebViewPage
@model ...
Surefire answered 12/10, 2011 at 20:59 Comment(7)
Thank you very much. My only regret is that I have but one up vote to give. =P Up Vote this answerPluvious
@ darin Dimitrov I don't suppose you know of a trick I could employ to encourage intellisense to treat this as a View when inside my dll? Right now, I have very little intellisense support with views that I placed within my dllPluvious
@MatthewCox, I don't know much about Intellisense in views. It's been long time ago when I stopped relying on it. Don't you get Intellisense if the file has the .cshtml extension?Surefire
Yes, but it is very limited, which is the odd part. For instance, it will not support @Html.[whatever]Pluvious
isn't this solved by moving the <pages section down one level into the main web.config?Yard
@FeistyMango - The intellisense for the @Html helper relies on namespaces being in the web.config within the Views folder. As the answer suggests, you should be able to import System.Web.Mvc.Html and System.Web.Mvc directly into the views to get intellisense for those.Phenomenal
@FeistyMango - I ran into this problem with my embedded views. I followed this article to get intellisense working. Just make sure the assembly versions in the web.config match up with the ones installed by nuget: thetoeb.de/2014/01/05/…Jesicajeske
C
7

I used the OPs answer as a base but expanded on it a bit and incorporated the answer to the question in my solution.

This seems like a somewhat commonly asked question here on SO and I haven't seen a complete answer so I thought it might be helpful to share my working solution.

I load my resources from a database and I have them cached in the default Cache (System.Web.Caching.Cache).

What I ended up doing was creating a custom CacheDependency on the KEY that I am using to lookup the resource. That way, whenever my other code invalidates that cache (on an edit, etc.) the cache dependency on that key is removed and the VirtualPathProvider in turn invalidates its cache and the VirtualFile gets reloaded.

I also changed the code so that it automatically prepends inherits statement so that it doesn't need to be stored in my database resource and I also automatically prepend a few default using statements as this "view" is not loaded via the normal channels, so anything default includes in your web.config or viewstart are not usable.

CustomVirtualFile:

public class CustomVirtualFile : VirtualFile
{
    private readonly string virtualPath;

    public CustomVirtualFile(string virtualPath)
        : base(virtualPath)
    {
        this.virtualPath = VirtualPathUtility.ToAppRelative(virtualPath);
    }

    private static string LoadResource(string resourceKey)
    {
        // Load from your database respository or whatever here...
        // Note that the caching is disabled for this content in the virtual path
        // provider, so you must cache this yourself in your repository.

        // My implementation using my custom service locator that sits on top of
        // Ninject
        var contentRepository = FrameworkHelper.Resolve<IContentRepository>();

        var resource = contentRepository.GetContent(resourceKey);

        if (String.IsNullOrWhiteSpace(resource))
        {
            resource = String.Empty;
        }

        return resource;
    }

    public override Stream Open()
    {
        // Always in format: "~/CMS/{0}.cshtml"
        var key = virtualPath.Replace("~/CMS/", "").Replace(".cshtml", "");

        var resource = LoadResource(key);

        // this automatically appends the inherit and default using statements 
        // ... add any others here you like or append them to your resource.
        resource = String.Format("{0}{1}", "@inherits System.Web.Mvc.WebViewPage<dynamic>\r\n" +
                                           "@using System.Web.Mvc\r\n" +
                                           "@using System.Web.Mvc.Html\r\n", resource);

        return resource.ToStream();
    }
}

CustomVirtualPathProvider:

public class CustomVirtualPathProvider : VirtualPathProvider
{
    private static bool IsCustomContentPath(string virtualPath)
    {
        var checkPath = VirtualPathUtility.ToAppRelative(virtualPath);
        return checkPath.StartsWith("~/CMS/", StringComparison.InvariantCultureIgnoreCase);
    }

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

    public override VirtualFile GetFile(string virtualPath)
    {
        return IsCustomContentPath(virtualPath) ? new CustomVirtualFile(virtualPath) : base.GetFile(virtualPath);
    }

    public override CacheDependency GetCacheDependency(string virtualPath, IEnumerable virtualPathDependencies, DateTime utcStart)
    {
        if (IsCustomContentPath(virtualPath))
        {
            var key = VirtualPathUtility.ToAppRelative(virtualPath);

            key = key.Replace("~/CMS/", "").Replace(".cshtml", "");

            var cacheKey = String.Format(ContentRepository.ContentCacheKeyFormat, key);

            var dependencyKey = new String[1];
            dependencyKey[0] = string.Format(cacheKey);

            return new CacheDependency(null, dependencyKey);
        }

        return Previous.GetCacheDependency(virtualPath, virtualPathDependencies, utcStart);
    }

    public override string GetFileHash(string virtualPath, IEnumerable virtualPathDependencies)
    {
        if (IsCustomContentPath(virtualPath))
        {
            return virtualPath;
        }

        return base.GetFileHash(virtualPath, virtualPathDependencies);
    }
}

Hope this helps!

Continuation answered 4/8, 2013 at 21:28 Comment(0)
N
1

I leaned heavily on the information in the OP as well as Darin Dimitrov's answer to create a simple prototype for sharing MVC components across projects. While those were very helpful, I still ran into a few additional barriers that are addressed in the prototype like using shared views with @model's.

Nuclei answered 18/3, 2015 at 16:10 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.