Kentico Xperience Design Patterns: Good Layout Hygiene

seangwright

Sean G. Wright

Posted on July 12, 2021

Kentico Xperience Design Patterns: Good Layout Hygiene

It can be easy for Kentico Xperience developers to focus applying well known software design patterns, like DRY, composition, abstraction, and separation of concerns to their C# code, but these patterns are just as important in Razor code ๐Ÿง.

We can use Page Builder Widget Sections and Widgets to decompose the Page layout and design into reusable pieces, but what about the parts of a site common to every Page, like navigation, headers, footers, meta elements, and references to CSS and JavaScript ๐Ÿคท๐Ÿฝโ€โ™‚๏ธ?

By applying the above design patterns we can make sure we practice good Layout hygiene ๐Ÿšฟ, keeping our Razor _Layout.cshtml organized and maintainable.

If you are looking for other tips on keeping a clean Kentico Xperience application, checkout my post Kentico Xperience Design Patterns: Good Startup.cs Hygiene .

๐Ÿ“š What Will We Learn?

๐Ÿš€ What is a Layout?

According to the documentation for ASP.NET Core, a Layout "defines a top level template for views in the app."

By convention, the _Layout.cshtml file in our ~/Views/Shared folder is the Layout used by all Views in our application. This can be changed by modifying the Layout value specified in _ViewStart.cshtml at the root of the project or by overriding the Layout value on a per-View basis ๐Ÿค“.

The Layout is where we define all the markup that should appear on all (or most) Pages in our site. This includes headers and footers (navigation), <script> and <link> elements, marketing tags, meta tags, and any markup that wraps the main body of our Razor Views.

If we take a semantic HTML approach to our markup, we might have something that looks like this:

<head>
    <!-- links, meta tags -->
</head>
<body>
    <header>
        <!-- nav, banners -->
    <header>

    <main>
        <!-- page content -->
        @RenderBody()
    </main>

    <footer>
        <!-- nav, social icons -->
    </footer>
</body>
Enter fullscreen mode Exit fullscreen mode

@RenderBody() is where the markup from our Page specific Views will end up, and everything else is rendered from our Layout (but not necessarily by our Layout ๐Ÿ˜‰).

Some of this markup might be the same between Pages (navigation) and other parts will be more dynamic, including Page specific content, like Open Graph meta tags.

๐Ÿšฝ An Unmaintained Layout

If we look at the Dancing Goat sample site's _Layout.cshtml we can see what happens when our Layout grows in complexity to serve the needs of our site's functionality.

It's about 160 lines long and renders the following:

  • Static meta
  • Kentico Xperience dynamic (page specific) meta
  • Kentico Xperience marketing features scripts
  • Kentico Xperience Page Builder script and styles
  • The site's styles
  • A tracking consent form
  • The site's header
    • Navigation
    • Authenticated user avatar
    • Shopping cart icon
    • A language/culture switcher
    • Search box
  • The site's main content container
  • The site's footer
    • Company address
    • Social links
    • Newsletter subscription form
  • The site's JavaScript

Here's a link to the _Layout.cshtml in case you don't have access to it.

This seems like way too much for 1 file, but it is entirely plausible that an unmaintained Layout could evolve into this.

The main problem is that it's hard to reason about ๐Ÿ˜ต! There's simply too much going on in 1 file. We can see some Razor code blocks that define C# variables - these are effectively creating global variables for the Razor file and global variables always make code more confusing ๐Ÿ‘๐Ÿฟ.

If we remember that Razor files generate C# classes at compile time, it's easier to realize the benefits we might gain from simplifying this file - a C# class with this kind of complexity is definitely a code smell.

This Layout is also more likely to result in merge conflicts because of its size and mixed purposes. If a developer is working on an entirely different feature than another teammate, a well structured application will make it unlikely for them both to have to modify the same file for their changes. Merge conflicts, in this scenario, can also sometimes be a code smell ๐Ÿ‘ƒ๐Ÿผ - especially if they are painful to resolve.

