Page Templates in Xperience by Kentico sollten stets eingerichtet werden, da nur mit Page Templates der vollständige Feature-Umfang des Page Builders verfügbar ist. Zudem ist es deutlich aufwendiger im Nachhinein Page Templates einzurichten. In diesem Beitrag erläutere ich dies Schritt für Schritt.

Was wollen wir erreichen?

Wir möchten in all unseren Page Views ein gemeinsames Basis Layout verwenden. In diesem möchten wir gemeinsame Basis Parameter nutzen, z.B. Einstellungen für die META Daten im HTML Header.

Wir möchten dann für unsere Page Content Items entsprechende Page Templates verwenden, die den individuellen Inhalt des Page Content Items darstellen. Die Templates sollen austauschbar sein, um statische Inhalte durch unterschiedliche Templates anders darstellen zu können.

Außerdem soll in den jeweils ausgewählten Page Templates die Art und Weise wie statische Inhalte auf der Seite dargestellt werden gesteuert werden können.

Unter den Page Template Eigenschaften finden wir welche, die stets in allen Page Templates gleich sind und zusätzlich individuelle Page Template Eigenschaften.

Was muss dafür implementiert werden?

  1. Im CMS verwenden wir ein Reusable Field Schema für alle Eigenschaften, die in allen Page Content Items stets gleich sein sollen. Diese verwenden wir im gemeinsamen Layout aller Page Views, z.B. die MetaRobots Eingeschaft.
  2. Zusatzliche individuelle Eigenschaften werden im CMS am Page Content Item direkt eingerichtet.
  3. Im Code verwenden ein Basis Page View Model welches alle Eigenschaften enthält, die in allen Page Views stets verfügbar sind und vorwiegend im gemeinsamen Layout verwendet werden.
  4. Im Code nutzen wir ein konkreteres Page View Model welches das Basis Page View Model erweitert um den individuellen Content für die Page Template View Component bereitzustellen.
  5. Im Code verwenden wir einen Page Controller der eine TemplateView ansteuert und das Page View Model bereitstellt.
  6. In der Page Template View im Code fügen wir die jetzt dort verfügbaren Page Template Properties und den aktuellen Page Context zu unserem Page View Model hinzu und nuten eine View Component um final die Page Template View Component mit unserem Page View Model zu rendern und dort das gemeinsame Layout zu nutzen.
  7. Im Code erstellen wir eine View Component für die Page View.
  8. Im Code nutzen wir die entsprechenden Eigenschaften des View Models um Inhalte im gemeinsamen Layout zu steuern.

Schritt 1 – Reusable Field Schema

Für unser Beispiel reicht erstmal eine Eigenschaft für die Meta Robots Einstellung.

Reusable Field Schema Beispiel PageMetaRobots

Ich entscheide mich dafür eine Radio Button Group zu verwenden, die ich folgendermaßen einstelle.

Schritt 2 – Page Content Item

Im Page Content Item nutze ich das Reusable Field Schema aus Schritt 1 und zusätzlich eine Eigenschaft für eine Headline.

Schritt 3 – Basis Page View Model

Um auf den Code der Xperience Objekte zugreifen zu können muss ich diesen erst generieren lassen. Wir brauchen den Code für den Page Content Type und das Reusable Field Schema. Dafür nutze ich das folgenden PowerShell Script.

dotnet run --no-build -- --kxp-codegen --type "PageContentTypes" --location "./Codegen/{type}/{dataClassNamespace}" --skip-confirmation --namespace "CardManager.Codegen.{type}.{dataClassNamespace}"
dotnet run --no-build -- --kxp-codegen --type "ReusableFieldSchemas" --location "./Codegen/{type}/{dataClassNamespace}" --skip-confirmation --namespace "CardManager.Codegen.{type}"

Der generierte Code für unsere Page sieht dann so aus.

//--------------------------------------------------------------------------------------------------
// <auto-generated>
//
//     This code was generated by code generator tool.
//
//     To customize the code use your own partial class. For more info about how to use and customize
//     the generated code see the documentation at https://docs.xperience.io/.
//
// </auto-generated>
//--------------------------------------------------------------------------------------------------

using System;
using System.Collections.Generic;
using CMS.ContentEngine;
using CMS.Websites;

