---
title: Refine properties to improve editor experience
---

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

## Change property names and order

Sometimes you want to rename, reorder, or consolidate widget properties to improve the editing experience in your target solution. You might also want to perform small property data changes specific to this widget without reusing them elsewhere.

You can add this functionality to your custom widget migration class using simple value mapping.

Consider an example:

The _HeroBanner_ widget in this example has the following properties in source instance:

- `Title` - the heading of the widget
- `CtaText` - the label for the widget's call-to-action button
- `CtaTarget` - a dropdown selector with a value _'\_blank'_ to open target page in a new tab, or _'\_self'_ to open target page in the same tab
- `CtaUrlInternal` - a page to navigate to when a visitor clicks the call-to-action button
- `CtaUrlExternal` - an external URL to navigate to when a visitor clicks the call-to-action button

The widget's code contains internal conditional logic to prioritize navigating to the **CtaUrlExternal** before **CtaUrlInternal** if it's set.

![Showing the old version of Hero Banner widget in the source instance](https://docs.kentico.com/docsassets/modules/advanced-property-techniques/hero-banner-old.png "Showing the old version of Hero Banner widget in the source instance")

The resulting _HeroBanner_ widget should feature an improved user experience for editors:

- `Title` - stays the same
- `CtaText` - stays the same
- `CtaOpenInNewTab` - a new check box, if checked (true), the page or link will open in a new tab
- `CtaTargetType` - a new a radio button group property; the editor can use this property to decide whether to reference a page or an external URL
- `CtaTargetPage` - still holds the page reference, but displays conditionally, based on _CtaTargetType_
- `CtaTargetUrl` - still holds the external absolute URL, but displays conditionally, based on _CtaTargetType_

![Showing the new version of Hero Banner widget in the target instance](https://docs.kentico.com/docsassets/modules/advanced-property-techniques/hero-banner-new.gif "Showing the new version of Hero Banner widget in the target instance")

Here's the sample code for the custom migration and widget property file in the target XbyK instance:

```csharp title="HeroBannerWidgetMigration.cs"
using Microsoft.Extensions.Logging;
using Migration.Tool.KXP.Api.Services.CmsClass;
using Newtonsoft.Json.Linq;

namespace Migration.Tool.Extensions.CustomWidgetMigrations;

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; // Dancing goat site ID in the source instance
    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"],
            ["ctaText"] = singleVariant["properties"]!["ctaText"],
            // Change the property name and convert to a boolean value
            ["ctaOpenInNewTab"] = CtaTargetToBool(singleVariant["properties"]!["ctaTarget"]),
            // Add property for better UX - to select between internal and external link in the target instance
            ["ctaTargetType"] = singleVariant["properties"]!["ctaUrlExternal"] != null && !string.IsNullOrEmpty(singleVariant["properties"]!["ctaUrlExternal"]!.ToString())
                ? "absolute" : "page",
            ["ctaTargetPage"] = singleVariant["properties"]!["ctaUrlInternal"],
            ["ctaTargetUrl"] = singleVariant["properties"]!["ctaUrlExternal"],
        };

        var propertyMigrations = new Dictionary<string, Type>
        { };

        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;

    // A simple method making a widget-specific property transformation that's not intended to be reused across widgets/solution
    private bool CtaTargetToBool(JToken? value) => value?.ToString() == "_blank";
}
```

```csharp title="HeroBannerWidgetProperties.cs"
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using CMS.ContentEngine;
using CMS.Websites;
using DancingGoatCore;
using Kentico.Components.Web.Mvc.FormComponents;
using Kentico.Forms.Web.Mvc;
using Kentico.PageBuilder.Web.Mvc;
using Kentico.Xperience.Admin.Base.FormAnnotations;
using Kentico.Xperience.Admin.Websites.FormAnnotations;

namespace Xperience.PageBuilder.Widgets
{
    public class HeroBannerWidgetProperties : IWidgetProperties
    {
        // The guide will point to the docs for widget properties
        // highlight adding the explanation text as a good practice
        [TextInputComponent(
            Label = "Title",
            ExplanationText = "The title is displayed as a heading at the top of the banner. Avoid ending the title with a period.",
            Order = 10)]
        [Required]
        public string Title { get; set; } = "";

        [TextInputComponent(
            Label = "Call-to-action Text",
            Order = 20)]
        public string CtaText { get; set; } = "";

        // Display a checkbox instead of a dropdown with technical values
        [CheckBoxComponent(
            Label = "Open in a new tab",
            Order = 30)]
        public bool CtaOpenInNewTab { get; set; } = false;

        //Conditionally show selector vs text input for a link
        [RadioGroupComponent(
            Label = "Call-to-action target type",
            Options = "page;Page\nabsolute;Absolute URL",
            Order = 35)]
        public string CtaTargetType { get; set; } = "page";

        [VisibleIfEqualTo(nameof(CtaTargetType), "page", StringComparison.OrdinalIgnoreCase)]
        [ContentItemSelectorComponent(
            Cafe.CONTENT_TYPE_NAME,
            Label = "Select page",
            ExplanationText = "Select a page to link to.",
            Order = 40)]
        public IEnumerable<ContentItemReference> CtaTargetPage { get; set; } = new List<ContentItemReference>();

        [VisibleIfEqualTo(nameof(CtaTargetType), "absolute", StringComparison.OrdinalIgnoreCase)]
        [TextInputComponent(
            Label = "Absolute URL",
            ExplanationText = "Enter a full URL, including http:// or https://",
            Order = 50)]
        public string CtaTargetUrl { get; set; } = "";
    }
}
```

> **Tip:** You can set default values with simple property initialization, for example:
>
> ```csharp title=""
> public string CtaTargetUrl { get; set; } = "";
> ```

## Migrate inline properties

Some of your widgets in KX13 contain so-called [inline properties](https://docs.kentico.com/13/developing-websites/distributing-custom-builder-components.md#inline-property-editors), values which the editor can modify in Page Builder edit mode without visiting the widget configuration. A good example is the CTA button widget in the Dancing Goat sample site:

![Editing an inline property of CTA button in KX13](https://docs.kentico.com/docsassets/modules/advanced-property-techniques/cta-button-inline-prop-kx13.gif "Editing an inline property of CTA button in KX13")

Inline properties with out-of-the-box data types migrate automatically. In this example, the property is of type Text.

The inline property needs no special decoration in the properties file. Here's comparison of the CTA button widget properties file in KX13 and in XbyK:

```csharp title="CTAButtonWidgetProperties.cs - KX13 source instance" highlight="10"
using Kentico.Components.Web.Mvc.FormComponents;
using Kentico.Forms.Web.Mvc;
using Kentico.PageBuilder.Web.Mvc;

namespace DancingGoat.Widgets
{
    public class CTAButtonWidgetProperties : IWidgetProperties
    {
        // Inline property - button text
        public string Text { get; set; }

        [EditingComponent(UrlSelector.IDENTIFIER, Order = 1, Label = "Link URL")]
        [EditingComponentProperty(nameof(UrlSelectorProperties.Placeholder), "Please enter a URL or select a page...")]
        [EditingComponentProperty(nameof(UrlSelectorProperties.Tabs), ContentSelectorTabs.Page)]
        public string LinkUrl { get; set; }

        [EditingComponent(CheckBoxComponent.IDENTIFIER, Order = 2, Label = "Open in a new tab")]
        public bool OpenInNewTab { get; set; }
    }
}
```

```csharp title="CTAButtonWidgetProperties.cs - XbyK target instance" highlight="11"
using Kentico.Components.Web.Mvc.FormComponents;
using Kentico.Forms.Web.Mvc;
using Kentico.PageBuilder.Web.Mvc;
using Kentico.Xperience.Admin.Base.FormAnnotations;

namespace DancingGoat.Widgets
{
    public class CTAButtonWidgetProperties : IWidgetProperties
    {
        // Inline property - button text
        public string Text { get; set; }

        [TextInputComponent(
        Label = "Link URL",
        ExplanationText = "Please enter a URL or select a page, for example, \"https://your-doma.in/contact-us#form\"",
        Order = 10)]
        public string LinkUrl { get; set; }

        [CheckBoxComponent(
        Label = "Open in a new tab",
        ExplanationText = "If enabled, the link will open in a new tab.",
        Order = 20)]
        public bool OpenInNewTab { get; set; }
    }
}
```

However, for inline editing to work properly in your XbyK instance, you have to implement the text editor partial view (or port it from the KX13 instance).

For example, let's migrate the CTA button widget from the Dancing Goat sample site:

Copy the _\_TextEditor.cshtml_ view and _TextEditorViewModel.cs_ view model into your solution.

```cshtml title="_TextEditor.cshtml"
@model DancingGoat.InlineEditors.TextEditorViewModel

@using (Html.Kentico().BeginInlineEditor("text-editor", Model.PropertyName,
    new
    {
        @class = "text-editor",
        contenteditable = "true",
        data_placeholder_text = Model.PlaceholderText
    }))
{
    @Model.Text
}
```

```csharp title="TextEditorViewModel.cs"
namespace DancingGoat.InlineEditors
{
    /// <summary>
    /// View model for Text editor.
    /// </summary>
    public sealed class TextEditorViewModel : InlineEditorViewModel
    {
        /// <summary>
        /// Editor text.
        /// </summary>
        public string Text { get; set; }

        /// <summary>
        /// Placeholder text.
        /// </summary>
        public string PlaceholderText { get; set; } = "Type your text";
    }
}
```

Render the text editor as a partial view in your CTA button widget view when the Page Builder is in edit mode:

```cshtml title="CTAButtonWidget.cshtml" highlight="10-15"
@using DancingGoat.InlineEditors
@using DancingGoat.Widgets

@model ComponentViewModel<CTAButtonWidgetProperties>

<div class="clear center-text">
    @if (Context.Kentico().PageBuilder().GetMode() == PageBuilderMode.Edit)
    {
        <div class="btn btn-more">
            <partial name="~/Components/InlineEditors/TextEditor/_TextEditor.cshtml"
                     model="new TextEditorViewModel
                        {
                            PropertyName = nameof(CTAButtonWidgetProperties.Text),
                            Text = Model.Properties.Text,
                        }" />
        </div>
    }
    else
    {
        <a href="@(Model.Properties.LinkUrl ?? "#")" class="btn btn-more" @(Model.Properties.OpenInNewTab ? "target=_blank" : "")>
            @Model.Properties.Text
        </a>
    }
</div>
```

Now the CTA button widget in XbyK looks nearly identical to its KX13 counterpart:

🎬 [Editing an inline property of CTA button in XbyK](https://docs.kentico.com/docsassets/modules/advanced-property-techniques/cta-button-inline-prop-xbyk.webm)

What about inline properties using custom data types?

Currently, the Kentico Migration Tool doesn't support custom property migrations for inline properties. However, if your widget uses an inline property of a custom type, you can work with the JSON data in a [custom widget migration](https://github.com/Kentico/xperience-by-kentico-kentico-migration-tool/blob/master/docs/customization/Widget-Migrations.md#create-custom-widget-migrations).

## Improve editing experience

An upgrade provides an excellent opportunity to improve not only your content model, but also the robustness and editing experience of your Xperience solution.

Consider these tips when migrating your widget properties:

- Use a [dropdown provider](https://docs.kentico.com/guides/development/page-builder/map-enum-to-dropdown.md) to fill dropdowns with dynamic data instead of hardcoded values.
- Add meaningful explanation texts.
- Set meaningful default values.
- Reevaluate control types, for example, checkbox versus dropdown.
- Use [visibility conditions](https://docs.kentico.com/documentation/developers-and-admins/customization/extend-the-administration-interface/ui-form-components/ui-form-component-visibility-conditions.md).
