Bits of Xperience: Supercharged Page Custom Data Fields
Tags: xperience, kentico, csharp, dotnet
Published: 2021-09-27 14:12:57

---
title: Bits of Xperience: Supercharged Page Custom Data Fields
published: true
description: Learn how to combine the power of Page Type fields and Page "Custom Data" to add a new dimension to your content modeling architecture
tags: xperience, kentico, csharp, dotnet
series: Bits of Xperience
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/91a47vt9de7x1jkcfrl8.jpg
---

Kentico Xperience's custom Page Types let us model a site's content using a combination of powerful built-in Form Controls and the flexible structuring of information in a site's Content Tree.

Usually, with Page Type fields, each field maps to one value (as a column in a database table), and each Page Type has a unique set of fields. But, what if we want to store multiple fields in a single database column, or have multiple Page Types that store data in a single location, making it easy to query 🤔?

There might not be an out-of-the-box solution, but fortunately, with a little code and configuration 🤓, we can use Page "Custom Data" to achieve both of these things.

> If you want to jump to the solution, check out our new NuGet package that does all the coding for you, [Xperience Page Custom Data Control Extender](https://github.com/wiredviews/xperience-page-custom-data-control-extender).

## 📚 What Will We Learn?

- What are Page "Custom Data" fields?
- What do the "Custom Data" Page fields lack?
- Using Global Events with Page "Custom Data"
- Using Custom Form Controls and a Control Extender
- Storing Page data directly in "Custom Data" fields

## ❓ What are Page "Custom Data" fields?

Before we get going, let's level set. Page "Custom Data" fields are the `DocumentCustomData` column in the `CMS_Document` table and the `NodeCustomData` column in the `CMS_Tree` table.

Anything in, or related to, the `CMS_Tree` table is going to apply to all cultures for a Page in the Content Tree, and likewise, anything in, or related to, the `CMS_Document` table is going to be specific to a single culture. Many sites only have a single culture, so this distinction might not be something you're used to thinking about 🤨.

> Read more about the [Kentico Xperience Page database architecture](https://docs.xperience.io/custom-development/working-with-pages-in-the-api/page-database-structure) 🧐.

All "Custom Data" database columns (there are many in non-Page ones Xperience) have an XML structure, and the C# API to interact with them works with a `XmlData` container behind the scenes, almost like a `string`-keyed dictionary.

"Custom Data" columns let us switch from a relational database architecture to more of a document structure where the schema of the data isn't defined in the database, but instead in our code.

It would be great 👍🏾 to leverage this alternate way of storing data to achieve what was discussed earlier (multiple values per column and Pages storing field values for multiple Page Types in the same location, instead of separate Page Type database tables).

## 🗃 What do the Page "Custom Data" Page Fields Lack?

First, let's review why we can't 😞 use Page "Custom Data" fields as they currently exist in Xperience.

If we look at the the documentation on the features of the [field editor](https://docs.xperience.io/custom-development/extending-the-administration-interface/developing-form-controls/reference-field-editor#ReferenceFieldeditor-Creatingnewfields), which is used for creating fields for custom Page Types, we can see there are a couple options for the "Field type". The one we are interested in is the "Page Field":

> Page field – only available for page type fields. Allows you to choose a general page column from the `CMS_Tree` or `CMS_Document` table, and link it to the page field.

When creating a new Page Type field and selecting a _Field type_ of _Page field_ we can select either `Page fields` or `Node fields` for the Group. This equates to columns from the `CMS_Document` and `CMS_Tree` tables.

If we select `Page fields` and then pick the _Field name_ of `DocumentCustomData`, we can start interacting directly with this value for each Page of the given Page Type 😀.

![Page Type field editor using DocumentCustomData directly](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h5qfvguiohzrzczm4leg.jpg)

Because the XML schema of these fields is flexible, there's no special "Custom Data" Form Control 😦 that let's us modify that XML in a way that is friendly to Content Managers.

The best we can do is a Rich Text editor:

![Rich text editor showing plain text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5ms5sdvphpwjb4zh2yie.jpg)

But it falls way too short of something usable for Content Managers 😣:

![Rich text editor showing XML](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j7o5omucm08on69wizry.jpg)

So what are we going to do if we want to leverage the schema flexibility of "Custom Data" fields for a Page? What Form Control gives us a good Content Management experience? Do we have to build a bunch of custom Web Forms Form Controls ☠?

## 📅 Using Global Events and Page "Custom Data"

Fortunately there's a couple different ways we can approach this problem 😅.

We already have a bunch of pre-built Form Controls which are designed for ease-of-use for Content Managers. Let's make sure our solution includes those 👏🏼 and doesn't require us to rewrite them!

Let's create a new field on our Page Type named `ArticleIsInSitemap`, using all the standard Page Type field functionality:

![Page Type field dialog](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/otf3ssd2rnx87e2t6t3o.jpg)

If we create new fields on our Page Type and use the appropriate standard Form Controls for those fields, we can get a solid Content Management experience, but the values go into individual database columns in each Page Type's database table 🤦🏽‍♀️ instead of the Page "Custom Data" columns.

Thankfully, Kentico Xperience has a full set of [Global Events](https://docs.xperience.io/custom-development/handling-global-events) that allow developers to react to things happening within the system 👨🏿‍🔬. We can use these events to copy data from our Page Type fields to the "Custom Data" XML structures of the Page.

Let's create a Custom Module that will give us a place to react to these events:

```csharp
using CMS;
using CMS.DataEngine;
using CMS.DocumentEngine;
using CMSApp;

[assembly: RegisterModule(typeof(DocumentEventsModule))]

namespace CMSApp
{
    public class DocumentEventsModule : Module
    {
        public DocumentEventsModule() : 
            base(nameof(DocumentEventsModule)) { }

        protected override void OnInit()
        {
            base.OnInit();

            DocumentEvents
                .Insert
                .Before += Insert_Before;

            DocumentEvents
                .Update
                .Before += Update_Before;
        }

        private void Update_Before(
            object sender, DocumentEventArgs e) => 
            SetValuesInternal(e);

        private void Insert_Before(
            object sender, DocumentEventArgs e) =>
            SetValuesInternal(e);
    }
}
```

Both of the event handlers above let us run our logic when any Page is inserted or updated with the following method:

```csharp
private void SetValuesInternal(DocumentEventArgs e)
{
    if (!(e.Node is Article article))
    {
        return;
    }

    article
        .DocumentCustomData
        .SetValue(
            nameof(Article.Fields.IsInSitemap), 
            article.Fields.IsInSitemap);
}
```

And that's it! Every time an Article is inserted into the Content Tree or updated, the value in `ArticleIsInSitemap` will be copied to an XML element in `CMS_DocumentCustomData` 😊, which will look like this in the database:

```xml
<CustomData>
  <IsInSitemap>true</IsInSitemap>
</CustomData>
```

What's the benefit here? Don't we already have the value in the `DancingGoatCore_Article` table's `ArticleIsInSitemap` column?

Well, for data that determines whether or not a Page is in the sitemap, we want to be able to query across **all** Pages of the site, not just  Articles, so that we generate the correct [XML sitemap](https://developers.google.com/search/docs/advanced/sitemaps/overview).

If we have a column in each table for all of our custom Page Types, we'd end up with a real ugly SQL `UNION` to get all the Pages in the sitemap. By copying the value to `DocumentCustomData`, we ensure the full sitemap can be generated by _only_ querying the `CMS_Document` table 💪🏻:

```sql
SELECT *
FROM CMS_Document
WHERE CAST(DocumentCustomData as XML).value('(//CustomData/IsInSitemap/text())[1]', 'bit') = 1
```

> Checkout Microsoft's documentation to read about [querying XML in SQL](https://docs.microsoft.com/en-us/sql/t-sql/xml/xml-data-type-methods?view=sql-server-ver15)

This is great 👍🏾! But it would be _really_ great if we didn't have to have an extra database table column per-Page Type and duplicate this data. We would prefer to write directly to the Page "Custom Data" field 😏.

## 🎛 Custom Form Controls and Control Extenders

Lucky for us, Kentico Xperience provides a convenient feature in the CMS architecture - [Control Extenders](https://docs.xperience.io/custom-development/extending-the-administration-interface/developing-form-controls/inheriting-from-existing-form-controls).

Control Extenders let us enhance the functionality of inherited Form Controls. But what does that mean?

{% youtube OQSNhk5ICTI %}

We can create new Form Controls in the Administration application, with either new code or inheriting the code and functionality from an existing Form Control. The 2nd option is preferable because it means less work for us 😄!

When we create a new Form Control that inherits from another, we can apply a Control Extender to it. A Control Extender is a component that wraps the original Form Control and gets to intercept interactions with the Control 🧐.

This is a valuable feature for us, because it will let us source the Control's value from `DocumentCustomData` when it is read and write it to `DocumentCustomData` when the Control value is updated - all without modifying the code or functionality of the original control. We can also apply this Control Extender to _any_ inheriting Form Control 😮.

In summary, this is what we want to accomplish:

- ✔ Create a Control Extender that redirects interactions with the Form Control's value to the `DocumentCustomData` field and store the value in an XML element with the same name as the field's name
- ✔ Create a new Form Control that inherits from the standard Check box Form Control and apply the Control Extender to the new Form Control
- ✔ Use the new Form Control as the control for our `Article` Page Type `IsInSitemap` field

### 🏗 Control Extender

The code for the Control Extender is pretty simple:

```csharp
public class CustomDataControlExtender : 
    ControlExtender<FormEngineUserControl>
{
    public override void OnInit()
    {
        // logic here
    }
}
```

There are multiple events the Control emits that we can register event handlers for and when the underlying Form Control initializes, we register our event handlers in `Control_Init`.

```csharp
public override void OnInit() => 
    Control.Init += Control_Init;

private void Control_Init(object sender, EventArgs e)
{
    Control.Form.OnGetControlValue += Form_OnGetControlValue;
    Control.Form.OnAfterDataLoad += Form_OnAfterDataLoad;

    Control.Form.FieldControls.Add(Control.Field, Control);
    Control.Data.ColumnNames.Add(Control.Field);
}
```

We are going to be creating our Page Type field as a "Field without database representation", which means it won't be listed in the `FieldControls` or `ColumnNames` collections that get processed when we load/save our Form, so we explicitly add it.

This way the Form treats our field as though it needs to be persisted/retrieved just like the other ones.

```csharp
private void Form_OnAfterDataLoad(object sender, EventArgs e)
{
    if (!(Control.Data is TreeNode page))
    {
        return;
    }

    Control.Value = page
        .DocumentCustomData
        .GetValue(Control.Field);
}

private void Form_OnGetControlValue(
    object sender, FormEngineUserControlEventArgs e)
{
    if (!(Control.Data is TreeNode page))
    {
        return;
    }

    if (e.ColumnName.Equals(
        Control.Field, 
        StringComparison.InvariantCultureIgnoreCase))
    {
        page.DocumentCustomData.SetValue(
            Control.Field, Control.Value);
    }
}
```

Then we define two event handlers - the first supplies the Form Control value from the correct Page "Custom Data" field when the value is loaded by the Control, and the second accepts the value coming from the Control and stores it in the correct Page "Custom Data" field.

After our interception, the Page gets created or saved and the field's value is saved along with it, except it's inside `DocumentCustomData` and not a Page Type specific database column 🤩.

### 👵🏽 Inheriting a Form Control

Inheriting from an existing Form Control and applying a Control Extender only takes a few steps!

First, navigate to the "Administration interface" module in the CMS application:

![Administration interface menu item](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8r470gaphltyfcxin3zy.jpg)

Then create a new "inheriting" Form Control, using the Check box Form Control as the source:

![New Form Control dialog](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yfay5cva2ijs9wg65kwh.jpg)

Be sure to select the Control Extender you just created!

Finally, select the places where this Form Control can be used. For our example it will be for "Boolean (Yes/No)" fields for "Page Types":

![Form Control assignment configuration](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nwvdysx1zwykg7ihvufs.jpg)

### 📃 Using a Page "Custom Data" Form Control

Now we can create the new field for our Page Type 😊!

Be sure to select **"Field without database representation"** for the Field type (otherwise the value will be saved in a newly created database table column for the Page Type 😬) and use our new extended Form Control (otherwise the value won't be saved _at all_).

Whatever we name this new field will end up being the XML element that the value is stored in, so a field named `ABC` would end up as `<CustomData><ABC>value</ABC></CustomData>`:

![New Page Type field dialog](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ivhidsqpv6ahumn8v2vy.jpg)

And that's it! When we save a Page of this Page Type, this specific field will only be saved to `DocumentCustomData`.

We can add as many "Custom Data" fields to a Page Type as we want, and if we define the same field on multiple Page Types, they'll all put data of the same XML schema in our `CMS_Document.DocumentCustomData` database column.

## 🏁 Conclusion

I hope by now both the motivation and process for using Page "Custom Data" fields as the backing store of Page Type fields are clear 🔍.

There's a few steps to set everything up - create a Control Extender, define new inheriting Form Controls using the Control Extender, and add a fields to Page Types using the extended Form Control - but the initial setup definitely pays off. It's worth noting the first step (creating a Control Extender) only needs performed once, and the second (creating an extended Form Control) only once per Form Control type (eg Text Box, Check Box, Page Selector).

Our "Sitemap" example saves us from performing a large SQL `UNION` when we generate a site's XML sitemap, but that's not the only use case.

What about [Open Graph](https://ogp.me/) metadata values for a Page - wouldn't it be nice to not have to create a separate database column for each value?

Or, a standard field inherited from a base Page Type that we aren't going to be likely to filter in SQL - like a 'primary image path'.

We could even make mixins, letting multiple Page Types share sets of fields and then access those field values across Page Types by querying the `CMS_Document` table only 🧐.

Are you thinking about implementing this yourself? Well, it's dangerous to go alone... 

![zelda meme](https://media.giphy.com/media/ElSNi8FdSB7RS/giphy.gif?cid=ecf05e47ef4jrqak87upnitkvd0qjmetu2ynz0iu87w40p54&rid=giphy.gif&ct=g)

So, take this... the [Xperience Page Custom Data Control Extender](https://github.com/wiredviews/xperience-page-custom-data-control-extender), a NuGet package containing an enhanced version of the above Control Extender with detailed setup instructions 👐.

As always, thanks for reading 🙏!

## References

- [Xperience Page Custom Data Control Extender](https://github.com/wiredviews/xperience-page-custom-data-control-extender)
- [Creating new fields in the Kentico Xperience field editor](https://docs.xperience.io/custom-development/extending-the-administration-interface/developing-form-controls/reference-field-editor#ReferenceFieldeditor-Creatingnewfields)
- [Kentico Xperience Global Events](https://docs.xperience.io/custom-development/handling-global-events)
- [Google - XML sitemaps Overview](https://developers.google.com/search/docs/advanced/sitemaps/overview)
- [SQL Server - xml Data Type Methods](https://docs.microsoft.com/en-us/sql/t-sql/xml/xml-data-type-methods?view=sql-server-ver15)
- [Kentico Xperience - Control Extenders](https://docs.xperience.io/custom-development/extending-the-administration-interface/developing-form-controls/inheriting-from-existing-form-controls)

---

We've put together a list over on [Kentico's GitHub account](https://github.com/Kentico/Home/blob/master/RESOURCES.md) of developer resources. Go check it out!

If you are looking for additional Kentico content, checkout the Kentico or Xperience tags here on DEV.

{% tag kentico %}

{% tag xperience %}

Or my [Kentico Xperience blog series](https://dev.to/seangwright/series), like:

- [Kentico Xperience Xplorations](https://dev.to/seangwright/series/8185)
- [Kentico Xperience MVC Widget Experiments](https://dev.to/seangwright/series/9483)
- [Kentico Xperience Design Patterns](https://dev.to/seangwright/series/10963)