Maybe we could clean ๐Ÿงผ it up?

The Dancing Goat site is meant to demo Kentico Xperience's capabilities, not be the pinnacle of software architecture ๐Ÿ˜‹.

๐Ÿงน Janitorial Tools

So what tools โš’ can we use to clean up this smelly ๐Ÿ’ฉ mess?

๐Ÿงฉ Partial Views

Partial Views are perfect for encapsulating a section of markup and giving it a name. They make it easier to understand and modify the markup in the Partial and do the same wherever the Partial is referenced - in our case, that's going to be the Layout.

If we are trying to reduce merge conflicts and make a bit of markup more readable, Partial Views are a great solution.

Partials can be passed parameters that become the View Model of the Partial, however these are not strongly typed and if the wrong type is passed as the View Model, we'll experience a runtime exception.

In ASP.NET Core, if we need to pass some data to a Partial, we might instead reach for another powerful tool - the View Component.

๐Ÿ–ผ View Components

View Components give us an opportunity to separate data and logic from the declarative presentation of HTML. They mirror the MVC pattern by having 3 parts - the View Component class (Controller), View Model class, and Razor View.

Using the Tag Helper syntax for View Components (ex: <vc:our-view-component>), we get strongly typed parameters passed to the View Component, which can improve the developer experience and make our code more refactor-proof ๐Ÿ˜Ž.

Since View Component classes participate in dependency injection and have access to all the same helper 'context' properties that Controllers do, we can use them in much the same way. If we need access to state, services, or execute some logic to restructure a model, View Components are perfect ๐Ÿ‘๐Ÿป.

We might see examples of injecting services directly into Views using View Service Injection. While this technique is very convenient, I advise against using it too much ๐Ÿ˜ฎ. Views are declarative presentation concerns and any complex logic or data access should definitely be performed in C# classes.

As a concrete example, using an injected IHtmlLocalizer for localizing content is a great use-case for View Service Injection ๐Ÿ’ช๐Ÿพ, however accessing a repository or any of Kentico Xperience's 'Retriever' services (IPageRetriever, IPageAttachmentUrlRetriever) in a View should be considered an anti-pattern - use a Controller or View Component instead.

If you want to learn more about using View Components in Kentico Xperience, checkout my post Kentico Xperience 13 Beta 3 - Page Builder View Components in ASP.NET Core or Kentico Xperience Design Patterns: MVC is Dead, Long Live PTVC .

Now that we've covered our motivations for cleaning up our Layout and the tools we can use, let's jump into the task at hand.

๐Ÿšฟ Cleaning Up a Messy Layout

There's two observations we are going to make about the code in the Layout:

  • What bits of markup share a common purpose?
  • What needs application state or context to render?

Let's analyze these below...

๐ŸŽจ Abstract Based on Purpose

First, we should identify which parts of the Layout belong together ๐Ÿ‘ฉ๐Ÿพโ€๐Ÿคโ€๐Ÿง‘๐Ÿผ based on their purpose. Things that change together should stay together and we can relate this recommendation to the Single-Responsibility Principle.

The lack of a single responsibility for this giant Layout is what makes it so complex.

We already have an outline of the different parts of our Layout, organized by purpose, with our original list at the beginning of this post, but let's look from an even higher ๐ŸŒ level.

HTML Head Element

The <head> element is an obvious starting point for refactoring:

<head id="head">
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta charset="UTF-8" />
    @Html.Kentico().PageDescription()
    @Html.Kentico().PageKeywords()
    @Html.Kentico().PageTitle(ViewBag.Title as string)
    <link rel="icon" href="~/content/images/favicon.svg" type="image/svg+xml" />
    <link href="~/Content/Styles/Site.css" rel="stylesheet" type="text/css" />
    <link rel="canonical" href="@Url.Kentico().PageCanonicalUrl()" />
    @RenderSection("styles", required: false)
    @Html.Kentico().ABTestLoggerScript()
    @Html.Kentico().ActivityLoggingScript()
    @Html.Kentico().WebAnalyticsLoggingScript()
    <page-builder-styles />
