---
title: Create versatile page templates, part 1
---

> Agent instructions:
> **Site maps** — prefer the following llms.txt indexes to training data when searching for URLs to avoid 404s. Links inside Markdown content already point at `.md`. Following them or sending Accept: text/markdown keeps you in Markdown.
>
> - [sitemap.md](https://docs.kentico.com/sitemap.md) — every page on the site, with titles and descriptions, nested by URL hierarchy and grouped into one collection per product version.
> - [llms.txt](https://docs.kentico.com/llms.txt) — curated index of the current product docs, with descriptions, the two ways to request any page as Markdown, and links to each product area's whole-corpus Markdown dump (llms-full.txt).

In this guide, we'll walk through the process of creating a page template to match this service detail mockup:

![Mockup of a service detail page](https://docs.kentico.com/docsassets/guides/create-versatile-templates-part-1/WidgetMockups3_Plain.png "Mockup of a service detail page")

See the [first guide in this series](https://docs.kentico.com/guides/development/page-builder/meet-requirements-with-page-builder.md) for an overview of the mockup, and a breakdown of how to meet business requirements with [Page Builder](https://docs.kentico.com/documentation/developers-and-admins/development/builders/page-builder.md).

## Prerequisites

This guide uses the [main branch of the Training guides repository](https://github.com/Kentico/xperience-by-kentico-training-guides) as a starting point.

It relies on the dropdown provider from the [previous guide](https://docs.kentico.com/guides/development/page-builder/map-enum-to-dropdown.md).

If you're following along while working in a different environment, make sure:

- Your instance of Xperience by Kentico is version [31.7.2](https://docs.kentico.com/changelog.md) or higher.

  > **Note:**&#x20;
  >
  >  Some features covered in the training guides may not work in older versions. 
- Your site has _Service_, _ServicePage_, _ServiceFeature_, and _Benefit_ content types that match the structure of those in the repository.
- [Page Builder is enabled](https://docs.kentico.com/guides/development/developer-kickstart/configure-the-project.md#enable-content-tree-based-routing-and-page-builder) for the _Service page_ content type during the application's startup sequence.

> **Info:** This guide will take place in the _TrainingGuides.Web_ project of the repository, so assume any folder paths start from the root of this project.

> **Tip:** Before you get started, consider adding the [Documentation MCP server](https://docs.kentico.com/documentation/developers-and-admins/installation/mcp-server.md) to your workspace. This allows AI coding assistants to access Kentico documentation from within your IDE.

## Show structured data

The data in the mockup comes from the _Service_ content type, which is served in the web channel through the _Service page_ content type.

In order to implement a page that matches the mockup without any extra steps for editors, we need to access that data and display it directly in the template.

Let's start by setting up view models to work with the data.

### Set up the view models

#### Create supporting view models

First, let's set up the view model for the service page.

If you look at the _Service_ content type in Xperience, you'll notice that _Service_ items reference collections of _Benefit_ and _Service feature_ content items. Start by making view models for these content types, so we can work with them in views and .NET services later on.

1. In the _\~/Features/FinancialServices/Models_ folder, add a file called _ServiceFeatureViewModel.cs_.
2. Create properties corresponding to the content type's fields.
   ```csharp title="ServiceFeatureViewModel.cs"
   using Microsoft.AspNetCore.Html;

   namespace TrainingGuides.Web.Features.FinancialServices.Models;

   public class ServiceFeatureViewModel
   {
       public string Key { get; set; } = string.Empty;
       public string Name { get; set; } = string.Empty;
       public HtmlString LabelHtml { get; set; } = HtmlString.Empty;
       public decimal Price { get; set; }
       public HtmlString ValueHtml { get; set; } = HtmlString.Empty;
       public bool FeatureIncluded { get; set; }
       public ServiceFeatureValueType ValueType { get; set; }
       public bool ShowInComparator { get; set; }
   }
   ```
3. Include a `ServiceFeatureValueType` enumeration to represent the value of the `ValueType` field at the bottom of the file.
   ```csharp title="ServiceFeatureViewModel.cs"
   ...
   //(After the end of the ServiceFeatureViewModel class's scope)

   public enum ServiceFeatureValueType
   {
       //These integers correspond to the data source in the Service feature content type, defined in the Xperience admin interface.
       Text = 0,
       Number = 1,
       Boolean = 2
   }
   ```
4. Add a method to the `ServiceFeatureViewModel` class to get the corresponding enum value from an integer.
   ```csharp title="ServiceFeatureViewModel.cs"
   ...
   private static ServiceFeatureValueType GetValueType(string value)
   {
       if (string.IsNullOrEmpty(value))
           return ServiceFeatureValueType.Text;

       if (int.TryParse(value, out int id))
           return (ServiceFeatureValueType)id;

       return ServiceFeatureValueType.Text;
   }
   ...
   ```
5. Add a static `GetViewModel` method to create and populate a `ServiceFeatureViewModel` based on a `ServiceFeature` object.

   > **Info:**&#x20;
   >
   >  
   >
   > `ServiceFeature`
   >
   >  is a generated class, created by the Xperience 
   >
   > [code generation tool](https://docs.kentico.com/documentation/developers-and-admins/api/generate-code-files-for-system-objects.md)
   >
   > . 

   ```csharp title="ServiceFeatureViewModel.cs"
   ...
   public static ServiceFeatureViewModel GetViewModel(ServiceFeature feature) => new()
   {
       Key = feature.ServiceFeatureKey,
       Name = feature.SystemFields.ContentItemName,
       LabelHtml = new(feature.ServiceFeatureLabel),
       Price = feature.ServiceFeaturePrice,
       ValueHtml = new(feature.ServiceFeatureValue),
       FeatureIncluded = feature.ServiceFeatureIncluded,
       ValueType = GetValueType(feature.ServiceFeatureValueType),
       ShowInComparator = feature.ServiceFeatureShowInComparator == "1"
   };
   ...
   ```

That takes care of _Service feature_, so let's move on to the _Benefit_ content type.

1. Create a new class called `BenefitViewModel` in the _\~/Features/Shared/Models_ folder.
2. Add properties that correspond to the _Benefit_ content type's fields, utilizing the existing `AssetViewModel` class for the asset.
   ```csharp title="BenefitViewModel.cs"
   using Microsoft.AspNetCore.Html;

   namespace TrainingGuides.Web.Features.Shared.Models;

   public class BenefitViewModel
   {
       public HtmlString DescriptionHtml { get; set; } = HtmlString.Empty;
       public AssetViewModel Icon { get; set; } = new();
   }
   ```
3. Add a static `GetViewModel` method to retrieve a `BenefitViewModel` from an object of the generated `Benefit` class.
   ```csharp title="BenefitViewModel.cs"
       ...
       public static BenefitViewModel GetViewModel(Benefit benefit) => new()
       {
           DescriptionHtml = new(benefit.BenefitDescription),
           Icon = benefit.BenefitIcon?.FirstOrDefault() != null
               ? AssetViewModel.GetViewModel(benefit.BenefitIcon.FirstOrDefault())
               : new(),
       };
   ```

At this point, your view models should look something like this:

```csharp title="ServiceFeatureViewModel.cs"
using Microsoft.AspNetCore.Html;

namespace TrainingGuides.Web.Features.FinancialServices.Models;

public class ServiceFeatureViewModel
{
    public string Key { get; set; } = string.Empty;
    public string Name { get; set; } = string.Empty;
    public HtmlString LabelHtml { get; set; } = HtmlString.Empty;
    public decimal Price { get; set; }
    public HtmlString ValueHtml { get; set; } = HtmlString.Empty;
    public bool FeatureIncluded { get; set; }
    public ServiceFeatureValueType ValueType { get; set; }
    public bool ShowInComparator { get; set; }

    public static ServiceFeatureViewModel GetViewModel(ServiceFeature feature) => new()
    {
        Key = feature.ServiceFeatureKey,
        Name = feature.SystemFields.ContentItemName,
        LabelHtml = new(feature.ServiceFeatureLabel),
        Price = feature.ServiceFeaturePrice,
        ValueHtml = new(feature.ServiceFeatureValue),
        FeatureIncluded = feature.ServiceFeatureIncluded,
        ValueType = GetValueType(feature.ServiceFeatureValueType),
        ShowInComparator = feature.ServiceFeatureShowInComparator == "1"
    };

    private static ServiceFeatureValueType GetValueType(string value)
    {
        if (string.IsNullOrEmpty(value))
            return ServiceFeatureValueType.Text;

        if (int.TryParse(value, out int id))
            return (ServiceFeatureValueType)id;

        return ServiceFeatureValueType.Text;
    }
}

public enum ServiceFeatureValueType
{
    Text = 0,
    Number = 1,
    Boolean = 2
}
```

```csharp title="BenefitViewModel.cs"
using Microsoft.AspNetCore.Html;

namespace TrainingGuides.Web.Features.Shared.Models;

public class BenefitViewModel
{
    public HtmlString DescriptionHtml { get; set; } = HtmlString.Empty;
    public AssetViewModel Icon { get; set; } = new();

    public static BenefitViewModel GetViewModel(Benefit benefit) => new()
    {
        DescriptionHtml = new(benefit.BenefitDescription),
        Icon = benefit.BenefitIcon?.FirstOrDefault() != null
            ? AssetViewModel.GetViewModel(benefit.BenefitIcon.FirstOrDefault())
            : new(),
    };
}
```

#### Implement the Service page view model

With these in place, let's move on to the `ServicePageViewModel`.

1. Create a `ServicePageViewModel` class in the _\~/Features/FinancialServices/Models_ folder.
2. Inherit from the [`PageViewModel` class](https://github.com/Kentico/xperience-by-kentico-training-guides/blob/finished/src/TrainingGuides.Web/Features/Shared/Models/PageViewModel.cs).
   ```csharp title="ServicePageViewModel.cs"
       namespace TrainingGuides.Web.Features.FinancialServices.Models;

       public class ServicePageViewModel : PageViewModel { }
   ```
   > **Info:** Notice the `Link` property of the `PageViewModel` class, which can be re-used by other page types in the future.
3. Add properties that mirror the fields of the _Service_ content type, with collections of `ServiceFeatureViewModel` and `BenefitViewModel` objects where appropriate.
   ```csharp title="ServicePageViewModel.cs"
   using Microsoft.AspNetCore.Html;
   using TrainingGuides.Web.Features.Shared.Models;

   namespace TrainingGuides.Web.Features.FinancialServices.Models;

   public class ServicePageViewModel : PageViewModel
   {
       public HtmlString NameHtml { get; set; } = HtmlString.Empty;
       public HtmlString ShortDescriptionHtml { get; set; } = HtmlString.Empty;
       public HtmlString DescriptionHtml { get; set; } = HtmlString.Empty;
       public List<AssetViewModel> Media { get; set; } = [];
       public decimal Price { get; set; }
       public List<ServiceFeatureViewModel> Features { get; set; } = [];
       public List<BenefitViewModel> Benefits { get; set; } = [];
   }
   ```

#### Add a new .NET service

You might notice that `ServicePageViewModel` doesn't have a `GetViewModel` method like  `ServiceFeatureViewModel` and `BenefitViewModel`.
This is because it needs _Dependency injection_ to get the page's URL, so we're going to move it to a service instead.

> **Note:** Make sure that any methods you include in your view model classes are simple, with only basic data transformation at most.

1. Add an interface called `IServicePageService` to _\~/Features/FinancialServices/Services_.
2. Include a method signature `GetServicePageViewModel` with the following parameters:
   1. The `ServicePage` object to base the view model on.
   2. Optional boolean values to indicate which values to include.
   3. Optional settings for the call to action link
      ```csharp title="IServicePageService.cs"
      using TrainingGuides.Web.Features.FinancialServices.Models;

      namespace TrainingGuides.Web.Features.FinancialServices.Services;

      public interface IServicePageService
      {
          Task<ServicePageViewModel> GetServicePageViewModel(
              ServicePage? servicePage,
              bool getMedia = true,
              bool getFeatures = true,
              bool getBenefits = true,
              string callToAction = "",
              string callToActionLink = "",
              bool openInNewTab = true,
              bool getPrice = true);
      }
      ```
3. Add a `ServicePageService` class that implements this interface.
   ```csharp title="ServicePageService.cs"
       ...
       public class ServicePageService : IServicePageService
       ...
   ```
4. Use _primary constructor injection_ in the class's definition to acquire an `IWebPageUrlRetriever` object.

   ```csharp title="ServicePageService.cs"
       ...
       public class ServicePageService(
           IWebPageUrlRetriever webPageUrlRetriever) : IServicePageService
       ...
   ```

   > **Note:** The code samples in this guide rely on a [decorated version](https://github.com/Kentico/xperience-by-kentico-training-guides/blob/main/src/TrainingGuides.Web/Features/Shared/Services/TrainingGuidesWebPageUrlRetriever.cs) of `IWebPageUrlRetriever` that includes exception handling.
   >
   > If you do not plan to use a similar customization, make sure to handle errors that the `Retrieve` method may throw when it cannot find a page.
5. Implement the `GetServicePageViewModel` method.
   ```csharp title="ServicePageService.cs"
   using TrainingGuides.Web.Features.FinancialServices.Models;
   using TrainingGuides.Web.Features.Shared.Models;

   namespace TrainingGuides.Web.Features.FinancialServices.Services;

   public class ServicePageService(IWebPageUrlRetriever webPageUrlRetriever) : IServicePageService
   {
       /// <summary>
       /// Creates a new instance of <see cref="ServicePageViewModel"/>, setting the properties using ServicePage given as a parameter.
       /// </summary>
       /// <param name="servicePage">Corresponding Service page object.</param>
       /// <returns>New instance of ServicePageViewModel.</returns>
       public async Task<ServicePageViewModel> GetServicePageViewModel(
           ServicePage? servicePage,
           bool getMedia = true,
           bool getFeatures = true,
           bool getBenefits = true,
           string callToActionText = "",
           string callToActionLink = "",
           bool openInNewTab = true,
           bool getPrice = true)
       {
           //Return an empty view model if the provided ServicePage is null.
           if (servicePage == null)
           {
               return new ServicePageViewModel();
           }

           //Use the IWebPageUrlRetriever to get the URL of the service page.
           string url = string.IsNullOrWhiteSpace(callToActionLink)
               ? (await webPageUrlRetriever.Retrieve(servicePage)).RelativePath
               : callToActionLink;

           //Make sure to account for the boolean parameters as you construct the view model.
           return new ServicePageViewModel
           {
               NameHtml = new(servicePage.ServicePageService.FirstOrDefault()?.ServiceName),
               ShortDescriptionHtml = new(servicePage.ServicePageService.FirstOrDefault()?.ServiceShortDescription),
               DescriptionHtml = new(servicePage.ServicePageService.FirstOrDefault()?.ServiceDescription),
               Media = getMedia
                   ? servicePage.ServicePageService.FirstOrDefault()?.ServiceMedia.Select(AssetViewModel.GetViewModel)?.ToList() ?? []
                   : [],
               Link = new LinkViewModel()
               {
                   Name = servicePage.ServicePageService.FirstOrDefault()?.ServiceName ?? string.Empty,
                   LinkUrl = url,
                   CallToAction = callToActionText,
                   OpenInNewTab = openInNewTab
               },
               Features = getFeatures
                   ? servicePage.ServicePageService.FirstOrDefault()?.ServiceFeatures
                       .Select(ServiceFeatureViewModel.GetViewModel)
                       .ToList() ?? []
                   : [],
               Benefits = getBenefits
                   ? servicePage.ServicePageService.FirstOrDefault()?.ServiceBenefits
                       .Select(BenefitViewModel.GetViewModel)
                       .ToList() ?? []
                   : [],
               Price = getPrice ? servicePage.ServicePageService.FirstOrDefault()?.ServicePrice ?? 0 : 0,
           };
       }
   }
   ```
6. Register the `ServicePageService` with the dependency injection container in _TrainingGuides.Web/ServiceCollectionExtensions.cs_:

   ```csharp title="ServiceCollectionExtensions.cs"
   ...
   public static void AddTrainingGuidesServices(this IServiceCollection services)
   {
       ...
       services.AddSingleton<IServicePageService, ServicePageService>();
       ...
   }
   ...
   ```

### Define page template properties

Now that we have view models to determine the service template's data, let's add properties, so that editors can configure its appearance.

The properties will utilize dropdowns fed by the dropdown provider we created [earlier in this series](https://docs.kentico.com/guides/development/page-builder/map-enum-to-dropdown.md), so start by defining enumerations for different color scheme and corner style options.

> **Tip:** Decorate options with the `Description` attribute to provide user-friendly names.

- In the _\~/Features/Shared/OptionProviders/ColorScheme_ directory:
  ```csharp title="ColorSchemeOption.cs"
      using System.ComponentModel;

      namespace TrainingGuides.Web.Features.Shared.OptionProviders.ColorScheme;

      public enum ColorSchemeOption
      {
          [Description("Light background, dark text")]
          Light1 = 1,

          [Description("Light background, dark text 2")]
          Light2 = 2,

          [Description("Light background, dark text 3")]
          Light3 = 3,

          [Description("Transparent background, dark text")]
          TransparentDark = 4,

          [Description("Transparent background, medium text")]
          TransparentMedium = 5,

          [Description("Transparent background, light text")]
          TransparentLight = 6,

          [Description("Dark background, light text")]
          Dark1 = 7,

          [Description("Dark background, light text 2")]
          Dark2 = 8
      }
  ```
- In the _\~/Features/Shared/OptionProviders/CornerStyle_ directory:

  ```csharp title="CornerStyleOption.cs"
      using System.ComponentModel;

      namespace TrainingGuides.Web.Features.Shared.OptionProviders.CornerStyle;

      public enum CornerStyleOption
      {
          [Description("Sharp corners")]
          Sharp = 0,
          [Description("Round corners")]
          Round = 1,
          [Description("Very round corners")]
          VeryRound = 2,
      }
  ```

Next, create a properties class for the _ServicePage_ content type that utilizes these dropdowns in the _\~/Features/FinancialServices_ folder.

```csharp title="ServicePagePageTemplateProperties.cs"
    using Kentico.PageBuilder.Web.Mvc.PageTemplates;
    using Kentico.Xperience.Admin.Base.FormAnnotations;
    using TrainingGuides.Web.Features.Shared.OptionProviders;
    using TrainingGuides.Web.Features.Shared.OptionProviders.CornerStyle;
    using TrainingGuides.Web.Features.Shared.OptionProviders.ColorScheme;

    namespace TrainingGuides.Web.Features.FinancialServices;

    public class ServicePagePageTemplateProperties : IPageTemplateProperties
    {
        [DropDownComponent(
            Label = "Color scheme",
            ExplanationText = "Select the color scheme of the template.",
            DataProviderType = typeof(DropdownEnumOptionProvider<ColorSchemeOption>),
            Order = 20)]
        public string ColorScheme { get; set; } = nameof(ColorSchemeOption.TransparentDark);

        [DropDownComponent(
            Label = "Corner style",
            ExplanationText = "Select the corner type of the template.",
            DataProviderType = typeof(DropdownEnumOptionProvider<CornerStyleOption>),
            Order = 30)]
        public string CornerStyle { get; set; } = nameof(CornerStyleOption.Round);
    }
```

### Set up the template

With the view models and service in place, we can create the page template, register it, and serve it from a controller.

> **Tip:** **Find more details here**
>
> This section briefly goes over processes covered in [Build a page template](https://docs.kentico.com/guides/development/developer-kickstart/build-a-page-template.md) step of our Kickstart series. Please refer to it if you'd like more detailed steps and information in the context of a different example.
>
> Note these sections in particular:
>
> - [Add the page template view](https://docs.kentico.com/guides/development/developer-kickstart/build-a-page-template.md#add-the-page-template-view)
> - [Register the page template](https://docs.kentico.com/guides/development/developer-kickstart/build-a-page-template.md#register-the-page-template)
> - [Define a page template controller](https://docs.kentico.com/guides/development/developer-kickstart/build-a-page-template.md#define-a-page-template-controller)

#### Define the view

With the view model and properties in place, you can create a view under _\~/Features/FinancialServices_.

Use the `GetTemplateModel` method to retrieve structured data and display it in a view. We'll expand on this in the future. For now, let's keep it simple.

```cshtml title="ServicePagePageTemplate.cshtml"
    @using TrainingGuides.Web.Features.FinancialServices.Models
    @using TrainingGuides.Web.Features.FinancialServices

    @model TemplateViewModel<ServicePagePageTemplateProperties>

    @{
        var templateModel = Model.GetTemplateModel<ServicePageViewModel>();
    }

    <div>
        <div>
            <h3>@templateModel.NameHtml</h3>
            <p>@templateModel.ShortDescriptionHtml</p>
        </div>
        
        <div>
            <img src="@templateModel.Media.FirstOrDefault()?.FilePath" alt="@templateModel.Media.FirstOrDefault()?.Description"/>
        </div>
    </div>
```

#### Register the template

Create a new file called _ServicePagePageTemplate.cs_ in the _\~/Features/FinancialServices_ folder, and use it to house the registration attribute for the page template.

```csharp title="ServicePagePageTemplate.cs"
    using Kentico.PageBuilder.Web.Mvc.PageTemplates;
    using TrainingGuides;
    using TrainingGuides.Web.Features.FinancialServices;

    [assembly: RegisterPageTemplate(
        identifier: ServicePagePageTemplate.IDENTIFIER,
        name: "Service page template",
        propertiesType: typeof(ServicePagePageTemplateProperties),
        customViewName: "~/Features/FinancialServices/ServicePagePageTemplate.cshtml",
        ContentTypeNames = [ServicePage.CONTENT_TYPE_NAME],
        IconClass = "xp-box")]

    namespace TrainingGuides.Web.Features.FinancialServices;

    public static class ServicePagePageTemplate
    {
        public const string IDENTIFIER = "TrainingGuides.ServicePageTemplate";
    }
```

#### Create the controller

1. In the _\~/Features/FinancialServices_ folder, add a file called _ServicePageController_.
2. Use an `IContentItemRetrieverService` to get the service data from Xperience.
3. Use an instance of the `IServicePageService` we defined [earlier](#add-a-new-.net-service) to convert the `ServicePage` to a `ServicePageViewModel`.
4. Add a new `ServiceFeatureViewModel` containing the service's price, so that it appears in the table.

```csharp title="ServicePageController.cs"
using Kentico.Content.Web.Mvc.Routing;
using Kentico.PageBuilder.Web.Mvc.PageTemplates;
using Microsoft.AspNetCore.Mvc;
using TrainingGuides;
using TrainingGuides.Web.Features.FinancialServices.Models;
using TrainingGuides.Web.Features.FinancialServices.Services;
using TrainingGuides.Web.Features.Shared.Services;

[assembly: RegisterWebPageRoute(
    contentTypeName: ServicePage.CONTENT_TYPE_NAME,
    controllerType: typeof(TrainingGuides.Web.Features.FinancialServices.ServicePageController))]

namespace TrainingGuides.Web.Features.FinancialServices;

public class ServicePageController(
    IContentItemRetrieverService contentItemRetriever,
    IServicePageService servicePageService) : Controller
{
    public async Task<IActionResult> Index()
    {
        var servicePage = await contentItemRetriever.RetrieveCurrentPage<ServicePage>(3);

        var model = await servicePageService.GetServicePageViewModel(servicePage);
        model.Features.Add(
                new ServiceFeatureViewModel
                {
                    Key = "price-from-service-content-item",
                    Name = "Price",
                    LabelHtml = new("Price"),
                    Price = model.Price,
                    ValueHtml = new(string.Empty),
                    FeatureIncluded = false,
                    ValueType = ServiceFeatureValueType.Number,
                    ShowInComparator = true,
                });

        return new TemplateResult(model);
    }
}
```

#### Check your progress

We've been focused on code for a while, so let's double-check that everything is working on the site.

1. Sign in to the Xperience admin and open the _Training guides pages_ channel.
2. Choose one of the _Service page_ pages under the _Services_ page in the tree, and edit the page on the _Page Builder_ tab.
3. Click the page icon in the bottom left corner of the Page Builder frame to configure the Page Builder properties.
4. Choose the _Service page template_, and see how the data from the service page is now displayed according to your templates.

🎬 [Video](https://docs.kentico.com/docsassets/guides/create-versatile-templates-part-1/VideoTemplateProgress.mp4)

> **Tip:** If you're not seeing any output, make sure the service page type is included in the `UsePageBuilder` call during the application's startup sequence, try putting breakpoints in the controller and view. If one or both of them is not being hit, it could indicate an issue in one of the registration attributes for the controller or page template.

## Style the template based on properties

You probably noticed in the last section that the template is not very pretty yet. You'll also find that the dropdowns for color scheme and corner style in the template properties don't change anything.

Let's create a service that maps the options from the dropdown to CSS classes, and a tag helper that uses it to style a `div` element.

### Create a service to retrieve styles

1. Define an `IComponentStyleEnumService` interface in the _\~/Features/Shared/Services_ folder.

2. Include method signatures to retrieve CSS classes based on a `ColorSchemeOption` and `CornerStyleOption` respectively.
   ```csharp title="IComponentStyleEnumService.cs"
   using TrainingGuides.Web.Features.Shared.OptionProviders.ColorScheme;
   using TrainingGuides.Web.Features.Shared.OptionProviders.CornerStyle;

   namespace TrainingGuides.Web.Features.Shared.Services;

   public interface IComponentStyleEnumService
   {
       IEnumerable<string> GetColorSchemeClasses(ColorSchemeOption colorScheme);

       IEnumerable<string> GetCornerStyleClasses(CornerStyleOption cornerStyle);
       ...
   ```

3. Add method signatures to convert the string representation of a selected option to its corresponding enum value, and to retrieve the style for links.
   ```csharp title="IComponentStyleEnumService.cs"
   ...
   CornerStyleOption GetCornerStyle(string cornerStyleString);

   ColorSchemeOption GetColorScheme(string colorSchemeString);

   ColorSchemeOption GetLinkStyle(string linkStyleString);
   }
   ```

4. Implement the interface, mapping the selected options to sets of CSS classes. Use the `Parse` method from the `EnumStringService` we defined [earlier in the series](https://docs.kentico.com/guides/development/page-builder/map-enum-to-dropdown.md) for the string conversion methods.

   ```csharp title="ComponentStyleEnumService.cs"
   using TrainingGuides.Web.Features.Shared.OptionProviders;
   using TrainingGuides.Web.Features.Shared.OptionProviders.ColorScheme;
   using TrainingGuides.Web.Features.Shared.OptionProviders.CornerStyle;

   namespace TrainingGuides.Web.Features.Shared.Services;

   public class ComponentStyleEnumService(IEnumStringService enumStringService) : IComponentStyleEnumService
   {
       public IEnumerable<string> GetColorSchemeClasses(ColorSchemeOption colorScheme) => colorScheme switch
       {
           ColorSchemeOption.Light1 => ["tg-bg-light-1", "tg-txt-dark"],
           ColorSchemeOption.Light2 => ["tg-bg-light-2", "tg-txt-dark"],
           ColorSchemeOption.Light3 => ["tg-bg-light-3", "tg-txt-dark"],
           ColorSchemeOption.Dark1 => ["tg-bg-primary", "tg-txt-light"],
           ColorSchemeOption.Dark2 => ["tg-bg-secondary", "tg-txt-light"],
           ColorSchemeOption.TransparentLight => ["tg-bg-none", "tg-txt-light"],
           ColorSchemeOption.TransparentMedium => ["tg-bg-none", "tg-txt-medium"],
           ColorSchemeOption.TransparentDark => ["tg-bg-none", "tg-txt-dark"],
           _ => [string.Empty],
       };

       public IEnumerable<string> GetCornerStyleClasses(CornerStyleOption cornerStyle) => cornerStyle switch
       {
           CornerStyleOption.Round => ["tg-corner-rnd"],
           CornerStyleOption.VeryRound => ["tg-corner-v-rnd"],
           CornerStyleOption.Sharp => ["tg-corner-shrp"],
           _ => [string.Empty],
       };

       public CornerStyleOption GetCornerStyle(string cornerStyleString) =>
           enumStringService.Parse(cornerStyleString, CornerStyleOption.Round);

       public ColorSchemeOption GetColorScheme(string colorSchemeString) =>
           enumStringService.Parse(colorSchemeString, ColorSchemeOption.TransparentDark);

       public ColorSchemeOption GetLinkStyle(string linkStyleString)
       {
           var colorScheme = enumStringService.Parse(linkStyleString, LinkStyleOption.TransparentDark);
           return (ColorSchemeOption)colorScheme;
       }
   }
   ```

   > **Info:** Each of the classes in this example are small, usually only setting one css property to minimize collisions across classes.
   >
   > Defining the styles themselves is outside the scope of this series, but you can find their definitions in the _\~/scss_ directory, where .scss styles are defined and later compiled to css.
   >
   > If you are using the [main branch of the Training guides repository](https://github.com/Kentico/xperience-by-kentico-training-guides) to follow along, the classes should already exist.

5. Remember to register the service with the dependency injection container in the _TrainingGuides.Web/ServiceCollectionExtensions.cs_ file:
   ```csharp title="ServiceCollectionExtensions.cs"
   ...
   public static void AddTrainingGuidesServices(this IServiceCollection services)
   {
       ...
       services.AddSingleton<IComponentStyleEnumService, ComponentStyleEnumService>();
       ...
   }
   ...
   ```

### Apply the styles with a tag helper

1. Add a new class called `ComponentStyleTagHelper`to the _\~/Features/Shared/Helpers/TagHelpers_ folder.
2. Inherit from the .NET `TagHelper` class, and decorate it with the `HtmlTargetElement` attribute to set its element name to `tg-component-style`. Acquire an `IComponentStyleEnumService` object through dependency injection.
   ```csharp title="ComponentStyleTagHelper.cs"
   ...
   [HtmlTargetElement("tg-component-style")]
   public class ComponentStyleTagHelper(
       IComponentStyleEnumService componentStyleEnumService) : TagHelper
   ...
   ```
3. Give the tag helper string properties called `ColorScheme` and `CornerStyle`, so that objects that store selections as strings, such as page template properties, can use them directly.
   ```csharp title="ComponentStyleTagHelper.cs"
       ...
       public string ColorScheme { get; set; } = string.Empty;
       public string CornerStyle { get; set; } = string.Empty;
       ...
   ```
4. Use a constant to hold the type of tag, in case we need to add more complex logic for determining the tag name in the future.
   ```csharp title="ComponentStyleTagHelper.cs"
   ...
   private const string DIV_TAG = "div";
   ...
   ```
5. In the `Process` method, use the service to retrieve styles for the output tag.
   ```csharp title="ComponentStyleTagHelper.cs"
   ...
   public override void Process(TagHelperContext context, TagHelperOutput output)
   {
       output.TagName = DIV_TAG;

       List<string> cssClasses = [];

       var colorScheme = componentStyleEnumService.GetColorScheme(ColorScheme);
       cssClasses.AddRange(componentStyleEnumService.GetColorSchemeClasses(colorScheme));

       var cornerStyle = componentStyleEnumService.GetCornerStyle(CornerStyle);
       cssClasses.AddRange(componentStyleEnumService.GetCornerStyleClasses(cornerStyle));

       if (cssClasses.Count > 0)
       {
           foreach (string cssClass in cssClasses)
           {
               output.AddClass(cssClass, HtmlEncoder.Default);
           }
       }
   }
   ...
   ```

In the end, your tag helper should look like this:

```csharp title="ComponentStyleTagHelper.cs"
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Mvc.TagHelpers;
using Microsoft.AspNetCore.Razor.TagHelpers;
using TrainingGuides.Web.Features.Shared.Services;

namespace TrainingGuides.Web.Features.Shared.Helpers.TagHelpers;

[HtmlTargetElement("tg-component-style")]
public class ComponentStyleTagHelper(
    IComponentStyleEnumService componentStyleEnumService) : TagHelper
{
    public string ColorScheme { get; set; } = string.Empty;
    public string CornerStyle { get; set; } = string.Empty;

    private const string DIV_TAG = "div";

    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
        output.TagName = DIV_TAG;

        List<string> cssClasses = [];

        var colorScheme = componentStyleEnumService.GetColorScheme(ColorScheme);
        cssClasses.AddRange(componentStyleEnumService.GetColorSchemeClasses(colorScheme));

        var cornerStyle = componentStyleEnumService.GetCornerStyle(CornerStyle);
        cssClasses.AddRange(componentStyleEnumService.GetCornerStyleClasses(cornerStyle));

        if (cssClasses.Count > 0)
        {
            foreach (string cssClass in cssClasses)
            {
                output.AddClass(cssClass, HtmlEncoder.Default);
            }
        }
    }
}
```

### Put the tag helper to use

The tag helper is now ready to be added to the view.
If you return to _ServicePagePageTemplate.cshtml_ you can wrap all of the existing markup with the new tag helper tag, and pass along the color scheme and corner style properties from the page template's configuration.

```cshtml title="ServicePagePageTemplate.cshtml"
    @using TrainingGuides.Web.Features.FinancialServices.Models
    @using TrainingGuides.Web.Features.FinancialServices

    @model TemplateViewModel<ServicePagePageTemplateProperties>

    @{
        var templateModel = Model.GetTemplateModel<ServicePageViewModel>();
    }

    <tg-component-style color-scheme="@Model.Properties.ColorScheme" corner-style="@Model.Properties.CornerStyle">
        <div>
            <div>
                <h3>@templateModel.NameHtml</h3>
                <p>@templateModel.ShortDescriptionHtml</p>
            </div>
            
            <div>
                <img src="@templateModel.Media.FirstOrDefault()?.FilePath" alt="@templateModel.Media.FirstOrDefault()?.Description"/>
            </div>
        </div>
    </tg-component-style>
```

If you sign in to the Xperience administration interface, you'll notice that you can now adjust the appearance of the box containing the service's data by clicking the gear icon in the bottom left of the _Page Builder_ pane.

However, if you set the template to use rounded corners and a colored background, you'll see that the corners of the image do not change to match the style of the parent div. The image may also be an unreasonable size, depending on the file.

![Screenshot of incomplete template with rounded corners set. Also showing the Configure template button.](https://docs.kentico.com/docsassets/guides/create-versatile-templates-part-1/ScreenshotTemplateProgress.png "Screenshot of incomplete template with rounded corners set. Also showing the Configure template button.")

### Create an image tag helper

1. In the _\~/Features/Shared/Helpers/TagHelpers_ folder, add a new self-closing TagHelper called `StyledImageTagHelper` with the tag name `tg-styled-image`. Inject an `IComponentStyleEnumService` instance.
   ```csharp title="StyledImageTagHelper.cs"
   ...
   namespace TrainingGuides.Web.Features.Shared.Helpers.TagHelpers;

   [HtmlTargetElement("tg-styled-image", TagStructure = TagStructure.WithoutEndTag)]
   public class StyledImageTagHelper(
       IComponentStyleEnumService componentStyleEnumService) : TagHelper
   ...
   ```
2. Set up properties in the same manner as the `ComponentStyleTagHelper`, leaving out the `ColorScheme` property this time.
   ```csharp title="StyledImageTagHelper.cs"
   ...

   public string CornerStyle { get; set; } = string.Empty;

   private const string IMG_TAG = "img";
   ...
   ```
3. In the `Process` method, set the `TagMode` of the rendered HTML tag to `TagMode.SelfClosing` and style the image according to the classes returned by the service.
   ```csharp title="StyledImageTagHelper.cs"
   ...
   public override void Process(TagHelperContext context, TagHelperOutput output)
   {
       output.TagName = IMG_TAG;
       output.TagMode = TagMode.SelfClosing;

       List<string> cssClasses = [];

       var cornerStyle = componentStyleEnumService
           .GetCornerStyle(CornerStyle ?? string.Empty);
       cssClasses.AddRange(componentStyleEnumService
           .GetCornerStyleClasses(cornerStyle));

       if (cssClasses.Count > 0)
       {
           foreach (string cssClass in cssClasses)
           {
               output.AddClass(cssClass, HtmlEncoder.Default);
           }
       }
   }
   ...
   ```

The resulting tag helper should look like this:

```csharp title="StyledImageTagHelper.cs"
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Mvc.TagHelpers;
using Microsoft.AspNetCore.Razor.TagHelpers;
using TrainingGuides.Web.Features.Shared.Services;

namespace TrainingGuides.Web.Features.Shared.Helpers.TagHelpers;

[HtmlTargetElement("tg-styled-image", TagStructure = TagStructure.WithoutEndTag)]
public class StyledImageTagHelper(
    IComponentStyleEnumService componentStyleEnumService) : TagHelper
{
    public string CornerStyle { get; set; } = string.Empty;

    private const string IMG_TAG = "img";

    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
        output.TagName = IMG_TAG;
        output.TagMode = TagMode.SelfClosing;

        List<string> cssClasses = [];

        var cornerStyle = componentStyleEnumService.GetCornerStyle(CornerStyle ?? string.Empty);
        cssClasses.AddRange(componentStyleEnumService.GetCornerStyleClasses(cornerStyle));

        if (cssClasses.Count > 0)
        {
            foreach (string cssClass in cssClasses)
            {
                output.AddClass(cssClass, HtmlEncoder.Default);
            }
        }
    }
}
```

The tag helper can be used in the view like this:

```cshtml title="Tag helper usage (e.g., in ServicePagePageTemplate.cshtml)"
@foreach (var asset in templateModel.Media)
{
    <tg-styled-image src="@asset.FilePath" 
        alt="@asset.Description" 
        corner-style="@Model.Properties.CornerStyle" 
        class="c-product-img object-fit-cover" />
}
```

## Complete the template

Comparing the current template to the mockup from the start of this series, you'll notice that the Service features table is missing.

![Mockup of a service detail page](https://docs.kentico.com/docsassets/guides/create-versatile-templates-part-1/WidgetMockups3_Plain.png "Mockup of a service detail page")

Its data is already in the template model, so it should be fairly straightforward to render to the page.

However, if you inspect the _ServiceFeature_ content type, you may notice that its value can come from multiple different places depending on the selected _ServiceFeatureValueType_.

Let's make one last tag helper, so that we don't need to clutter the view with conditionals.

### Add a service feature value tag helper

Since this one is specifically related to Service features, and not general styling, we can put it in the _\~/Features/FinancialServices/TagHelpers_ folder.

1. Create a new self-closing tag helper called `ServiceFeatureValueTagHelper`, with the tag name `tg-service-feature-value`.
2. Add a `ServiceFeatureViewModel` property and a constant for the `span` tag.
3. In the `Process` method, determine the formated value depending on the provided `ServiceFeatureValueType` from the view model and render it.

```csharp title="ServiceFeatureValueTagHelper.cs"
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Razor.TagHelpers;

using TrainingGuides.Web.Features.FinancialServices.Models;

namespace TrainingGuides.Web.Features.FinancialServices.TagHelpers;

/// <summary>
/// Formats Service feature value based on its type.
/// </summary>
//This TagStructure allows the helper to be called with a single self-closing tag in razor views.
[HtmlTargetElement("tg-service-feature-value", TagStructure = TagStructure.WithoutEndTag)]
public class ServiceFeatureValueTagHelper : TagHelper
{
    public ServiceFeatureViewModel? Feature { get; set; }

    private const string SPAN_TAG = "span";

    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
        output.TagName = SPAN_TAG;
        // Make sure to set the output's  TagMode to StartTagAndEndTag.
        // This ensures that even though the tag helper is called with a single tag,
        // the rendered output closes the `span` tag that wraps the value.
        output.TagMode = TagMode.StartTagAndEndTag;

        string? formattedValue = Feature?.ValueType switch
        {
            ServiceFeatureValueType.Text => Feature.ValueHtml.Value,
            ServiceFeatureValueType.Number => "$" + Feature.Price.ToString("n2"),
            ServiceFeatureValueType.Boolean => Feature.FeatureIncluded ? "✔" : "-",
            _ => string.Empty
        };

        output.Content.SetHtmlContent(new HtmlString(formattedValue));
    }
}
```

### Add the service features list to the template view

1. In the _\~/Features/FinancialServices/ServicePagePageTemplate.cshtml_ folder, add a `foreach` loop to cycle through the service features.
2. Retrieve each feature's `LabelHtml` directly, and use the new tag helper to display its value
3. Wrap the whole thing in the `tg-component-style` tag helper, setting `color-scheme` directly and `corner-style` based on the template properties.

```cshtml title="ServicePagePageTemplate.cshtml"
...
<tg-component-style color-scheme="@ColorSchemeOption.Light1" corner-style="@Model.Properties.CornerStyle"  class="c-table">
    @foreach (var feature in templateModel.Features)
    {
        <div>
            <div>@feature.LabelHtml</div>
            <div>
                <tg-service-feature-value feature="@feature"/>
            </div>
        </div>
    }
</tg-component-style>
...
```

### Finish styling the template.

Now if you run the site, you'll see all the data, but it's still not formatted in a way that matches the mockup.

![Screenshot of unfinished template](https://docs.kentico.com/docsassets/guides/create-versatile-templates-part-1/ScreenshotTemplateProgress2.png "Screenshot of unfinished template")

Add CSS styles and any necessary elements to make the template more visually appealing.

```cshtml title="ServicePagePageTemplate.cshtml"
@using TrainingGuides.Web.Features.FinancialServices.Models
@using TrainingGuides.Web.Features.FinancialServices
@using TrainingGuides.Web.Features.Shared.OptionProviders.ColorScheme

@model TemplateViewModel<ServicePagePageTemplateProperties>

@{
    var templateModel = Model.GetTemplateModel<ServicePageViewModel>();
}

<tg-component-style color-scheme="@Model.Properties.ColorScheme" corner-style="@Model.Properties.CornerStyle">
    <div>
        <div>
            <h3>@templateModel.NameHtml</h3>
            <p>@templateModel.ShortDescriptionHtml</p>
        </div>
        
        @foreach (var asset in templateModel.Media)
        {
            <tg-styled-image src="@asset.FilePath" 
                alt="@asset.Description" 
                corner-style="@Model.Properties.CornerStyle" 
                class="c-product-img object-fit-cover" />
        }

        <tg-component-style color-scheme="@ColorSchemeOption.Light1" corner-style="@Model.Properties.CornerStyle"  class="c-table">
            @foreach (var feature in templateModel.Features)
            {
                <div>
                    <div>@feature.LabelHtml</div>
                    <div>
                        <tg-service-feature-value feature="@feature"/>
                    </div>
                </div>
            }
        </tg-component-style>
    </div>
</tg-component-style>
```

At the end of it all, you should be able to achieve this by configuring your template.

![Screenshot of unfinished, but nicely formatted template](https://docs.kentico.com/docsassets/guides/create-versatile-templates-part-1/ScreenshotTemplateProgress3.png "Screenshot of unfinished, but nicely formatted template")

## What's next?

[Part 2 of the \*Create versatile templates\* guide](https://docs.kentico.com/guides/development/page-builder/create-versatile-templates-part-2.md) covers the process of adding toggleable _Page Builder_ functionality to this template, so that your editors can customize its content in special cases.

If you started in this guide, we recommend circling back to check out these guides form earlier in the series:

- [Meet business requirements with Page Builder](https://docs.kentico.com/guides/development/page-builder/meet-requirements-with-page-builder.md), a guide that analyzes how and when to apply Page Builder to business scenarios
- [Add a custom dropdown provider for administration components](https://docs.kentico.com/guides/development/page-builder/map-enum-to-dropdown.md), a guide that demonstrates the process of creating custom providers that provide options to dropdown menus in Page Builder component properties.
