Recently I got slapped back into reality receiving a Twitter message from my collegue Patrik Akselsson in which he kindly asked me to hand him a GitHub link to the code from one of my blog posts. Being long overdue uploading my code into a public repository and constructing NuGet packages for my EPiServer features, I decided to do so for all my relevant posts. While at it, I have also been moving my code away from the EPiServer AlloyTech sample site that I use into separate Visual Studio 2010 projects resulting in the need to embed various resources in assemblies; such as for instance JavaScripts and Stylesheets, as well as pages and user controls. Links to all the sample code in this post may be found at the bottom.
Embedding an EPiServer GuiPlugIn in a binary assembly
The problem with doing this is that the EmbeddedPlugin.aspx file holding the custom EPiServer tool will not like being included in a library class project. But why would it? It probably knows that it will be hard for EPiServer to find it if it is stashed away somewhere without a proper URL. In order to use your aspx and ascx files as embedded resources in a binary assembly, you must first tell Visual Studio that it should treat them as such. This is done by bringing up the Properties pane (the bottom option on the right mouse click context menu) for the file in question, setting the Build Action option to Embedded Resource. The images below show where this is done for the EmbeddedControl.ascx file.
Obviously it is important that you do not touch this setting for the code-behind EmbeddedControl.ascx.cs and its designer file as they need to be compiled into the assembly rather than being used as resources.
The approach that we will take embedding these files is based on using a custom VirtualPathProvider to dynamically load the pages via a resource stream as they are requested. Our EmbeddedResourceProvider class extends VirtualPathProvider and will help us doing so.
EmbeddedResourceProvider.cs
public class EmbeddedResourceProvider : VirtualPathProvider {
private static bool IsEmbeddedResourcePath(string virtualPath) { var path = VirtualPathUtility.ToAppRelative(virtualPath); return path.StartsWith("~/EmbeddedResource/", StringComparison.InvariantCultureIgnoreCase); }
The first thing that we need is a way to determine whether or not a request was made trying to retrieve one of our embedded resources. I decided to construct these imaginary URLs always starting with ~/EmbeddedResource/ (33). This will allow me to only handle the relevant requests, and pass the other ones up the ladder to the base methods. It may be a good idea to pick a name which you know will not be used as the name of an EPiServer page directly under the Start node, or as a directory with files in the web root; you may regret it if you do.
public override VirtualFile GetFile(string virtualPath) { return IsEmbeddedResourcePath(virtualPath) ? new VirtualResourceFile(virtualPath) : base.GetFile(virtualPath); }
As ASP.NET wants to resolve a virtual path, it makes a request to the VirtualPathProvider that was registered most recently and asks if the resource exists. If it does, it makes a call the the GetFile method above to retrieve it. As you can see, we first check if this is an attempt to get our EPiServer GuiPlugIn, or just another visitor. If it indeed is someone trying to access our custom tool, we will need to construct the virtual file ourselves before we can return it; we will get back to how this is done with the VirtualResourceFile class in just a bit. If the request was not made with embedded resources in mind, we simply pass it along to the default GetFile method upstairs. Before we take a look at how to conjure up an imaginary file, there is one last important thing worth mentioning.
public override CacheDependency GetCacheDependency(string virtualPath, IEnumerable virtualPathDependencies, DateTime utcStart) { return IsEmbeddedResourcePath(virtualPath) ? null : base.GetCacheDependency(virtualPath, virtualPathDependencies, utcStart); }
To prevent ASP.NET from meddling in attempting to monitor changes in our non-exisiting file (thus throwing FileNotFound exceptions all over the place), the GetCacheDependency must return a null value; see the examples over at MSDN if you would like to know more.
VirtualResourceFile.cs
public override Stream Open() { var pathArray = _path.Split('/'); var resource = pathArray[3]; var assemblyFile = Path.Combine(HttpRuntime.BinDirectory, pathArray[2]); var assembly = System.Reflection.Assembly.LoadFile(assemblyFile); var stream = assembly.GetManifestResourceStream(resource); if(stream == null) { throw new Exception(string.Format("Unable to get resource '{0}' manifest resource stream from assembly '{1}'.", resource, assemblyFile)); } return stream; }
The interesting part of the VirtualResourceFile is the Open method overridden from the extended VirtualFile class. This is where we need to parse our fake resource path and load our EPiServer GuiPlugIn. You can probably do this in a more elegant way, but since we know what an embedded resource URL will look like and since we are in control of all the requests being made, it is easier just to do a string split (19). Apart from the ~/EmbeddedResource/ flag, there are two other components that are needed in order to load our resource; an assembly to load from, and the requested resource. The resource URLs will look as follows:
~/EmbeddedResource/{assembly}.dll/{namespace}.{resource filename}
You are free to add your own query parameters at your discresion; we will have a look at doing this in a moment. So, the path is split up into different sections, and knowing which one contains our assembly filename, we can use it to bring up the proper binary (23). Then it is just a matter of calling the GetManifestResourceStream method with the namespace together with the name of our EPiServer GuiPlugIn page (24), and we will be ready to go.
EmbeddedPlugin.aspx.cs
[GuiPlugIn( DisplayName = "Embedded Plugin", Description = "Plugin that was embedded and distributed as a binary assembly file.", Area = PlugInArea.AdminMenu, RequiredAccess = AccessLevel.Administer, Url = "~/EmbeddedResource/EmbeddedEPiServerResources.dll/EmbeddedEPiServerResources.EmbeddedPlugin.aspx" )] public partial class EmbeddedPlugin : SystemPageBase {
The only thing that is different in the embedded EPiServer GuiPlugIn page code-behind file is the Url attribute parameter (15). In order to have EPiServer go look for the custom tool in the correct place, we need to point it to an address on the form that we previously defined. That is really almost all there is to it; if you would like to pass along query parameters in say for instance an HTML link, just concatenate them at the end like you normally would:
href=”/EmbeddedResource/EmbeddedEPiServerResources.dll/EmbeddedEPiServerResources.EmbeddedPlugin.aspx?id=19&color=red“
The request object’s query parameters are not touched by the VirtualPathProvider, so you can handle them as you wish at this end as well.
var container = new HtmlGenericControl("div"); foreach (var key in Request.QueryString.AllKeys) { container .Controls .Add(new Literal { Text = string.Format("<p>Query parameter named <strong>{0}</strong> has value <strong>{1}</strong>.</p>", key, Request.QueryString[key]) }); }
Acquiring embedded resources is no different whether you request them from code-behind, or from code-infront via links or Register directives. It is just the URL that has changed.
<%@ Register TagPrefix="uc" TagName="EmbeddedControl" Src="~/EmbeddedResource/EmbeddedEPiServerResources.dll/EmbeddedEPiServerResources.EmbeddedControl.ascx" %> <uc:EmbeddedControl runat="server" /> <a href="/EmbeddedResource/EmbeddedEPiServerResources.dll/EmbeddedEPiServerResources.EmbeddedControl.ascx" title="..">Embedded control</a>
var embeddedControl = LoadControl("/EmbeddedResource/EmbeddedEPiServerResources.dll/EmbeddedEPiServerResources.EmbeddedControl.ascx") as EmbeddedControl;
The last thing needed to be done is making ASP.NET use our EmbeddedResourceProvider trying to access virtual paths. This is normally done in one of two different ways (and the web.config file is not one of them) before any page parsing or compilation is performed by the Web application; either by using the RegisterVirtualPathProvider method in the Application_Start event of Global.asax, or through an AppInitialize method defined in the App_Code directory.
Global.asax.cs
public class Global : System.Web.HttpApplication { void Application_Start(object sender, EventArgs e) { HostingEnvironment.RegisterVirtualPathProvider(new EmbeddedResourceProvider()); }
App_Code\AppStart.cs
public static class AppStart { public static void AppInitialize() { HostingEnvironment.RegisterVirtualPathProvider(new EmbeddedResourceProvider()); } }
The two examples above are modified versions of the ones from the examples section at MSDN. The files are not included in the Visual Studio 2010 sample project, but are located in the ExampleInitialization directory.
Embedding JavaScript, Stylesheet and image resources in a binary assembly
Embedding and using other resources is a whole lot easier than retrieving embedded pages and user controls. There are only a few steps needed to be taken, first of which is setting the BuildAction to Embedded resource for all of the files that you wish to embed; as shown in the images at the top of this post. There is no need for a VirtualPathProvider, and all example code for this section may be found in the EmbeddedControl.ascx.cs file.
EmbeddedControl.ascx.cs
[assembly: System.Web.UI.WebResource( "EmbeddedEPiServerResources.Styles.EmbeddedStylesheet.css", "text/css")] [assembly: System.Web.UI.WebResource( "EmbeddedEPiServerResources.Styles.Images .Embedded_EPiServer_logo.gif", "image/gif")] [assembly: System.Web.UI.WebResource( "EmbeddedEPiServerResources.Scripts.EmbeddedJavaScript.js", "application/javascript")] namespace EmbeddedEPiServerResources { public partial class EmbeddedControl : UserControlBase {
After you have fiddled with the BuildAction of your files, it is time to expose them as embedded resources by adding a few lines of code above your namespace (4-6). You may also do this in the AssemblyInfo.cs file in the Properties directory. When this is done, your resources are accessible by calling the ClientScriptManager‘s GetWebResourceUrl method with the proper type and resource, and it is up to you how you would like to use them on your site. If they do not show up, chances are that you made a typo in the {namespace}.{resource name} string.
private void IncludeJavaScript() { var jsPath = Page .ClientScript .GetWebResourceUrl(typeof(EmbeddedControl), "EmbeddedEPiServerResources.Scripts.EmbeddedJavaScript.js"); Page.ClientScript.RegisterClientScriptInclude( "EmbeddedEPiServerResources.Scripts.EmbeddedJavaScript.js", jsPath); } private void IncludeStylesheet() { var cssPath = Page .ClientScript .GetWebResourceUrl(typeof(EmbeddedControl), "EmbeddedEPiServerResources.Styles.EmbeddedStylesheet.css"); var cssLink = new HtmlLink { Href = cssPath }; cssLink.Attributes.Add("rel", "stylesheet"); cssLink.Attributes.Add("type", "text/css"); cssLink.Attributes.Add("media", "screen"); Page.Header.Controls.Add(cssLink); } private void IncludeImage() { var imagePath = Page .ClientScript .GetWebResourceUrl(typeof(EmbeddedControl), "EmbeddedEPiServerResources.Styles.Images .Embedded_EPiServer_logo.gif"); EmbeddedImage.ImageUrl = imagePath; }
The EmbeddedImage (46) above is a normal ASP.NET Image control. After building and surfing to the GuiPlugIn interface in EPiServer’s Admin Mode, all of the embedded resources will be used as expected.
Source code
Source code: EmbeddedEPiServerResources.zip
GitHub link: https://github.com/matkun/Blog/tree/master/SamplesAndExamples/EmbeddedEPiServerResources