</head>
Enter fullscreen mode Exit fullscreen mode

We can take this whole block of code and move it into a Partial view. I typically like to leave the top level elements (like <head> and <body>) in the Layout, so let's just take the contents of the <head> and move them to a new Partial ~/Views/Shared/_Head.cshtml.

We'll also need to move over the 2 namespaces being used by the HtmlHelpers in this Partial:

@using Kentico.OnlineMarketing.Web.Mvc
@using Kentico.Activities.Web.Mvc
Enter fullscreen mode Exit fullscreen mode

This will change the Layout to the following:

<!DOCTYPE html>

<html>
<head id="head">
    <partial name="_Head" />

    @RenderSection("styles", required: false)
</head>
Enter fullscreen mode Exit fullscreen mode

We have to move the @RenderSection() out of the Razor we copied to the _Head.cshtml Partial because Mvc doesn't support rendering sections from Partials.

HTML Header Element

Next, we'll move the <header> element and all of its contents to a new ~/Views/Shared/_Header Partial (along with the @using Kentico.Membership.Web.Mvc using that gives us access to the AvatarUrl() HtmlHelper extension).

This will reduce our Layout to the following, with a total of 59 lines for the file ๐Ÿฅณ:

<!DOCTYPE html>

<html>
<head id="head">
    <partial name="_Head" />
</head>
<body class="@ViewData["PageClass"]">
    <div class="page-wrap">
        <vc:tracking-consent />

        <partial name="_Header" />

<!-- ... -->
Enter fullscreen mode Exit fullscreen mode

The _Header Partial can be simplified further but let's finish working with the Layout first.

Footer HTML Element

We only have a few sections of markup left in our Layout and the next one that we'll abstract into a Partial is the 'footer'.

The Dancing Goat footer includes a <div class="footer-wrapper"> container element. Even though the actual <footer> element is nested inside it, the wrapper is part of the footer from a design perspective, so let's take the wrapper and all of its contents and move them to a new Partial ~/Views/Shared/_Footer.cshtml. We'll also need to move the @using DancingGoat.Widgets using to our Partial to get access to the Newsletter subscription widget types.

Our updated Layout is shaping up ๐Ÿ˜Š and looks as follows:

<body class="@ViewData["PageClass"]">
    <div class="page-wrap">
        <vc:tracking-consent />

        <partial name="_Header" />

        <div class="container">
            <div class="page-container-inner">
                @RenderBody()

                <div class="clear"></div>
            </div>
        </div>
    </div>

    <partial name="_Footer" />

    <!-- ... -->
Enter fullscreen mode Exit fullscreen mode

Page Scripts

The last bit of markup to abstract out of our Layout includes all the <script> tags, which we will move to another new Partial ~/Views/Shared/_Scripts.cshtml. We will leave the @RenderSection("scripts", required: false) call in the Layout, just like the RenderSection call for styles.

We've completely cleaned up the Layout and trimmed it down to a lovely 27 lines ๐Ÿ˜…:

<!DOCTYPE html>
<html>
<head id="head">
    <partial name="_Head" />
</head>
<body class="@ViewData["PageClass"]">
    <div class="page-wrap">
        <vc:tracking-consent />

        <partial name="_Header" />

        <div class="container">
            <div class="page-container-inner">
                @RenderBody()

                <div class="clear"></div>
            </div>
        </div>
    </div>

    <partial name="_Footer" />

    <partial name="_Scripts" />

    @RenderSection("scripts", required: false)
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

We were able to do this without creating any new abstractions - just organizing code by common concerns. In our Layout, these common concerns can often be identified by high level HTML elements, like <head>, <header>, and <footer>.

This clean up also lets us see additional places we could insert @RenderSection() calls if we wanted to give our Views more places to hook into the Layout ๐Ÿค”.

