Custom Data Models in the Strong Typed APIs in 2sxc 17+
The Copilot Data Model Generator will generate classes for each Content-Type in the AppCode.Data
namespace.
The details are documented there.
This guide focuses on how to use these classes in your Razor and C# code,
and how to extend them to best fit your needs.
Setup for this Guide
This guide assumes you have the following Content-Types in your app:
Product
with fields likeTitle
,Teaser
,Packshot
(image),OnSale
(bool),Categories
(of typeCategory
),Company
(typeCompany
) andDescription
(HTML)Company
with fields likeName
,Logo
(image),Description
andWebsite
(URL)Category
with fields likeName
,Description
andParent
(typeCategory
)
We have used Copilot to generate the files, so our folder structure looks like this:
/AppCode/Data/Product.Generated.cs
/AppCode/Data/Company.Generated.cs
/AppCode/Data/Category.Generated.cs
Tip
The auto-generated files should never be modified, as you will want to re-generate and overwrite them again.
What's in the Auto-Generated Classes?
The auto-generated files contain two classes: one is the auto-generated class, and the other is the base class.
There is some extensive reasoning to this, best check out the Copilot documentation for details.
For example, the Product.Generated.cs
contains the following classes:
AppCode.Data.AutoGenerated.ZagProduct
- this one is specially named, so you won't accidentally use it in IntelliSense and so the property names never conflict with the class name.AppCode.Data.Product
- this one ispartial
so you can extend it in a separate file later on.
Internally, the ZagProduct
has a lot of code which looks like this:
/// <summary>
/// Title as string. <br/>
/// For advanced manipulation like scrubHtml, use .String("Title", scrubHtml: true) etc.
/// </summary>
public bool OnSale => _item.Bool("OnSale");
/// <summary>
/// Packshot as link (url). <br/>
/// To get the underlying value like 'file:72' use String("Packshot")
/// </summary>
public string Packshot => _item.Url("Packshot");
/// <summary>
/// Get the file object for Packshot - or null if it's empty or not referencing a file.
/// </summary>
public IFile PackshotFile => _item.File("Packshot");
/// <summary>
/// Get the folder object for Packshot.
/// </summary>
public IFolder PackshotFolder => _item.Folder("Packshot");
/// <summary>
/// Title as string. <br/>
/// For advanced manipulation like scrubHtml, use .String("Title", scrubHtml: true) etc.
/// </summary>
public new string Title => _item.String("Title", fallback: "");
/// <summary>
/// Fields as list of Category.
/// </summary>
/// <remarks>
/// Generated to return child-list child because field settings had Multi-Value=true. The type Category was specified in the field settings.
/// </remarks>
/// <returns>
/// An IEnumerable of specified type, but can be empty.
/// </returns>
public IEnumerable<Category> Categories => _categories ??= _item.Children<Category>("Categories");
private IEnumerable<Category> _categories;
Using the Strong Typed Classes in Razor or C# Files
TODO:
Modify the Strong Typed Classes
As mentioned before, you don't want to touch the generated files. Instead, you should create a new file with the same name, but without the .Generated
part.
Here's an example of a /AppCode/Data/Product.cs
file which extends the auto-generated class Product
with some custom properties and methods:
namespace AppCode.Data
{
public partial class Product
{
// Add your own properties and methods here
}
}
The compiler will then combine the auto-generated Product
with yours.
Example: Add a Main Category Property
Now let's say that the first category is used a lot, so you want a MainCategory
property which is just a shortcut to the first category.
This is how you would do it:
namespace AppCode.Data
{
public partial class Product
{
public Category MainCategory => Categories.FirstOrDefault();
}
}
That's it! You could of course optimize it, so that it only does this once, and add some comments, like this:
namespace AppCode.Data
{
public partial class Product
{
/// <summary>
/// The main / primary category
/// </summary>
public Category MainCategory => _mainCategory ??= Categories.FirstOrDefault();
private Category _mainCategory;
}
}
Doing this is a bit better for performance, and gives you intellisense when you hover over MainCategory
in your Razor files.
Example: Add a CssClasses property
Now let's say you want to add a CssClasses
property which should be the same in many Razor views, but change based on the OnSale
status.
This is how you would do it:
namespace AppCode.Data
{
public partial class Product
{
public string CssClasses => OnSale ? "on-sale" : "";
}
}
Example: Override the Title Property
Imagine you want the title to always have a 🌟 emoji at the end, when it's on sale:
namespace AppCode.Data
{
public partial class Product
{
public new string Title => base.Title + (OnSale ? " 🌟" : "");
}
}
Note the word new
- this is because the Title
property is already defined in the auto-generated class, and we're just extending it.
It's not necessary, but it's a good practice to make it clear that you're replacing the Title
.
Example: Add a Typed Presentation property
Every Typed-Item has a Presentation
property which is an ITypedItem
- and it can also be null
.
But let's imagine that you always use Product
together with ProductPresentation
so you want to make it typed as well.
This is how you would do it:
namespace AppCode.Data
{
public partial class Product
{
public new ProductPresentation Presentation => _presentation ??= As<ProductPresentation>(base.Presentation);
private ProductPresentation _presentation;
}
}
Note the new
- as were replacing the Presentation
property, and base.Presentation
- without the base
prefix, you would create an infinite loop.
Super-Extending the Strong Typed Classes
The previous examples all worked because every new property we created used existing data to determine the new value.
In some cases, you would like to add a property which is not based on existing data, but on other data.
For example, you may want to add a IsRecommended
based on the current User.
This is a bit more tricky, because your classes are based on CustomItem
and does not know about the current user.
Our recommended way is to do the following:
- Add a property for the external information - eg.
User
- to be passed in from the Razor file - Use this property to calculate the new property
Here's an example:
namespace AppCode.Data
{
public partial class Product
{
public ICmsUser User { get; set; }
public bool IsRecommended => User.IsSystemAdmin;
}
}
When converting the data to a strong-typed object, you would then pass in the user like this:
@{
var product = AsList<Product>(MyItems)
.Select(i => {
i.User = MyUser; // Set this property, so it can be used in calculated properties of Link
return i;
})
.ToList();
}
APIs you should Know About
All your custom data objects will be based on CustomItem which has a lot of methods to get data. These are some you will need to know about:
_item
is the underlying object which is being wrapped. You can always access it's methods such as_item.String("Teaser")
or_item.Children("Categories")
As<T>(object)
will convert an item to a strong-typed objectAsList<T>(object)
will convert a list of items to a list of strong-typed objectsChild<T>(name)
will get a child item and convert it to a strong-typed objectChildren<T>(name)
will get a list of child items and convert them to a list of strong-typed objectsParent<T>(type: name, field: name)
will get the parent item (or the first parent in all the matching parents) and convert it to a strong-typed objectParents<T>(type: name, field: name)
will get the list of parent items and convert them to a list of strong-typed objects
History
- HotBuild introduced in v17.00 but was enhanced a lot in each additional version
- Copilot was introduced in 17.03 and is still getting improved
- AppCode was introduced as
ThisApp
but renamed to AppCode in 17.02