Developing widgets

Page builder widgets are reusable components that can be easily manipulated by content editors and other non-technical users. Widgets and the page builder feature give non-technical users more power and flexibility when adjusting page content, in addition to basic editing of text and images. By working with widgets, users can decide which components are placed on pages and where. For the list of widgets available in the system by default, check the system widget reference.

For inspiration, explore the implementation of sample widgets on the Dancing Goat site.

On this page, you can find information about:

Implementing widgets

On a basic level, widgets are pieces of HTML output code, which are then placed within a suitable location in the page structure. The main step in the development of a widget is to create a partial view that defines the output.

Within the MVC architecture, the widget partial view is served by a controller and a model is used to pass any required data. In many cases, widgets can utilize a default controller and view model provided by the Xperience API. See the following sub-sections for more information:

In both cases you can develop widgets with properties, which allow content editors to adjust the widget content or behavior directly in the administration interface. For widgets with configurable properties, you need to create an additional model class that represents the widget properties. See Defining widget properties to learn more.

MVC Areas

Widgets are designed to be used in the global scope and their code files must be placed in the application root of your MVC project (not in an MVC Area). Creating widgets in MVC Areas may lead to unexpected behavior.

On a basic level, widgets are pieces of HTML output placed within a suitable location in the page structure. You can develop two types of widgets:

In both cases the widgets can contain configurable properties, which allow content editors to adjust the widget content or behavior directly in the administration interface. For widgets with properties, you need to create an additional model class that holds the properties data. See Defining widget properties to learn more.

Areas

Widgets are designed to be used in the global scope and their code files must be placed in the application root of your Core project (not in an Area). Creating widgets in Areas may lead to unexpected behavior.

Example of widget development

For a step-by-step guide showcasing the development of a simple widget, see Example - Developing a widget.


Basic widgets

Use the following process to develop a widget:

  1. Prepare a partial view that defines the output of the widget according to general MVC best practices.
    • We recommend storing widget views in the ~/Views/Shared/Widgets folder, and using a view name that matches the identifier assigned to the widget upon its registration prefixed with the underscore ('_') character. Replace any period characters ('.') in the identifier with underscores ('_') in your view name.
    • Alternatively, you can use any required view location or name, and then specify it when registering the widget.

    Accessing the widget's page

    If you need to work with the data of the page containing the currently processed widget, use the ComponentViewModel class (Kentico.PageBuilder.Web.Mvc namespace) as the view's model and access its Page property. The property returns a TreeNode object representing the given page. If you need to load values from the fields of a specific page type, you can convert the TreeNode object to an instance of a specific page type wrapper class (the page containing the widget must then be of the given page type).

  2. Register the widget into the system. See Registering widgets.

With this approach, the widget's view is automatically displayed using a default controller provided by the Xperience API. The values of any properties defined for the widget can be passed to the view by using the default ComponentViewModel<TPropertyModel> class as the model.

Example of widget development

To see a scenario with full code samples which will guide you through the process of developing a simple widget, visit Example - Developing a widget.

Use the following process to develop a widget:

  1. Prepare a partial view that defines the output of the widget according to general MVC best practices.
    • We recommend storing widget views in the ~/Components/Widgets/<WidgetName> folder and using a view name that matches the identifier assigned to the widget upon its registration prefixed with the underscore ('_') character.
    • Alternatively, you can use any required view location or name, and then specify it when registering the widget.

    Accessing the widget's page

    If you need to work with the data of the page containing the currently processed widget, use the ComponentViewModel class as the view's model and access its Page property. The property returns a TreeNode object representing the given page. If you need to load values from the fields of a specific page type, you can convert the TreeNode object to an instance of a specific page type wrapper class (the page containing the widget must then be of the given page type).

  2. Register the widget into the system. See Registering widgets.

When added via the page builder, the widget's view is automatically displayed using logic provided by the Xperience API. The values of any properties defined for the widget can be accessed by using the ComponentViewModel<TPropertyModel> class as the model.

@using Kentico.PageBuilder.Web.Mvc
 
@model ComponentViewModel<CustomWidgetProperties>

Widgets with a custom controller

When developing widgets with advanced functionality, you may need to take full control over the widget's logic. You can do this by implementing the widget's controller and view model, in addition to the partial view. This allows you to run any custom code within the widget's controller, pass any type of required data to the view, or even switch between completely different views based on the current scenario.