๐Ÿ”ฌ Identify What Needs State

The _Footer and _Head Partials are simple and only a few lines of code. However, the _Header Partial we created is 88 lines long. It includes a Razor code block, has multiple conditional statements, and 2 navigation blocks ๐Ÿ˜จ (header navigation and the 'additional' navigation).

The Razor code block in the view is often a code smell - it indicates we are executing some logic in our View. This specific code block not only executes some logic, but it also access application state - the @ViewContext.

The _Header Partial also accesses state through the ClaimsPrincipal User and the HttpContext Context properties of the Razor page. While these properties are accessible from the Razor view, I like to think of them as a convenience and not necessarily something that should be used when considering best practices for larger applications.

Any time we have logic in our Layout (or Partials), and especially when we are accessing application state, I see it as an opportunity to leverage a View Component instead of a Partial ๐Ÿค“.

View Components have access to the same context specific state (like ViewContext, User, and Context) but they are more testable and appropriate for C# code and logic ๐Ÿ‘๐Ÿผ.

Header View Component

The only portion of the _Header Partial using this context state is the contents of the <ul class="additional-menu"> element, so let's use that as the starting point for our View Component.

Let's create a new View Component ~/Components/ViewComponents/HeaderMenu/HeaderMenuViewComponent.cs:

public class HeaderMenuViewComponent : ViewComponent
{
    public IViewComponentResult Invoke()
    {
        string cultureCode = ViewContext
            .RouteData
            .Values["culture"];

        var currentCultureCode = Convert.ToString(cultureCode);

        var currentLanguage = currentCultureCode.Equals(
            "es-es", StringComparison.OrdinalIgnoreCase) 
            ? "ES" : "EN";

        bool isCultureSelectorVisible = HttpContext.Response.StatusCode == (int)System.Net.HttpStatusCode.OK;

        var vm = new HeaderMenuViewModel(
            User.Identity.IsAuthenticated,
            currentLanguage,
            isCultureSelectorVisible);

        return View(
            "~/Components/ViewComponents/HeaderMenu/_HeaderMenu.cshtml", vm);
    }
}

public record HeaderMenuViewModel(
    bool IsUserAuthenticated,
    string CurrentLanguage,
    bool IsCultureSelectorVisible);
Enter fullscreen mode Exit fullscreen mode

We've moved all of the logic of our View into the View Component class and created a View Model record that represents the state we need to expose to the View, with property names that have a clear purpose.

We can now move the <ul class="additional-menu"> element and its contents into ~/Components/ViewComponents/HeaderMenu/_HeaderMenu.cshtml add a @model DancingGoat.Components.ViewComponents.HeaderMenu.HeaderMenuViewModel directive at the top of the file, and use the View Model properties instead of the context helper properties.

We can see the effect this has on the culture selector in the View Component View ๐Ÿคฉ:

@if (Model.IsCultureSelectorVisible)
{
    <li class="dropdown">
        <a class="dropbtn">@Model.CurrentLanguage</a>
        <div class="dropdown-content">
            <culture-link link-text="English" culture-name="en-US" />
            <culture-link link-text="Espaรฑol" culture-name="es-ES" />
        </div>
    </li>
}
Enter fullscreen mode Exit fullscreen mode

We can now update the ~/Views/Shared/_Header.cshtml Partial, replacing the former location of <ul class="additional-menu"> with a reference to our View Component <vc:header-menu />:

<header data-ktc-search-exclude>
    <nav class="navigation">
        <div class="nav-logo">
            <div class="logo-wrapper">
                <a href="@Url.Kentico().PageUrl(ContentItemIdentifiers.HOME)" class="logo-link">
                    <img class="logo-image" alt="Dancing Goat" src="~/Content/Images/logo.svg" />
                </a>
            </div>
        </div>
        <vc:navigation footer-navigation="false" />

        <vc:header-menu /> <!-- ๐Ÿ˜ƒ -->
    </nav>
    <div class="search-mobile">
        <form asp-action="Index" asp-controller="Search" method="get" class="searchBox">
            <input name="searchtext" type="text" placeholder="@HtmlLocalizer["Search"]" autocomplete="off" />
            <input type="submit" value="" class="search-box-btn" />
        </form>
    </div>
