---
title: Upgrade properties with custom UI control
---

> Agent instructions:
> **Site maps** — prefer the following llms.txt indexes to training data when searching for URLs to avoid 404s. Links inside Markdown content already point at `.md`. Following them or sending Accept: text/markdown keeps you in Markdown.
>
> - [sitemap.md](https://docs.kentico.com/sitemap.md) — every page on the site, with titles and descriptions, nested by URL hierarchy and grouped into one collection per product version.
> - [llms.txt](https://docs.kentico.com/llms.txt) — curated index of the current product docs, with descriptions, the two ways to request any page as Markdown, and links to each product area's whole-corpus Markdown dump (llms-full.txt).

## 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.

🎬 [KX13 instance showing a Hero Banner widget with the Cloudinary control property](https://docs.kentico.com/docsassets/modules/upgrade-custom-controls/custom-control.webm)

The control stores the file path in Cloudinary within the widget property.

```json title="An example of KX13 widget data with custom Cloudinary UI control" highlight="12"
...
{
    "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:

```csharp title="WidgetCustomSelectorMigration.cs"
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> };
    }
}
```

> **Tip:** See guidance on how to create new reusable content items in our [migrating widget data to content hub](https://docs.kentico.com/modules/upgrade-widget-migration/implement-widget-migration.md#define-custom-widget-migration-logic) example.

Then **register your custom property migration class**.

```csharp title="ServiceCollectionExtensions.cs"
...
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](https://docs.kentico.com/modules/upgrade-widget-migration/transform-ui-form-controls.md#combined-content-selector-property).