The following steps describe the advanced development process for widgets:

  1. Create a controller class for the widget.
    • We recommend storing widget controllers in the ~/Controllers/Widgets folder.
  2. Make the controller inherit from the WidgetController class (available in the Kentico.PageBuilder.Web.Mvc namespace).
  3. Implement the default Index action in the controller, which is used to retrieve the widget markup. The action must return the widget's HTML content, typically a partial view. The implementation of the action depends on the purpose of the widget.

    Notes

    • Do not disable POST requests for the Index action (e.g., by using the HttpGet attribute). POST requests to the Index action are used by the page builder feature.
    • Widget controller actions used to retrieve the markup cannot be asynchronous (cannot use the async function declaration). Actions that render widget markup are called as child actions when rendering the markup of an editable area, but MVC 5 does not support asynchronous child controller actions.
  4. Create any required view model classes used to pass data from the widget controller to the partial view according to the general MVC best practices.
    • We recommend storing widget models in the ~/Models/Widgets/<widget name> folder.
    • For widgets with configurable properties, do not directly pass the property model to your widget views. Passing data to views is the responsibility of the widget's view model, and we strongly recommend keeping the models separate.
  5. Prepare a partial view that defines the output of the widget.
    • We recommend storing widget views in the ~/Views/Shared/Widgets folder.
  6. Register the widget into the system. See Registering widgets.

With this advanced development approach, you have full responsibility and control over the widget's controller, view model, and partial view.

Accessing the data of the current page

If you need to access fields or other data of the page containing the currently processed widget, obtain an instance of the IPageDataContextRetriever service (we recommend using dependency injection) and call its Retrieve<TPageType> method. Specify either a TreeNode object or a page type wrapper class as the generic parameter. The method returns an IPageDataContext<TPageType> object with the following properties:

// Contains an instance of the IPageDataContextRetriever service (e.g., obtained via dependency injection)
private readonly IPageDataContextRetriever pageDataContext;

// Gets the page of the Article page type where the currently processed widget is placed
var article = pageDataContext.Retrieve<Article>().Page;

Accessing the data of the current page in POST actions

Fields of the current page are not by default accessible in controller actions that handle POST requests. Such requests do not contain sufficient information to identify the page from which they originate. 

To access page data in POST actions, you need to include information about the current page into the data submitted by the corresponding form in the widget's output – call the Html.Kentico().PageData extension method within the given form tag in your widget view.

Example
@using System.Web.Mvc.Ajax
@using Kentico.Web.Mvc
@using Kentico.PageBuilder.Web.Mvc

...

@using (Ajax.BeginForm("PostAction", "CustomWidget", null, new AjaxOptions
{
    HttpMethod = "POST",
    UpdateTargetId = "widgetForm"
}, new { id = "widgetForm" }))
{
    @Html.Kentico().AntiForgeryToken()
    @Html.Kentico().PageData()
	...
    <input type="submit" value="Submit" />
}

The method renders a hidden form field that persists information about the current page. The page data can be retrieved via IPageDataContextRetriever.Retrieve<TPageType> in the corresponding POST action.

Widgets based on a view component

The basic implementation of a widget consists of only a partial view (and possibly a properties class). You may need additional logic, for example if you need to:

  • react to the configuration of the widget's properties
  • perform interactions based on the page where the widget is rendered
  • execute general business logic not suitable for views (e.g., database operations)

For this purpose, you can develop widgets based on ASP.NET Core view components. Such widgets consist of a view component class and the corresponding partial view it renders.

