Kentico Xperience Design Patterns: Good Layout Hygiene
Sean G. Wright
Posted on July 12, 2021
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 Razor Layout
- The problems with unmaintained Razor Layouts
- When to use Partial Views and View Components
- How to clean up and organize a messy Layout
๐ 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>
@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>
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
This will change the Layout to the following:
<!DOCTYPE html>
<html>
<head id="head">
<partial name="_Head" />
@RenderSection("styles", required: false)
</head>
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" />
<!-- ... -->
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" />
<!-- ... -->
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>
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);
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>
}
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>
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 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>
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 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>
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 ๐!
References
- Kentico Xperience Design Patterns: Good Startup.cs Hygiene
- ASP.NET Core Docs - Layouts
- Stop using so many divs! An intro to semantic HTML
- Kentico Xperience Docs - Dancing Goat Sample Site Installation Process
- Kentico Xperience sample sites and their differences
- ASP.NET Core Docs - Razor Directives
- Martin Fowler - Code Smells
- ASP.NET Core Docs - Partial Views
- ASP.NET Core Docs - View Components
- ASP.NET Core Docs - View Service Injection
- Kentico Xperience 13 Beta 3 - Page Builder View Components in ASP.NET Core
- Kentico Xperience Design Patterns: MVC is Dead, Long Live PTVC
- Single-Responsibility Principle
- ASP.NET Core Docs - Tag Helpers
- Andrew Lock - If Tag Helper
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.
Or my Kentico Xperience blog series, like:
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
March 14, 2022
November 8, 2021