Utilising MEF to self-register HTTP modules

by Tom Pipe 16. January 2012 22:08

After reading about .NET 4’s new PreApplicationStart method being used to register build providers without touching the web.config. I thought about other instances where editing the web.config was mundane. The first thing that came to mind was registering HTTP modules.

HTTP Modules are a fantastic way to intercept HTTP requests, and “inject” code at each stage of the request lifecycle, and they are invaluable for sectioning code into pluggable components for reuse in another application. The only thing that’s bugged me is the need to register each module by adding an entry in the <httpModules> section within the web.config.

Clearly great minds think alike. David Ebbo, an architect on the ASP.NET team announced a RegisterModule() API had been added in MVC3 allowing HttpModules to be registered without touching config, by using code like below:

    [assembly: System.Web.PreApplicationStartMethod(typeof(Initializer), "Initialize")]
    public static class Initializer
    {
        public static void Initialize()
        {
            DynamicModuleUtility.RegisterModule(typeof(ModuleA));
            DynamicModuleUtility.RegisterModule(typeof(ModuleB));
            DynamicModuleUtility.RegisterModule(typeof(ModuleC));
        }
    }

Yes it has it’s advantages, but this feels more like a step sideways than a step forward. Each module still has to be explicitly defined, so it’s almost exactly the same steps as adding a HttpModule via the web.config, albeit a slightly different syntax.

This got me wondering if there was an easy way to discover modules and automatically register them using the RegisterModule method, and that seemed like a perfect scenario for MEF.

Managed Extensibility Framework (MEF)

MEF is a component of .NET 4.0 for creating lightweight, extensible applications. Extensions (known as parts, or exports) can be dynamically discovered (imported) from a configurable location (known as a Catalog) and used at run-time with no configuration required.

MEF uses an attributed programming model where “imports” and “exports” are declared by decorating classes or members with the Import and Export attributes.

An Import is matched with one or more Exports, providing they have the same “contract”. The contract consists of a string, called the contract name, and the type of the exported or imported object, called the contract type. Only if both the contract name and contract type match is an export considered to be a match.

Attributes