Implementing a widget based on a view component follows this general process:

  1. Create a view component for the widget.
    • We recommend storing view component files in the ~/Components/Widgets/<WidgetName> directory together with other files required by the widget. For reusable code shared across components, you can create a ~/Components/Shared directory.
  2. Implement the component's Invoke or InvokeAsync method (the synchronous and asynchronous approaches are both supported, this choice depends solely on your requirements). 
    • The system invokes the view component when the widget is inserted via the page builder. As part of the rendering process, the system passes the widget's properties (the ComponentViewModel class) into the component's Invoke method. For this reason, the method's signature needs to expect the ComponentViewModel parameter. For widgets with custom properties, you also need to specify the properties class as the generic parameter:

      // The signature of a view component's InvokeAsync method for widgets without custom properties
      public async Task<IViewComponentResult> InvokeAsync(ComponentViewModel widgetProperties)
      
      // The signature of a view component's InvokeAsync method for widgets with custom properties
      public async Task<IViewComponentResult> InvokeAsync(ComponentViewModel<TWidgetPropertiesClass> widgetProperties)
  3. The return statement of the component's Invoke method, needs to specify the full relative path to the component's partial view. For example:

    return View("~/Components/Widgets/MyWidget/_MyWidget.cshtml", model);
  4. Create any required view model classes used to pass data from the widget view component to the partial view according to general ASP.NET Core MVC best practices.
    • We recommend storing widget models in the ~/Components/Widgets/<WidgetName> folder together with other files required by the widget.
    • For widgets with configurable properties, do not directly pass the property model to your widget views. Passing data to views is the responsibility of the widget's view model, and we strongly recommend keeping the models separate.
  5. Prepare a partial view that defines the output of the widget.
    • We recommend storing widget views in the ~/Components/Widgets/<WidgetName> folder together with other files used by the widget.
  6. Register the widget in the system.

When the widget is inserted using the page builder, the system invokes the corresponding view component and renders its partial view. Using this approach, you can decouple business and view-layer code, maintaining separation of concerns.

Accessing the data of the current page

If you need to access fields of the page where the widget is currently rendered, use the Page property of the ComponentViewModel class (from the component's Invoke method).

// Retrieves the name of the page on which the widget is currently rendered
string name = widgetProperties.Page.DocumentName;

Handling POST actions

If your widget needs to communicate with the server using POST actions, create a custom controller class containing the required action methods and logic. We recommend storing the class in the ~/Components/Widgets/<WidgetName> folder together with other files required by the widget.

Note that the fields of the page the widget is currently rendered on are not by default accessible from within POST actions. Common POST requests do not contain sufficient information to identify the page from which they originate. 

To access the page data in POST actions, you need to include information about the current page into the data submitted by the corresponding form in the widget's output by calling the Html.Kentico().PageData extension method (or the method's Tag Helper alternative) within the given form tag in your widget view.

@using Kentico.Web.Mvc
@using Kentico.PageBuilder.Web.Mvc

<form asp-controller="WidgetPostActionsController" asp-action="HandlePost" method="post">

    ...

    @Html.Kentico().PageData()

    <input type="submit" value="Submit" />
</form>

The method renders a hidden form field that persists information about the current page. The page data can be retrieved via the IPageDataContextRetriever service in the corresponding controller action. 

Obtain an instance of the IPageDataContextRetriever service (we recommend using dependency injection) and call its Retrieve<TPageType> method. Specify either a TreeNode object or a page type wrapper class as the generic parameter. The method returns an IPageDataContext<TPageType> object that contains the current page object in its Page property. You can also access the metadata and evaluate the permissions and authentication requirements of the page via the object's Metadata and Security properties.

// Contains an instance of the IPageDataContextRetriever service (e.g., obtained via dependency injection)
private readonly IPageDataContextRetriever pageDataContext;

// Gets the page of the Article page type where the widget is placed
var article = pageDataContext.Retrieve<Article>().Page;

Registering widgets

Every widget needs to be registered into the system to be available in the page builder. Register widgets using the RegisterWidget assembly attribute (available in the Kentico.PageBuilder.Web.Mvc namespace).

To register basic widgets (without a custom controller class), we recommend adding the assembly attributes to a dedicated code file. For example, you can create a file named PageBuilderComponentRegister.cs in your project's ~/App_Start folder and use it to register your page builder components. For basic widgets, specify the following attribute parameters:

  • Identifier – the unique identifier of the widget. We recommend using a unique prefix in your widget identifiers to prevent conflicts when deploying widgets to other projects, for example matching your company's name.
  • Name – the name used to identify the widget when displayed in the administration interface.
  • PropertiesType – only required for widgets with properties. Specifies the System.Type of the widget's property model class.
  • (Optional) CustomViewName – specifies the name and location of the view that defines the widget's output. If not set, the system searches for a corresponding _<Identifier>.cshtml view in the ~/Views/Shared/Widgets folder (any period characters '.' in the identifier are replaced by underscores '_').

    Basic widget registration example
    [assembly: RegisterWidget("CompanyName.CustomWidget", "Custom widget", typeof(CustomWidgetProperties), "Widgets/_CustomWidgetView")]

