Deep dive - New customization options in Kentico CMS 4.1

   —   
As I promised, I bring the details of new customization features of Kentico CMS 4.1. Just don't read this post if you don't have any previous experience, because it could just confuse you. At least study the basics and see the API before you start.
Hi everybody,

During the development of version 4.1, we have cooperated with some of our larger partners who did an extensive integrations with our solution. Their part of the cooperation was to tell us what they need for the smooth integration, while ours was to design and implement new features that would provide this kind of easy customization.

So far, I told you how you can integrate your custom code to the admin interface, which was already covered in the previous versions quite well, but as we were discussing the lower levels of API, it has come to mind that some features that would need customization might require quite a lot of changes in the source code even for quite simple achievements. When this happens, you have to deal with these changes on every upgrade, which is not as smooth as we all wish.

The main topics were:
  1. How to customize our system objects and extend them with additional structured data and fields - This was sort of possible before, but only with system tables, which limits the developer to only SQL field types and specific objects within the CMS.
  2. If someone needs to completely move one database table to another storage (not necessarily SQL DB), how to deal with existing foreign keys to such table and provide valid views that use this table. - A typical example is the table of users, which is joined to many views for specific reasons (typically listings)
  3. How to be able to preprocess the queries and postprocess the results with custom code - Since some parts of the UI work with DataSets and not objects, the same type of customization as in point 1 may be needed for the entire DataSets of object data.
Luckily, we found quite handy solutions for all of them, making the solution better customizable than ever before.

Before you start, you should at least know something about current ways of customization, see How to effectively customize Kentico CMS UI for details.

Customize any system objects with your data and objects

This is probably the best customization improvement that we did, because it has a lot of potential in it. Making the objects customizable was something that most of the integrations needed, because you usually need to ship your own, related objects with ours.

The magic key to do this since version 4.1 is the property named RelatedData (object) and interface IRelatedData. If the object implements this interface, you can use this property to connect just any object to the CMS object and ship your data with it.

How to use it? Very simply, just assign the object with the data, and retrieve it (if you are working with the same object later).

Not only that, you are even able to make this object behave quite as a native part of the CMS object and retrieve it's properties as it were the CMS object properties. If your custom object assigned to RelatedData implements the interface IDataContainer, it is automatically connected to the methods GetValue and SetValue of the parent object.

