This post was written for Xperience by Kentico v30.1.0. Please consult the documentation for any feature or API changes when using a different version. Be sure to check the System Requirements in the product documentation.

Xperience by Kentico's powerful multichannel content management capabilities are used to help marketing teams craft messages and deliver them through web, email, or headlessly driven experiences. Those messages can have a big impact for a business when they drive customer engagement.

A key goal of marketing is delivering the right message, to the right customer, at the right time because this increases the chance the customer will engage.

  • You sign up for something like the Kentico Community Newsletter because it gives you information about the Kentico community, which you are interested in - "right message".
  • You only the receive the newsletter if you double opt-in - "right customer".
  • By the very nature of email as a channel you aren't interrupted by an email when you are focused on something else, like sleeping - "right time".

Marketers use the tool of personalization to help accomplish this trifecta of customer experience.

Let's look at a real example of using Xperience's Page Builder widget personalization along with the Form Builder to encourage visitors to the Kentico Community Portal to join as members and engage.

The goal: community contributions

We should always start with a goal, not a tool or a feature.

My goal is for members of the Kentico community to share their Kentico-related contributions and activities so we can promote them, increasing their impact. I also want community members to create member accounts on the Kentico Community Portal so they ask questions in our Q&A discussions and show off their profile.

I want visitors to register before they share activities so they get credit in their member profile and because I think they'll benefit from participating in Q&A discussions about topics that interest them.

The contribution form on the Kentico Community Portal, covered by an overlay requesting registration.

Above is a screenshot of the personalized outcome - a form with a message requesting me to log in or register and an overlay preventing me from accessing the form.

View of the contribution form on the Kentico Community Form for an authenticated member with no overlay or restricted access to the form.

Once I log in I can see the whole form and share my contribution!

Additionally, the form was designed for authenticated members so I don't need to supply my email address or any other information - Xperience already knows who I am (more on that later).

Building conditional form visibility

Extended form widget

To restrict access to the form for unauthenticated visitors, we create an extended Form widget. Extending widgets keeps all of the original widget's functionality while also allowing you to add behavior.

I'm calling this one a "Fallback form" because it has fallback content which is displayed when the form is hidden.

[assembly: RegisterWidget(
    FallbackFormWidget.IDENTIFIER,
    FallbackFormWidget.WIDGET_NAME,
    typeof(FallbackFormWidgetProperties),
    customViewName: "/Components/Widgets/FallbackForm/FallbackForm.cshtml",
    IconClass = KenticoIcons.FORM)]

namespace Kentico.Community.Portal.Web.Components.Widgets.FallbackForm;

public class FallbackFormWidget
{
    public const string IDENTIFIER = "CommunityPortal.FallbackFormWidget";
    public const string WIDGET_NAME = "Fallback form";

    public static bool GetIsFormHidden<T>(ViewDataDictionary<T> viewData) =>
        viewData.TryGetValue(
          $"{IDENTIFIER}_IS_HIDDEN", 
          out object? isHiddenObj) 
      && isHiddenObj is bool isHidden 
      && isHidden;

    /// <summary>
    /// Stores the state of the <see cref="FallbackFormWidget"/> 
    ///  in <see cref="ViewDataDictionary{TModel}"/>
    /// for child widgets and form components
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="viewData"></param>
    /// <param name="props"></param>
    public static void SetIsFormHidden<T>(
      ViewDataDictionary<T> viewData, 
      FallbackFormWidgetProperties props) =>
        viewData.Add($"{IDENTIFIER}_IS_HIDDEN", props.IsHidden);
}

public class FallbackFormWidgetProperties : FormWidgetProperties
{
    [CheckBoxComponent(
        Label = "Is hidden?",
        Order = 0,
        ExplanationText = 
          "If true, the specified fallback text value will be displayed."
    )]
    public bool IsHidden { get; set; }

