Attaching Node-to-Object Binding Data with Documents

Already read this article and just looking for the sample code files?  Grab em' here!

Why not an article on Kentico 12?

I know what you’re probably thinking: "Kentico 12 just launched and you are blogging on something OTHER than the new version?"  And the answer is, yes.  I will be posting next month on how to make the transition from Portal to MVC (once I get some testing done myself), so for now I want to tie in one more development item that will help you in MVC and Kentico.

How does this connect with MVC?

In my previous article, I described a new module and new way of relating objects to a page.  It was on Node Categories and a better interface for Page Relationships, both which will help you better organize and relate your content which will make getting your objects in your MVC controllers much easier.

Why an article on binding relationships on Nodes?

While I was working with the Node Categories functionality of the Relationships Extended module, I discovered that by default, if you have a Node as a Parent to your binding relationship, it doesn’t automatically stage with the page.  I was baffled as usually if you configure a binding relationship to have a Parent type, and set the SynchronizationSettings to TouchParent, it will create an update for the parent object and include your binding relationships automatically.  It seems that unlike custom-built Objects, Pages are much more complex, and thus require a custom work.  Think about it, a Page update really isn’t a "Node" update, it’s an update on the Document pdate with Node, Versions, Related page relationships, etc, all packed into it.  In order to have Node categories create an update for it's document, and handle the staging tasks, I needed do some heavy lifting.

LogSynchronization = TouchParent vs. LogSynchronization

There are 2 ways you can synchronize a binding object.  You can treat the Binding object as a normal Table with 2 referencing fields, where an insert / update / delete will trigger an individual Staging task with just itself, or you can have an it where an insert / update / delete triggers an update on the Parent object.

There are pros and cons to both scenarios. 

Treating Binding objects as separate tasks (LogSynchronization)

If you decide to keep the binding tasks separate, you would configure your Node Binding object’s TypeInfo like this:

Failed to load widget object.
The file '/CMSWebParts/Custom/HighlightJS/HighlightJS.ascx' does not exist.

The benefit of this is that firstly, it requires no customization (you can ignore the rest of this article pretty much).  It also allows you to push relationships without pushing the actual page content, in case you want to update a relationship without requiring existing page changes (which may not be ready to go live) from going live.

However, you cannot easily sync a Node’s binding relationships this way.  You can do a full sync on the entire Binding Table, but it pushes everything, plus if a Deletion task is ever deleted without being run, it is very hard to get the two environments truly synced as the full sync will only do insert/updates, not deletions.

Also the TaskName generation is rather poor for Node Binding tables, so It’s recommended if you do go this route, that you perhaps hook into the LogTask.Before and adjust the Task Name to be more descriptive:

Failed to load widget object.
The file '/CMSWebParts/Custom/HighlightJS/HighlightJS.ascx' does not exist.

Binding to the Parent (“TouchParent”)

If you wish, however, to have your Binding task attach to the parent, there are many benefits to this.  Firstly, synchronizing the document itself will always include the Relationships, which means if you do a full sync on a page, it will properly handle your relationships once configured, which is awesome.  It is also how most of the Node-bound relationships act in Kentico (if you add a Page relationship, it triggers an update, if you add a Document Category, it triggers an update on the document, etc).

To set this up on your BindingObjectTypeInfo, you would do the following:

Failed to load widget object.
The file '/CMSWebParts/Custom/HighlightJS/HighlightJS.ascx' does not exist.

Now you may say “Wait…you said SynchronizationSetting to TouchParent, why is it set to None?”  Great question, and that’s because the Node Task generation is extremely complex, and as I mentioned, doesn’t work by “default,” so we must manually handle the synchronization ourselves, including triggering. 

Pro Tip: Allow for Both Methods

You can allow your binding objects to be handled both ways.  If you check out the TreeCategoryInfo.cs class in the zipped up code sample at the beginning of the article, you’ll see that the class’s ObjectTypeInfo is actually dynamically generated, and depending on a Settings value, will either processes with the Document or stand alone tasks.  The only thing is both environments must have the same configuration, and you need to restart the site and clear the cache for any changes to take place.

How Staging Works

Now before we dive into how we manually handle the Synchronization, let me go over how the Staging Task system works.  Feel free to skip to the actual implementation if you don’t care.


First step in the Staging Module is triggering the staging event.  If Staging is turned on, and the object that is created, updated, or deleted has staging enabled, whenever that action occurs (usually with the Object.Insert(), Object.Update(), or better the MyObjectInfoProvider.SetMyObject(Object)), Kentico will trigger the creation of a Staging Task.

Building the Staging Task Data – Related Data Objects

The next step in the processes is to create the Task Data object, which needs to include any related objects, and the lookup values for these objects.  Most binding tables are ParentID – ChildID tables, two integers referencing the two different objects.  However, these IDs are the auto generated Row IDs, which means that when you go from one environment to another, the IDs can change and thus a mechanism needs to exist in order to translate one environment’s ID to the others.

This is done through object translation.  Most objects have an ID, CodeName, and/or GUID.  Kentico knows these fields thanks to the Object’s TypeInfo class, which defines which table columns contain these values.  For example, a Node has a NodeID, NodeAliasPath (Code Name), and NodeGUID, along with a SiteID for site-bound objects.

