In this post I will describe how to create virtual views for ASP.NET MVC. These are views that do not exist as files in the regular place in the file system (~/Views/xxx/yyy.aspx), instead they are stored somewhere. Elsewhere in this case can be a database or perhaps a .ZIP file. This is all made possible by the VirtualPathProvider class (and some supporting classes) in ASP.NET. While the example will use MVC framework and views, the class provides much more. With it you can virtualize any web content (css, gif/jpg, js, aspx, etc) and it can be used in any ASP.NET application, nut just MVC.
Use cases
When would you need such a virtual view system?
If you want users to be able to customize Views, but without them having access to the Web solution or source code. In this case the user could upload the View into the database, where it is stored. When the application wants to display the view, instead of reading the view source from the file system it will be read from the database. MVC will not know the difference and believes it is found under the regular ~/Views/... path.
Another possibility would be to use a ZIP file based virtual path provider. Here the user could create a custom skin by creating or customizing views (.ascx, .aspx) and adding new content (css, images). The user would then pack it into a ZIP, upload into the server, and server would serve the custom skin directly from the .ZIP file. This way with multiple skins installed the file system would not be polluted by a vast amount of files.
There are of course many more possibilities I could think of, but I hope you get the point
Getting ready
Before we begin, lets create a simple database table that will store the data for our example. It is very badly designed I have to admit , but will serve our purpose.
I called the table Pages and the idea is that it will serve dynamic pages into my MVC application. Kind of like a very simple CMS. However, I want to customize the view layout (.aspx), and not just the data itself. So I ended up with a table like this:
The table contains the data for the page (Body, Title) and the name/virtual path of the view (ViewName) and of course the view file itself (ViewData, uploaded as binary). Here is an example row. I uploaded a simple .ASPX view into the row.
In my example solution I added a LINQ to SQL .dbml to the solution (named MyMvcVp), dragged and dropped my table into the designer. I could then use the generated data context to access the database.
Controller
Next is my PagesController.cs that will serve our dynamic content pages. To display such a page I decided to use default route already available in the MVC template: /controller/action/id. I added a single Display action to the controller which will get the id parameter as a string value. This will have to correspond to the PageId field in the database. For example: /Pages/Display/d9b07a02-7c47-41d9-8c21-bf546841bb6c. The resulting code follows:
public class PagesController : Controller { public ActionResult Display ( string id ) { Guid guid = new Guid ( id ); MyMvcVpDataContext context = new MyMvcVpDataContext (); var res = (from p in context.Pages where p.PageId == guid select p).SingleOrDefault (); if ( res == null ) { return RedirectToAction ( "Index", "Home" ); } ViewData["Title"] = res.Title; ViewData["Body"] = res.Body; return View ( System.IO.Path.GetFileNameWithoutExtension ( res.ViewName ) ); } }
The code is very simple: it uses LINQ to look for the page, and if found, we extract the data from it, and instruct MVC to show it using our ViewName. To make the example very simple I included the virtual path in the database column, so here I use GetFileNameWithoutExtension() to get just the View name.
Because I wanted to keep the example simple I decided not to support virtual folders (which is also possible). So the folder where the virtual files seem to reside needed to be created also. I created a new Pages folder under /Views in the solution. If you implement virtual folders than this step could be left out.
At this point we still miss the actual VirtualPathProvider implementation. Without that you will get an error message when accessing this action, because MVC will not find the View.
VirtualPathProvider
So I added a new file MyVirtualPathProvider.cs to the solution. This contains the MyVirtualPathProvider class, that derives from VirtualPathProvider. I overrided two methods, FileExists() and GetFile(). Both get the virtual path (application relative). The first checks if a file exists, and the second returns the file from the file system - in this case the database.
Here is the code I used. Note that Page in this case refers to the LINQ class that was generated for the Pages table, and not the ASP.NET Page class.
public class MyVirtualPathProvider : VirtualPathProvider { public override bool FileExists ( string virtualPath ) { var page = FindPage ( virtualPath ); if ( page == null ) { return base.FileExists ( virtualPath ); } else { return true; } } public override VirtualFile GetFile ( string virtualPath ) { var page = FindPage ( virtualPath ); if ( page == null ) { return base.GetFile ( virtualPath ); } else { return new MyVirtualFile ( virtualPath, page.ViewData.ToArray () ); } } private Page FindPage ( string virtualPath ) { MyMvcVpDataContext context = new MyMvcVpDataContext (); var page = (from p in context.Pages where p.ViewName == virtualPath select p).SingleOrDefault (); return page; } }
As you can see if I don't find the virtual path in question from the database, the base implementation is called. That will just try to look up the file from the regular file system location.
You should notice that I used a class that I did not yet talk about: MyVirtualFile. This is just a simple implementation of the VirtualFile abstract class. Its purpose is to return a Stream where the file can be read. In my example I will just return a MemoryStream using the data from the database table. But this is where you would read the file from the actual place and pass it back to the framework.
public class MyVirtualFile : VirtualFile { private byte[] data; public MyVirtualFile ( string virtualPath, byte[] data ) : base ( virtualPath ) { this.data = data; } public override System.IO.Stream Open () { return new MemoryStream ( data ); } }
If you wanted to support virtual directories you would also override the DirectoryExists() and GetDirectory() methods from the VirtualPathProvider. You would also need a custom VirtualDirectory derived class.
Registering the custom VirtualPathProvider
As a last step the MyVirtualPathProvider must be registered with ASP.NET. To accomplish this you need to add the following call to the application:
HostingEnvironment.RegisterVirtualPathProvider ( new MyVirtualPathProvider () );
The best place is probably in the Init() override of the application class or perhaps the Application_Start event handler (global.asax, unless you coded a custom application class somewhere in which case that would be the ideal place).
Conclusion
Virtual files and directories can be used to extend an existing MVC application (or a Web Forms application) by allowing you to store files in a place that is different from the regular file system. Be it a database, ZIP file or a remote network location, by using VirtualPathProvider the rest of the application will not have to know anything about the actual storage strategy.