---
title: Model a product page template
---

> 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/modules/model-template-data/WidgetMockups3_Plain.png "Mockup of a service detail page")

See the [first guide in this series](https://docs.kentico.com/modules/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).

## 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>();
       ...
   }
   ...
   ```
