MEF and Razor Views inside Class Library
Asked Answered
C

4

28

I have a composite ASP .NET MVC 3 Razor application using MEF. Everything goes fine if I am to deploy plugins as DLL files and views (CSHTML) under the regular Views folder from the application. But this is not very clean and it won't be a real plugin if I don't place views as embedded resources within the DLL files (along with both controllers and models).

I've followed many articles (most of them are outdated). In fact there is one quite good one here on Stack Overflow: Controllers and Views inside a Class Library

I've also checked docs for VirtualPathProvider and I've been able to build a custom one that finds the file within the assembly and loads it perfectly (or at least gets the stream to it). For this I've followed the VirtualPathProvider documentation on MSDN.

There is also an implementation for VirtualFile but not yet for VirtualDirectory.

Here is the problem. I'm working with Razor views. I do know that they need config specs from the web.config file for Razor to build them. But if I embed them within the DLL this config is simply lost.

I wonder if that's why I keep getting the error:

The view at '~/Plugins/CRM.Web.Views.CRM.Index.cshtml' must derive from WebViewPage, or WebViewPage.

Maybe I just need to add some code to make it work? Any ideas?

Cypress answered 15/2, 2011 at 17:56 Comment(15)
Something to think about: If I am to add "@inherits System.Web.Mvc.WebViewPage" on every cshtml file everything goes quite fine. However I can't simply do that (it would be a huge effort for something that is already automatic using regular view files). So, any ideas?Cypress
This post might be of some help but I simply can't test it right now. I'll give it a try ASAP and post the results/answer here. #6466355Cypress
Have you already tried to put also the config file into an adequate virtual path, in such a way to simulate the "standard arrangement" we have under the Mvc View Folder?Hutto
This question is rather old and I can't try this again right now (other priority projects) but the last time I have tried this it didn't work. I'm not 100% sure if I have tested every possible option with the VirtualPath but as far as I can remember Razor wouldn't see the web.config from the virtual path provider.Cypress
It is strage that it doesn't work. Virtual Path is an abstraction of a file system used BY ALL asp.net classes, so Razor would break this pattern by accessing directly th Actual file system without using the virtual path provider...difficult to belive that Microsoft people have done such a Big Design error. Maybe. your problems are due someway to error in relative paths. Give a look to this old post: thecodinghumanist.com/Content/VirtualPathProviderExample.aspx There is also some software you can download...may be there is something missing in your implementation compare with itHutto
The link is old, and yes it works fine with aspx and ascx. Not working with CSHTML.Zigrang
@LordALMMa: Did you solve the problem? I am trying to do the same as you.Ultann
@Ultann I'm sending a zip file with views for now. Nothing yet. I didn't have much time to test provided ideas because there is another project here "dragging" all of my attention, but I'll get back into this as soon as this other project is finished (most likely to be after 2012-08-15).Cypress
@LordALMMa: I will wait for that. If I can manage it myself, I will let you know :)Ultann
You can use my EmbeddedResourceVirtualPathProvider which can be installed via Nuget. It loads resources from referenced assemblies, and also can be set to take dependencies on the source files during development so you can update views without needing a recompile.Maceio
@Maceio THANKS! This looks REALLY interesting!Cypress
If you find it useful, feel free drop a tweet or something. I have loads of useful things on github and no-one using them!Maceio
@ALMMa - did you find a solid solution for building your plugin architecture ?Basement
@Maceio - by any chance is there any sample / documentation on the embeddedResourceVirtualPathProvider ?Basement
There is a sample project in the github repo. Its fairly simple - make the files embedded resources, reference the assembly in the App_Start file (added automatically by Nuget). If you do find it useful, and are feeling like a good citizen, my fave pull requests are documentation :)Maceio
P
7

My preferred way to embed Razor Views in a Class Library is to copy them into the MVC website's Views/Areas folders with a post build event. Custom view locations can be specified if you override the ViewEngine or VirtualPathProvider.

The tricky part for me was getting intellisense to work in these View Class libraries. First, you must add a Web.Config to your View assembly. Note that you don't have to actually include it in your assembly. It only has to be in the assembly root directory (or views folder). Here is an example. Regard the important Assemblies/Compilation section.

<?xml version="1.0"?>
<configuration>
  <configSections>
    <sectionGroup name="system.web.webPages.razor" type="System.Web.WebPages.Razor.Configuration.RazorWebSectionGroup, System.Web.WebPages.Razor, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35">
      <section name="host" type="System.Web.WebPages.Razor.Configuration.HostSection, System.Web.WebPages.Razor, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" />
      <section name="pages" type="System.Web.WebPages.Razor.Configuration.RazorPagesSection, System.Web.WebPages.Razor, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" />
    </sectionGroup>
  </configSections>

  <system.web.webPages.razor>
    <host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
    <pages pageBaseType="System.Web.Mvc.WebViewPage">
      <namespaces>
        <add namespace="System.Web.Mvc" />
        <add namespace="System.Web.Mvc.Ajax" />
        <add namespace="System.Web.Mvc.Html" />
        <add namespace="System.Web.Routing" />
      </namespaces>
    </pages>
  </system.web.webPages.razor>

  <appSettings>
    <add key="webpages:Enabled" value="false" />
  </appSettings>

  <system.web>
    <compilation targetFramework="4.0">
      <assemblies>
        <add assembly="System.Web.Abstractions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
        <add assembly="System.Web.Routing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
        <add assembly="System.Data.Linq, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089"/>
        <add assembly="System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
        <add assembly="System.Web.WebPages, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
      </assemblies>
    </compilation>

    <httpHandlers>
      <add path="*" verb="*" type="System.Web.HttpNotFoundHandler"/>
    </httpHandlers>

    <!--
        Enabling request validation in view pages would cause validation to occur
        after the input has already been processed by the controller. By default
        MVC performs request validation before a controller processes the input.
        To change this behavior apply the ValidateInputAttribute to a
        controller or action.
    -->
    <pages
        validateRequest="false"
        pageParserFilterType="System.Web.Mvc.ViewTypeParserFilter, System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"
        pageBaseType="System.Web.Mvc.ViewPage, System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"
        userControlBaseType="System.Web.Mvc.ViewUserControl, System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35">
      <controls>
        <add assembly="System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" namespace="System.Web.Mvc" tagPrefix="mvc" />
      </controls>
    </pages>
  </system.web>
  <system.webServer>
    <validation validateIntegratedModeConfiguration="false" />
    <handlers>
      <remove name="BlockViewHandler"/>
      <add name="BlockViewHandler" path="*" verb="*" preCondition="integratedMode" type="System.Web.HttpNotFoundHandler" />
    </handlers>
  </system.webServer>