Let's see some examples, I will customize the SiteInfo object (which you cannot customize using system tables), and my extension SiteRegistration will add two new fields and one method:
  • SiteOwner - String, just the name of the site owner (let's pretend I have a hosting company)
  • SiteValidUntil - DateTime until which the site is valid (the hosting fee is prepaid)
  • bool IsValid() - Returns true if the site is valid at current time
Here is my SiteRegistration class code: SiteRegistration.cs

Now, if i can define the values, I can insert them to the SiteInfo object and use them like this (just put this into the layout of the page to validate it is working):

<asp:Label runat="server" id="lblSiteInfo" />
<script runat="server">
  protected void Page_PreRender(object sender, EventArgs e)
  {
    CMS.SiteProvider.SiteInfo currentSite = CMSContext.CurrentSite;
    if (currentSite.RelatedData == null)
    {
       // Get the related data from your external storage (make sure it implements IDataContainer. I will just create some on-the-fly
       SiteRegistration sr = new SiteRegistration();
       sr.SiteOwner = "Martin Hejtmanek";
       sr.SiteValidUntil = DateTime.Now.AddDays(1);

       currentSite.RelatedData = sr;
    }

    this.lblSiteInfo.Text = String.Format("Site '{0}' is valid until {1} and owned by {2}.", currentSite.DisplayName, currentSite.GetValue("SiteValidUntil"), currentSite.GetValue("SiteOwner"));
  }
</script>


The result is: Site 'Corporate Site' is valid until 9/15/2009 1:09:50 PM and owned by Martin Hejtmanek.

So this is the case where I can access the physical object before I need to access the value. But that is not that often, let's see how to do that before anything accesses the data. This example is sort of stupid, because you can access the data within the context of execution, this gives you just some sort of caching for this data (within the context object). The important thing here is that after I fill the object with data I may work with the data as it were native object data (through GetValue, SetValue methods).

Where this becomes interesting is the point, where you can bind the related data dynamically on request to any object because then you can deliver them to just any code that uses such object. Let's see the example:

There is an event OnLoadRelatedData (among other) which you can use for specific object types. The event is part of the type information object referenced from the object info class. So just create a new event like this and register it in the AfterApplicationStart event in ~/App_Code/Global/CMS/CMSApplication.cs (just move the SiteRegistration initialization code in there):

///
/// Fires after the application start event
///
public static void AfterApplicationStart(object sender, EventArgs e)
{
  // Add your custom actions
  CMS.SiteProvider.SiteInfo.TYPEINFO.OnLoadRelatedData += new CMS.SettingsProvider.TypeInfo.ObjectLoadRelatedDataEventHandler(SiteInfo_OnLoadRelatedData);
}

static object SiteInfo_OnLoadRelatedData(CMS.SettingsProvider.IInfoObject infoObj)
{
  // Get the related data from your external storage (make sure it implements IDataContainer. I will just create some on-the-fly
  SiteRegistration sr = new SiteRegistration();
  sr.SiteOwner = "Martin Hejtmanek";
  sr.SiteValidUntil = DateTime.Now.AddDays(1);

  return sr;
}


Now any site object whenever you ask it about the property and if it already doesn't have the related data fires this event, and you give the object the data. You can check it if you change your code in the layout to following:

<asp:Label runat="server" id="lblSiteInfo" />
<script runat="server">
  protected void Page_PreRender(object sender, EventArgs e)
  {
    CMS.SiteProvider.SiteInfo currentSite = CMSContext.CurrentSite;
   
    this.lblSiteInfo.Text = String.Format("Site '{0}' is valid until {1} and owned by {2}.", currentSite.DisplayName, currentSite.GetValue("SiteValidUntil"), currentSite.GetValue("SiteOwner"));
  }
</script>


See how nicely it works? Now we can get rid of the C# code in our layout, because we ensured that the site object always provides these additional fields. Let's replace this with just a macro. Put a static text web part to your page template and set it's text property to:

Site '{%CMSContext.CurrentSite.SiteDisplayName%}

And the result is the same as the one produced by our previous code! This is exactly how you can use your own related data in the properties of your web parts by using macros. You can do the same with users / products / just anything.

The method IsValid is for you, I didn't use it on purpose so you may try to play around with it. You use it like this: ((SiteRegistration)CMSContext.CurrentSiteName.RelatedData).IsValid(), what you use it for is completely up to you, I would recommend redirecting to some special page in case the site is not valid or so :-)

As you can see, I din't do any changes to the source code, and I used the AfterApplicationStart event which is completely upgrade-proof. Everything else was just custom code (and later just text) within the page template.

And that's all about shipping your objects with ours. I know this example doesn't do much, it would be too long if it would, so just try to think if you can use something like this for your integration and let us other know how you did.

How to preprocess / postprocess queries

This basically covers both last points from our list of topics, because in case of missing foreign keys and data in our tables, you can have the default views without the data, and fill the missing data to the result in the result post-processing.

Before you start with this part, backup you database and project, if you plan to use it further! You may break something while trying different things with these. And if you use this, backup that also before any other significant change of your project.

I don't recommend you to use this unless this is absolutely necessary, there are other things you can do and this is by far the lowest level customization (side by side with your own data provider). I just wanted you to know that something like this is available.

There are two events for this in the SqlHelperClass: OnBeforeExecuteQuery and OnAfterExecuteQuery

They do pretty much what their names are. The first one is executed before any query is executed and you may influence the query behavior and code dynamically on the fly, here is the delegate definition for it.

/// <summary>
/// Query execution event handler
/// </summary>
/// <param name="query">Executed query</param>
/// <param name="conn">Connection</param>
public delegate void BeforeExecuteQueryEventHandler(QueryParameters query, IDataConnection conn);


You may modify the QueryParameters object, which contains not only the query name (so you can change the particular query only, but also the query itself which you can modify). Here is some example of how you use it (again, you register it in the AfterApplicationStart event):

///
/// Fires after the application start event
///
public static void AfterApplicationStart(object sender, EventArgs e)
{
  // Add your custom actions
  CMS.SettingsProvider.SqlHelperClass.OnBeforeExecuteQuery += new CMS.SettingsProvider.SqlHelperClass.BeforeExecuteQueryEventHandler(BeforeExecuteQuery);
}

static void BeforeExecuteQuery(CMS.SettingsProvider.QueryParameters query, CMS.IDataConnectionLibrary.IDataConnection conn)
{
  if (query.Name != null)
  {
    switch (query.Name.ToLower())
    {
      case "cms.user.selectall":
        query.Text = query.Text.Replace("CMS_User", "View_CMS_User");
        break;
    }
  }
}


I didn't come up with other example that would work with default database, so I am just switching selection from table CMS_User to view View_CMS_User which contains the table (to not break anything), if you are experienced user with specific needs, just thing about something useful you could need to do this way (if you really can't do that another way).

The other handler is probably the one you may use more often, it has signature like this:

/// <summary>
/// Query execution event handler
/// </summary>
/// <param name="query">Executed query</param>
/// <param name="conn">Connection</param>
/// <param name="result">Query result</param>
public delegate void AfterExecuteQueryEventHandler(QueryParameters query, IDataConnection conn, ref DataSet result);


And as you can see, you may change the result of the query. As you can see, it is a DataSet, so it is available only for the calls using ExecuteQuery, not others (like ExecuteReader etc.)

