Jumping on the page type bandwagon
With the sun shining and the thermometer approaching body temperature I thought it was god time to have some fun. Quick from thought to action I fired up my trusty black warrior to explore another white spot on the map.
My objective for the day was to get to get aquainted with the page type builder. One of those really neat features your manager never will understand.
I was pleased from the first moment to notice that after including it in my solution and adding my page type it worked right away. It's just like magic, and I cannot but love the firm presence of a piece of text sitting in my source code repository.
The natural next step was learning about the inner workings. Living on the practical side of the fence I tend to learn best by using, exploring and finding interesting aspects to change. With the using already under my belt I ventured into the exploration phase.
Some mental mapping revealed some logical roles:
* An API to map code to episerver page types and the discovery thereof
* Synchronization between code and database for page type settings and properties
* A data factory wrapper to access the typed pages
Looking for interesting aspects to change I first took aim at the startup process. The page type builder uses a very cleverly constructed plugin attribute to trigger the build up and synchronization the page types. Beeing slightly paranoid I wanted this a bit more explicit. At the price of a tiny bit of extra code comes a cosier feeling of control.
protected void Application_Start(Object sender, EventArgs e)
{
PageTypeBuilder.Initializer.Startup(PageTypeBuilder.InitializerOptions.CallProperties);
}
The actual initializer then waits until the opportune moment (EPiServer is initialized during the first request) and then synchronizes:
public static void Startup(InitializerOptions options)
{
EPiServer.Web.InitializationModule.FirstBeginRequest += InitializationModule_FirstBeginRequest;
}
static void InitializationModule_FirstBeginRequest(object sender, EventArgs e)
{
PageTypeBuilder.Synchronization.PageTypeBuilder.UpdatePageTypes();
}
The next change is about pushing typed page data objects into the EPiServer APIs. One particular aspect I like about the page type builder is the way it fits with the existing model. Re-tailoring for an even tighter fit I latched onto the data factory event system to push the typed pages down the stack and into EPiServer.
public static void Startup(InitializerOptions options)
{
//...
EPiServer.DataFactory.Instance.LoadedPage += DataFactoryInstance_LoadedPage;
}
static void DataFactoryInstance_LoadedPage(object sender, EPiServer.PageEventArgs e)
{
Type type = TypeResolver.GetTypeForPageType(e.Page.PageTypeID);
//...
e.Page = Activator.CreateAndPopulateInstance(e.Page, type);
}
The final and most fun change is related to the property usage. This is how strongly typed properties can be constructed:
[PageTypeBuilder.PageTypeProperty]
public string SecondaryBody
{
get { return GetPropertyValue<TextPage, string>(p => p.SecondaryBody); }
set { SetPropertyValue<TextPage, string>(p => p.SecondaryBody, value); }
}
This approach while very pragmatic is not the most readable. The property getters and setters are responsible for using the correct property data and the lambdas provides some nice refactoring support. Inspired by one of my colleagues I decided to extend this using one of my favorite toys: DynamicProxy2. After introducing a dynamic activator the code in the page type would look like this:
[PageTypeBuilder.PageTypeProperty]
public virtual string MainBody { get; set; }
Slightly less, isn't it? Of course the code for the activator itself is a bit longer, but not that long. Instead of creating a regular page data instance it uses DynamicProxy2 to create a special instance with a method call interceptor (it overrides and intercepts the getting and setting of properties). The interceptor then analyzes calls to the page data objects and uses GetValue/SetValue instead of calling the actual properties.
Update (20 july): Krzysztof pointed out a more elegant solution using proxy generation hooks and the code has been updated.
protected override TypedPageData CreateInstance(Type typedType)
{
(TypedPageData)generator.CreateClassProxy(typedType, options, interceptors);
}
Creating the page proxy will allow for this interceptor to be called instead of the reguar property.
internal class PageTypePropertyInterceptor : IInterceptor
{
public void Intercept(IInvocation invocation)
{
string propertyName = invocation.Method.GetPropertyName();
if (invocation.Method.IsGetter())
invocation.ReturnValue = page.GetValue(propertyName);
else
page.SetValue(propertyName, invocation.Arguments[0]);
}
}
The generator hook allows us to select which properties should be intercepted.
internal class PageTypePropertiesOnlyProxyGenerationHook : IProxyGenerationHook
{
public bool ShouldInterceptMethod(Type type, System.Reflection.MethodInfo memberInfo)
{
return memberInfo.IsGetterOrSetterForPropertyWithAttribute(typeof(PageTypePropertyAttribute))
&& memberInfo.IsCompilerGenerated();
}
//...
}
Boy, that was fun! And educative. Hope I didn't overdo it and that some of this (thoroughly untested) experimentation can be of any use. Oh, and here's the code in case anyone is interested.
Extremely interesting stuff Cristian! I especially like how you organized the source code. I'll have to take a closer look at "pushing typed page data objects into the EPiServer APIs" but unless I'm mistaken that could actually make the PageDataFactory redundant (except for decoupling from DataFactory and some convenience methods)? Regarding how you access the properties, I found that very interesting as well. I actually used DynamicProxy2 for testing in the project for a while but I hadn't thought of using it this way. Still, I like the idea of being able to put more logic into the properties, but perhaps a combination could be used where properties are only autoimplemented if that is specified in the attribute?
/ Joel Abrahamsson
You're right, the page data factory isn't really needed for the page type builder. Good idea about restricting to page type properties. I put a check for compiler-generated properties only in the code.
/ Cristian
You could utilize DynamicProxy more fully by using IInterceptorSelector and IProxyGenerationHook, you could then get rid of the string invokedMethodName = invocation.Method.Name; bool isPropertyGet = invokedMethodName.StartsWith("get_"); bool isPropertySet = invokedMethodName.StartsWith("set_"); if (isPropertyGet|| isPropertySet) check in your Intercept method, which would make the code cleaner and faster.
/ Krzysztof Koźmic
@Krzysztof: Great tip. I just had to try it out and I updated the post. @Joel: I added the restriction on page type properties.
/ Cristian Libardo (crli)