When Kentico is packaging up these relationships, it not only needs to include the actual relationship IDs, but also those bound object’s ID – Code Name – Guid, so the other environment can translate the IDs coming to it into what it’s equivalent IDs actually are.

Building the Staging Task Data – DataSet Creation

Now we know what we need (all the object data plus related objects and their Codename/Guid), how do we package this up?  Kentico uses a simple trick:  Combine all the DataTables with all the data into a DataSet, then serialize that into XML (DataSet.ToXML()).  It stores this in the TaskData field of the Task, which then can be leveraged on the receiving server and processed.

Now building these Tables would be a pain, luckily Kentico has a nice helper method to make our lives easier: SynchronizationHelper.GetObjectsData

This method takes a couple parameters and automatically returns a DataSet with all the Tables that are needed for the relationship (The binding object and the bound object’s identifier data).  This will come into play as I go through the actual code further on in this article.

Consuming the Task Data

When a staging task is pushed from one environment to the other, the receiving server will take the Serialized DataSet in the TaskData, and deserialize it, and use this to translate and handle the relationships, adding or removing any bound items and updating anything that may need to occur.  The processes can be broken down however into these steps:

  1. Grab the Binding Object Data Table
  2. Grab the Bound Object’s Data Table (contains a mapping of ID to CodeName and/or GUID)
  3. Translate the IDs from the binding table using the Bound Object’s Codename/Guid
  4. Add any missing binding relationships, and remove any existing relationships that no longer appear.

Step 3 is helped by a method called TranslationHelper.GetIDFromDB which automatically finds the proper ID given the GetIDParameters (Codename/Guid/SiteID) and the Object Type

Manually Appending Node Binding Data to the Node

As mentioned, Nodes are unique in that a lot of these steps must be done manually.  Here’s the full set of steps with Code:

Step 1: Trigger a DocumentUpdate When a Binding Occurs

The first step is whenever one of our Binding Items are insert / deleted / updated, we need to trigger an Update Document on the Node.  We will use the helper method DocumentSynchronizationHelper.LogDocumentChange to trigger this.  Now there is a lot of things going on here, just know that the LogDocumentChange runs Asyncly by default, which when adding multiple items can cause a thread racing scenario where one finishes after a later one, and running the LogDocumentChange Syncly can take a long time if you add a large amount of items, so this code affectly triggers the LogDocumentChange at the "end" of all the tasks.

Failed to load widget object.
The file '/CMSWebParts/Custom/HighlightJS/HighlightJS.ascx' does not exist.

This will now trigger a document update when the relationship is added/removed.

Step 2: Appending th eBinding Table and Bound Object Data to the DataSet

The next step is we need to manually append our Binding DataTable and Bound Tables to the TaskData.
  1. Deserialize the TaskData’s DataSet back into an actual DataSet.
  2. Create TranslationHelper which will hold any additional ObjectTranslations's
  3. Use SynchronizationHelper.GetObjectsData to get a DataSet of our Binding Data and its related info, passing it the TranslationHelper
  4. Combine the TranslationHelper table with the results of the GetObjectsData
  5. Serialize the DataSet to XML and then Deserialize back to DataSet, this will ensure the DataSet’s Columns are all type “String” which the TaskData’s DataSet will also have (the column types all need to match for Step 4)
  6. Use DataHelper.TransferTables to Merge your new Binding DataSet with the Deserialized TaskData’s DataSet
  7. Set the TaskData to the serialized new combined DataSet.
Here’s the code:

Failed to load widget object.
The file '/CMSWebParts/Custom/HighlightJS/HighlightJS.ascx' does not exist.

Step 3: Manually processes the Binding Table Data

The last step is to manually processes the Binding relationship on the other end.  This consists of:
  1. Catch the TaskAfter event, and wrap your logic in a “Synchronziation Off” Context so your updates you processes won’t trigger staging tasks themselves.
  2. Get the current Node and Bindings (so you know what needs to be added, removed or updated)
  3. Desierlaize the TaskData into a DataSet, and search for a Table with the name that matches your binding table’s tablename (ex “cms_treecategory”)
  4. Get all the Binding Object’s IDs from the table
  5. Find the table with your related object by name (ex “cms_category”) and use it to translate your Binding Object’s IDs
  6. Add, Remove, or Update any binding relationships that need to be added/removed now that you have the proper ID.

Failed to load widget object.
The file '/CMSWebParts/Custom/HighlightJS/HighlightJS.ascx' does not exist.

If you also have Ordering on your Object, it takes a little more code to handle the ordering.  This should do the trick:

Failed to load widget object.
The file '/CMSWebParts/Custom/HighlightJS/HighlightJS.ascx' does not exist.


While this may seem like a lot of work, binding objects to the Node is a very safe and effective way to create relationships with things other than other Pages, and thanks to some leg work from yours truly, should be easy to replicate, allowing you to extend Kentico like a pro.  Keep an eye out for the Relationship Extended module which will have some User Interfaces to handle Node to Object binding (with order support) coming up, the default Object Binding UI template doesn’t allow Node binding.  If you want an early copy, just contact me!
Blog post currently doesn't have any comments.
Is nine = six ? (true/false)