Kentico EMS: MVC Widget Experiments Part 1 - Dynamic Views

seangwright

Sean G. Wright

Posted on March 16, 2020

Kentico EMS: MVC Widget Experiments Part 1 - Dynamic Views
Photo by Hal Gatewood on Unsplash

Widget Experiments

This series dives into Kentico 12 MVC Widgets and the related technologies that are part of Kentico's Page Builder technology - Widget Sections, Widgets, Form Components, Inline Editors, and Dialogs 🧐.

Join me 👋, as we explore the nooks and crannies of Kentico EMS MVC Widgets and discover what might be possible with this powerful technology...

Goals

This post is going to cover how we can create a Widget that changes its Razor view file dynamically (based on configuration) and why this functionality might be useful to both developers and content managers.

Widgets and Views

The Kentico EMS documentation says a Widget can switch between views when building a Widget using a custom controller, but what does this mean 🤔?

Normally, when building a Kentico MVC Widget, we create a new Razor partial view file in the ~/Views/Shared/Widgets folder and name the file to match the name we use when registering the Widget:

[assembly: RegisterWidget(
    "CompanyName.CustomWidget", 
    typeof(CustomWidgetController), 
    "Custom widget")]
Enter fullscreen mode Exit fullscreen mode
From the Kentico docs

The Razor view for this CustomWidget would be created at ~/Views/Shared/Widgets/_CustomWidget.cshtml, and we would help MVC find this view by specifying a part of the path in the Widget's Controller Index() method:

return PartialView("Widgets/_CustomWidget", viewModel);
Enter fullscreen mode Exit fullscreen mode

This pattern relies somewhat on ASP.NET MVC's convention-over-configuration to find this file under ~/Views/Shared/Widgets/_CustomWidget, since we only specified the last part of the path 🤓.

Note that there is no strict requirement that the Widget Identifier (CompanyName.CustomWidget) and the view name _CustomWidget match when creating a Widget with a custom controller - only when creating a Basic Widget.

However, it's a good convention to follow when creating a Widget with a custom controller with a single view (more on that later 😏).

Example Widget: CardWidget

Let's create a "Card" Widget that we can use to explore the idea of dynamic Widget views.

Card widget displayed in a browser. A black border surrounds an evenly spaced title, content, and call-to-action link

This is what our initial Card Widget will look like with some Bootstrap 4 Card Component styles

First, lets create the view model class so we know what the controller needs to populate with data and what we need to render in the view:

