Module: Page Builder

4 of 18 Pages

Model a product page template

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

See the first guide in this series for an overview of the mockup, and a breakdown of how to meet business requirements with Page Builder.

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.
    C#
    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.
    C#
    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.
    C#
    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.
    ServiceFeature is a generated class, created by the Xperience code generation tool.
    C#
    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.
    C#
    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.
    C#
    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:
C#
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
}
C#
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.
    C#
    ServicePageViewModel.cs
    
         namespace TrainingGuides.Web.Features.FinancialServices.Models;
    
         public class ServicePageViewModel : PageViewModel { }
     
    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.
    C#
    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.

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
      C#
      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.

    C#
    ServicePageService.cs
    
         ...
         public class ServicePageService : IServicePageService
         ...
     
  4. Use primary constructor injection in the class’s definition to acquire an IWebPageUrlRetriever object.

    C#
    ServicePageService.cs
    
         ...
         public class ServicePageService(
             IWebPageUrlRetriever webPageUrlRetriever) : IServicePageService
         ...
     

    The code samples in this guide rely on a decorated version 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.

    C#
    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:

    C#
    ServiceCollectionExtensions.cs
    
     ...
     public static void AddTrainingGuidesServices(this IServiceCollection services)
     {
         ...
         services.AddSingleton<IServicePageService, ServicePageService>();
         ...
     }
     ...