I have several modifications that I could use as an example, I will show you how you can globally format the user data coming from the database (not that it would be useful, just so you can see you can modify the resulting data). Again, I am binding the event in the AfterApplicationStart event:

///
/// Fires after the application start event
///
public static void AfterApplicationStart(object sender, EventArgs e)
{
  // Add your custom actions
  CMS.SettingsProvider.SqlHelperClass.OnAfterExecuteQuery += new CMS.SettingsProvider.SqlHelperClass.AfterExecuteQueryEventHandler(AfterExecuteQuery);
}

static void AfterExecuteQuery(CMS.SettingsProvider.QueryParameters query, CMS.IDataConnectionLibrary.IDataConnection conn, ref DataSet result)
{
  if (query.Name != null)
  {
    switch (query.Name.ToLower())
    {
      case "cms.user.selectall":
        if (result != null)
        {
          DataTable dt = result.Tables[0];
          foreach (DataRow dr in dt.Rows)
          {
            dr["FullName"] = dr["FirstName"] + " " + dr["MiddleName"] + " " + dr ["LastName"];
          }
        }
        break;
    }
  }
}


This code basically gives dynamically generated full name instead of the one that is set in the user settings. It may not be present in the admin UI, since it is only replacing the results of this one query and admin UI uses more than that, but it should give you decent example of what you can do with it.

And that is pretty much all for now. I hope it wasn't too low-level for you, and that you get what you needed.

See you next time.
Share this article on   LinkedIn Google+

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

Martin Hejtmanek commented on

From the architectural perspective the property is not needed, because this object is never used in staging/export, however you are right it should be there so the object behaves consistently with these properties.

Anyway, you don't need it. You know you are working with ShoppingCartInfo so just cast the object to ShoppingCartInfo and get the ID from it's property ShoppingCartID.

Nortech commented on

Hi,

I'm trying to extend the ShoppingCartInfo class using the method described above. However, the class ShoppingCartInfo does not have a TYPEINFO property and therefore the extension does not work correctly (the ObjectId is 0). Why the property is missing? Can I added? Will it be added in future releases?

Regards,
Ulysses

Martin Hejtmanek commented on

I see,

The related data supports single object only, what you should do is put this into the Related data through a single handler:

object[] related = new object[] { installments, parameters }

And then access it like

object[] related =(object[])paymentOptionObj.RelatedData;
((PaymentOptionInstallments)related[0]).GetInstallments();

That should do the work

Ulysse commented on

Sorry about that . . .

I need to relate PaymentOption class to 2 different classes: PaymentOptionParameters and PaymentOptionInstallments. Each of these classes stores information to its own custom table. When using just one related class everything is ok.

Abstract from App_Code/Global/CMS/CMSApplication:

public static void AfterApplicationStart(object sender, EventArgs e)
{
CMS.Ecommerce.PaymentOptionInfo.TYPEINFO.OnLoadRelatedData += new TypeInfo.ObjectLoadRelatedDataEventHandler(PaymentOptionInstallments_OnLoadRelatedData);
}

static object PaymentOptionInstallments_OnLoadRelatedData(CMS.SettingsProvider.IInfoObject infoObj)
{
PaymentOptionInstallments poi = new PaymentOptionInstallments(infoObj.ObjectID);
return poi;
}

However, the code below does not work.

public static void AfterApplicationStart(object sender, EventArgs e)
{
CMS.Ecommerce.PaymentOptionInfo.TYPEINFO.OnLoadRelatedData += new TypeInfo.ObjectLoadRelatedDataEventHandler(PaymentOptionInstallments_OnLoadRelatedData);
CMS.Ecommerce.PaymentOptionInfo.TYPEINFO.OnLoadRelatedData += new TypeInfo.ObjectLoadRelatedDataEventHandler(PaymentOptionParameters_OnLoadRelatedData);
}

Any suggestions on how could I implement something like that:

((PaymentOptionInstallments)paymentOptionObj.RelatedData).GetInstallments()

((PaymentOptionParameter)paymentOptionObj.RelatedData).GetValue("aparameter")

Martin Hejtmanek commented on

Hi, I do not understand the question, can you clarify what you need to achieve?

Ulysses commented on

Is there a was to do something like that?

CMS.SiteProvider.SiteInfo.TYPEINFO.OnLoadRelatedData += new CMS.SettingsProvider.TypeInfo.ObjectLoadRelatedDataEventHandler(SiteInfo_OnLoadRelatedData);

CMS.SiteProvider.SiteInfo.TYPEINFO.OnLoadRelatedData += new CMS.SettingsProvider.TypeInfo.ObjectLoadRelatedDataEventHandler(SiteInfo_OnLoad[---!!!---SomeMore---!!!---]RelatedData);

The above approach, does not work.