Compare Two Ways of Creating MVC Widgets
Kentico Xperience’s MVC support has come a long way since the old days. With the MVC widgets, content editors can have more fun assemble a page using components. As a solution architect, I get asked a lot about how to make decisions on creating widgets. In this article, I’m going to compare two ways of creating MVC widgets to get the same output. The results may be the same, but the decision makings are different.
What’s the plan?
Our design goal is to have some tiles that content editors can manage. Each tile contains 3 things, an image, a headline, and a teaser.
Decision making
Create a tile widget to represent individual tile would be a good way to go because it gives editor the flexibility to update, change position, and better for content personalization as well.
With that said, here are some questions that can help us make decision on how to implement the requirement.
- Should the content in the tiles be reusable or ad-hoc?
- Can all the data in the widget be configured directly or involve data from other objects?
Simple widget
If all the displayed data in the widget can be configured directly in the widget property and there is no need to reuse the data, then the simple widget can be the solution.
With this approach, all you need is have the developer write two files in the MVC application solution. One property class to define all the data needed for the tile and one partial view to render the output.
Here are my code examples for the Tile – Adhoc widget.
The property class contains the 3 properties needed for the widget, a URL selector for media library image, a text field for headline and a text area for teaser. It also contains the widget registration and location of the partial view.
[assembly: RegisterWidget("DG.Widgets.TileAdhocWidget", "Tile - Adhoc", typeof(TileAdhocWidgetProperties),
customViewName: "~/Components/Widgets/TileAdhocWidget/_TileAdhocWidget.cshtml", IconClass = "icon-rectangle-paragraph")]
namespace DancingGoat.Components.Widgets
{
public class TileAdhocWidgetProperties : IWidgetProperties
{
[EditingComponent(UrlSelector.IDENTIFIER, Label = "Tile Image", Order = 0)]
[EditingComponentProperty(nameof(UrlSelectorProperties.Tabs), ContentSelectorTabs.Media)]
[EditingComponentProperty(nameof(UrlSelectorProperties.MediaAllowedExtensions), ".gif;.png;.jpg;.jpeg")]
public string TileImage { get; set; }
[EditingComponent(TextInputComponent.IDENTIFIER, Label = "Tile Headline", Order = 1)]
public string TileHeadline { get; set; }
[EditingComponent(TextAreaComponent.IDENTIFIER, Label = "Tile Teaser", Order = 2)]
public string TileTeaser { get; set; }
}
}
The partial view that uses widget properties directly for display.
@using Kentico.PageBuilder.Web.Mvc
@model ComponentViewModel<DancingGoat.Components.Widgets.TileAdhocWidgetProperties>
<div class="article-tile article-tile-small">
<div class="article-tile-inner">
<img class="article-tile-image" src="@Model.Properties.TileImage" />
<div class="article-tile-info">
<div class="article-tile-date">@Model.Properties.TileHeadline</div>
<div class="article-tile-content">
<p class="article-tile-text">@Model.Properties.TileTeaser</p>
</div>
</div>
</div>
</div>
Here is how it’s used in page builder.
Widget using View Component
If the requirement is that the tile content should be reused multiple times across different pages, then the ad-hoc simple widget would not be the right choice because multiple copy of the data would be hard to keep up to date.
In this case, we are going to use a page type to define and represent each tile content, and build an inventory of tile content in the content tree. Then build a widget that allows editor to select the tile content from the inventory.
Because this approach require the widget to retrieve page content, we’ll need to use the view component approach.
First we need to define the page type and bring the code for the page type class into the MVC application model.
Don’t forget to create the tile content in the content tree.
As for the code, there are four piece to it. First, the properties class which we only need a page selector component.
public class TileContentWidgetProperties : IWidgetProperties
{
[EditingComponent(PageSelector.IDENTIFIER, Label = "Tile Content", Order = 0)]
[EditingComponentProperty(nameof(PageSelectorProperties.RootPath), "/Tiles")]
[Required]
public IEnumerable<PageSelectorItem> SelectedTile { get; set; } = new List<PageSelectorItem>();
}
Then the view model to define what the view component would return to the view.
public class TileContentWidgetViewModel
{
public string TileImage { get; set; }
public string TileHeadline { get; set; }
public string TileTeaser { get; set; }
public static TileContentWidgetViewModel GetViewModel(TileContent tileContent)
{
return tileContent == null
? new TileContentWidgetViewModel()
: new TileContentWidgetViewModel
{
TileImage = tileContent.TileImage,
TileHeadline = tileContent.TileHeadline,
TileTeaser = tileContent.TileTeaser
};
}
}
The view component class which takes the page GUID been selected from the page selector and retrieve the data.
[assembly: RegisterWidget("DG.Widgets.TileContentWidget", typeof(TileContentWidgetViewComponent), "Tile - Content", typeof(TileContentWidgetProperties), IconClass = "icon-rectangle-paragraph")]
namespace DancingGoat.Components.Widgets
{
public class TileContentWidgetViewComponent : ViewComponent
{
private readonly IPageRetriever pageRetriever;
public TileContentWidgetViewComponent(IPageRetriever pageRetriever)
{
this.pageRetriever = pageRetriever;
}
public ViewViewComponentResult Invoke(TileContentWidgetProperties properties)
{
var selectedTileContent = properties.SelectedTile.FirstOrDefault();
var page = (selectedTileContent != null) ? pageRetriever.Retrieve<TileContent>(
query => query
.WhereEquals("NodeGUID", selectedTileContent.NodeGuid))
.FirstOrDefault() : null;
return View("~/Components/Widgets/TileContentWidget/_TileContentWidget.cshtml", TileContentWidgetViewModel.GetViewModel(page));
}
}
}
Finally the view which is pretty much the same as the simple widget view except the model is different.
@model DancingGoat.Components.Widgets.TileContentWidget.TileContentWidgetViewModel
@if (String.IsNullOrEmpty(Model.TileHeadline))
{
<h2>@HtmlLocalizer["No tile selected"]</h2>
}
else
{
<div class="article-tile article-tile-small">
<div class="article-tile-inner">
<img class="article-tile-image" src="@Model.TileImage" />
<div class="article-tile-info">
<div class="article-tile-date">@Model.TileHeadline</div>
<div class="article-tile-content">
<p class="article-tile-text">@Model.TileTeaser</p>
</div>
</div>
</div>
</div>
}
Here is how the widget is configured and displayed.
Conclusion
There are multiple ways to archive the same result using MVC widget. But based on business requirements and complexities you can build the widget the simple way or using view component for more complex object or reusable purposes. How do you make decisions on which way to build a widget? Feel free to share your experiences with us in the comments below or ask questions.