    [MarkdownComponent(
        Label = "Fallback content",
        Order = 1,
        ExplanationText = """
          If specified, this will be displayed instead of a form. 
          If no value is provided, nothing will be displayed.
          """
    )]
    [VisibleIfTrue(nameof(IsHidden))]
    public string FallbackMarkdown { get; set; } = "";
}

The widget's Razor view needs to conditionally render the nested widget it extends to enable to visible/hidden form feature. We have access to our extended widget's properties and the original widget's properties which we pass to Html.Kentico().RenderNestedWidgetAsync.

@model ComponentViewModel<FallbackFormWidgetProperties>

@{
    bool isPreview = Context.Kentico().Preview().Enabled;
    bool hasFallbackContent = !string.IsNullOrWhiteSpace(Model.Properties.FallbackMarkdown);

    if (Model.Properties.IsHidden && !isPreview && !hasFallbackContent)
    {
        return;
    }

    FallbackFormWidget.SetIsFormHidden(ViewData, Model.Properties);
}

<div class="fallback-form-widget" 
  @(Model.Properties.IsHidden ? "overlay" : "")>

    @await Html.Kentico().RenderNestedWidgetAsync(
      SystemComponentIdentifiers.FORM_WIDGET_IDENTIFIER,
      Model.Properties)

    @if (Model.Properties.IsHidden)
    {
        <div class="fallback-content">
            @if (isPreview)
            {
                <div>
                    <small>This form will be hidden on the live website.</small>
                    @if (hasFallbackContent)
                    {
                        <br>
                        <small>The following fallback content will be displayed instead.</small>
                    }
                </div>
            }
            @if (hasFallbackContent)
            {
                <div>
                    @Renderer.RenderUnsafe(Model.Properties.FallbackMarkdown)
                </div>
            }
        </div>
    }
</div>

We handle a couple of scenarios in the view.

  1. The widget is hidden, we're not in preview mode, and we don't have fallback content - return and don't display the form.
  2. The widget is hidden, we're not in preview mode, and we do have fallback content - display the form, display the overlay and fallback message, and store the "hidden" state in ViewData.
  3. The widget is hidden and we're in preview mode - display the message "This form will be hidden on the live website" to the marketer.
  4. The widget is not hidden - display the form.

Is Authenticated personalization condition type

Personalization, at its core, is the act of changing customer experiences based on some context or information you know about them. In ASP.NET Core this is as easy as writing @if (SomeCondition) { ... } else { ... } in your Razor view.

But our goal is to enable marketers to craft experiences without requiring developers to make changes. Page Builder personalization in Xperience by Kentico only requires at least one personalization condition type.

Our personalization condition in this scenario is authentication - is the visitor a registered and logged in member?

[assembly: RegisterPersonalizationConditionType(
    identifier: IsAuthenticatedConditionType.IDENTIFIER,
    type: typeof(IsAuthenticatedConditionType),
    name: "Is authenticated",
    Description =
      "Evaluates based on the visitor's authentication status.",
    IconClass = "icon-app-membership",
    Hint = 
      "Display personalized experiences to visitors who are authenticated")]

namespace Kentico.PageBuilder.Web.Mvc.Personalization;

public class IsAuthenticatedConditionType(
    IHttpContextAccessor contextAccessor) : ConditionType
{
    public const string IDENTIFIER = 
        "Kentico.Community.Portal.Personalization.IsAuthenticated";

    private readonly IHttpContextAccessor contextAccessor = contextAccessor;

    public override bool Evaluate()
    {
        var context = contextAccessor.HttpContext;
        if (context is null)
        {
            return false;
        }

        var identity = context.User.Identity;

        return identity is not null && identity.IsAuthenticated;
    }
}

ASP.NET Core makes this extremely easy - we don't need to retrieve anything from the database or know anything else about the visitor or request. We check the HttpContext.User.Identity and we're done.

Displaying a fallback message