For widgets with a custom controller, you can add the assembly attribute directly into the controller code file (above the controller class). In this case, specify the following attribute parameters:

  • Identifier – the unique identifier of the widget. We recommend using a unique prefix in your widget identifiers to prevent conflicts when deploying widgets to other projects, for example matching your company's name.
  • ControllerType – the System.Type of the widget's controller class.
  • Name – the name used to identify the widget when displayed in the administration interface.

    Controller widget registration example
    [assembly: RegisterWidget("CompanyName.CustomWidget", typeof(CustomWidgetController), "Custom widget")]

When registering any type of widget, you can also set the following optional attribute properties:

  • Description – the description of the widget displayed as a tooltip.
  • IconClass – the font icon class displayed when viewing the widgets in the widget list.
[assembly: RegisterWidget("CompanyName.CustomWidget", typeof(CustomWidgetController), "Custom widget", Description = "Widget displaying a custom message.", IconClass = "icon-one")]

Every widget needs to be registered into the system to be available in the page builder. Register widgets using the RegisterWidget assembly attribute (from the Kentico.PageBuilder.Web.Mvc namespace).

For basic widgets that do not have any additional files, we recommend adding their registration attributes to a dedicated code file. This keeps your registrations organized. For example, you can create a file named ComponentRegister.cs in your project's ~/Components folder and use it to register your page builder components. 

When registering basic widgets, specify the following parameters:

  • Identifier – the unique identifier of the widget. We recommend using a unique prefix in your widget identifiers to prevent conflicts when deploying widgets to other projects, for example matching your company's name.
  • Name – the name used to identify the widget when displayed in the administration interface.
  • (Optional) PropertiesType – only required for widgets with properties. Specifies the System.Type of the widget's property model class.
  • CustomViewName – specifies the name and location of the view that defines the widget's output. If not set, the system searches for a corresponding _<Identifier>.cshtml view in the ~/Views/Shared/Widgets folder (any period characters '.' in the identifier are replaced by underscores '_').

    Basic widget registration example
    [assembly: RegisterWidget("CompanyName.CustomWidget", "Custom widget", typeof(CustomWidgetProperties), "~/Components/Widgets/<WidgetName>/_CustomWidgetView")]

For widgets based on view components, you can add the assembly attribute directly into the component code file. In this case, specify the following attribute parameters:

  • Identifier – the unique identifier of the widget. We recommend using a unique prefix in your widget identifiers to prevent conflicts when deploying widgets to other projects, for example matching your company's name.
  • ViewComponentType – the System.Type of the widget's view component class.
  • Name – the name used to identify the widget when displayed in the administration interface.

    View component widget registration example
    [assembly: RegisterWidget("CompanyName.CustomWidget", typeof(CustomWidgetViewComponent), "Custom widget")]

For both types of widgets you can also set the following optional properties:

  • AllowCache – a boolean value indicating whether the output of the widget can be cached.
  • Description – the description of the widget displayed as a tooltip.
  • IconClass – the font icon class displayed when viewing the widgets in the widget list.

Localizing widget metadata

To allow content editors to experience the page builder in their preferred UI culture, you can localize the Name and Description values of widgets.

Storing files for use in the page builder

You may wish to develop widgets that use or display various files. You can store such files in the system either as page attachments or using media libraries. These two approaches vary in different ways:

  • Page attachment files are associated only with a specific page. You should store media files as page attachments only if the files are considered to be a content of a particular page (e.g. a language specific variant of an image).
  • Media library files are available for all pages of a particular site. You should store files using media libraries when the file content is reusable or the files are more relevant to the general visual design of the page than to its content (e.g. a background image used on all pages with a similar layout).

This is important to remember especially when creating custom page templates from existing pages. New pages created using templates do not display any page attachments, because the attachments stay as content bound to the original pages. On the other hand, pages based on templates display media library files automatically.

Adding scripts and styles for widgets

To add JavaScript and CSS styles required by your page builder widgets, place script and stylesheet files into sub-folders under the ~/Content/Widgets directory of your MVC project (you may need to create the Widgets directory). Use sub-folders that match the identifiers of individual widgets, or a Shared sub-folder for assets used by mutliple widgets.

The system automatically creates bundles containing all .js and .css files located under ~/Content/Widgets. The bundles are then linked on all pages with page builder editable areas.