namespace CardManager.Codegen.PageContentTypes.CM
{
	/// <summary>
	/// Represents a page of type <see cref="StandardPage"/>.
	/// </summary>
	[RegisterContentTypeMapping(CONTENT_TYPE_NAME)]
	public partial class StandardPage : IWebPageFieldsSource, IPagemeta
	{
		/// <summary>
		/// Code name of the content type.
		/// </summary>
		public const string CONTENT_TYPE_NAME = "CM.StandardPage";


		/// <summary>
		/// Represents system properties for a web page item.
		/// </summary>
		[SystemField]
		public WebPageFields SystemFields { get; set; }


		/// <summary>
		/// StandardPageHeadline.
		/// </summary>
		public string StandardPageHeadline { get; set; }


		/// <summary>
		/// PageMetaRobots.
		/// </summary>
		public string PageMetaRobots { get; set; }
	}
}

Für das Basis Page View Model definiere ich ein Interface, welches gleichzeitig alle Interfaces erbt, die durch die Reusable Field Schemas erstellt werden. So stelle ich sicher, dass alle Eigenschaften aller Reusable Field Schemas auch definiert werden müssen.
Zusätzlich nehme ich noch zwei Eigenschaften mit auf, die XbyK standardmäßig in Page Template View Models bereitstellt.

using CardManager.Features.PageTemplates;
using Kentico.Content.Web.Mvc;

namespace CardManager.Models
{
    public interface IBasePageViewModel : IPagemeta
    {
        public RoutedWebPage Page { get; set; }
        public IBasePageTemplateProperties Properties { get; set; }
    }
}

Dann erstelle ich das BasePageViewModel welches dieses Interface implementiert.

using CardManager.Features.PageTemplates;
using Kentico.Content.Web.Mvc;

namespace CardManager.Models
{
    public class BasePageViewModel : IBasePageViewModel
    {
        // interface implementations IBasePageViewModel
        public RoutedWebPage Page { get; set; }
        public IBasePageTemplateProperties Properties { get; set; }

        // interface implementations IPagemeta ReusableFieldSchema with default values
        // intended to be set from the content type class properties when available
        public string PageMetaRobots { get; set; } = "all";
    }
}

Schritt 4 – Page View Model

Jetzt erstelle ich ein konkreteres Page View Model, welches für genau diesen Page Content Item Typ genutzt wird. Es enthält alle Basis Eigenschaften und zusätzlich die Eigenschaften der Page Content Items für welches dieses View Model erstellt wird.
Ich verwende gerne einen Konstruktor, der ein Objekt der generierten Klasse des Page Content Items nutzt um das View Model zu initialisieren. Dabei mappe ich alle Eigenschaften auf die entsprechenden Eigenschaften des View Models.
In unserem Beispiel ist das recht einfach, da man 1:1 die Eigenschaften übernehmen kann. Aber es ist auch möglich komplexere Logik auszuführen um evtl. weitere View Model Eigenschaften zu initialisieren.

using CardManager.Models;

namespace CardManager.Features.Pages.StandardPage
{
    public class StandardPageViewModel : BasePageViewModel
    {
        //own implementation for properties only used in template
        public string Headline { get; set; }
        
        public StandardPageViewModel(Codegen.PageContentTypes.CM.StandardPage page)
        {
            // from page properties
            Headline = page.StandardPageHeadline;

            // from reusable field schema properties
            PageMetaRobots = page.PageMetaRobots;
        }
    }
}

Schritt 5 – Page Controller

Die Implementierung des Page Controllers entspricht im Grunde exakt dem, was Kentico in der Dokumentation vorschlägt.

Was passiert hier?
Im Grunde wird hier mit Hilfe der via Dependency Injection bereitgestellten Services eine Abfrage an die Datenbank ausgeführt, die uns den entsprechenden Datensatz der aktuellen Page liefert.
Mit der gefundenen Page wird dann das Page View Model initialisiert und das Template Result zurückgegeben.

