I've been using the
BlogEngine.net for some time and I must say that it rocks. It's easy to manage and easier to extend. The BlogEngine uses a
Event model, so we just need to create a listener.
What I've done is create a extension loader, and write a extension data provider for persisting the extensions data.
NOTE:
The BlogEngine team is already working on a Extensions model, so the following is more a fun thing to write.
Extension model
After reading Mads Kristensen post on how to Make your ASP.NET application extendable, I decided to write a simple Extension loader that alowed me to code some aditional functionalities to the BlogEngine, drop the assemblies on the Bin folder, and manage them on the Admin area, all without changing the BlogEngine code.
The workflow is simple: The loader class looks on the "Bin/Extensions" folder for types with a specific interface, and for each type that matched, the loader creates a new instance. The extensions do, well, whatever they have to do :P .
The Interface
So all extensions inherit from the IExtension interface:
using System.Web.UI;
namespace BlogEngine.Core.Extensions
{
public interface IExtension
{
Control GetUI(TemplateControl page);
string Name { get; }
string Description { get; }
void Save();
void Load();
}
}
The GetUI method returns to the extensions page, that we will se later on, the UI for managing this extension. The properties Name and Descrition identify the Extension, while the Save and Load methods are used to persist the extensions settings.
The Loader
Each extension will be loaded one time, when the application starts, so we need to write a HttpApplication class to perform this job:
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Web;
namespace BlogEngine.Core.Extensions
{
public class ExtensionsApplication : HttpApplication
{
public const string EXTENSIONS_PATH = "~/Bin/Extensions";
public static List Extensions;
///
/// Executes custom initialization code after all event handler modules have been added.
///
public override void Init()
{
base.Init();
LoadExtensions();
}
///
/// Adds a to the collection.
///
/// The to add to the collection.
private void AddExtension(IExtension extension)
{
if (extension==null){return;}
if (Extensions == null) { Extensions = new List(); }
bool exists = Extensions.Exists(delegate(IExtension ext) {
return ext.Name.Equals(extension.Name,
StringComparison.
OrdinalIgnoreCase); });
if (!exists) { Extensions.Add(extension); }
}
///
/// Gets the assemblies files found in the given directory.
///
/// The directory where the files will be searched.
///
private string[] GetFilesFrom(string directory)
{
return Directory.GetFiles(directory, "*.dll");
}
///
/// Gets the assemblies files found in the given directory.
///
/// The directory where the files will be searched.
///
private string[] GetAssembliesFilesFrom(string directory)
{
List _out = new List();
string[] dirs = Directory.GetDirectories(directory);
Array.ForEach(dirs,delegate(string d)
{
_out.AddRange(GetFilesFrom(d));
});
return _out.ToArray();
}
///
/// Searches for Extensions and loads them.
///
private void LoadExtensions()
{
List assemblies = new List();
string d = Server.MapPath(EXTENSIONS_PATH);
if (Directory.Exists(d))
{
string[] files = GetAssembliesFilesFrom(d);
Array.ForEach(files, delegate (string file)
{
assemblies.Add(Assembly.LoadFrom(file));
});
}
assemblies.ForEach(delegate(Assembly assembly)
{
Type[] types = assembly.GetTypes();
foreach (Type type in types)
{
if (typeof(IExtension).IsAssignableFrom(type))
{
object a = assembly.CreateInstance(type.FullName);
AddExtension(a as IExtension);
}
}
});
}
}
}
The Data Provider
Now that we have the interface and the loader, it time to write the extensions data provider. Following the BlogEngine.net, I'm going to write a Xml data provider.
ExtensionProvider Section
This section allows us to define wich data provider our extensions will use:
using System.Configuration;
namespace BlogEngine.Core.Extensions.Providers
{
///
/// A configuration section for web.config.
///
///
/// In the config section you can specify the extension data provider you want to use for BlogEngine.NET.
///
public class ExtensionProviderSection : ConfigurationSection
{
///
/// A collection of registered providers.
///
[ConfigurationProperty("providers")]
public ProviderSettingsCollection Providers
{
get { return (ProviderSettingsCollection)base["providers"]; }
}
///
/// The name of the default provider
///
[StringValidator(MinLength = 1)]
[ConfigurationProperty("defaultProvider", DefaultValue = "XmlExtensionProvider")]
public string DefaultProvider
{
get { return (string)base["defaultProvider"]; }
set { base["defaultProvider"] = value; }
}
}
}
Extension Provider
The first thing to write now is the abstract class for the provider. This class defines all the logic available for the extensions data providers:
using System;
using System.Configuration.Provider;
using System.Web.UI;
namespace BlogEngine.Core.Extensions.Providers
{
public abstract class ExtensionProvider : ProviderBase
{
public abstract Control GetUIFrom(TemplateControl page, IExtension extension);
// Settings
///
/// Loads the settings for the .
///
public abstract T LoadSettings(IExtension extension);
///
/// Saves the settings for the .
///
public abstract void SaveSettings(IExtension extension, T settings);
}
///
/// A collection of all registered providers.
///
public class ExtensionProviderCollection : ProviderCollection
{
///
/// Gets a provider by its name.
///
public new ExtensionProvider this[string name]
{
get { return (ExtensionProvider)base[name]; }
}
///
/// Add a provider to the collection.
///
public override void Add(ProviderBase provider)
{
if (provider == null)
throw new ArgumentNullException("provider");
if (!(provider is ExtensionProvider))
throw new ArgumentException
("Invalid provider type", "provider");
base.Add(provider);
}
}
}
Since each extension can have its own class for settings, we use generics for the LoadSettings and SaveSettings. This gives some freedom to the extension developer. All he has to do is mark the settings classes with the Serializable attribute. This way, the data provider just need to serialize the class and persist to whatever media it uses, such as Xml files ou Sql databases.
Xml Data Extension Provider
Now that the abstract class is all done, let's write the data provider for Xml:
using System;
using System.IO;
using System.Web;
using System.Web.UI;
using System.Xml.Serialization;
namespace BlogEngine.Core.Extensions.Providers.XmlProvider
{
public class XmlExtensionProvider:ExtensionProvider
{
///
/// Gets the extension file from the of the extension.
///
/// The of the extension.
///
private string GetExtensionFileNameFrom(Type extensionType)
{
return HttpContext.Current.Server.MapPath(string.Format("~/App_Data/{0}.xml", extensionType.Name));
}
public override Control GetUIFrom(TemplateControl page, IExtension extension)
{
return extension.GetUI(page);
/*
string p = Path.GetDirectoryName(extension.GetType().Assembly.Location);
return page.LoadControl(Path.Combine(p, extension.UI));
* */
}
///
/// Loads the settings from the provider.
///
public override T LoadSettings(IExtension extension)
{
string filename = GetExtensionFileNameFrom(extension.GetType());
XmlSerializer x = new XmlSerializer(typeof(T));
TextReader w = new StreamReader(filename);
T settings = (T)x.Deserialize(w);
w.Close();
return settings;
}
///
/// Saves the settings to the provider.
///
public override void SaveSettings(IExtension extension, T settings)
{
string filename = GetExtensionFileNameFrom(extension.GetType());
XmlSerializer x = new XmlSerializer(settings.GetType());
TextWriter w = new StreamWriter(filename);
x.Serialize(w, settings);
w.Close();
}
}
}
Extension Data Service Provider
Since we already have a provider, we need a helper class for calling whatever provider defined in the web.config. So what we need is a extension data service provider:
using System.Configuration.Provider;
using System.Web.Configuration;
using System.Web.UI;
namespace BlogEngine.Core.Extensions.Providers
{
public class ExtensionsService
{
#region Provider model
private static ExtensionProvider _provider;
private static ExtensionProviderCollection _providers;
private static object _lock = new object();
///
/// Gets the current provider.
///
public static ExtensionProvider Provider
{
get { return _provider; }
}
///
/// Gets a collection of all registered providers.
///
public static ExtensionProviderCollection Providers
{
get { return _providers; }
}
///
/// Load the providers from the web.config.
///
private static void LoadProviders()
{
// Avoid claiming lock if providers are already loaded
if (_provider == null)
{
lock (_lock)
{
// Do this again to make sure _provider is still null
if (_provider == null)
{
// Get a reference to the section
ExtensionProviderSection section =
(ExtensionProviderSection)WebConfigurationManager.GetSection("BlogEngine/ExtensionProvider");
// Load registered providers and point _provider
// to the default provider
_providers = new ExtensionProviderCollection();
ProvidersHelper.InstantiateProviders(section.Providers, _providers, typeof(ExtensionProvider));
_provider = _providers[section.DefaultProvider];
if (_provider == null)
throw new ProviderException("Unable to load default ExtensionProvider");
}
}
}
}
#endregion
///
/// Returns a Post based on the specified id.
///
public static Control GetUIFrom(TemplateControl page, IExtension extension)
{
LoadProviders();
return _provider.GetUIFrom(page, extension);
}
///
/// Returns a Post based on the specified id.
///
public static void SaveSettings(IExtension extension, T settings)
{
LoadProviders();
_provider.SaveSettings(extension, settings);
}
///
/// Returns a Post based on the specified id.
///
public static T LoadSettings(IExtension extension)
{
LoadProviders();
return _provider.LoadSettings(extension);
}
}
}
For this class I used(copy/past :) ) part of the code already available on the BlogEngine.net (why re-invent the wheel...).
Setting up the web.config and the global.asax
Now that we have all the pieces together, all we need to do next is wire up the web.config with our new provider...
<BlogEngine>
<blogProvider defaultProvider="XmlBlogProvider">
<providers>
<add name="XmlBlogProvider" type="BlogEngine.Core.Providers.XmlBlogProvider"/>
<add name="MSSQLBlogProvider" type="BlogEngine.Core.Providers.MSSQLBlogProvider"/>
</providers>
</blogProvider>
<ExtensionProvider defaultProvider="XmlExtensionProvider">
<providers>
<add name="XmlExtensionProvider" type="BlogEngine.Core.Extensions.Providers.XmlProvider.XmlExtensionProvider"/>
</providers>
</ExtensionProvider>
</BlogEngine>
... and since we are using a HttpApplication class for loading the extensions, we need also to setup a global.asax file:
<%@ Application Language="C#" Inherits="BlogEngine.Core.Extensions.ExtensionsApplication" %>
The Extension Admin Page
To manage the available extensions, we create a admin page for them:
This is a simple page, that needs some work but:
<%@ Page Language="C#" MasterPageFile="~/admin/admin1.master" AutoEventWireup="true"
CodeFile="Extensions.aspx.cs" Inherits="admin_Pages_Extensions" Title="Untitled Page" %>
<asp:Content ID="Content1" ContentPlaceHolderID="cphAdmin" Runat="Server">
<div style="margin-bottom:30px">
<asp:Label runat="server" AssociatedControlID="ddlExtensions" Text="Available Extensions"></asp:Label>
<asp:DropDownList ID="ddlExtensions" runat="server" AutoPostBack="true"
OnSelectedIndexChanged="ddlExtensions_SelectedIndexChanged"></asp:DropDownList>
</div>
<asp:PlaceHolder ID="plhExtensions" runat="server"></asp:PlaceHolder>
<div style="text-align:right">
<asp:Button runat="server" ID="btnSaveSettings" ValidationGroup="settings" />
</div>
</asp:Content>
using System;
using System.Collections.Generic;
using System.Web.UI;
using BlogEngine.Core.Extensions;
using BlogEngine.Core.Extensions.Providers;
public partial class admin_Pages_Extensions : Page
{
#region Private Declarations
private IExtension activeExtension;
private List<IExtension> extensions;
#endregion
#region Properties
/// <summary>
/// Gets or sets the active <see cref="IExtension"/>.
/// </summary>
/// <value>The active <see cref="IExtension"/>.</value>
private IExtension ActiveExtension
{
get
{
if (activeExtension==null)
{
object vs = this.ViewState["ActiveExtension"];
if (vs!=null)
{
activeExtension = Extensions.Find(delegate(IExtension extUI)
{
return
extUI.Name.Equals(vs.ToString(),
StringComparison.
OrdinalIgnoreCase);
});
}
}
return activeExtension;
}
set
{
if (activeExtension != value)
{
this.ViewState["ActiveExtension"] = value.Name;
activeExtension = value;
}
}
}
/// <summary>
/// Gets the available <see cref="IExtension"/> extensions.
/// </summary>
/// <value>The <see cref="List{T}"/> <see cref="IExtension"/> extensions.</value>
public List<IExtension> Extensions
{
get
{
if (extensions==null) extensions = ExtensionsApplication.Extensions;
return extensions;
}
}
#endregion
#region UI Events
#region Page_Load
/// <summary>
/// Handles the Load event of the Page control.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
ListExtensions();
}
if (ActiveExtension!=null)
{
LoadExtension(ActiveExtension);
}
btnSaveSettings.Text = Resources.labels.save + " " + Resources.labels.settings.ToLowerInvariant();
btnSaveSettings.Click += new EventHandler(btnSaveSettings_Click);
}
#endregion
#region ddlExtensions_SelectedIndexChanged
/// <summary>
/// Handles the SelectedIndexChanged event of the ddlExtensions control.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
protected void ddlExtensions_SelectedIndexChanged(object sender, EventArgs e)
{
if (extensions != null)
{
IExtension ext = FindExtension(ddlExtensions.SelectedValue);
if (ext != null && ext != ActiveExtension)
{
LoadExtension(ext);
ext.Load();
}
}
}
#endregion
#region btnSaveSettings_Click
/// <summary>
/// Handles the Click event of the btnSaveSettings control.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
private void btnSaveSettings_Click(object sender, EventArgs e)
{
ActiveExtension.Save();
}
#endregion
#endregion
#region Helpers
/// <summary>
/// Loads a <see cref="IExtension"/> extension.
/// </summary>
/// <param name="extension">The <see cref="IExtension"/> extension to load.</param>
private void LoadExtension(IExtension extension)
{
plhExtensions.Controls.Clear();
Control extUI = ExtensionsService.GetUIFrom(this, extension);
if (extUI!=null)
{
plhExtensions.Controls.Add(ExtensionsService.GetUIFrom(this, extension));
}
else
{
plhExtensions.Controls.Add(new LiteralControl("<strong>This extension dosen't have a UI.</strong>"));
}
ActiveExtension = extension;
}
/// <summary>
/// Lists the available extensions.
/// </summary>
private void ListExtensions()
{
if (Extensions != null)
{
ddlExtensions.DataSource = Extensions;
ddlExtensions.DataTextField = "Name";
ddlExtensions.DataBind();
if (ActiveExtension==null)
{
ActiveExtension = Extensions[0];
}
}
}
/// <summary>
/// Finds a extension.
/// </summary>
/// <param name="name">The name of the extension.</param>
/// <returns>A <see cref="IExtension"/> extension.</returns>
private IExtension FindExtension(string name)
{
return extensions.Find(delegate(IExtension extUI)
{
return
extUI.Name.Equals(name,
StringComparison.
OrdinalIgnoreCase);
});
}
#endregion
}
Writing a Extension
Writing a extension is now easy. We just need to make our extension class inherit from our IExtension interface. With this, we are "forced" (since Vs.net 2005 already stubs the necessary code) to create the overrides:
Our sample extension will attach itself to the Post.Save event of the BlogEngine.net, and when a post is created or updated, the extension writes some statistics that we can later check, on the Extensions page in the Admin section of the site:
using System;
using System.IO;
using System.Web.UI;
using System.Web.UI.WebControls;
using BlogEngine.Core;
using BlogEngine.Core.Extensions;
using BlogEngine.Core.Extensions.Providers;
namespace BlogEngine.Extensions.SampleExtension
{
public class MyExtension:IExtension
{
private myExtensionSettings settings;
private GridView grv;
private PlaceHolder container;
#region Constructor
/// <summary>
/// Initializes a new instance of the <see cref="MyExtension"/> class.
/// </summary>
public MyExtension()
{
Post.Saved += new EventHandler<SavedEventArgs>(Post_Saved);
}
/// <summary>
/// Handles the Saved event of the Post control.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="BlogEngine.Core.SavedEventArgs"/> instance containing the event data.</param>
private void Post_Saved(object sender, SavedEventArgs e)
{
Post post = sender as Post;
if (post!=null)
{
if (settings==null)settings=new myExtensionSettings();
settings.Data.Add(new myExtensionData(post.Id,post.Title, e.Action.ToString()));
Save();
}
}
#endregion
#region IExtension Members
public Control GetUI(TemplateControl page)
{
container = new PlaceHolder();
grv = new GridView();
grv.EnableViewState = false;
container.Controls.Add(grv);
FillUIWithData();
return container;
}
public string Name
{
get { return "My blogEngine.net Extension"; }
}
public string Description
{
get { return "We can now easily extended the BlogEngine.net with custom extensions!"; }
}
public void Save()
{
ExtensionsService.SaveSettings<myExtensionSettings>(this, settings);
}
public void Load()
{
try
{
settings = ExtensionsService.LoadSettings<myExtensionSettings>(this);
FillUIWithData();
}
catch (FileNotFoundException fnf)
{
}
catch (Exception ex)
{
}
}
private void FillUIWithData()
{
if (settings != null)
{
grv.DataSource = settings.Data;
grv.DataBind();
}
}
#endregion
}
}
Our extension saves is data to the myExtensionSettings:
using System;
using System.Collections.Generic;
namespace BlogEngine.Extensions.SampleExtension
{
[Serializable]
public class myExtensionSettings
{
private List<myExtensionData> data;
/// <summary>
/// Gets or sets the data.
/// </summary>
/// <value>The data.</value>
public List<myExtensionData> Data
{
get { return data; }
set { data = value; }
}
/// <summary>
/// Initializes a new instance of the <see cref="myExtensionSettings"/> class.
/// </summary>
public myExtensionSettings()
{
data=new List<myExtensionData>();
}
}
[Serializable]
public class myExtensionData
{
private Guid postID;
private string postTitle;
private DateTime date;
private string action;
/// <summary>
/// Gets or sets the post ID.
/// </summary>
/// <value>The post ID.</value>
public Guid PostID
{
get { return postID; }
set { postID = value; }
}
/// <summary>
/// Gets or sets the post title.
/// </summary>
/// <value>The post title.</value>
public string PostTitle
{
get { return postTitle; }
set { postTitle = value; }
}
public DateTime Date
{
get { return date; }
set { date = value; }
}
public string Action
{
get { return action; }
set { action = value; }
}
/// <summary>
/// Initializes a new instance of the <see cref="myExtensionData"/> class.
/// </summary>
/// <param name="postID">The post ID.</param>
/// <param name="postTitle">The post title.</param>
/// <param name="action">The action.</param>
public myExtensionData(Guid postID, string postTitle, string action)
: this()
{
this.postID = postID;
this.postTitle = postTitle;
this.date = DateTime.Now;
this.action = action;
}
/// <summary>
/// Initializes a new instance of the <see cref="myExtensionData"/> class.
/// </summary>
public myExtensionData()
{
}
}
}

Conclusion
It's very simple to extend the BlogEngine.net. It's Event Model makes it easy. With this code you can now add even more functionality to it.
With this on the way, I'm writing a CrossPost extension. What other extensions whould you like to have?
[Update]
The code can be now downloaded here. Sorry for the delay.