Module: Page Builder
4 of 15 Pages
Model a product page template
In this guide, we’ll walk through the process of creating a page template to match this product 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 Product content type, which is served in the web channel through the Product 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 product page.
If you look at the Product content type in Xperience, you’ll notice that products have collections of Benefit and Product feature content items. Start by making view models for these content types, so we can use them in the product.
- In the ~/Features/Products/Models folder, add a file called ProductFeatureViewModel.cs.
- Create properties corresponding to the content type’s fields.
C#ProductFeatureViewModel.cs
using Microsoft.AspNetCore.Html; namespace TrainingGuides.Web.Features.Products.Models; public class ProductFeatureViewModel { 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 ProductFeatureValueType ValueType { get; set; } public bool ShowInComparator { get; set; } }
- Include a
ProductFeatureValueType
enumeration to represent the value of theValueType
field at the bottom of the file.C#ProductFeatureViewModel.cs... //(After the end of the ProductFeatureViewModel class's scope) public enum ProductFeatureValueType { //These integers correspond to the data source in the Product feature content type, defined in the Xperience admin interface. Text = 0, Number = 1, Boolean = 2 }
- Add a method to the
ProductFeatureViewModel
class to get the corresponding enum value from an integer.C#ProductFeatureViewModel.cs... private static ProductFeatureValueType GetValueType(string value) { if (string.IsNullOrEmpty(value)) return ProductFeatureValueType.Text; if (int.TryParse(value, out int id)) return (ProductFeatureValueType)id; return ProductFeatureValueType.Text; } ...
- Add a static
GetViewModel
method to create and populate aProductFeatureViewModel
based on aProductFeature
object.ProductFeature
is a generated class, created by the Xperience code generation tool.C#ProductFeatureViewModel.cs... public static ProductFeatureViewModel GetViewModel(ProductFeature feature) => new() { Key = feature.ProductFeatureKey, Name = feature.SystemFields.ContentItemName, LabelHtml = new(feature.ProductFeatureLabel), Price = feature.ProductFeaturePrice, ValueHtml = new(feature.ProductFeatureValue), FeatureIncluded = feature.ProductFeatureIncluded, ValueType = GetValueType(feature.ProductFeatureValueType), ShowInComparator = feature.ProductFeatureShowInComparator == "1" }; ...
That takes care of Product feature, so let’s move on to the Benefit content type.
- Create a new class called
BenefitViewModel
in the ~/Features/Shared/Models folder. - Add properties that correspond to the Benefit content type’s fields, utilizing the existing
AssetViewModel
class 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
GetViewModel
method to retrieve aBenefitViewModel
from an object of the generatedBenefit
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(), };
using Microsoft.AspNetCore.Html;
namespace TrainingGuides.Web.Features.Products.Models;
public class ProductFeatureViewModel
{
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 ProductFeatureValueType ValueType { get; set; }
public bool ShowInComparator { get; set; }
public static ProductFeatureViewModel GetViewModel(ProductFeature feature) => new()
{
Key = feature.ProductFeatureKey,
Name = feature.SystemFields.ContentItemName,
LabelHtml = new(feature.ProductFeatureLabel),
Price = feature.ProductFeaturePrice,
ValueHtml = new(feature.ProductFeatureValue),
FeatureIncluded = feature.ProductFeatureIncluded,
ValueType = GetValueType(feature.ProductFeatureValueType),
ShowInComparator = feature.ProductFeatureShowInComparator == "1"
};
private static ProductFeatureValueType GetValueType(string value)
{
if (string.IsNullOrEmpty(value))
return ProductFeatureValueType.Text;
if (int.TryParse(value, out int id))
return (ProductFeatureValueType)id;
return ProductFeatureValueType.Text;
}
}
public enum ProductFeatureValueType
{
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 Product page view model
With these in place, let’s move on to the ProductPageViewModel
.
- Create a
ProductPageViewModel
class in the ~/Features/Products/Models folder. - Inherit from the
PageViewModel
class.C#ProductPageViewModel.csnamespace TrainingGuides.Web.Features.Products.Models; public class ProductPageViewModel : PageViewModel { }
Notice theLink
property of thePageViewModel
class, which can be re-used by other page types in the future. - Add properties that mirror the fields of the Product content type, with collections of
ProductFeatureViewModel
andBenefitViewModel
objects where appropriate.C#ProductPageViewModel.csusing Microsoft.AspNetCore.Html; using TrainingGuides.Web.Features.Shared.Models; ... public class ProductPageViewModel : 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<ProductFeatureViewModel> Features { get; set; } = []; public List<BenefitViewModel> Benefits { get; set; } = []; }
Add a supporting service
You might notice thatProductViewModel
doesn’t have a GetViewModel
method like ProductFeatureViewModel
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
IProductPageService
to ~/Features/Products/Services.Include a method signature
GetProductPageViewModel
with the following parameters:- The
ProductPage
object to base the view model on. - Optional boolean values to indicate which values to include.
- Optional settings for the call to action link
C#IProductPageService.cs
using TrainingGuides.Web.Features.Products.Models; namespace TrainingGuides.Web.Features.Products.Services; public interface IProductPageService { public Task<ProductPageViewModel> GetProductPageViewModel( ProductPage? productPage, bool getMedia = true, bool getFeatures = true, bool getBenefits = true, string callToAction = "", string callToActionLink = "", bool openInNewTab = true, bool getPrice = true); }
- The
Add a
ProductPageService
class that implements this interface.C#ProductPageService.cs... public class ProductPageService : IProductPageService ...
Use dependency injection to acquire an
IWebPageUrlRetriever
object.C#ProductPageService.cs... private readonly IWebPageUrlRetriever webPageUrlRetriever; public ProductPageService(IWebPageUrlRetriever webPageUrlRetriever) { this.webPageUrlRetriever = webPageUrlRetriever; } ...
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 theIWebPageUrlRetriever
that includes exception handling.Retrieve
method may throw when it cannot find a page.Implement the
GetProductPageViewModel
method.C#ProductPageService.csusing Microsoft.IdentityModel.Tokens; using TrainingGuides.Web.Features.Products.Models; using TrainingGuides.Web.Features.Shared.Models; namespace TrainingGuides.Web.Features.Products.Services; public class ProductPageService : IProductPageService { private readonly IWebPageUrlRetriever webPageUrlRetriever; public ProductPageService(IWebPageUrlRetriever webPageUrlRetriever) { this.webPageUrlRetriever = webPageUrlRetriever; } /// <summary> /// Creates a new instance of <see cref="ProductPageViewModel"/>, setting the properties using ProductPage given as a parameter. /// </summary> /// <param name="productPage">Corresponding Product page object.</param> /// <returns>New instance of ProductPageViewModel.</returns> public async Task<ProductPageViewModel> GetProductPageViewModel( ProductPage? productPage, 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 ProductPage is null. if (productPage == null) { return new ProductPageViewModel(); } //Use the IWebPageUrlRetriever to get the URL of the product page. string url = callToActionLink.IsNullOrEmpty() ? (await webPageUrlRetriever.Retrieve(productPage)).RelativePath : callToActionLink; //Make sure to account for the boolean parameters as you construct the view model. return new ProductPageViewModel { NameHtml = new(productPage.ProductPageProduct.FirstOrDefault()?.ProductName), ShortDescriptionHtml = new(productPage.ProductPageProduct.FirstOrDefault()?.ProductShortDescription), DescriptionHtml = new(productPage.ProductPageProduct.FirstOrDefault()?.ProductDescription), Media = getMedia ? productPage.ProductPageProduct.FirstOrDefault()? .ProductMedia.Select(AssetViewModel.GetViewModel)? .ToList() ?? [] : [], Link = new LinkViewModel() { Name = productPage.ProductPageProduct.FirstOrDefault()?.ProductName ?? string.Empty, LinkUrl = url, CallToAction = callToActionText.IsNullOrEmpty() ? string.Empty : callToActionText, OpenInNewTab = openInNewTab }, Features = getFeatures ? productPage.ProductPageProduct.FirstOrDefault()? .ProductFeatures .Select(ProductFeatureViewModel.GetViewModel) .ToList() ?? [] : [], Benefits = getBenefits ? productPage.ProductPageProduct.FirstOrDefault()? .ProductBenefits .Select(BenefitViewModel.GetViewModel) .ToList() ?? [] : [], Price = getPrice ? productPage.ProductPageProduct .FirstOrDefault()?.ProductPrice ?? 0 : 0, }; } }
Register the
ProductPageService
with the dependency injection container in TrainingGuides.Web/ServiceCollectionExtensions.cs:C#ServiceCollectionExtensions.cs... public static void AddTrainingGuidesServices(this IServiceCollection services) { ... services.AddSingleton<IProductPageService, ProductPageService>(); ... } ...