Global Components Builders
Everyone knows what a website looks like. Each has a recognizable template consisting of a header, content, and a footer at the bottom. These are so standard that even the HTML specification contains tags for them.
At first glance, there seems to be nothing complex about it, but of course, the devil is in the details. As always. When a developer must implement a header or footer, it suddenly turns out it can be complicated. Let's take a header as an example. It can contain a logo, a search bar, a favorites icon, a quick panel for managing an account, a drawer for a shopping cart, and of course - a multilevel, fancy, personalized mega menu. All of these must be well thought through in frontend development, as well as in backend. This post describes my approach to creating these kinds of components wherever I can.
Firstly, we need a model that will contain all the necessary data and will be the result built by the described builders. It can be a global, large model like a LayoutModel, which is responsible for all elements on the site, such as the header, footer, etc. Or it can be smaller if we use the MVC View Components approach, like just a HeaderLayout. Then, we need an interface for the builder. Of course, it needs a model as a parameter. It is also very helpful to provide a second parameter for the current page link, as many things depend on where we currently are. In the Optimizely world, this is represented by the Content Reference of the current page.
public interface ILayoutBuilder
{
void Build(LayoutModel layoutModel, ContentReference currentContentLink);
}
It is simply a Builder design pattern. What is needed next are specific implementations of certain components, such as HeaderBuilder, FooterBuilder, or MegaMenuBuilder. Depending on the size of these components, they can have their own builders, which will build smaller parts. It starts from the top, seeing a global picture and splitting it into smaller parts.
Once the builders are done, they must be used. All that is needed here is another pattern - dependency injection. The builders must be registered in the DI container.
services.AddScoped<ILayoutBuilder, HeaderLayoutBuilder>();
services.AddScoped<ILayoutBuilder, FooterLayoutBuilder>();
services.AddScoped<ILayoutBuilder, CartLayoutBuilder>();
services.AddScoped<ILayoutBuilder, UserProfileLayoutBuilder>();
services.AddScoped<ILayoutBuilder, FavoritesProductsLayoutBuilder>();
services.AddScoped<ILayoutBuilder, SearchLayoutBuilder>();
The last unanswered question is where and when to execute builders. One way is to store a LayoutModel in each page's ViewModel and inject it into the implementation of the IResultFilter. I used this approach back in the days of older projects.
public void OnResultExecuting(ResultExecutingContext context)
{
var controller = context.Controller as Controller;
var model = controller?.ViewData.Model;
if (model is IViewModel viewModel)
{
var currentContentLink = context.HttpContext.GetContentLink();
viewModel.LayoutModel ??= _pageLayoutModelFactory.Create(currentContentLink);
}
}
Here we see another design pattern, Factory - it simply takes all the builders and requests them to build! Builders are injected here by the Dependency Injection Container. The factory iterates through all available implementations and asks each to fill in the data for which they are responsible.
public class PageLayoutModelFactory
{
private readonly IEnumerable<ILayoutBuilder> _builders;
public PageLayoutModelFactory(IEnumerable<ILayoutBuilder> builders)
{
_builders = builders;
}
public LayoutModel Create(ContentReference currentContentLink)
{
var model = new LayoutModel();
foreach (var builder in _builders)
{
builder.Build(model, currentContentLink);
}
return model;
}
}
Another approach (used nowadays) is to use MVC View Components. It is even more elegant because it doesn't require a global layout model. Each part, like the header or footer, can have its component that builds view models and passes them to the required views.
public class HeaderViewComponent : ViewComponent
{
private readonly HeaderViewModelFactory _headerViewModelFactory;
public HeaderViewComponent(HeaderViewModelFactory headerViewModelFactory)
{
_headerViewModelFactory = headerViewModelFactory;
}
public async Task<IViewComponentResult> InvokeAsync()
{
var viewModel = await _headerViewModelFactory.CreateViewModel();
return View("~/Views/Shared/Layouts/_Header.cshtml", viewModel);
}
}
The only difference is the entry point. Once started, the flow remains the same. A component requires a Factory to build a view model. If it's complex enough, the Factory requests Builders, which collectively assemble all the parts.
As already mentioned, I personally like and use this approach to build global components. The idea here is very simple, as is the code structure, which helps with maintaining it. Apart from that, it uses well-known design patterns that are tested by many, and other developers will quickly understand what is happening.
Simplicity is the key.
More articles
Optimizely SaaS CMS + Coveo Search Page
Short on time but need a listing feature with filters, pagination, and sorting? Create a fully functional Coveo-powered search page driven by data from the Optimizely SaaS CMS - all during just a break between coffee refills.
Block type selection doesn't work
Imagine you're trying to create a new block in a specific content area. You click the "Create" link, expecting to see a CMS modal with a list of available blocks. Instead, you're greeted with an empty view and a console error. What's going on?
Custom payment in Optimizely Commerce 14
The Commerce platform offers a few of the most common payment methods. However, they are not always enough, and it is absolutely natural to need a fully customized approach.
SEO redirects in .NET + Optimizely
Nice and easy way to add necessary SEO redirects
Optimizely Autocomplete (Statistics)
A user starts typing in the search input, and it returns suggestions for phrases they might be searching for. How to achieve this?