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:

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.
- In the ~/Features/FinancialServices/Models folder, add a file called ServiceFeatureViewModel.cs.
- 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; } } - Include a
ServiceFeatureValueTypeenumeration to represent the value of theValueTypefield 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 } - Add a method to the
ServiceFeatureViewModelclass 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; } ... - Add a static
GetViewModelmethod to create and populate aServiceFeatureViewModelbased on aServiceFeatureobject.ServiceFeatureis 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.
- Create a new class called
BenefitViewModelin the ~/Features/Shared/Models folder. - Add properties that correspond to the Benefit content type’s fields, utilizing the existing
AssetViewModelclass for the asset.C#BenefitViewModel.csusing 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(); } - Add a static
GetViewModelmethod to retrieve aBenefitViewModelfrom an object of the generatedBenefitclass.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(), };
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
}
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.
- Create a
ServicePageViewModelclass in the ~/Features/FinancialServices/Models folder. - Inherit from the
PageViewModelclass.C#ServicePageViewModel.csnamespace TrainingGuides.Web.Features.FinancialServices.Models; public class ServicePageViewModel : PageViewModel { }Notice theLinkproperty of thePageViewModelclass, which can be re-used by other page types in the future. - Add properties that mirror the fields of the Service content type, with collections of
ServiceFeatureViewModelandBenefitViewModelobjects where appropriate.C#ServicePageViewModel.csusing 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.
Add an interface called
IServicePageServiceto ~/Features/FinancialServices/Services.Include a method signature
GetServicePageViewModelwith the following parameters:- The
ServicePageobject to base the view model on. - Optional boolean values to indicate which values to include.
- 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); }
- The
Add a
ServicePageServiceclass that implements this interface.C#ServicePageService.cs... public class ServicePageService : IServicePageService ...Use primary constructor injection in the class’s definition to acquire an
IWebPageUrlRetrieverobject.C#ServicePageService.cs... public class ServicePageService( IWebPageUrlRetriever webPageUrlRetriever) : IServicePageService ...The code samples in this guide rely on a decorated version of
If you do not plan to use a similar customization, make sure to handle errors that theIWebPageUrlRetrieverthat includes exception handling.Retrievemethod may throw when it cannot find a page.Implement the
GetServicePageViewModelmethod.C#ServicePageService.csusing 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, }; } }Register the
ServicePageServicewith the dependency injection container in TrainingGuides.Web/ServiceCollectionExtensions.cs:C#ServiceCollectionExtensions.cs... public static void AddTrainingGuidesServices(this IServiceCollection services) { ... services.AddSingleton<IServicePageService, ServicePageService>(); ... } ...