Module: Migrate widgets and custom code
6 of 9 Pages
Upgrade properties with custom UI control
To migrate a property implemented with a custom control, you can also define a custom property migration. Then use Rank or direct property assignment in widget migration as described in the previous section.
Let’s look at an example. Consider a Hero banner widget in KX13 that uses a Cloudinary custom control, which allows editors to use images and media files stored in a connected Cloudinary account.

The control stores the file path in Cloudinary within the widget property.
...
{
"identifier": "3209a0e7-6cdc-46c2-b738-f1bf01414cb4",
"type": "Xperience.Widgets.HeroBannerWidget",
"variants": [
{
"identifier": "45e0c03d-850e-4c77-bf71-076d609683e8",
"properties": {
"title": "Kentico Xperience Developer hub",
"content": "The hub collects all the essential resources you need to know to develop websites in Kentico Xperience.",
"logo": null,
"image": "In-house photos/Working at computer/Kentico_010_mjtcjo",
"ctaText": null,
...
}
}
]
}
...
You could decide to keep your data in Cloudinary and reimplement the custom control in your target instance. In that case no custom property migration would be necessary. But let’s look at a scenario where you want to migrate the referenced media to an out-of-the-box reusable content item (for example, Legacy media file) or to a custom reusable content item, then use an out-of-the-box combined content selector.
First define a custom IWidgetPropertyMigration as in the previous example.
For example:
using CMS.Core;
using Microsoft.Extensions.Logging;
using Migration.Tool.KXP.Api.Services.CmsClass;
using Newtonsoft.Json.Linq;
public class WidgetCustomSelectorMigration(
ILogger<WidgetCustomSelectorMigration> logger) : IWidgetPropertyMigration
{
private const string MigratedComponent = "CloudinarySelectorComponent"; //The code name of your custom selector
public int Rank => 100_002;
public bool ShallMigrate(WidgetPropertyMigrationContext context, string propertyName)
=> MigratedComponent.Equals(context.EditingFormControlModel?.FormComponentIdentifier, StringComparison.InvariantCultureIgnoreCase);
// Migrate the property to combined content selector with content item references
public Task<WidgetPropertyMigrationResult> MigrateWidgetProperty(
string key, JToken? value, WidgetPropertyMigrationContext context)
{
(int siteId, _) = context;
var refsToMedia = new List<object>();
if (value != null && !string.IsNullOrEmpty(value.ToString()))
{
refsToMedia.Add(CreateReusableContentItemFromCloudinary(value));
}
var resultAsJToken = JToken.FromObject(refsToMedia);
return Task.FromResult(new WidgetPropertyMigrationResult(resultAsJToken));
}
private ContentItemReference CreateReusableContentItemFromCloudinary(JToken value)
{
// Retrieve the necessary data from the Cloudinary platform
// code TODO ...
// Create the content item based on the Cloudinary data
// Reference the new content item GUID
return new ContentItemReference { Identifier = <NEW-CONTENT-ITEM-GUID> };
}
}
See guidance on how to create new reusable content items in our migrating widget data to content hub example.
...
services.AddTransient<IWidgetPropertyMigration, WidgetCustomSelectorMigration>();
...
If you migrated your property to an out-of-the-box combined content selector, decorate the property in your target instance and adjust content retrieval as shown in the previous example.