public class CardWidgetViewModel
{
    public string Title { get; set; }
    public string Content { get; set; }
    public string LinkUrl { get; set; }
    public string CallToAction { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Then, we create the custom controller class, CardWidgetController.cs:

public class CardWidgetController : WidgetController
{
    public ActionResult Index()
    {
        // Imagine 🧠 that this content came from the Document 
        //  in the Content Tree instead of being hard-coded here!

        var viewModel = new CardWidgetViewModel
        {
            Title = "Search our products",
            Content = "We have many items for sale",
            LinkUrl = "/products",
            CallToAction = "Search Now!"
        };

        return PartialView("Widgets/_CardWidget", viewModel);
    }
}
Enter fullscreen mode Exit fullscreen mode

Next, we register the Widget so Kentico can expose it as an option in the Page Builder UI:

[assembly: RegisterWidget(
    "Sandbox.CardWidget", typeof(CardWidgetController), "Card")]
Enter fullscreen mode Exit fullscreen mode

Finally, we create a Razor view to render the view model contents:

@model CardWidgetViewModel

<div class="card" style="width: 18rem;">
  <div class="card-body">
    <h5 class="card-title">@Model.Title</h5>
    <p class="card-text">@Model.Content</p>
    <a href="@Model.LinkUrl" class="card-link">@Model.CallToAction</a>
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

Configurable Design

This Widget looks nice 👍 and allows content editors to add this card of content anywhere on the site that has Page Builder functionality enabled.

However, our view's design is static.

If a content manager wants to re-use this content but change the way it appears, the way the Widget is currently built won't help 😔.

Imagine 🧠 that there are 2 ways, in our site's design, that this content can be displayed.

There's the original design we had above ✔:

Original Card Widget UI rendered on a web page

And a wide, centered design with a header ✔:

New Card Widget UI with a separate gray header containing the title, centered text and full page width

We can allow content managers to choose a design for the content through Widget Properties, so let's add some.

With most Widgets, exposing some configuration, through Widget Properties, for content managers is a good idea.

First, we create a Widget Properties class:

public class CardWidgetProperties : IWidgetProperties
{
    [EditingComponent(
        RadioButtonsComponent.IDENTIFIER,
        DefaultValue = "Simple", Label = "Design")]
    [EditingComponentProperty(
        nameof(RadioButtonsProperties.DataSource),
        "Simple\r\nWide")]
    [Required]
    public string Design { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Then, we update our controller class to inherit from the generic version of WidgetController:

public class CardWidgetController : WidgetController<CardWidgetProperties>
Enter fullscreen mode Exit fullscreen mode

Next, we use the GetProperties() method of the controller to get the properties and pass the value configured for the Widget to our view model:

public ActionResult Index()
{
    CardWidgetProperties properties = GetProperties();

    var viewModel = new CardWidgetViewModel
    {
        Title = "Search our products",
        Content = "We have many items for sale",
        LinkUrl = "/products",
        CallToAction = "Search Now!",
        Design = properties.Design ?? "Simple"
    };

    return PartialView("Widgets/_CardWidget", viewModel);
}
Enter fullscreen mode Exit fullscreen mode

Finally, we update our Razor view to render different HTML depending on the value of the CardWidgetViewModel.Design property:

@model CardWidgetViewModel

@if (Model.Design == "Simple")
{
    <div class="card" style="width: 18rem;">
        <div class="card-body">
            <h5 class="card-title">@Model.Title</h5>
            <p class="card-text">@Model.Content</p>
            <a href="@Model.LinkUrl" class="card-link">@Model.CallToAction</a>
        </div>
    </div>
}
else if (Model.Design == "Wide")
{
    <div class="card text-center">
        <div class="card-header">
            @Model.Title
        </div>
        <div class="card-body">
            <p class="card-text">@Model.Content</p>
            <a href="@Model.LinkUrl" class="card-link">@Model.CallToAction</a>
        </div>
    </div>
}
else if (Context.Kentico().PageBuilder().EditMode)
{
    <h3>The selected "Design" (@Model.Design) of this widget
        is not supported.</h3>
}
Enter fullscreen mode Exit fullscreen mode

What we end up with is a dialog in the Page Builder UI that allows us to toggle which of the two layouts we want to use:

EMS Page Builder UI dialog with radio button options

Awesome! We did it 👏! Our Widget displays content with a nice design and can be configured to use 2 different layouts...

Except now we've been asked to support another 4 layouts, all meant for different areas of a page and parts of our site... 😱

At some point the @if(...) { } and elseif(...) { } blocks are going to become complex, hard to read, and will distract from the HTML - not that different from a giant C# class with many if/else conditionals.

How can we resolve this 🤔?

Dynamic Views

Instead of varying the markup in a single view, based on the value of the CardWidgetViewModel.Design property, we can choose to render different Razor views based on how the Widget properties have been configured 🤯!

We're going to work backwards, since we know our goal is simplify the Razor view files.

First, we create a new folder ~/Views/Shared/Widgets/Card/ and move our _CardWidget.cshtml file into this folder.

We will also copy that file and rename both so we end up with _Simple.cshtml and _Wide.cshtml:

Visual Studio solution tree showing two new files under the paths ~/Views/Shared/Widgets/Card/_Simple.cshtml and ~/Views/Shared/Widgets/Card/_Wide.cshtml

Both view files will only contain the markup needed for design it represents, and both still use the same view model class, CardWidgetViewModel:

<!-- _Simple.cshtml -->

@model CardWidgetViewModel

<div class="card" style="width: 18rem;">
    <div class="card-body">
        <h5 class="card-title">@Model.Title</h5>
        <p class="card-text">@Model.Content</p>
        <a href="@Model.LinkUrl" class="card-link">@Model.CallToAction</a>
    </div>
</div>
Enter fullscreen mode Exit fullscreen mode
<!-- _Wide.cshtml -->

@model CardWidgetViewModel

<div class="card text-center">
    <div class="card-header">
        @Model.Title
    </div>
    <div class="card-body">
        <p class="card-text">@Model.Content</p>
        <a href="@Model.LinkUrl" class="card-link">@Model.CallToAction</a>
    </div>
</div>
Enter fullscreen mode Exit fullscreen mode

Next, we can remove the Design property from the CardWidgetViewModel class:

public class CardWidgetViewModel
{
    public string Title { get; set; }
    public string Content { get; set; }
    public string LinkUrl { get; set; }
    public string CallToAction { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Finally, we will update our Widget controller class to pick the correct view based on the selected Widget Properties value:

public ActionResult Index()
{
    var properties = GetProperties();

    var viewModel = new CardWidgetViewModel
    {
        Title = "Search our products",
        Content = "We have many items for sale",
        LinkUrl = "/products",
        CallToAction = "Search Now!",
    };

    return PartialView($"Widgets/Card/_{properties.Design}", viewModel);
}
Enter fullscreen mode Exit fullscreen mode

Now, our Widget will function exactly the same, but we have much more maintainable Razor view files 💪🏾!

This will be especially important if the overall layout isn't the only thing that needs to be customizable about the Widget.

For example, we could expose Widget Properties for border color, text color, call-to-action design, ect...

Each additional configuration adds complexity to the views because markup will need to be rendered conditionally.

However, by separating our views into different high-level layout files, we help to decrease the growing complexity 🤗.

Conclusion

MVC Widgets with configurable designs are appealing to developers and content managers since they make content reusable and reduce repetitive HTML.

Dynamic views are an easy way to make these kinds of Widgets more maintainable for developers since they abstract different layouts into separate files.

They can help scale and manage the complexity of MVC Widgets in our applications.

This mirrors how we make our C# code more maintainable by breaking larger classes and methods into smaller ones 🧐.

As always, thanks for reading 🙏!


We've put together a list over on Kentico's GitHub account of developer resources. Go check it out!

If you are looking for additional Kentico content, checkout the Kentico tag here on DEV:

#kentico

Or my Kentico blog series:

💖 💪 🙅 🚩
seangwright
Sean G. Wright

Posted on March 16, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related