Content modeling is part of the foundation of any successful Kentico Xperience application (and is often under-valued โน).
It's up to software developers to implement a content model in Xperience's Page Types and other content management features...
- Related Pages or Linked Pages?
- Normalized or de-normalized data?
- Attachments or the Media Library?
- Page Type names, Scopes, and Parent/Child hierarchies?
- Content + design or design agnostic content?
There are many important decisions to make, but once some content has been modeled, developers are always faced with another... how do we structure our code to best align with the content model ๐คท๐ฝโโ๏ธ?
What do we do with content models that inevitably include optional or missing data?
๐ What Will We Learn?
- Modeling code to match Content
- An Example 'Call To Action' Page Type
- Using nullable reference types
- Where do we fall short with nullable reference types?
๐ Modeling the Models
We like it when there are well defined rules around how our code works, and when our code works with data, we also prefer that data to be orderly and predictable ๐ฉ๐ปโ๐ฌ.
When using Kentico Xperience, we want to create Page Types that most accurately model the content (data) being stored in the application.
The field names should match the words the stakeholders use and we don't want overly complex Page Types, with too many fields, that make content management difficult and violate the SOLID Design principles.
At the same time, we want to be flexible where it counts, making content management easier...
๐คธ๐พโโ๏ธ Flexible Page Types: A Call To Action
Let's imagine we heed to model content that represents a call to action.
Some call to actions in our site might have images, while others might not.
If we are modeling our content with precision, we will up with two different Page Types for our call to action - one with an image field, and one without.
However, this probably isn't a good approach...
Why? ๐ค
Any given Call To Action might have an image added or removed from it in the future, and we don't want to force the content managers to create/delete a Page just to make this change - that's a bad (content management) user experience.
It's also likely we'll want to display both imaged and image-less call to actions in the same places on the site, without having to query for the data of two different Page Types.
The existence or absence of an Image from a Call to Action is part of the content model, not something we should try to avoid ๐ฎ.
Instead, we should allow flexibility in the Page Type and handle the existing or missing image in our code ๐๐พ.
โ Displaying Optional Values
So, let's assume we've created a Page Type, CallToAction
, that has a set of optional fields for our image (I'm leaving out a lot of the auto-generated Page Type code for brevity):
public class CallToAction : TreeNode
{
/// A checkbox โ
in the form
[DatabaseField]
public bool HasImage
{
get => ValidationHelper.GetBoolean(
GetValue("HasImage"), false);
set => SetValue("HasImage", value);
}
/// A URL Selector form control
[DatabaseField]
public string ImagePath
{
get => ValidationHelper.GetString(
GetValue("ImagePath"), "");
set => SetValue("ImagePath", value);
}
/// A Text field form control
[DatabaseField]
public string ImageAltText
{
get => ValidationHelper.GetString(
GetValue("ImageAltText"), "");
set => SetValue("ImageAltText", value);
}
// Other Call To Action fields ...
}
Now, let's write our C# to retrieve this content and present it in a Razor View:
I am putting data access code in an MVC
Controller
for brevity. In practice these operations should be behind an abstraction ... not in the presentation layer of our application ๐.
public class HomeController : Controller
{
private readonly IPageRetriever pageRetriever;
private readonly IPageDataContextRetriever contextRetriever;
// ... Constructor
public ActionResult Index()
{
var home = contextRetriever.Retrieve().Page;
var cta = retriever.Retrieve<CallToAction>(
q => q.WhereEquals("NodeGUID", home.Fields.CTAPage))
.FirstOrDefault();
var viewModel = // What do we do here?
return View(viewModel);
}
}
Retrieving our content is easy with Xperience's IPageRetriever
service, but what about creating our View Model that we'll use to send our Call to Action content to the Razor View? How should this class be defined
Let's try modeling it ๐:
public class HomeViewModel
{
public string Title { get; set; }
public bool HasImage { get; set; }
public string ImagePath { get; set; }
public string ImageAltText { get; set; }
}
I have a few concerns ๐ about this model:
-
HasImage
needs to be checked before trying to render the image, but nothing about the model expresses or enforces this -
ImagePath
could have a value whileHasImage
isfalse
if the content manager unchecked the "Has Image?" checkbox without removing the "Image Path" value -
HasImage
is about whether or not some content should be displayed, whereImagePath
andImageAltText
are the actual pieces of content
So, let's try again to resolve these issues:
public class HomeViewModel
{
public string Title { get; set; }
public bool HasImage { get; set; }
public ImageViewModel Image { get; set; }
}
public class ImageViewModel
{
public string Path { get; set; }
public string AltText { get; set; }
}
Now we are grouping the content in a new ImageViewModel
class and the HomeViewModel
has an instance of it.
However, here we're writing our code with C# 8.0 (or newer), so let's use nullable reference types and get rid of the HasImage
property:
Here's a full step by step guide to enabling nullable reference types in your application.
While we're at it, we should set all warnings from nullable reference types as errors, otherwise it's easy to ignore the benefit they bring ๐.
public class HomeViewModel
{
public string Title { get; set; }
public ImageViewModel? Image { get; set; }
}
public class ImageViewModel
{
public string Path { get; set; }
public string AltText { get; set; }
}
Now, if CallToAction.HasImage
is false
, the HomeViewModel.Image
value will be null
:
var viewModel = new HomeViewModel
{
Title = home.Fields.Title,
Image = cta.HasImage
? new ImageViewModel
{
Path = cta.ImagePath,
AltText = cta.AltText
}
: null;
}
In our Razor view we can add a check for a null
value and render the image conditionally:
Note: I use the C# 9.0
not
operator for pattern matching here. The C# 8.0 alternative isModel.Image is object
.
@model HomeViewModel
<h1>@Model.Title</h1>
@if (Model.Image is not null)
{
<img src="@(Model.Image.Path)" alt="@(Model.Image.AltText)" />
}
This appears to express the intent of the functionality and the content being modeled ๐ช๐ฝ.
๐ฆ What Does the Docs Say?
The Kentico Xperience documentation discusses using nullable properties for non-required fields.
This is a neat idea, but has a few too many caveats at the moment:
- Generated Page Type classes don't include nullable properties
- Re-generating the Page Type class will remove customized nullable properties
- Putting nullable types on a separate
partial
class doesn't enforce their use (the non-nullable ones are still accessible) - Only nullable value types are supported if the Page Type class is shared between ASP.NET Core and the Web Forms Content Management application (.NET 4.8 doesn't support nullable reference types)
At the same time, I think nullable types in generated code is definitely something to keep an eye on ๐ as Xperience moves to run fully on ASP.NET Core in the next version of the platform, Odyssey.
๐ฃ A Fork in the Road: Missing Data
We could say we've solved this problem and head home for dinner ๐ฅ!
But I think we should consider another common variation of this issue...
We decided to use nullable reference types to represent an optional value (or set of values), but what about data that should exist but is missing (unintentionally)?
If we look back at our HomeController
example, we can see that we found our CallToAction
content by querying for the node with a NodeGUID
that matched the value of our Home Page's CTAPage
field:
var home = contextRetriever.Retrieve().Page;
var cta = retriever.Retrieve<CallToAction>(
q => q.WhereEquals("NodeGUID", home.Fields.CTAPage))
.FirstOrDefault();
This means there's a field in the Home
Page Type that holds a reference to the CallToAction
Page in the content tree. But what happens if that Page is deleted ๐คทโโ๏ธ
Currently, our application would throw an exception because we try to access the HasImage
property on a cta
variable that has the value of null
๐คฆ๐ฝ.
The C# LINQ method .FirstOrDefault()
knows it's returning a reference type and the "default" value of a reference type is null
, so it's marked as possibly returning a null
value (CallToAction?
).
What Is Null? Baby, Don't Hurt Me!
Jokes aside, we are now in a situation where null
now represents two different ideas.
Let's look at our HomeViewModel
again:
public class HomeViewModel
{
public string Title { get; set; }
public ImageViewModel? Image { get; set; }
}
public class ImageViewModel
{
public string Path { get; set; }
public string AltText { get; set; }
}
The nullable public ImageViewModel? Image { get; set; }
represents an optional value - it's ok if it's not there, and we should account for that.
However, with the .NET LINQ .FirstOrDefault()
method, a null
value doesn't have any meaning except what we decide it should have... and in that case, it's definitely not an optional value.
When we can't find the CallToAction
Page with our query, we might want to treat that as an error and log it to the Xperience event log. We might also want to display a message to the Content Manager when the View is rendered in the Page tab ๐ค.
Here are some important questions that should be answerable if we want to design robust and maintainable applications:
- In our code, where is the value
null
found and what does it mean? - If I'm a new developer coming into this code base and I see a method returning a nullable value, do
null
results imply something I should expect? (Should I log/display an error or not?) - Is
null
a normal value that represents the content model (intentionally missing) or is it the result of bad data (unintentionally missing)?
Unfortunately, when null
represents two different things at the same time, we will always have a hard time answering these questions or we'll arrive at inconsistent answers over time and across the development team ๐ซ.
๐คจ Conclusion
We've seen how the process of content modeling impacts the way we write our code, and this is a good thing!
Accounting for optional or missing data is the responsibility of software developers, especially when working with content in a platform like Kentico Xperience.
The C# nullable reference type language feature is a great way to expose the hidden use of null
in our code, and I recommend we all enable in all of our ASP.NET Core projects. However, since it is a low level language feature, it doesn't have much meaning on its own, besides 'there is no value here'.
In my next post we'll look at how combining nullable reference types with the null object pattern can get us closer to modeling content accurately in our code.
As always, thanks for reading ๐!
References
- What is Content Modeling (Kentico Kontent docs)
- Defining Website Content Structure (Kentico Xperience docs)
- Planning Content Management (Kentico Xperience Advantage guide)
- Ubiquitous Language as part of Domain Driven Development
- Creating Page Types (Kentico Xperience docs)
- C# Nullable Reference Types (Microsoft Docs)
- Guide to enabling nullable reference types
- Treat nullable warnings as errors
We've put together a list over on Kentico's GitHub account of developer resources. Go check it out!
If you are looking for additional Kentico content, checkout the Kentico or Xperience tags here on DEV.
Or my Kentico Xperience blog series, like:
Top comments (0)