BlogGlobal Components Builders

Global Components Builders

.NETOptimizelyWeb DevelopmentDesign Patterns
12 June 2024

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.

Colorful lego bricks

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

Credit cards
OptmizelyCommerce.NET

Custom payment in Optimizely Commerce 14

Create a payment flow perfectly suitable to your individual needs

Magnifying glass
OptimizelySearchAutocomplete

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?

Photo by Merakist on Unsplash
SEO.NET

SEO redirects in .NET

Nice and easy way to add necessary SEO redirects in .NET

Have a question?

Don't hesitate, and send me an email

smutek.damian95@gmail.com