How to effectively customize Kentico CMS UI

   —   
UI customization is an often topic in our discussions with clients. I put together a few examples and best practices of how you can do things so they are upgrade procedure proof and done as easy as you can.
Hi there,

I am sure some of you have experiences with customizing of the UI, and then having trouble with upgrade procedure, where you need to take care about anything you have changed, do you? Some of the others probably didn't even know that it is possible to customize the UI and the rest just didn't need it or was afraid of it. So this post should be helpful.

Connection points

There is a group of connection points specifically placed for the UI customization reasons. You can find it in you project in ~/App_Code/Global/CMS folder. As you can see it is in the recommended list of folders to use: http://devnet.kentico.com/docs/devguide/folder_structure_and_importexport.htm

This way you can get the upgrade prodedure proof and import/export proof customization.

The available events are following:
  • CMSApplication.cs - List of the events within application life cycle where you can initialize your modules or do other global things.
  • CMSCustom.cs - Where you can define your own custom macro handler, see http://devnet.kentico.com/docs/devguide/appendix_a___macro_expressions.htm for details.
  • CMSPageEvents.cs - Page life cycle events, where you can customize your pages. Any page that inherits CMSPage (any UI page) calls these events. This does not apply to the live site (content) pages since they are custom and are using custom web part by their nature.
  • CMSRequest.cs - Request life cycle events, where you can customize the request processing.
  • CMSSession.cs - Session events, where you can do additional tasks if session changes.
  • CMSUserControlEvents.cs - Where you can customize most of the the UI user controls of Kentico CMS (I say most, because there can be few which do not fire these events, in general anything that inherits CMSUserControl does)
So this is the list of events, let's go to the customization itself. Just before you do that, I recommend you to run your project in the debug mode to see how and when the events are called. For particular customization, you just need to choose the right event for the action so you may need to check if the data you are about to change are already present.

Customization of existing page

I will show you a simple example (actually two) how to add the tab to the existing page. First, we add the Google tab to the CMSDesk header. It is a page (/CMSDesk/Header.aspx) and it is loading the tabs in the load event. So we use the CMSPageEvents and BeforePreRender (next event called) to modify the tabs:

using CMS.UIControls;

...

/// <summary>
/// Fires before page PreRender
/// </summary>
/// <returns>Returns true if default action should be performed</returns>
public static bool BeforePagePreRender(object sender, EventArgs e)
{
  // Add your custom actions
  CMSPage page = (CMSPage)sender;
  switch (page.RelativePath.ToLower())
  {
    case "/cmsdesk/header.aspx":
      BasicTabControl tabControl = (BasicTabControl)page["TabControl"];
      // Add tabs
      string[,] tabs = BasicTabControl.GetTabsArray(1);

      tabs[0, 0] = "Google";
      tabs[0, 2] = "http://www.google.com";

      tabControl.AddTabs(tabs);

      break;
  }

  return true;
}


Sometimes (well, mostly :-) ) the page uses master page so you need to reference the tabs from the master page like this:

// Add your custom actions
CMSPage page = (CMSPage)sender;
switch (page.RelativePath.ToLower())
{
  case "/cmsmodules/bizforms/tools/bizform_edit_header.aspx":
  BasicTabControl tabControl = ((CMSMasterPage)page.Master).Tabs;
  // Add tabs
  string[,] tabs = BasicTabControl.GetTabsArray(1);

  tabs[0, 0] = "Google";
  tabs[0, 2] = "http://www.google.com";

  tabControl.AddTabs(tabs);

  break;
}

return true;


This second one is in the properties of the BizForm. Try this code and see for yourselves. What we do is to use the CMSPage class (the base class) and use some information from it. The property TabControl in the first case (you may also find the control by searching the controls collection) and the master page property Tabs in second case.

You can also add specific controls to the page like this:

/// <summary>
/// Fires after page Load
/// </summary>
public static void AfterPageLoad(object sender, EventArgs e)
{
  // Add your custom actions
  CMSPage page = (CMSPage)sender;
  switch (page.RelativePath.ToLower())
  {
    case "/cmsdesk/header.aspx":
    page.Controls[0].Controls.AddAt(0, new LiteralControl("<span style=\"position: absolute; z-index: 1000;\">My control</span>"));
    break;
  }
}


Easy, isn't it? I think it is, so let's go further ...

Replacing the page with your customized page

If you customize the page directly, you have to take care that the upgrade procedure doesn't overwrite it. So it would always be better to have a copy of the page and customize the copy ... But how do you replace the original page without modifying another page? Luckily, it is not that easy with page events. We just transfer the page to the different one (we could do the similar in the request events in beginrequest or so):