Usually classes are marked with the ExportAttribute but I wanted something that’s more distinct and self-explanatory. Therefore I created a new attribute, derived from the standard ExportAttribute. The code for the attribute is as follows:

    [MetadataAttribute]
    [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
    public class AutoRegisterHttpModuleAttribute : ExportAttribute, IMefMetaData
    {
        public AutoRegisterHttpModuleAttribute() : base(typeof(IHttpModule))
        {
            Enabled = true;
        }

        public bool Enabled
        {
            get; set;
        }

        public string Name
        {
            get; set;
        }
    }

Nothing unusual here, except the MetadataAttribute. This informs MEF that there is additional metadata to be exposed. There isn’t necessarily a requirement for any metadata, but I’ve taken the opportunity to add some basic properties to assist us (I’ll describe this later).

The metadata is abstracted to an interface named IMefMetaData , known as a metadata view. A metadata view must have only properties, and those properties must only have get accessors.

    public interface IMefMetaData
    {
        bool Enabled
        {
            get;
        }

        string Name
        {
            get;
        }
    }

Back to the attribute, note the Type parameter passed into the base constructor, this just defines the Type of our contract stating it must be of type IHttpModule.

Now we have an attribute with which to decorate our HttpModules, we need to instruct MEF to discover and import any classes marked with this attribute and compose them into a Catalog.

Discovery and Import

As mentioned above, a MEF contract is just a string (when importing by type, it’s just the name of that type). There are two ways you can define an Import in MEF, Import and ImportMany with cardinality being the difference.

The Import attribute refers only to single imports. (zero-to-one and exactly-one cardinalities). Marking a property with this attribute specifies there is a single instance of the stated contract, and there is a single source to match.

Using ImportMany attribute declares zero-to-many cardinality, and states that instead of a single export, there can be many exports which match the contract. Using it on an IEnumerable, as shown below, MEF will fill the IEnumerable with all parts it can find that export that type, in this case IHttpModule.

        [ImportMany]
        public IEnumerable<IHttpModule> Modules { get; set; }

Usually this is sufficient, but because we want to use some metadata, we need to alter the property signature in order to expose it. MEF provides an overload of Lazy<T> to allow access to the metadata without instantiating the underlying export. The corresponding property signature looks like this:

        [ImportMany(typeof(IHttpModule))]
        public IEnumerable<Lazy<IHttpModule, IMefMetaData>> Modules
        {
            get; set;
        }

The property alone won’t do a thing, we need to instruct MEF to discover parts and populate this property. Firstly we tell MEF we want to discover attributed parts within the application’s bin directory by using the DirectoryCatalogclass. Next we create a CompositionContainer to hold the parts for composition and finally we tell MEF to Compose the Import parts within this class:

        var catalog = new DirectoryCatalog(HttpRuntime.BinDirectory);
        var container = new CompositionContainer(catalog);
        container.ComposeParts(this);

This populates the Modules property with all classes decorated with the attribute we defined earlier, so we should now have an IEnumerable list of HttpModules that need to be auto-registered, so we now just need to call the RegisterModule method for each module:

        foreach (var httpModule in this.Modules.Where(m => m.Metadata.Enabled))
        {
            var msg = httpModule.Metadata.Name ?? httpModule.GetType().FullName;

            log.InfoFormat("Adding SelfRegistering HttpModule {0}", msg);
            DynamicModuleUtility.RegisterModule(httpModule.Value.GetType());
        }

So by utilising the metadata added earlier, we can add some rudimentary notifications using log4net, showing which modules are being registered (helpful for debugging) and we also have a facility to easily disable modules in code to also assist in debugging etc.

Conclusion

So now instead of registering a HttpModule via the web.config, we simply decorate it with the AutoRegisterHttpModuleAttribute. It will get automatically registered and will intercept any requests as normal.

In my opinion this “shortcut” is an absolute godsend for distributing self-contained projects and sharing code between solutions with minimal configuration - HttpModules can now really be the “plug-ins” I think they were intended to be.

Gotchas

I foresee only one issue whereby it could take other developers a while to find and debug any issues if they are being caused by a self-registered HttpModule. However once we become accustomed to using this method of registering modules, it should be easy enough to find modules using the “Find usages” feature within Visual Studio. A minor issue compared to the benefits this offers.

Future ideas and Code download

I will shortly be uploading the full VS solution (probably to EPiCode), but first I want to add a few useful additions in there that utilise this technique, and I’ll write some more blog posts explaining those awesome plugins.

Watch this space. Smile

Tags:

.Net | HttpModule | MEF

CMS Tab Captions

by Tom Pipe 23. November 2011 15:44

By default EPiServer CMS creates some “standard” tabs. Here’s a screenshot of the DB showing the default captions etc:

clip_image001

However, the GroupCaption is NOT the caption which is displayed in the CMS, that is taken from the language XML file. Here’s a snippet from the LanguageEN.xml file showing the English translations:

image

Whilst most of the tabs translations are identical, the Information tab will display “Content“ as it’s caption, not “Information”.

Take care not to create a tab with a caption of “Content” or “Information”, or you could create a confusing situation where two identically names tabs appear in the CMS. e.g:

clip_image002

Which can result in this:

image

This can confuse some non savvy editors. Try not to make things difficult for them Smile

Tags:

EPiServer | CMS | Translation

EPiServer Community and image thumbnailing woes

by Tom Pipe 23. September 2011 20:40

The task…
The EPiServer community product has an integrated ImageGallery Module which handles persisting and retrieval of Images within the system. The module also offers a handy image thumbnailing feature via a number of overloaded methods within the ImageGalleryHandler class.

The thumbnailing is controlled by some settings within the web.config file, as shown below:

<imageGallery 
    imageAbsoluteFilePath="C:\Site\webRoot\ImageGallery\Originals" 
    thumbnailVirtualFilePath="~/ImageGallery/Thumbnails" 
    imgExtension=".jpg" 
    imgMaxWidth="640" 
    imgMaxHeight="640" 
    saveOriginal="true" 
    maxUserImageQuota="0" 
    imgQuality="100">
</imageGallery>

These settings appear to allow the location of the images to be changed, which is all well and good in theory, but in practice it restricts the location of the thumbnails to always reside beneath the site root.

Due to our release process (a full release package, and new, dated IIS root folder for each release) the images HAD to reside outside the site root.

The answer…
Easy right? This is a perfect opportunity to use a Virtual Path Provider, isn't this exactly what they were designed for? EPiServer offer easy registration of various provider types within the episerver.config file and a boat load of custom providers within the EPiServer.Web.Hosting namespace. Hell, they even use them to relocate the UI and Util directories from within the program files directory.

So, registering the following provider within episerver.config should solve the issue right?

<add virtualPath="~/ImageGallery/" 
     physicalPath="C:\Site\ImageGallery"
     name="Galleries"
     type="EPiServer.Web.Hosting.VirtualPathNativeProvider,EPiServer" />


Unfortunately not.

The issue…
The thumbnail is actually created within the ImageGalleryFactory by an aptly named CreateThumbnail method.

Within the method body is the following if/else statement:

if (HostingEnvironment.IsHosted)
{
    absoluteImageGalleryPath = HostingEnvironment.MapPath(ImageGalleryModule.GetVirtualThumbnailPath(image.ImageGalleryId));
}
else
{
    absoluteImageGalleryPath = ImageGalleryModule.GetAbsoluteImageGalleryPath(image.ImageGalleryId);
}

GetVirtualThumbnailPath concatenates the ImageGalleryID onto the thumbnailVirtualPath specified in the web.config and in this example returns something like "~/ImageGallery/Thumbnails/8/8"

HostingEnvironment.MapPath therefore returns the physical path of the folder, as if it resided under the site root "C:\Site\webRoot\ImageGallery\Thumbnails".

The VPP we configured earlier is not taken into consideration, and thumbnails will always be created under the site root regardless.

The solution….
My original intention was to simply define a custom factory derived from ImageGalleryFactory and override the CreateThumbnail method. Unfortunately someone at EPiServer decided we shouldn't be allowed to customise the ImageFactory, and marked the class as internal, so that idea could progress no further.

To workaround this, I ended up creating a custom ImageGalleryHandler, derived from the standard ImageGalleryHandler. Defined a new static Instance property (getting and setting the underlying ImageGallery.Instance, cast to the custom type) which gets instantiated within the static constructor, and therefore overwrites the base.Instance.

Because it's a derived version of the ImageGalleryHandler, the instance can be passed around as a standard ImageGalleryHandler, and will appear to be so to other libraries and classes, which can use it as "normal". Polymorphism at its most useful!

I then created an override to the GetThumbnail method and within it, called the base.GetThumbnail overload, then if the createThumbnail parameter is true. I raise the OnThumbnailCreated event passing in the created thumbnail.

I subscribed to the OnThumbnailCreated event within the Application_Start in Global.asax, and within the event handler I use HostingEnvironment.VirtualPathProvider.GetDirectory method instead of Server.MapPath. This returns a NativeDirectory object which has a LocalPath property containing the physical path to the intended thumbnail folder as defined in the episerver.config.

I now have a reference to the new thumbnail object (and therefore the folder beneath the site root), and i also have a reference to the intended physical location. So after some brief sanity checks I can copy (or move) the new thumbnail to the destination folder.

OBJECTIVE COMPLETE Laughing

Conclusion…
Whilst probably not the most elegant of solutions, it definitely achieves the goal. I consider this to be a 'filthy hack' to workaround the limitations imposed by EPiServer's internal classes. If anyone has an suggestions, alternative methods, or improvements – feel free to discuss them below.

Within the source I've taken the idea further by subscribing to the ImageRemoved event exposed by the standard ImageGalleryHandler so I can "tidy up" the thumbnails when the original image is removed.

Update - 25/09/2011 23:16
Added an additional sanity check to create a subdirectory under the VPP folder if it doesn't already exist.

Source

namespace Custom.Community.Handlers
{
    using System;
    using System.Drawing;
    using System.IO;
    using System.Linq;
    using System.Web.Hosting;

    using EPiServer.Common;
    using EPiServer.Common.Exceptions;
    using EPiServer.Community.ImageGallery;
    using EPiServer.Web.Hosting;

    using Image = EPiServer.Community.ImageGallery.Image;

    public class CustomImageGalleryHandler : ImageGalleryHandler
    {
        static CustomImageGalleryHandler()
        {
            Instance = new CustomImageGalleryHandler();
        }

        protected CustomImageGalleryHandler()
        {
        }

        public event EPiServerCommonEventHandler ImageThumbnailCreated;

        public static new CustomImageGalleryHandler Instance
        {
            get
            {
                return (CustomImageGalleryHandler)ImageGalleryHandler.Instance;
            }
            set
            {
                ImageGalleryHandler.Instance = value;
            }
        }

        public static void Instance_ImageRemoved(string sender, EPiServerCommonEventArgs e)
        {
            var image = (Image)e.Object;
            string path = GetThumbnailVPP(image.ImageGallery).LocalPath;

            try
            {
                if (Directory.Exists(path))
                {
                    foreach (string str2 in Directory.GetFiles(path, image.ID + "_*").Where(File.Exists))
                    {
                        File.Delete(str2);
                    }
                }
            }
            catch (Exception exception3)
            {
                throw new FrameworkException("Could not delete thumbnails", exception3);
            }
        }

        public static void ThumbnailCreated(string sender, EPiServerCommonEventArgs e)
        {
            var thumb = (Thumbnail)e.Object;
            var vpp = GetThumbnailVPP(thumb.Parent.ImageGallery);

            if (!Directory.Exists(vpp.LocalPath))
            {
                try
                {
                    Directory.CreateDirectory(vpp.LocalPath);
                }
                catch (Exception exception2)
                {
                    throw new FrameworkException("Failed to create directory: \"" + vpp.LocalPath + "\".", exception2);
                }
            }

            var imagePath = HostingEnvironment.MapPath("~" + thumb.Url);
            if (string.IsNullOrEmpty(imagePath)) return;

            var source = new FileInfo(imagePath);
            var filename = Path.GetFileName(source.FullName);

            if (source.Exists && !string.IsNullOrEmpty(filename))
            {
                var dest = new FileInfo(Path.Combine(vpp.LocalPath, filename));
                if (!dest.Exists || source.LastWriteTime > dest.LastWriteTime)
                {
                    source.CopyTo(dest.FullName, true);
                }
            }
        }

        public override Thumbnail GetThumbnail(Image image, int width, int height, ThumbnailFormat thumbnailFormat, bool createThumbnail, Watermark thumbnailTag, Color thumbBgColor)
        {
            var thumbnail = base.GetThumbnail(image, width, height, thumbnailFormat, createThumbnail, thumbnailTag, thumbBgColor);

            if (createThumbnail)
            {
                this.OnThumbnailCreated(thumbnail);
            }

            return thumbnail;
        }

        protected virtual void OnThumbnailCreated(Thumbnail thumb)
        {
            if (this.ImageThumbnailCreated != null)
            {
                this.ImageThumbnailCreated(ImageGalleryModule.Instance.UniqueName, new EPiServerCommonEventArgs(thumb.ID.ToString(), "created_thumbnail", thumb));
            }
        }

        private static NativeDirectory GetThumbnailVPP(ImageGallery gallery)
        {
            try
            {
                var virtualPath = ImageGalleryModule.GetVirtualThumbnailPath(gallery.ID);
                var vpp = HostingEnvironment.VirtualPathProvider.GetDirectory(virtualPath) as NativeDirectory;

                if (vpp == null)
{ var dir = UnifiedDirectory.CreateDirectory(virtualPath.TrimStart('~')); vpp = dir
as NativeDirectory;
}

                if (vpp != null)
                {
                    return vpp;
                }

                throw new ArgumentNullException(null, string.Format("No provider configured for the {0} virtual path", virtualPath));
            }
            catch (Exception exception2)
            {
                throw new FrameworkException("Could not get path to upload directory", exception2);
            }
        }
    }
}

Tags:

EPiServer | CMS | Community | Relate

EPiServer and Page Property sorting

by Tom 13. February 2011 04:05

EPiServer CMS 6 brought with it the much needed drag and drop sorting to a PageType's properties page. Yet despite looking (and acting) pretty much the same, the Dynamic Properties page doesn't have this facility. Although drag and drop is a massive improvement on CMS 5's "click click click" ordering, what if you want to reorder the entire list by name, group by tab or even property type? It's still a time consuming and annoying process.

In the past I've reordered the properties in code and it's easy to do, but I thought these features would be a great addition to the UI.

After inspecting the page's code behind with Reflector, I thought I only needed to add a couple of CSS classes to the HTML <table> and <tr> tags which could easily be achieved with a Page adapter to enable the drag and drop feature.

Creating the adapter

I added a new Page adapter and registered it in the sites App_Browsers folder:

    <controlAdapters>
        <adapter controlType="EPiServer.UI.Admin.EditPageType"
                 adapterType="EPiServerPropertyListAdapter.PropertyListAdapter" />

        <adapter controlType="EPiServer.UI.Admin.EditDynProp"
                 adapterType="EPiServerPropertyListAdapter.PropertyListAdapter" />
    </controlAdapters>

I obtained a reference to the property list repeater using the FindControlRecursive extension method:

protected override void OnInit(System.EventArgs e)
{
    base.OnInit(e);

    if (!Page.IsPostBack)
    {
        var repeater = this.Page.FindControlRecursive("PropertyList") as Repeater;
        if (repeater != null)
        {
            // Add Css classes to table head
        }
    }
}

Initially I thought I'd be able to just change the html in the HeaderTemplate with string.Replace or a regular expression, but it turns out it wasn't that easy! I ended up writing a new ITemplate and replacing the existing HeaderTemplate with:

if (this.Page is EditDynProp)
{
    repeater.HeaderTemplate = new DynamicPropertyHeaderTemplate { Page = this.Page };
}

The DynamicPropertyHeaderTemplate mimics the standard columns rendered by EPiServer, but within the InstantiateIn method, I've amended the HTML to output the extra CSS classes needed to enable the drag and drop features.

Now the HTML is rendering with the appropriate CSS classes, I needed to add the following javascript function to handle the drag and drop which was found within the original EditPageType.aspx page:

<script type="text/javascript">
        $(document).ready(function() {
            // Initialise the table
            $(".epi-table-sortable").tableDnD(
                {
                    onDragClass: "epi-table-sortableRow-drag",
                    onDrop: function(table, row) {
                        var rows = table.tBodies[0].rows;
                        var newpropertyOrder = "";
                        for (var i = 0; i < rows.length; i++) {
                            newpropertyOrder += rows[i].id + ",";
                        }
                        $.ajax({
                            type: 'POST',
                            url: "?newPropertyOrder=" + newpropertyOrder,
                            data: '{}',
                            contentType: 'application/json; charset=utf-8',
                            dataType: 'json',
                            success: function(e) {
                                window.location = window.location;
                            }
                        });
                    }
                });
        });
</script>

This is targeting the ID on each row which for some reason isn't output on the dynamic property page. I didn't want to add another ITemplate and replace the repeaters ItemTemplate, so I adjusted the javascript to target the hyperlink in the 2nd column, and strip out everything but the typeId querystring parameter, leaving only the ID:

var cell2 = $(rows[i]).find('td:eq(2)');
var id = cell2.find('a').attr('href').replace('EditPageTypeField.aspx?typeId=', '');

For neatness I wrapped a property around the sorting script, and added the other required scripts to the page in the OnPreRenderMethod:

protected override void OnPreRender(System.EventArgs e)
{
    base.OnPreRender(e);

    if (this.Page is EditDynProp)
    {
        var type = this.GetType();
        var dndscript = Paths.ToResource("cms", "ClientResources/jquery.tablednd.js");
        var jquery = Paths.ToResource("shell", "ClientResources/EPiJQuery.js");

        ScriptManager.RegisterClientScriptInclude(type, "tablednd", dndscript);
        ScriptManager.RegisterClientScriptInclude(type, "jquery", jquery);
        ScriptManager.RegisterClientScriptBlock(type, "tabledndSort", SortingScript);
    }
}

The javascript function calculates the new order client side, and creates a JSON structure sending it server side via an AJAX postback. I duplicated the following code from the original EditPageType into the OnLoad method to handle the reorder server side:

if (this.Page.IsPostBack && this.Page is EditDynProp)
{
if
(!string.IsNullOrEmpty(this.Page.Request.QueryString["newPropertyOrder"]) && this.Page.Request.ContentType.Contains("application/json")) { string param = "newPropertyOrder";
string[] strArray = this.Page.Request.QueryString[param].Split(new char[] { ',' }); int num = 1; foreach (string str in strArray) { int num2; if (int.TryParse(str, out num2)) { PageDefinition definition = PageDefinition.Load(num2); if (definition != null) { definition.FieldOrder = num; definition.Save(); num++; } } } this.Page.Response.Clear(); this.Page.Response.ContentType = "application/json"; this.Page.Response.Write("{'success': true}"); this.Page.Response.End(); }

Awesome. Now the dynamic properties page has drag and drop sorting! Smile

Adding column sorting

To add sorting to the columns, I had to add a couple of ImageButtons in the new ITemplate. As the columns on the Properties page are different to the Dynamic Properties page I duplicated the template and moved some common tasks out to an abstract HeaderTemplate class and create two new derived types,  DynamicPropertyHeaderTemplate and PropertyHeaderTemplate.

Here I encountered a problem. Despite subscribing to the ImageButton Click/Command events, my delegates just weren't being hit during the postback. After much deliberating I concluded that it was a page lifecycle issue and/or related to EPiServer's anti forgery token (please comment if you know why!). I opted for a workaround when I spotted the ImageButton's control ID in the Request.Form.Keys collection. After a quick google I found this post.

So after determining the ID of control that caused postback, I sort the property list if one of the new ImageButtons caused the postback:

var postbackId = Page.GetPostBackControlId();

if (postbackId.EndsWith("SortAscendingBtn") || postbackId.EndsWith("SortDescendingBtn"))
{
    switch (postbackId)
    {
        case "NameSortDescendingBtn":
        case "NameSortAscendingBtn":
                Properties.Sort(dp => dp.Name);
                break;

        case "CaptionSortDescendingBtn":
        case "CaptionSortAscendingBtn":
                Properties.Sort(dp => dp.EditCaption);
                break;

        // snip
        
        if (postbackId.EndsWith("SortDescendingBtn"))
        {
            Properties.Reverse();
        }       

        var position = 0;
        foreach (var prop in Properties)
        {
            prop.FieldOrder = position;
            prop.Save();
            position++;
        }
    }
 }
Finally, as the standard EPiServer page doesn't rebind the DataSource on PostBack, I wanted to rebind the repeater with the new ordered property list. Though on further inspection, there are some additional tasks performed by the EPiServer page and I didn't want to risk interfering with them, perhaps missing some important check, so I decided just to cause a redirect back to the original page.

The result, some new arrows in the columns which when clicked, sorts that column in either ascending or descending order.

image_thumb1

image_thumb6

And of course drag and drop enabled in dynamic properties (it just doesn't screenshot very well)

image_thumb8

Conclusion

I completed all the above before I decided to blog about it, so the steps above were not necessarily the order originally performed. I've also simplified some code snippets for brevity.

I'm not responsible if you use this on a live site and it causes data loss, server meltdown or starts an inter-galactic thermo-nuclear war. I've tested as thoroughly as I can –  but you use the code at your own risk (make sure to test in your own environment).

Feel free to add any suggestions or comments.

Download the source code here

Tags: ,

About the author

I'm Tom Pipe and I'm a Senior .net developer at twentysix, an award winning digital agency based in Leeds and EPiServer's UK partner of the year 2010.

I'm passionate about my work, and have contributed towards websites, intranets and applications for UK government agencies and massive global brands.

Tag cloud