Module: Migrate widgets and custom code

7 of 9 Pages

Refine properties to improve editor experience

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

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

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

C#
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";
}
C#
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; } = "";
    }
}
You can set default values with simple property initialization, for example:
C#

public string CtaTargetUrl { get; set; } = "";

Migrate inline properties

Some of your widgets in KX13 contain so-called inline properties, 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

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:

C#
CTAButtonWidgetProperties.cs - KX13 source instance

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; }
    }
}
C#
CTAButtonWidgetProperties.cs - XbyK target instance

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
_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
}
C#
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
CTAButtonWidget.cshtml

@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

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.

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