Code customization in Kentico CMS 6

   —   
Kentico CMS version 6 comes with a reviewed customization options which give you more flexibility. This article describes what the changes are and how they simplify your work.
Hi there,

As you may notice if you install Kentico CMS 6 and compare it to 5.5 R2, especially if you made some customizations, some significant changes happened. Every change has a trigger, so before I tell you what is different, let's see the version 5.5 R2 from a critical point of view to see what wasn't good enough.

Not enough flexibility in version 5.5 R2

The heading pretty much describes it, all the items I will mention are about the overall flexibility of the solution.

First of all, there was an issue that you needed to solve if you wanted to integrate Kentico CMS with an existing ASP.NET application. The issue was the Global.asax.cs file, which contained a lot of code. This caused two major problems, one was taking care about merging of the existing code and Kentico CMS code, and the other was maintaining this during upgrades and hotfixing.

Another problem that we often saw was too complicated customization if you only needed a few lines of code to customize. Whether you wanted to define custom handlers for documents, modify a single method in the E-commerce provider, or write your own e-mail sending method, you always had to start with some of our sample libraries, add them to the project (which itself required Visual Studio), and compile that against our DLLs as well as register it. After you finally did that, the solution was working, but there was another problem if you had to hotfix your project. You simply had to recompile your library to make it work. This pretty much made the ability to provide cross-version modules for our Marketplace impossible. There was one improvement in version 5.5 that handled that scenario by being able to register those items from your App_Code (http://devnet.kentico.com/Blogs/Martin-Hejtmanek/June-2010/New-in-5-5--Provide-your-classes-from-App_Code.aspx), but especially with the larger libraries, it was very heavy to set that up.

In fact, there was another problem that came with this solution. And that problem was in particular that different modules from the Marketplace could overwrite each others' custom code in the predefined App_Code files with their customizations.

There is one last remaining thing I should cover, which was told us many times, but there was never time to make so significant changes. That issue were the custom handlers. In fact, this was caused more by historical reasons when the original version was done as a custom library mentioned above to provide enough customization options. It seemed great, because it allowed a whole new level of customization at that time, but the consequences were that it was very hard for us to update that (it simply caused the need to update your custom code), and it also couldn't use strongly typed method headers because the library was sitting too low.

As you can see, there were problems with customization. Luckily, given the feature set we offer these customizations weren't needed that often, but to those who used them, it could sometimes cause headache.

The good news is that Kentico CMS 6 handles all of these issues, providing much better flexibility and ease of customization! Let's see what has changed and how it effects your existing projects. Another good news is that for most of the changes, we provided backward compatibility.

Kentico CMS 6 application code

As I mentioned above, the Global.asax.cs file in 5.5 R2 was full of code which made it very complicated to merge it with an existing ASP.NET application that used it. What we did in version 6 is following:
 

  • The code from Global.asax.cs was moved to App_Code/Application/CMSAppBase.cs, this is the basic code that executes within the request processing
  • There is a new class CMSHttpApplication located in App_Code/Application/CMSHttpApplication.cs which connects the Global.asax.cs to the moved code
  • Some of the event processing was moved to an HTTP module called CMSApplicationModule to provide even better options for customization.
What this means at the end is pretty obvious if you look at the 6.0 Global.asax.cs. This is exactly the only thing that you need to ensure if you merge Kentico CMS with an existing ASP.NET application. One thing is to inherit the application class from CMSHttpApplication, and the other is to provide the region with the system version (this is one thing that we had to keep for backward compatibility of external tools that depend on the version).

/// <summary>
/// Application methods.
/// </summary>
public class Global : CMSHttpApplication
{
    #region "System data (do not modify)"

    /// <summary>
    /// Application version, do not change.
    /// </summary>
    /// const string APP_VERSION = "6.0";

    #endregion
}


I know, there may be situations where the inheritance is not possible, this is exactly why there is the CMSHttpApplication class. If you cannot inherit from it, you just need to include its content to the existing application code. It is just a few lines of code, so even in that case, it is much easier now to integrate with an existing ASP.NET application.

I would also like to mention that modifying the default application code is not something you usually need to do (unless you want to change the system default behavior in some way). In most of the cases you just need to add some extra code to execute within a particular request event. Whether it is redirection, setting of some request variable, we have handlers that you can hook without modifying any of our code as you will see in a few moments.

Backward compatibility

Since this is something you should never modify in an ideal scenario, we don't provide any backward compatibility pack for that. If you have some existing modifications, either see if you can move them to the new handlers or independent custom modules, or simply move the code to CMSAppBase.cs file.

Custom modules in Kentico CMS 6

The most significant change that we did in version 6 is the ability to create a module that is completely independent and doesn't change any system code or files, meaning that no matter if you upgrade, hotfix or install other modules, none of these actions will break it. The magic behind it is quite obvious for experienced programmers, but I expect all sorts of people to read this blog post, so I will try to explain it in more details. If you are experienced, just see the sample codes in App_Code/Samples/Modules, and if you understand, just ignore the next few paragraphs.

The best thing about this is that your module can have it's own physical code file for initialization, and this code will be automatically executed without modifying any of the custom code if done properly.

First, see the file App_Code/Samples/Modules/SampleProviderModule.cs, if you look at it closer, you can see that there are two nested classes. One of them is a CMSModuleLoader class which is a partial class that provides the connection to the system. The other class is an attribute (you can learn more about what attributes are here http://msdn.microsoft.com/en-us/library/aa720076.aspx)

That attribute class is a place that you need to define with a unique name, in this sample code it is "SampleProviderModuleLoader" (marked red). It can have any name, and doesn't even have to end with ModuleLoader, but it must inherit from CMSLoaderAttribute.

So technically, if you create any custom file with a unique name somewhere in App_Code (preferrably App_Code/Global/<your folder> so it is exported with our export module) e.g. App_Code/Global/MyModule/Loader.cs, and name the attribute with a unique name, e.g. MyModuleLoaderAttribute, then this is what defines your custom module initialization code.

The CMSLoaderAttribute has a single Init method that you need to override. This is where you run you initialization code for the module. The code will typically be replacing a provider object (I will mention it later), or hook up some event. The Init code will get executed within the application start, right after all necessary data is initialized in the system.

/// <summary>
/// Sample Provider module class. Partial class ensures correct registration.
/// </summary>
[SampleProviderModuleLoader]
public partial class CMSModuleLoader
{
    /// <summary>
    /// Module registration
    /// </summary>
    private class SampleProviderModuleLoaderAttribute : CMSLoaderAttribute
    {
        /// <summary>
        /// Initializes the module
        /// </summary>
        public override void Init()
        {
            // -- Uncomment this line to register the provider programatically
            //SiteInfoProvider.ProviderObject = new CustomSiteInfoProvider();
        }
    }
}

If you already did some customizations with version 5.5 R2 or prior, you most probably used the AfterApplicationStart event in App_Code/Global/CMS/CMSApplication.cs, which was causing that overwriting issue by other modules I mentioned. That is why we got rid of those files, and replaced them with the new way of registering modules. This wouldn't be possible without having event handlers for all those actions, which is the next topic I wanted to cover.

Backward compatibility

If you import a package from version 5.5 R2 or another version prior to version 6, you import package will most probably contain the App_Code/Global/CMS folder. We still support this, but it is no longer recommended, so this folder is not part of 6.0 anymore. Instead, in case you import an older package, we provide a file that connects all those methods to the appropriate event handlers. In case of need, you can find this file as App_Data/Compatibility/Compatibility55.cs.rename, but you shouldn't need it, it is more for the case when you need to move some code manually. The import process will take this file and includes it into the Global/CMS folder with removed .rename extension so it compiles. Also, in the CMSCustom.cs file, you need to add using CMS.Compatibility; to the usings and do the following replacements:
  • MacroResolver.OnResolveCustomMacro -> MacroResolverCompatibility.OnResolveCustomMacro
  • MacroResolver.MacroHandler -> MacroResolverCompatibility.MacroHandler
  • ClassHelper.OnGetCustomClass -> ClassHelperCompatibility.OnGetCustomClass
  • ClassHelper.GetClassEventHandler -> ClassHelperCompatibility.GetClassEventHandler
This ensures that all custom code will work even with 6.0.

This is in case of heavy customizations. If they are just through AfterApplicationStart or another single method, it is probably easier for you to hook it up the new way. If you didn't use any of the custom files from this folder, you don't need to worry about that.

New event handlers in 6

We have done a major review of the event handlers, with main focus on the custom handler library to allow for simpler customization. There is no more need to add and compile the library of handler classes in your project, instead, you can simply add your custom module as described above, and hook up the handler as a regular event in the Init method.

Each set of handlers is provided within a particular class, e.g. DocumentEvents, and they correspond to the particular handlers that were provided before. Here is some basic list of classes where to look for events:
  • ObjectEvents - events that are fired for any object type (as in former CustomDataHandler)
  • UserInfo.TYPEINFO.Events - events that are fired only for a particular type of object (not available before)
  • DocumentEvents - events that are fired for document operations without workflow or for the published version (former CustomTreeNodeHandler).
  • WorkflowEvents - events that are fired for workflow operations (former CustomWorkflowHandler).
  • SystemEvents - general events (former CustomExceptionHandler).
  • SecurityEvents - general security events including authentication (former CustomSecurityHandler).
There are also application-specific events that replace the events in files from folder App_Code/Global/CMS:
  • CMSApplicationEvents - application start, end and error event (replaces CMSApplication.cs).
  • CMSRequestEvents - request cycle events (replaces CMSRequest.cs).
  • CMSSessionEvents - session start and end events (replaces CMSSession.cs).
There are two cllasses that had the events before and were connected to former CMSControlEvents.cs and CMSPageEvents.cs, they are:
  • AbstractUserControl - events fired for any user control within the system.
  • CMSPage - events fired for any admin UI page within the system.
There are also other "smaller" events that were there before, such as the MacroResolver.OnResolveCustomMacro, ClassHelper.OnGetCustomClass, OutputFilter events, etc., basically everything that was available before through CMSCustom.cs, but also the new stuff. But that is for another blog post to cover. This one covers the general functionality. I do not want to spend too much time on this, because we have the full list of the available events in the documentation:

http://devnet.kentico.com/docs/6_0/devguide/index.html?event_handlers_overview.htm

Each of these events is named by the action name, e.g. ObjectEvents.Insert, and either contains two sub-events (Before, After) or a single one (Execute) if differentiation between before and after doesn't make sense. Just for instance, one simple example showing what a module that adds event registration may look like:

[SampleEventModuleLoader]
public partial class CMSModuleLoader
{
  private class SampleEventModuleLoader : CMSLoaderAttribute
  {
    public override void Init()
    {
      UserInfo.TYPEINFO.Events.Insert.Before += BeforeInsertUser;
    }

    private void BeforeInsertUser(object sender, ObjectEventArgs e)
    {
      UserInfo user = (UserInfo)e.Object;

      user.SetValue("UserCreatedFromIPAddress", HTTPHelper.GetUserHostAddress());
    }
  }
}


In a similar way as described previously, you just add the module class and register the particular event with the given event handler. Notice the event registration (marked red), this is how you bind to the Before insert event for UserInfo objects only.

Backward compatibility

In case you are currently using the CustomEventHandler library, it will still work. The system contains code that will automatically bind the handler library to the new handlers if it is enabled.

Customization of the providers and helpers

In previous versions, it was only available to customize particular libraries of providers, e.g. the E-commerce or E-mail provider. With version 6 it changes and it is now possible to customize almost any provider in the system. How do you know which providers can be customized? It is very simple. You just check if the provider contains the ProviderObject property. If it does, the provider can be customized.

The ProviderObject property contains a singleton of the provider through which all the operations are called. I will cover the details of the architecture in some of my next blog posts, for now the only thing you need to know is that you can inherit our provider and override some of its internal methods. The best way to show you this is our sample code. There are actually two samples of this in the default installation:
  • App_Code/Samples/Modules/SampleProviderModule.cs - general example on the Site info provider.
  • App_Code/CMSModules/Ecommerce/Samples/SampleEcommerceModule.cs - more specific example on extending the E-commerce module with a new functionality.
I will show you the example on the E-commerce sample, but you can use the same concept with almost any provider in the system. If you want to learn more on how the customization of the E-commerce changed from 5.5 R2 to 6, see Petr's blog post that he's just published:

http://devnet.kentico.com/Blogs/Petr-Vozak/October-2011/E-commerce-6--New-customization-model.aspx

If you look closer at the mentioned file, you can see the same concept as previously, there is a module file that does some action on the Init event, and that action is replacing the original E-commerce providers with the overriden ones:

public override void Init()
{
  ShippingOptionInfoProvider.ProviderObject = new CustomShippingOptionInfoProvider();

  ShoppingCartInfoProvider.ProviderObject = new CustomShoppingCartInfoProvider();

  ShoppingCartItemInfoProvider.ProviderObject = new CustomShoppingCartItemInfoProvider();

  SKUInfoProvider.ProviderObject = new CustomSKUInfoProvider();
}


As I said, we are just replacing the original provider singleton with a new instance of the custom class. If we look at one of them, you can notice it inherits from the original provider (to maintain all functionality) and overrides the internal method that provides the functionality (both marked in red). All the methods that you may override and correspond to publicly available static methods are named internal. This method demonstrates two very simple examples, one is for the "Happy hours" funcionality and the other for a discount on orders larger than a specific amount:

public class CustomShoppingCartInfoProvider : ShoppingCartInfoProvider
{
  /// <summary>
  /// Calculates discount which should be applied to the total items price.
  /// </summary>
  /// <param name="cart">Shopping cart</param>       
  protected override double CalculateOrderDiscountInternal(ShoppingCartInfo cart)
  {
    double result = base.CalculateOrderDiscountInternal(cart);

    // Example of order discount based on the time of shopping - Happy hours (4 PM - 7 PM)
    if ((DateTime.Now.Hour >= 16) && (DateTime.Now.Hour <= 19))
    {
      // 20% discount
      result = result + cart.TotalItemsPriceInMainCurrency * 0.2;
    }
    // Example of order discount based on the total price of all cart items
    else if (cart.TotalItemsPriceInMainCurrency > 500)
    {
      // 10% discount
      result = result + cart.TotalItemsPriceInMainCurrency * 0.1;
    }
       
    return result;
  }
}
 

If you look at the provider or other providers closer, you can see a lot of overridable methods, so it is just up to you which ones you want to use.

The other example with site provider just logs something to the event log when a site object is saved, but as you can see above, you can do the same through the SiteInfo.TYPEINFO.Update.After event, so it is more demonstrative than really useful.

You can do the same with some helpers such as CacheHelper or MediaHelper, they have the HelperObject property to do that.

Registering providers and helpers from libraries

There is one more option for registering providers. You can have the provider in a compiled library and register it through your web.config. This helps to organize your project in case you have some more complex customization and have everything in one place. There is a new configuiration section that you can register within your web.config:

<?xml version="1.0"?>
<configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0">
  <configSections>
    ...
    <section name="cms.extensibility" type="CMS.SettingsProvider.CMSExtensibilitySection" />
 
</configSections>
  ...
  <cms.extensibility>
    <providers>
      <add name="SiteInfoProvider" assembly="MyLibrary" type="CustomSiteInfoProvider" />
      ...
    </providers>
    <helpers>
      <add name="CacheHelper" assembly="MyLibrary" type="CustomCacheHelper" />
      ...
    </helpers>
  </cms.extensibility>
...
</configuration>


This example loads the custom site provider and custom cache helper from the DLL "MyLibrary". Note that the provider and helper must inherit from the correct base system provider in order for this to work.

Still, for simple scenarios, I recommend the App_Code approach with replacing the provider object programatically, it is much easier.

... well, and that is all for today, I hope you like the new ways of customizations, see some of our samples in App_Code/Samples, play around, and have a great time!
Share this article on   LinkedIn

Martin Hejtmanek

Hi, I am the CTO of Kentico and I will be constantly providing you the information about current development process and other interesting technical things you might want to know about Kentico.

Comments

Andrew Corkery commented on

This is a nice improvement. I'm starting a new v6 project in a week or two, with a lot of custom requirements. This will definitely make it cleaner to separate the client-specific functionality. Cheers.

Jeroen Fürst commented on

Wow Martin, excellent post! Thanks for the detailed explanation!

Martin Hejtmanek commented on

Hi Martin,

This particular method returns the discount, not the total price, so the discount really is 20% ;-)

Anyway, I am glad you liked it :-)

Martin commented on

Interesting article - easy to follow and digest. We're looking forward to using version 6.0 on our next project.

Btw - multiplying by 0.2 is an 80% discount ;-)