using CMS.ContentEngine;
using CMS.Websites;
using CMS.Websites.Routing;
using Kentico.Content.Web.Mvc;
using Kentico.Content.Web.Mvc.Routing;
using Kentico.PageBuilder.Web.Mvc.PageTemplates;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace CardManager.Features.Pages.StandardPage
{
    public class StandardPageController : Controller
    {
        private readonly IContentQueryExecutor _contentQueryExecutor;
        private readonly IWebPageDataContextRetriever _webPageDataContextRetriever;
        private readonly IWebsiteChannelContext _webSiteChannelContext;
        private readonly IPreferredLanguageRetriever _preferredLanguageRetriever;

        public StandardPageController(
            IContentQueryExecutor contentQueryExecutor,
            IWebPageDataContextRetriever webPageDataContextRetriever,
            IWebsiteChannelContext webSiteChannelContext,
            IPreferredLanguageRetriever preferredLanguageRetriever)
        { 
            _contentQueryExecutor = contentQueryExecutor;
            _webPageDataContextRetriever = webPageDataContextRetriever;
            _webSiteChannelContext = webSiteChannelContext;
            _preferredLanguageRetriever = preferredLanguageRetriever;
        }

        public async Task<IActionResult> Index()
        {
            WebPageDataContext context = _webPageDataContextRetriever.Retrieve();
            ContentItemQueryBuilder builder = new ContentItemQueryBuilder()
                .ForContentType(
                    Codegen.PageContentTypes.CM.StandardPage.CONTENT_TYPE_NAME,
                    config => config
                        .Where(
                            where => where
                                .WhereEquals(
                                    nameof(WebPageFields.WebPageItemID),
                                    context.WebPage.WebPageItemID))
                        .WithLinkedItems(1)
                        .ForWebsite(_webSiteChannelContext.WebsiteChannelName)                        
                )
                .InLanguage(_preferredLanguageRetriever.Get());

            ContentQueryExecutionOptions contentQueryExecutionOptions = new()
            {
                ForPreview = _webSiteChannelContext.IsPreview,
                IncludeSecuredItems = true
            };

            IEnumerable<Codegen.PageContentTypes.CM.StandardPage> pages = await _contentQueryExecutor
                .GetMappedWebPageResult<Codegen.PageContentTypes.CM.StandardPage>(
                    builder, 
                    contentQueryExecutionOptions);

            StandardPageViewModel model = new(pages.FirstOrDefault());

            return new TemplateResult(model);
        }
    }
}

Der Page Controller muss in XbyK noch registriert werden. Dies mache ich gerne in einer eigenen Datei.

using CardManager.Codegen.PageContentTypes.CM;
using CardManager.Features.Pages.StandardPage;
using Kentico.Content.Web.Mvc.Routing;

[assembly: RegisterWebPageRoute(
    contentTypeName: StandardPage.CONTENT_TYPE_NAME,
    controllerType: typeof(StandardPageController))]

Schritt 6 – Page Template View

Die Page Template View nutze ich lediglich um in meinem Page View Model noch Eingeschaften zu setzen, die erst hier verfügbar sind.
XbyK liefert diese direkt am Template View Model mit, und ich finde das recht einfach so zu machen, da ich diese dann nicht selber definieren und initialisieren muss.
Ich nutze daher aus dem standard XbyK Tempalte View Model die Eigenschaften Model.Page und Model.Properties und mappe diese auf mein eigenes Page View Model.
Danach rufe ich die View Component mit meinem View Model auf.
Das mache ich auf diese Weise, da ich so mein eigenes Page View Model nutzen kann und nicht das XbyK standard Template View Model. Das würde nämlich etwas schwieriger werden dieses im gemeinsamen Layout zu nutzen.
Die Lösung ein eigenes View Model in der Template View zu initialisieren und damit eine View Component aufzurufen finde ich sehr elegant.

@using CardManager.Features.PageTemplates.Standard
@using CardManager.Features.Pages;
@using CardManager.Features.Pages.StandardPage

@model TemplateViewModel<StandardPageTemplateProperties>

@{
    StandardPageViewModel standardPageViewModel = Model.GetTemplateModel<StandardPageViewModel>();
    standardPageViewModel.Page = Model.Page;
    standardPageViewModel.Properties = Model.Properties;
}

@await Component.InvokeAsync(typeof(StandardPageTemplateViewComponent), standardPageViewModel)

Ich verwende hier noch eigene Page Template Properties um Inhalte auf der Page zu steuern. Hier nutze ich ebenfalls ein Interface um die für alle Templates stets gleichen Eigenschaften sicherzustellen.

using Kentico.PageBuilder.Web.Mvc.PageTemplates;
using Kentico.Xperience.Admin.Base.FormAnnotations;

namespace CardManager.Features.PageTemplates
{
    public interface IBasePageTemplateProperties : IPageTemplateProperties
    {
        public bool HideBreadcrumb { get; set; }
        public bool HideNavigation { get; set; }
    }

