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:
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);
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.
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; }
}
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);
}
}
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")]
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>
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 β:
And a wide, centered design with a header β:
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; }
}
Then, we update our controller class to inherit from the generic version of WidgetController
:
public class CardWidgetController : WidgetController<CardWidgetProperties>
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);
}
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>
}
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:
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
:
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>
<!-- _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>
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; }
}
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);
}
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:
Or my Kentico blog series:
Top comments (0)