EPiServer plugin in a single assembly - NOT PUBLISHED

by Dan Matthews 27. February 2008 15:48

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:

  1. Open up VS.NET 2005
  2. Create a new class library project called SingleAssemblyPlugin (I'm using C# for this article)
  3. 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
  4. Delete the Class1.cs file... the namespace won't be set correctly and I've never seen the point of it :)
  5. Add a reference to System.Web and the EPiServer.dll file (you'll find a copy in the BIN folder of your EPiServer website)
  6. Create a new class file called AssemblyResource.cs
  7. Cut-and-paste the AssemblyResourceProvider and AssemblyResourceVirtualFile classes from this CodeProject article from Kurt Harriger into your new class
  8. 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 and .ASCX.CS files to 'Embedded Resource'. This will mean they are embedded as resources within the assembly and not compiled. We don't actually want them compiled until needed at runtime anyway.

There is another snag though. The way that EPiServer searches for plugins is to use Reflection across the assemblies in the BIN folder of the website looking for types that have the EPiServer plugin attributes set. The user control we created we told the project to embed rather than compile. This means that the assembly won't actually have any type with the EPiServer plugin attributes set as the class needs to be compiled to be a visible type. Thankfully, we can use one of the lovely features of .NET 2 to get around this - partial classes. All we do is to create a 'hook' class with the same class name as inside the user control's .CS file and add our EPiServer plugin tag to that. We then let that class compile into the assembly. All it is there to do is to tell EPiServer the details of the plugin, nothing more.

The final snag is that when a resource is embedded, it must be embedded with a full namespace. That's fine, except that the way that LoadControl works when EPiServer actually renders the control is to look at the 'CodeFile' attribute of the .ASCX file to determine the .ASCX.CS file. By default the 'CodeFile' attribute does not contain the namespace as well, just the filename. We therefore need to change the 'CodeFile' attribute to include the name of the .ASCX.CS file with its full namespace. The VS.NET2005 IDE will show an error because it doesn't like this... it will tell you that the .ASCX.CS file 'was not found', but the actual compiler won't throw an error because in fact 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:

  1. Add a new user control to the class library called HelloWorld
  2. 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
  3. Also in the HelloWorld.ascx file, change the CodeFile attribute to include the namespace from earlier. In my example, it would be BusinessDecision.EPiSample.SingleAssemblyPlugin.HelloWorld.ascx.cs
  4. On the property page for HelloWorld.ascx and HelloWorld.ascx.cs, change the Build Action to 'Embedded Resource'
  5. Add a new class file called HelloWorldHook
  6. Add the following code into the new class (remember to change the URL if you are using a different namespace)

[EPiServer.PlugIn.GuiPlugIn(DisplayName = "HelloWorld",
Description = "Hello World!",
Area = EPiServer.PlugIn.PlugInArea.ActionWindow,
Url = "~/App_Resource/SingleAssemblyPlugin.dll/BusinessDecision.EPiSample.SingleAssemblyPlugin.HelloWorld.ascx")]
public partial class temp_HelloWorld : System.Web.UI.UserControl
{
}

Now we can compile our class library and our assembly is ready to be dropped into the BIN folder of our EPiServer website. 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 , but 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. That works of course, but I don't like adding references if I can avoid it. I prefer to use .NET-style 'late binding'. Therefore, I use this code in my Global.asax.cs file:

protected void Application_Start(object sender, EventArgs e)
{
    System.Reflection.Assembly lateBoundAssembly = System.Reflection.Assembly.LoadFrom(Server.MapPath(@"\bin\SingleAssemblyPlugin.dll"));

    System.Web.Hosting.HostingEnvironment.RegisterVirtualPathProvider((System.Web.Hosting.VirtualPathProvider)lateBoundAssembly.CreateInstance("BusinessDecision.EPiSample.SingleAssemblyPlugin.AssemblyResourceProvider"));
}


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. Note also that if you already have code in your Application Start event then you'll just need to add these 2 lines somewhere in there.

Now recompile your website (assuming you're using a precompilation model). Fire up your EPiServer 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, hooked 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', you can just drop an updated plugin into the BIN folder of your website
  • The VPP provider could be extracted into a separate library and re-used... maybe EPiServer would even build it into the next version of EPiServer or a service pack so that we can all use a URL with a standard convention and have our plugins automatically picked up! I must ask... :)

There are a couple of things I don't like so much about this technique though and any feedback on these matters, although minor, would be appreciated.

Firstly, although the VPP only needs to be placed in the Global.asax.cs once, I'd like to avoid it altogether if possible. I can't think of any other way to get the VPP in the chain though. Ideas?

Secondly, 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!

Tags:

.NET/C# | EPiServer

Powered by BlogEngine.NET 1.5.0.7
Theme by Interakting

Interakting

A full service digital agency offering online strategy, design and usability, systems integration and online marketing services that deliver real business benefits and ensure your online objectives are met.

Calendar

<<  February 2012  >>
MoTuWeThFrSaSu
303112345
6789101112
13141516171819
20212223242526
2728291234
567891011

View posts in large calendar