Module: Migrate widgets and custom code
5 of 9 Pages
Transform properties to use a different UI form control
Keeping the data type
Imagine you have a widget in your source KX13 instance with a simple text input property. During the usage of the widget it turned out that editors need to store a longer sentence or even a paragraph in this property.

Let’s change the UI control from a text input to a text area.
Since the data type remains the same (text), you don’t need to perform any data migration work.
When adjusting widget properties in the code of your target instance, simply use the TextAreaComponent attribute:
[EditingComponent(TextInputComponent.IDENTIFIER,
Label = "Content",
Order = 1)]
public string Content { get; set; }
[TextAreaComponent(
Label = "Content",
MinRowsNumber = 10,
Order = 20)]
public string Content { get; set; } = "";
The result will look seamless.

Changing the data type
For a more complex scenario, consider a widget property that uses a page selector component in KX13:

The migration tool already handles differences in the Page Builder data structure for page selector properties between KX13 and XbyK, with the default page selector migration. This default transformation will work for you if you’re aiming for a lift-and-shift migration.
To use a different control for page selection, for example, the combined content selector, which is the Kentico-recommended practice, define a custom widget property migration.
First, examine the Page Builder data structures. Both use an array, but the page selector in KX13 references pages by nodeGuid, while the combined content selector in XbyK uses Identifier (a content item GUID).
...
{
...
"type": "Xperience.Widgets.HeroBannerWidget",
"variants": [
{
"identifier": "d9146566-6bfc-4e62-b18c-87466e6f639c",
"properties": {
"title": "Who should take this course?",
...
"ctaUrlInternal": [
{
"nodeGuid": "5f4f8058-44fa-46ec-be60-01b66d7ae63c"
}
],
...
}
}
]
}
...
...
{
...
"type": "Xperience.Widgets.HeroBannerWidget",
"variants": [
{
"identifier": "c9cd393b-8b2b-438a-9e1d-eba41be0960c",
"name": null,
"properties": {
"title": "Who should take this course?",
...
"ctaUrlInternal": [
{
"Identifier": "17053ede-cc2c-4430-bd75-168179780a52"
}
],
"ctaTargetUrl": "https://kentico.com",
...
},
"conditionTypeParameters": null
}
]
}
...
You can view the Page Builder JSON data by selecting from CMS_Document.DocumentPageBuilderWidgets in a KX13 database, and by selecting CMS_ContentItemCommonData.ContentItemCommonDataVisualBuilderWidgets in an XbyK database.
Let’s write the custom data migration to transform our widget JSON accordingly.
In the Migration.Tool.Extensions project, create a new WidgetPageSelectorToCombinedSelectorMigration class file. We recommend organizing custom migrations separately from default ones, for example in a CustomWidgetMigrations folder.
Define the class as shown below.
Read about the custom widget migration class structure in our GitHub documentation.
using CMS.ContentEngine;
using CMS.Core;
using Microsoft.Extensions.Logging;
using Migration.Tool.Common.Enumerations;
using Migration.Tool.Common.Services;
using Migration.Tool.KXP.Api.Services.CmsClass;
using Migration.Tool.Source.Services.Model;
using Newtonsoft.Json.Linq;
public class WidgetPageSelectorToCombinedSelectorMigration(
ISpoiledGuidContext spoiledGuidContext,
ILogger<WidgetPageSelectorToCombinedSelectorMigration> logger) : IWidgetPropertyMigration
{
private const string MigratedComponent = Kx13FormComponents.Kentico_PageSelector;
// Set higher priority (lower number) than migrations you want to override
public int Rank => 100;
// This migration should happen only for Page selector properties
public bool ShallMigrate(WidgetPropertyMigrationContext context, string propertyName)
=> MigratedComponent.Equals(context.EditingFormControlModel?.FormComponentIdentifier, StringComparison.InvariantCultureIgnoreCase);
// Define the property migration
public Task<WidgetPropertyMigrationResult> MigrateWidgetProperty(
string key, JToken? value, WidgetPropertyMigrationContext context)
{
(int siteId, _) = context;
// Read the KX13 value if it's not empty
if (value?.ToObject<List<PageSelectorItem>>() is { Count: > 0 } items)
{
// Map each page selector object to a content item reference - the target data type
var result = items.Select(pageSelectorItem => new ContentItemReference
{
// Retrieve the correct GUID of the migrated page item
Identifier = spoiledGuidContext.EnsureNodeGuid(pageSelectorItem.NodeGuid, siteId)
}).ToList();
// Serialize and return the new structure
var resultAsJToken = JToken.FromObject(result);
return Task.FromResult(new WidgetPropertyMigrationResult(resultAsJToken));
}
else
{
logger.LogError("Failed to parse '{ComponentName}' json {Json}", MigratedComponent, value?.ToString() ?? "<null>");
// Leave value as it is
return Task.FromResult(new WidgetPropertyMigrationResult(value));
}
}
}
Pay attention to the Rank property. If you look at the default Page selector migration, you’ll see that its ShallMigrate function looks the same - it targets page selector properties.
To prioritize your custom migration over the default WidgetPageSelectorMigration, set the Rank to a lower number.
In general, system/default migrations are ranked 100,000 or higher, allowing plenty of space to prioritize custom classes.
Next, register your custom migration.
...
using Migration.Tool.Extensions.CustomWidgetMigrations;
namespace Migration.Tool.Extensions;
public static class ServiceCollectionExtensions
{
public static IServiceCollection UseCustomizations(this IServiceCollection services)
{
...
services.AddTransient<IWidgetPropertyMigration, WidgetPageSelectorMigration>();
services.AddTransient<IWidgetPropertyMigration, WidgetPageSelectorToCombinedSelectorMigration>();
return services;
}
}
Rebuild the migration tool for the changes to take effect.
When you run the data migration now, all KX13 page selector widget properties will migrate to combined content selector properties in the target instance.
Apply the migration to a subset of properties
What if you want to use the custom migration only for specific properties or widgets, instead of across the board?
You have two options:
Test for a specific
propertyName, and specificsiteIdin theShallMigratemethod of theWidgetPageSelectorToCombinedSelectorMigration.Make the custom migration lower priority than the default by setting the
Rankand call it solely for specific properties in a custom widget migration.For example:
C#Example custom widget migration... public class HeroBannerWidgetMigration(ILogger<HeroBannerWidgetMigration> logger) : IWidgetMigration { public int Rank => 100; public const string SOURCE_WIDGET_IDENTIFIER = "Xperience.Widgets.HeroBannerWidget"; public const int SOURCE_SITE_ID = 1; public Task<WidgetMigrationResult> MigrateWidget(WidgetIdentifier identifier, JToken? value, WidgetMigrationContext context) { value!["type"] = "Xperience.Widgets.HeroBannerWidget"; var variants = (JArray)value!["variants"]!; var singleVariant = variants[0]; singleVariant["properties"] = new JObject { ["title"] = singleVariant["properties"]!["title"], ["content"] = singleVariant["properties"]!["content"], ... ["ctaTargetPage"] = singleVariant["properties"]!["ctaUrlInternal"] }; //For new properties, we must explicitly define property migration classes var propertyMigrations = new Dictionary<string, Type> { // Use the custom property widget migration ["ctaTargetPage"] = typeof(WidgetPageSelectorToCombinedSelectorMigration) }; return Task.FromResult(new WidgetMigrationResult(value, propertyMigrations)); } public bool ShallMigrate(WidgetMigrationContext context, WidgetIdentifier identifier) => string.Equals(SOURCE_WIDGET_IDENTIFIER, identifier.TypeIdentifier, StringComparison.InvariantCultureIgnoreCase) && SOURCE_SITE_ID == context.SiteId; }
Regardless of which approach you chose, let’s adjust the code in your target instance. In the widget properties file, decorate the migrated property with the ContentItemSelectorComponent attribute and adjust content retrieval logic:
...
[ContentItemSelectorComponent(
Cafe.CONTENT_TYPE_NAME,
Label = "Select page",
ExplanationText = "Select a page to link to.",
Order = 70)]
public IEnumerable<ContentItemReference> CtaTargetPage { get; set; } = new List<ContentItemReference>();
...
private async Task InitializeModel(HeroBannerWidgetModel model, HeroBannerWidgetProperties properties,
LandingPage document1)
{
...
// Retrieve the web page based on the migrated content item's GUID
var contentItemGuid = properties.CtaTargetPage.FirstOrDefault()?.Identifier ?? Guid.Empty;
// Call a service method that retrieves the Webpage data
var page = await RetrieveWebPageByContentItemGuid(contentItemGuid);
// assign the page URL to the model to be rendered
model.CtaUrl = page != null ? page.GetUrl().AbsoluteUrl : String.Empty;
...
}
The content retrieval is beyond the scope of this material. See an example implementation of the RetrieveWebPageByContentItemGuid method in our Training guides repository.