The same bundles also contain script and stylesheet files added for sections in the ~/Content/Sections directory and inline property editors in the ~/Content/InlineEditors directory (inline editor scripts and styles are only included in the administration bundles, which are linked when pages are displayed in Edit mode within the Pages application).

CSS notes

  • Only use the ~/Content/Widgets directory to add basic styles that are required for the widget to render correctly. Any site-specific styles that finalize the live site design of the widget should be handled separately within the given site's main stylesheet.
  • To avoid potential conflicts between styles from other third-party components, we recommend adding a unique prefix to your CSS classes and identifiers (for example #CompanyName-mid-button), or employ similar measures to ensure their uniqueness.
  • Do not make any assumptions about the relative order of the source CSS in the resulting bundles – individual stylesheet files contained in the bundle may or may not precede each other.

Initializing widget scripts

In many cases, you will need to initialize your scripts from the views of widgets (for example if you need to call a function on page load or register an event listener). For most types of page or element events, you can use HTML Event Attributes of elements in your views.

For scripts that you want to run on page load, you need to consider the following:

  • The bundles containing your main scripts are added at the end of the HTML document's body tag, so they are not available in the widget code during the page load process. A solution is to run the initialization script during the DOMContentLoaded event.
  • Widgets in the page builder interface may be added dynamically after the page is loaded. In this case, the DOMContentLoaded event has already occurred and will not fire again.

For example, the following script demonstrates how to reliably call a custom function on page load:

if (document.readyState === "loading") {
    // Calls the function during the 'DOMContentLoaded' event, after the HTML document has been completely loaded
    document.addEventListener("DOMContentLoaded", function () {
        customFunction();
    });
} else { 
    // Calls the function directly in cases where the widget is rendered dynamically after 'DOMContentLoaded' has occurred
    customFunction();
}

This approach ensures that the initialization script runs correctly when the widget is displayed on the live site, as well as in the page builder interface.

Note: Apart from initialization code, avoid linking or executing scripts directly within widget views – this could lead to duplicated scripts on pages that contain multiple instances of the same widget.

To add JavaScript and CSS styles required by your widgets, we recommend placing script and stylesheet files into sub-folders under:

  • ~/wwwroot/PageBuilder/Public/Widgets/<WidgetName> – scripts and styles intended for the live site
  • ~/wwwroot/PageBuilder/Admin/Widgets/<WidgetName> – scripts and styles intended for the administration interface (when working with the widget in the page builder editing interface). For example, inline editor registration scripts.

You can use sub-folders that match the identifiers of individual widgets, or a Shared sub-folder for assets used by multiple widgets. Note that this recommendation only applies when using the default configuration of the bundling support provided by Xperience and may be different for your project. See Bundling static assets of builder components.

CSS notes

  • Only use the specified directories to add basic styles that are required for the widget to render correctly. Any site-specific styles that finalize the live site design of the widget should be handled separately within the given site's main stylesheet.
  • To avoid potential conflicts between styles from other third-party components, we recommend adding a unique prefix to your CSS classes and identifiers (for example #CompanyName-mid-button), or use similar measures to ensure their uniqueness.

Initializing widget scripts

In many cases, you will need to initialize your scripts from the views of widgets (for example if you need to call a function on page load or register an event listener). For most types of page or element events, you can use HTML Event Attributes of elements in your views.

For scripts that you want to run on page load, you need to consider the following:

  • Your main scripts are added at the end of the HTML document's body tag, so they are not available in the widget code during the page load process. A solution is to run the initialization script during the DOMContentLoaded event.
  • Widgets in the page builder interface may be added dynamically after the page is loaded. In this case, the DOMContentLoaded event has already occurred and will not fire again.

For example, the following script demonstrates how to reliably call a custom function on page load:

    if (document.readyState === "loading") {
        // Calls the function during the 'DOMContentLoaded' event, after the HTML document has been completely loaded
        document.addEventListener("DOMContentLoaded", function () {
            customFunction();
        });
    } else {
        // Calls the function directly in cases where the widget is rendered dynamically after 'DOMContentLoaded' has occurred
        customFunction();
    }

This approach ensures that the initialization script runs correctly when the widget is displayed on the live site, as well as in the page builder interface.

Note: Apart from initialization code, avoid linking or executing scripts directly within widget views – this could lead to duplicated scripts on pages that contain multiple instances of the same widget.