/// <summary>
/// Fires before page PreInit
/// </summary>
/// <returns>Returns true if default action should be performed</returns>
public static bool BeforePagePreInit(object sender, EventArgs e)
{
  // Check the path of current file
  string path = HttpContext.Current.Request.AppRelativeCurrentExecutionFilePath.ToLower();
  switch (path)
  {
    // Custom user details page
    case "~/cmssitemanager/administration/users/user_edit_general.aspx":
      // Just change the path to the file you need
      HttpContext.Current.Server.Transfer("~/cmsglobalfiles/my_user_edit_general.aspx");
      break;
  }

  return true;
}


I am catching the PreInit event and transfer the execution to my own file. The file is in the ~/CMSGlobalFiles folder to be included into the export package. It is just the copy of the original page, with changed class name and modified with some additional code and controls. Notice, that I am not using the RelativePath property, because it would cause a loop (it is constant during the request processing). I might as well do the Response.Redirect to use completely different application in that part of the user interface.

Customizing the UI user controls

You can do similar things to the UI controls, just not the redirection (correct me if I am wrong) so in case you need to replace the entire control with your own customized version, I would recommend to customize the page that contains it. What you can do is modify the controls collection of the user control like in the page like this (modify the file CMSUserControlEvents.cs):

/// <summary>
/// Fires after UserControl Load
/// </summary>
public static void AfterUserControlLoad(object sender, EventArgs e)
{
  // Add your custom actions
  if (sender is CMSUserControl)
  {
    CMSUserControl userControl = (CMSUserControl)sender;
    switch (userControl.RelativePath.ToLower())
    {
      case "/cmsadmincontrols/ui/pagetitle.ascx":
      // Modify the control
      userControl.Controls.AddAt(0, new LiteralControl("<a href=\"http://www.kentico.com\">Visit Kentico.com</a>"));
      break;
    }
  }
}


This will add link to Kentico.com to all of your page titles. Not too useful modification, but you get the concept now (hopefully :-) ) ... Of course you can work with the control in many different ways, that is entirely up to you.

Customizing forms (form controls)

Customizing the pages and controls can be too complicated in some cases, mostly when you need to customize the forms based on the document type or other definitions. One of the oldest concept for this (much older than those events above) are the Form controls. If you look to your Site manager -> Development -> Form controls, you can see the list of the controls and that you can configure particular ones for specific dialogs. You can also register one and that's the place for our customization. The default form controls are in the folder ~/CMSFormControls or under ~/CMSModules/... but you can place them anywhere. They will be always part of your export package if you export the object (they contain the path to the file).

So create a new folder called ~/MyFormControls and create a new control myformcontrol.ascx in it, with the ASCX code:

<%@ Control Language="C#" AutoEventWireup="true" CodeFile="myformcontrol.ascx.cs" Inherits="MyFormControls_myformcontrol" %>
<asp:TextBox runat="server" ID="txtValue" />
<asp:RequiredFieldValidator ID="valValue" runat="server" ErrorMessage="Client side validation says that the value cannot be empty!" ControlToValidate="txtValue" />


And the code behind:

using System;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

using CMS.FormControls;

public partial class MyFormControls_myformcontrol : FormEngineUserControl
{
  protected void Page_Load(object sender, EventArgs e)
  {
  }

  /// <summary>
  /// Gets / sets the value
  /// </summary>
  public override object Value
  {
    get
    {
      return this.txtValue.Text;
    }
    set
    {
      this.txtValue.Text = (string)value;
    }
  }

  /// <summary>
  /// Returns true if the value is valid
  /// </summary>
  public override bool IsValid()
  {
    if (this.txtValue.Text.Length == 5)
    {
      return true;
    }
    else
    {
      this.ValidationError = "Server side validation says that the value length must be 5 characters.";
      return false;
    }
  }
}


You can see you can do anything like with standard user control, you just inherit from FormEngineUserControl. We use both client side and server side validation in this case (just to show you how that works). Now just register the control in the Site manager, with the path ~/MyFormControls/myformcontrol.ascx, for the Text fields and for instance in the Document types.

Now when you edit the text field in the document type, you have one more control to select. If you select it and edit the document of that type, you can see how it works (it is not that nice, but it works :-) )

Customize the live site

For customizing the live site, you can just type the ASPX code in the tranformation, page layout or create your custom web parts, or develop inline controls, you have direct ways how to do that our of the box, see these topics for that:

http://devnet.kentico.com/docs/devguide/developing_web_parts.htm
http://devnet.kentico.com/docs/devguide/how_to_develop_inline_controls.htm
http://devnet.kentico.com/docs/devguide/adding_custom_functions_to_transformations.htm
http://devnet.kentico.com/docs/devguide/page_layouts.htm
http://devnet.kentico.com/docs/devguide/creating_a_new_aspx_page_template.htm

and many others ...