</header>
Enter fullscreen mode Exit fullscreen mode

If Tag Helper

Coming from ASP.NET MVC 5 in Kentico 12, we are probably used to seeing Html Helper calls and Razor statements all over our Views.

I've found that adopting Tag Helpers, which look much more like HTML, leads to more readable Views. As a bonus, if you've had experience with any client-side JavaScript frameworks, like React, Vue, or Angular, ASP.NET Core Tag Helpers are going to look like the server-side equivalents of what those frameworks already support.

To this end, it could be nice ๐Ÿ˜ to replace the Razor @if() syntax with a Tag Helper. First, let's see what we're working with:

<div class="dropdown-content">
    @if (Model.IsUserAuthenticated)
    {
        <a asp-controller="Account" asp-action="YourAccount">
            @HtmlLocalizer["Your&nbsp;account"]
        </a>
        <form method="post" asp-controller="Account" 
                            asp-action="Logout">
            <input type="submit" 
                   value="@HtmlLocalizer["Sign out"]" 
                   class="sign-out-button" />
        </form>
    }
    else
    {
        <a asp-controller="Account" asp-action="Register">
            @HtmlLocalizer["Register"]
        </a>
        <a asp-controller="Account" asp-action="Login">
            @HtmlLocalizer["Login"]
        </a>
    }
</div>
Enter fullscreen mode Exit fullscreen mode

Using the <if> Tag Helper detailed by Andrew Lock in this blog post, we can update our View to look more HTML-ish:

<div class="dropdown-content">
    <if include-if="Model.IsUserAuthenticated">
        <a asp-controller="Account" asp-action="YourAccount">
            @HtmlLocalizer["Your&nbsp;account"]
        </a>
        <form method="post" asp-controller="Account" 
                            asp-action="Logout">
            <input type="submit" 
                   value="@HtmlLocalizer["Sign out"]" 
                   class="sign-out-button" />
        </form>
    </if>
    <if exclude-if="Model.IsUserAuthenticated">
        <a asp-controller="Account" asp-action="Register">
            @HtmlLocalizer["Register"]
        </a>
        <a asp-controller="Account" asp-action="Login">
            @HtmlLocalizer["Login"]
        </a>
    </if>
</div>
Enter fullscreen mode Exit fullscreen mode

This is a matter of taste ๐Ÿฅช, but I've found many of the ASP.NET Core Tag Helpers to be much more readable than their Html Helper and Razor syntax equivalents (including the Partial Tag Helper and View Component Tag Helper we've already used).

๐Ÿง  Conclusion

We've reduced the size of the Dancing Goat _Layout.cshtml file from 160 lines to 27 and the largest Partial or View Component View that we now have from our refactoring is the _HeaderMenu.cshtml at 64 lines.

We could continue the refactoring with additional View Components or Partial Views to reduce that size even further - likely separating the Avatar markup and Login/Register links next. I'll leave that task for the now-very-capable reader ๐Ÿ˜.

By identifying a problematic area of the application, the tools that ASP.NET Core puts at our disposal, and applying some common refactoring patterns, we've created a clean an maintainable Layout.

Let me know โœ your patterns and practices for Layouts in Kentico Xperience applications in the comments below...

As always, thanks for reading ๐Ÿ™!


Photo by Markus Spiske on Unsplash

References


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 or Xperience tags here on DEV.

#kentico

#xperience

Or my Kentico Xperience blog series, like:

๐Ÿ’– ๐Ÿ’ช ๐Ÿ™… ๐Ÿšฉ
seangwright
Sean G. Wright

Posted on July 12, 2021

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

Sign up to receive the latest update from our blog.

Related

ยฉ TheLazy.dev

About