</configuration>

Next, you need to modify your class library's vbproj file so that all OutputPath elements point to 'bin\' instead of 'Debug\bin\' or 'Release\bin\'. This is the main difference I found between class libraries and ASP.Net web project types that can cause intellisense bugs.

If you still recieve your must inherits error, consider using @Inherits System.Web.Mvc.WebViewPage in your views. If you are not copying your views into your website project, you may be loading them from Embedded Resources using a custom ViewEngine / VirtualPathProvider. If that is the case, you definately need the Inherits so Razor knows what your view base class is unfortunately.

Good luck.

Preeminent answered 23/11, 2011 at 17:41 Comment(1)
Interesting. I'll give this a try and post results.Cypress
Q
3

You might take a look at the following blog post.

Quiescent answered 15/2, 2011 at 18:16 Comment(4)
Interesting. But this requires me to pre-compile all views. I'm not into that, I simply want to add the cshtml file inside the class library and that's all (Embedded Resource and just that). Other thing that I think you simply couldn't see at all is that I AM BUILDING PLUGINS! Correct me if I'm wrong but I've never seen the main app reference a plugin that it simply don't even know whether the plugin exists or not! =)Cypress
@LordALMM, why? How does the provided solution doesn't meet your requirements in externalizing views in separate assemblies?Quiescent
"Interesting. But this requires me to pre-compile all views. I'm not into that, I simply want to add the cshtml file inside the class library and that's all (Embedded Resource and just that)." (Feb, 15). It works? Yes. But it's not the best way. Just because you can kill an ant with bomb this don't makes it the best approach for the task. I'm looking for a way to get Razor and some sort of provider together so I could go for an embedded resource (but NOT pre-compiled classes, just plain CSHTML files).Cypress
Also because if I am to create customizable plugins it's quite easier to have source code than assembly or IL. What I'm trying is to create some sort of MVC MEF application that allows my customer to create CSHTML files and add more features of their own (like SAP with the customized code).Cypress
N
2

Hossam,

The post you're talking about is what Darin has already suggested. The main down side to that approach is using the custom MvcRazorClassGenerator compiler to convert the CSHTML view files in to class files. To do so you have to set every CSHTML view in your project to Content and set the Custom Tool to MvcRazorClassGenerator.

I can't speak for LordALMMa but I did download the compiler source and gave it a shot and it doesn't exactly work the way I was hoping.

My other approach was to include the CSHTML files as Embeded Resources in the external DLL, read in the raw contents of the file and execute the view as a string (See the RazorEngine on CodeProject for an example: http://razorengine.codeplex.com/)

I didn't want to fully depend on the RazorEngine in an enterprise application because I don't know how well it is compatiable with all of the Razor syntax so I gave up on that for now.

I'm coming from a prototype I built in ASP.NET MVC 2.0 that is a multi-tennant application. On a server farm we have one instance of an application running where all clients share the same code base. In my MVC 2.0 prototype I was able to determine what "client" the request was being made for, check for a custom controller that over-rides the base (for customizations of the core code) and also check for custom views (for customizations of the core view). What this does is allow us to deploy a "plugin" per say for each client. The software detects if the client has a custom controller that matches the request as well as a custom action that matches and if it does, it uses the customized controller/action instead.

When I started migrating my prototype to MVC 3 I ran in to the same problem as LordALMMa, the error "The view at '...Index.cshtml' must derive from WebViewPage, or WebViewPage". I'll look in to placing "@inherits System.Web.Mvc.WebViewPage" on my CSHTML views and see if that gets me any closer to getting it to work.

Since I have a working MVC 2.0 prototype using MVC 3 Razor is not a top priority and I don't waste a ton of time on it. I'm sure I can port the MVC 2.0 to MVC 3.0 using the WebForms engine if we need to leverage the 4.0 Framework.

Nosh answered 25/5, 2011 at 3:13 Comment(0)
P
0

Hey I suspect you have good reasons for wanting views inside DLLs. However also consider that it is an unusual way of packaging everything into one entity.

If you are developing a plugin, these days people opt for packaging in the NUGET format, which also solves your kind of problem among other things. It has a .nupkg structure which is also one way of distributing plugins as packages and libraries.

Another solution which communities generally follow is (if they do not want something as elaborate as nuget) they code up the plugin DLLs such that, it does not use view engines like razor, instead outputs HTML all by itself using the old primitive way of Response.Write and thus become independent of cshtml files. If you still want to use cshtml - see this blog entry for precompiling those into classes.

Peculium answered 23/11, 2011 at 17:55 Comment(1)
Hmm I know the answer is not what the question is looking for, but my point is to discourage the asker, from going along this way of solving his deeper problem of packaging.Peculium

© 2022 - 2024 — McMap. All rights reserved.