What else do you need?

I suppose you can customize virtually anything (from the UI point of view) with these few concepts. Let me know in the comments if you encountered anything that cannot be handled by one of these, I will point you to the right direction or think about how we could improve the current experience if something is too complicated.
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

Martin Hejtmanek commented on

Hi Ricky,

Well, Kentico CMS itself is a platform, not a plugin (module) to attach to another CMS (why would anyone do such thing?). You start with Kentico CMS web site and then add your own custom functionality or change the default one, which is not uncommon with any CMSes so there is no need to add it as a module for that reason. It just is there. The more code you have in the web site itself, the more functionality you can change without having source code license, that is actually one of our great advantages on market.

The only thing the unexpecienced programmer has to do (maybe not even programmer but just web designer) has to do is to run the setup. Why would he even bother modifying web.config to make his simple web site work?

In case it would be a module, how would you allow the clients without Visual studio to customize it? Or even without its source code? Or how would they update the code if it was already on the hosting? (they can do it quite simply with current way). There is a need for this defined by our clients, and this need is actually making Kentico CMS very popular for everybody so I don't believe making it module is a good way to go (that's why we didn't do it, it is on purpose).

Automatic properties is a nice feature, but it is a C# 3.0 feature, so not really an option while we still completely support .NET 2.0 and VS 2005. And moreover, the properties aren't saved to any local variables, but to the generic structure within the web part (they are not hardcoded and you can completely change them through admin interface). In fact, the properties in web part code behind are there just to support web parts in ASPX development model properly, they are not really needed in Portal engine at all.

Just to summarize:

What you see in our solution is defined by the needs of the majority of our clients, and if they need it, we don't have problems making that for them. They are very happy with the customization options, and it doesn't change or remove any functionality (module version would behave the same, just have less customization options). Same for the reason why it is a web site and not web application, by starting with web site, you can do both models. And really, it is all a great selling point!

Just in case you are having trouble make something work due to these reasons (I don't see any other reason why you would not like having so much source code available out of the box without special license, even in Free edition), do not hesitate to let our support know, we can help you with that to make it work if that is your case.

Ricky D commented on

First of all, if your code was a module, this is all the unexperience programmer has to do:

<httpModules>
<add />
</httpModules>

The result is that all the code is gone from App_Code. Don't you think that an unexperience programmer is better helped with seeing less code he/she doesn't understand or needs to change, than when he/she is confronted with a big pile of 'scary' code? I think so.

Also, if you do it 'correct', new users will also learn the correct way. Now you are just setting them up for failure later.

Lastly, aren't you worried that you compromise on quality code for the sake of more users, you will suffer in the end? Looking at the code in web parts code behind, that must be a nightmare to write, it's 100's of lines of boilerplate. I think you might have code generation in place. But if you consider using automatic properties and annotations you could cut back on sooo much code there alone.

I can only say I wish you best of luck with the current code base.

Martin Hejtmanek commented on

Hi Ricky,

Thank you for your comment.

Believe or not, lots of our clients are "beginners" who often do not have any clue how the request life cycle or page life cycle exactly looks like (if you ask them, they will Google it to give you the asnwer), and sometimes do not even have Visual studio. That's basically one of the value of our product, that you don't need to be professional to work with it and you don't need to compile the assemblies to make the customizations. If you tell them to use HttpModule (which by the way you still can, but it isn't value of Kentico CMS, but .NET in general so it is up to Microsoft to present it), they will think it is too complicated to do.

Specifically this example is to show that it is possible with just few lines of code, without the need to compile the DLLs, so even the non-tech users can understand it. The result is that the beginner is able to do this, and professional can do it with better code or choose to do that with HttpModules or other "advanced" things in .NET.

I mean the switch completely seriously, it produces the same code as simple if, you can easily extend it to other cases, and definitely has much less overhead than all the handling code around modules whether you like it or not. If we would be talking about switch with a large number of cases, that might be different, but in such case it is up to developer to make the code right. I only wrote some examples that need to be "understandable to anybody", that is the first rule of writing samples.

I am dealing with general performance optimizations several times a week, and to be honest, I have a hard time finding problems in our code, because the most expensive calls are always the ones from the .NET libraries and whenever we decide to use some of their "extensibility" features, it is a great pain because it produces much more overhead. Framework looks nice, but is extremely slow in lot of cases. That is basically why we use this approach. On the other way, replacing native .NET code calls and handlers with our way gives us better performance than the competition, which is good for us and also for our clients :-)

Ricky D commented on

Editing existing files in App_Code is not really what I would call extensibility. And writing switch cases... seriously? I __strongly__ suggest some research into Managed Extensibility Framework, Dependency Injection and HttpModules.

Lee Englestone commented on

Just what I was looking for! yey!

-- Lee