    public class BasePageTemplateProperties : IBasePageTemplateProperties
    {
        [CheckBoxComponent(Order = 10, Label = "Hide Breadcrumb")]
        public bool HideBreadcrumb { get; set; }

        [CheckBoxComponent(Order = 20, Label = "Hide Navigation")]
        public bool HideNavigation { get; set; }
    }
}

Letztendlich muss das Template mit den Template Properties noch für den Page Content Item Typ registriert werden. Wenn dann eine neue Page dieses Typs angelegt wird, dann wird auch dieses Template verwendet. Gibt es mehrere registrierte Tempaltes für diesen Typ, dann kann man im Dialog wählen welches Template verwendet werden soll. Zudem kann man das Template auch nach der Erstellung jederzeit wechseln.

using CardManager.Codegen.PageContentTypes.CM;
using CardManager.Features.PageTemplates;
using CardManager.Features.PageTemplates.Standard;
using Kentico.PageBuilder.Web.Mvc.PageTemplates;

[assembly: RegisterPageTemplate(
    identifier: StandardPageTemplate.IDENTIFIER,
    name: "Standard Page Template",
    propertiesType: typeof(StandardPageTemplateProperties),
    customViewName: "~/Features/PageTemplates/Standard/StandardPageTemplate.cshtml",
    ContentTypeNames = [StandardPage.CONTENT_TYPE_NAME],
    Description = "simple one editable area wrapped by default layout",
    IconClass = "icon-doc")]

namespace CardManager.Features.PageTemplates;

public static class StandardPageTemplate
{ 
    public const string IDENTIFIER = "CM.PageTemplate.Standard";
}

Schritt 7 – View Component

Für die eigentliche Page View verwenden wir eine View Component. Man könnte das auch direkt in der Template View machen, aber aus genannten Gründen (Nutzen des gleichen View Models im Layout) mache ich das gerne über diesen Weg.

Dazu erstellen wir im Code die Invoke Methode, die wir bereits in der Template View angesprochen haben.

using CardManager.Features.Pages.StandardPage;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ViewComponents;
using System.Threading.Tasks;

namespace CardManager.Features.PageTemplates.Standard
{
    public class StandardPageTemplateViewComponent : ViewComponent
    {
        public StandardPageTemplateViewComponent() { }

        public async Task<ViewViewComponentResult> InvokeAsync(StandardPageViewModel model)
        {
            return await Task.FromResult(View(
                "~/Features/PageTemplates/Standard/StandardPageTemplateViewComponent.cshtml",
                model));
        }
    }
}

Letztendlich erstellen wir dann unsere View Component View.
Hier wird das gemeinsame Layout verwendet.
Und ich kann mit den Page Template Properties in unserem Beispiel steuern, ob ich meine Navigation oder den Breadcrumb ausblenden will.
Auf diese Weise kann alles was nur hier in diesem Template passiert über die eigenen Template Properties gesteuert werden.

@{
    Layout = "_Layout";
}

@using CardManager.Features.Pages.StandardPage;
@using CardManager.Models;

@model StandardPageViewModel

@if (!Model.Properties.HideNavigation)
{
    <div>I am the navigation</div>
}

@if (!Model.Properties.HideBreadcrumb)
{
    <div>I am the breadcrumb</div>
}

<p>@Model.Headline</p>

Schritt 8 – Layout

Im gemeinsamen Layout kann ich jetzt durch die Vererbung und die Interfaces auf die Basis Page View Model Eigenschaften zugreifen und Inhalte im Layout steuern, z.B. die Meta Robots Eigenschaft.

@model IBasePageViewModel

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <meta name="robots" content="@Model.PageMetaRobots" />
    <page-builder-styles />
</head>
<body>
    <div class="container">
        @RenderBody()
    </div>
    <page-builder-scripts />
</body>
</html>

Fazit

Auf diese Weise kann ich sehr gut steuern was genau in meinen Model für das Layout und die Page genau passieren soll und welche Inhalte dort enthalten sind.

Zudem kann ich jetzt alle Page Builder Features im Zusammenhang mit Page Templates nutzen.

Ich habe eine sehr gute Kontrolle darüber was genau im Code initialisiert wird und kann an mehreren Stellen im Code darauf Einfluss nehmen.

Siehe auch