I was writing an EPiServer plugin recently and it was bothering me slightly that EPiServer only supports .ASCX or .ASPX pages for plugins. What I wanted to do was just drop a single DLL file into the BIN folder of my website. EPiServer 5 does have much better plugin management, true, but it still bothered me. I wanted to have control over the content of my plugin and didn't want to have to package my control along with a DLL.
After rummaging around a bit to see if EPiServer would pick up any overridden render methods if I missed the URL attribute off a plugin (it doesn't) I decided to drop Steve Celius over at EPiServer an email. He said that he'd been having much the same thoughts, and suggested I think about using a custom Virtual Path Provider. I'd already thought about that myself, but Steve suggested the magical twist to it that hadn't occurred to me... he suggested packaging the ASCX/ASPX actually within the plugin assembly DLL itself!
The idea really appealed to me of the assembly both registering the plugin AND providing the control as well, and so I started my investigation with some links that Steve sent me of others that had been doing it in general ASP.NET coding. What I needed to do was work out a way to make it work cleanly with EPiServer.
It turns out that it is actually incredibly easy. So easy, in fact, that I can give you a step-by-step guide to writing a simple single-assembly plugin. Note that I'm using a user control (.ASCX) in the Action Window rather than a page (.ASPX) but I imagine the principles would still broadly apply.
Before we start, I want to give a major shout out to Kurt Harriger and his post over at CodeProject for doing all of the legwork around a custom VPP for assembly-embedded resources. To start with, lets do a step-by-step so that we have the 'infrastructure' in place:
- Open up VS.NET 2005
- Create a new class library project called SingleAssemblyPlugin (I'm using C# for this article)
- Set the default namespace of the project... I'm using BusinessDecision.EPiSample.SingleAssemblyPlugin but you can use anything you like - just make sure you amend your code as appropriate
- Delete the Class1.cs file... the namespace won't be set correctly and I've never seen the point of it :)
- Add a reference to System.Web and the EPiServer.dll file (you'll find a copy in the BIN folder of your EPiServer website)
- Create a new class file called AssemblyResource.cs
- Cut-and-paste the AssemblyResourceProvider and AssemblyResourceVirtualFile classes from this CodeProject article from Kurt Harriger into your new class
- Add a 'using' statement for System.Web and System.Web.Hosting to your class file
At this point we have a class library all ready to go and ready to start embedding resources. We have a drawback though. We can create a user control in the class library but it will just get compiled and when you distribute the DLL it won't be present as a resource. The way to get around this is to add the user control but then to set 'Build Action' property of the .ASCX file to 'Embedded Resource'. This will mean it is embedded as a resource within the assembly and not compiled.
The other snag is that the way that Visual Studio figures out the code behind file is to look at the 'CodeBehind' or 'CodeFile' attribute of the .ASCX file to determine the .ASCX.CS file. As we aren't using the designer, we don't need to worry about the attribute. Just delete it. Scary I know, but if we don't Visual Studio will complain. The compiler won't throw an error because we set the user control to be an embedded resource rather than compiled.
That all might sound complicated but in reality it's very simple to implement. Let's see this step-by-step:
- Add a new user control to the class library called HelloWorld
- In the HelloWorld.ascx file, just put in some 'Hello World' text so that we can be sure it's working when we use it later
- Also in the HelloWorld.ascx file, delete the CodeFile (or CodeBehind attribute)
- On the property page for HelloWorld.ascx change the Build Action to 'Embedded Resource'
- In HelloWorld.ascx.cs, add the following snippet just before the class declaration
1: [EPiServer.PlugIn.GuiPlugIn(DisplayName = "HelloWorld",
2: Description = "Hello World!",
3: Area = EPiServer.PlugIn.PlugInArea.ActionWindow,
4: Url = "~/App_Resource/SingleAssemblyPlugin.dll/BusinessDecision.EPiSample.SingleAssemblyPlugin"]
Just one thing left to do. Everything is there ready to go and EPiServer would pick up the presence of the plugin, but when you tried to use it the EPiServer site would throw an error about not being able to find the user control. This is because it is trying to load the user control via the URL like any other control - it doesn't know about our custom VPP. We therefore need to tell it about the custom VPP. Kurt Harriger's article gives one way to do this via adding a reference and putting some code in the Global.asax.cs file of our website. That works, and you could also use .NET-style 'late binding' instead, but Steve Celius told me that there is a neater way to do it using some built-in EPiServer VPP support (thanks also to Mats Hellström for the original code).
Doing this again step-by-step:
- Add a new class to the class library called AssemblyResourceRegistration
- Add 'using' statements for System.Reflection, System.Web and System.Web.Hosting
- Delete the automatically created class declaration
- Copy-and-Paste in the class code below
1: [EPiServer.PlugIn.PagePlugIn("Vpp Initializer Plugin", "")]
2: public class VppInitializer
3: {
4: public static void Initialize(int optionFlag)
5: {
6: System.Diagnostics.Debug.Write("VppInitializer.Initialize called with: " + optionFlag.ToString());
7:
8: if (HttpContext.Current == null)
9: {
10: // Running from the scheduler, skip registration
11:
12: System.Diagnostics.Debug.Write("VppInitializer called without HttpContext. Exiting");
13:
14: return;
15: }
16:
17: // Register
18:
19: Assembly lateBoundAssembly = System.Reflection.Assembly.LoadFrom(HttpContext.Current.Server.MapPath(@"\bin\SingleAssemblyPlugin.dll"));
20:
21: HostingEnvironment.RegisterVirtualPathProvider((VirtualPathProvider)lateBoundAssembly.CreateInstance("BusinessDecision.EPiSample.SingleAssemblyPlugin.AssemblyResourceProvider"));
22: }
23: }
What this code is doing is adding our custom VPP to the 'chain' of Virtual Path Providers so that it will be evaluated whenever there is a call for a virtual path (which LoadControl does). Note again that you'll need to change the name of the namespace to the one you want.
Now compile your class library and drop the DLL into the BIN folder of your EPiServer site. Fire up your site, and login to the Edit Mode. Open the Action Window and you should have a new entry!

And if we click the new entry we see:

And it is that simple! We've just built a single-assembly plugin, dropped it in and fired it up. The scope for simple single-assembly plugins is really limitless, and there are a number of advantages to doing things this way.
- One of the nice things about using this technique rather than a Server Control is that it's a much nice development environment. If you've ever written a Server Control, then you will know that they often end up as piles of code. This way, you can even write the user control as part of your site before abstracting it later and take advantage of all the IDE tools.
- Because the assembly is 'late-bound' and registers itself, you can just drop an updated plugin into the BIN folder of your website
There is one minor thing I don't like so much about this technique though. The VPP is checked with every request for a virtual path. Now, as you can see in the code it is doing a string comparison. I can't see that this would perform badly but it's one more thing per page hit and on a huge site it might have some tiny impact. I think this is unavoidable but hopefully negligible.
Any other feedback or improvements on this technique would be appreciated!