I’ve blogged in the past about the extensible component model for MVC Turbine, this post is a continuation on that concept, except with views as embedded resources.
Virtual Path Provider: The Secret Sauce
Not sure how many of you know this, but a VirtualPathProvider (VPP) is a way to provide the ASP.NET runtime with resources from a virtual file system. In other words, you can provide files such ash web forms, scripts or anything else that’s served to the ASP.NET run time to process. To learn how to this more in detail, check out the KB Article (910441). The one thing to know is that you can only have one registered VPP within your web application. So when you have this code:
1: HostingEnvironment.RegisterVirtualPathProvider(embeddedProvider);
You’re overriding the default, and internal, MapPathBasedVirtualPathProvider. So you’ll either need to implement all the pieces of the default VPP or compose your new VPP with default one and forward any requests that do not match your specific case. The EmbeddedViewVirtualPathProvider that ships with the blade, we’ll do just that.
EmbeddedViewBlade Internals
The code for the EmbeddedViewBlade is pretty simple:
1: public class EmbeddedViewBlade : Blade {
2: public override void Spin(IRotorContext context) {
3: IServiceLocator serviceLocator = GetServiceLocatorFromContext(context);
4: IEmbeddedViewResolver resolver = GetEmbeddedViewResolver(serviceLocator);
5:
6: // Get the current VPP so we can compose the new one
7: VirtualPathProvider virtualPathProvider = HostingEnvironment.VirtualPathProvider;
8:
9: // Get all the embedded views from the assemblies
10: EmbeddedViewTable table = resolver.GetEmbeddedViews();
11:
12: // If no views are found, don't bother.
13: if(table == null || table.Views.Count == 0) return;
14:
15: // Create the new VPP, pass in the new default VPP and register
16: var embeddedProvider = new EmbeddedViewVirtualPathProvider(table);
17: embeddedProvider.SetDefaultVirtualPathProvider(virtualPathProvider);
18:
19: HostingEnvironment.RegisterVirtualPathProvider(embeddedProvider);
20: }
21:
22: protected virtual IEmbeddedViewResolver GetEmbeddedViewResolver(IServiceLocator serviceLocator) {
23: try {
24: return serviceLocator.Resolve<IEmbeddedViewResolver>();
25: }
26: catch {
27: return new EmbeddedViewResolver();
28: }
29: }
30: }
As you can see the steps it performs are pretty simple
- Get the default VirtualPathProvider so we can use it later
- Parse all the assemblies in the AppDomain and get the embedded views (if none, skip the extra work and return).
- Create the EmbeddedViewVirtualPathProvider and associate the default VPP to it.
Once the EmbeddedViewVirtualPathProvider is registered, the ASP.NET runtime (hosting components specifically) can work with any embedded views. The code is as follows:
1: public class EmbeddedViewVirtualPathProvider : VirtualPathProvider {
2: private readonly EmbeddedViewTable embeddedViews;
3: private VirtualPathProvider defaultProvider;
4:
5: public EmbeddedViewVirtualPathProvider(EmbeddedViewTable table) {
6: if (table == null) {
7: throw new ArgumentNullException("table", "EmbeddedViewTable cannot be null.");
8: }
9:
10: embeddedViews = table;
11: }
12:
13: public void SetDefaultVirtualPathProvider(VirtualPathProvider provider) {
14: defaultProvider = provider;
15: }
16:
17: private bool IsEmbeddedView(string virtualPath) {
18: string checkPath = VirtualPathUtility.ToAppRelative(virtualPath);
19:
20: return checkPath.StartsWith("~/Views/", StringComparison.InvariantCultureIgnoreCase)
21: && embeddedViews.ContainsEmbeddedView(checkPath);
22: }
23:
24: public override bool FileExists(string virtualPath) {
25: return (IsEmbeddedView(virtualPath) ||
26: defaultProvider.FileExists(virtualPath));
27: }
28:
29: public override VirtualFile GetFile(string virtualPath) {
30: if (IsEmbeddedView(virtualPath)) {
31: EmbeddedView embeddedView = embeddedViews.FindEmbeddedView(virtualPath);
32: return new AssemblyResourceFile(embeddedView, virtualPath);
33: }
34:
35: return defaultProvider.GetFile(virtualPath);
36: }
37:
38: public override CacheDependency GetCacheDependency(
39: string virtualPath,
40: IEnumerable virtualPathDependencies,
41: DateTime utcStart) {
42: return IsEmbeddedView(virtualPath)
43: ? null
44: : defaultProvider.GetCacheDependency(virtualPath, virtualPathDependencies, utcStart);
45: }
46: }
There are other types such as the AssemblyResourceFile that do the actual work of pulling the view out of the assembly via the Assembly.GetManifestResourceStream method.
Wiring Things Up
The Embedded Views sample application has two assemblies with embedded views as well as the Spark View Engine as project references. These are
- SparkViews – Views for the Spark View Engine
- WebFormViews – Views for the WebForm View Engine
These references are set only to copy the assemblies into the bin folder for the application, there is no need to have a explicit reference to them.
To run the application, press F5 and click around. You’ll see the views being rendered as expected:
If you have the time, check out the source and tell me what you think!
Happy Coding!