This next section describes some customizations of Xperience by Kentico that are easy to implement and work as expected but they are not officially supported - you can't send a support request and ask for help but you can ask questions in the discussion for this blog post!

ASP.NET Core MVC allows developers to override Razor views packaged in a library by creating a Razor file in their application with the same name and path as the one in the library - this includes Xperience by Kentico.

To customize the built-in Form widget's form rendering behavior we can create a new Razor file at ~/Views/Shared/Kentico/Widgets/FormWidget/_FormWidgetForm.cshtml and add the following code to it.

@model Kentico.Forms.Web.Mvc.Widgets.FormWidgetViewModel

@{
    if (Model.FormConfiguration == null)
    {
        <h3 class="no-form-selected">
          @ResHelper.GetString("kentico.formbuilder.widget.noformselected")
        </h3>
    }
}

@* We replace the <form> with a <div> 
  to prevent spam submissions of the programmatically functional form *@
@if (Model.FormConfiguration is not null && FallbackFormWidget.GetIsFormHidden(ViewData))
{
    <div class="fallback-form">
        @{
            var markup = await Html.Kentico().FormFieldsAsync(Model);
            @markup

            var button = Html.Kentico().FormSubmitButton(Model);
            @button
        }
    </div>

    return;
}

@using (Html.Kentico().BeginForm(Model))
{
    var markup = await Html.Kentico().FormFieldsAsync(Model);
    @markup

    var button = Html.Kentico().FormSubmitButton(Model);
    @button
}

Here we replace the <form> that Xperience would normally render with a <div> when the FallbackFormWidget has set the "hidden" state to true in the ViewData.

This customization is necessary when our fallback content is displayed over the form. Bots will find the form and submit it with spam unless we have a captcha, but captchas don't really work that well and we definitely don't want to display one to authenticated members because everyone hates captchas.

Instead we wrap all the render all the form elements with a <div> to make sure there's nothing to submit.

If you completely hide the form from unauthenticated visitors you can skip this step entirely and render the fallback content by itself. I kinda like the look of the form with the overlay though 😅.

Page Builder personalization

We now have the technical building blocks in place and we can let the marketer do their thing!

The Page Builder of the Kentico Community Portal website showing the widget properties of the Fallback form.

We add a Fallback form widget to the page. This is the original variant of the widget and it has the Is hidden option checked and some fallback content along with all the other required properties from the built-in Form widget.

This variant is the default displayed to all visitors that don't meet the requirements (being authenticated) of our personalization condition.

Xperience's Page Builder showing a widget personalization variants dialog - the original variant and a selected one for authenticated visitors.

Next, we use the Is authenticated personalization condition type to create a personalized variant of the widget which shows the form to authenticated members.

This is achieved by unchecking the Is hidden property and leaving the rest of the properties as is.

If we publish the page, the Page Builder will revert to the original "hidden" variant of the widget but we can switch the view by selecting each of the variants.

Where can we go from here?

Visiting the website and navigating to the form shows us the different personalized experiences based on our visitor context. Log in and you'll see something different than if you're logged out.

You can use this technique with any widget - including the built-in Rich text and Form widgets. You can make as many personalization condition types as you want and they can be based on anything Xperience or ASP.NET Core has access to - contact data, segmentation with contact groups or some custom business logic.

You can display a different message, or a completely different experience, or remove a widget from a page using personalization based on rules as simple as "is this visitor a member?"

By putting the decision making into the hands of marketers you empower them to be creative and focus on customer experience while you, as a developer, can prioritize things like deployment automation and system integrations.

But, remember, start with the marketing goal and then look for tools that help you achieve it.

Wrap up

You might have noticed I didn't explain how we capture the member's information with the form since it only has fields for a contribution URL and description.

I'll cover that in a follow up blog post which requires more unsupported customization of Xperience but enables some interesting scenarios for marketers.

In the mean time you can find all the source code for the Kentico Community Portal on GitHub, including the Fallback form widget.