Developing page builder widgets in ASP.NET Core

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

Explore implementations of example widgets on the Dancing Goat sample site.

This page describes how to create widgets as an ASP.NET Core site developer. You can find information about:

Implementing widgets

On a basic level, widgets are pieces of HTML output code, which are 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

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

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 ~/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 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 (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.

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

For both types of widgets you can also set the following optional 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.

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


Was this page helpful?