Overview Custom Content Settings (BusinessObjectCustomSetting<T>) Overview Custom Settings are extra settings objects that sit alongside the site's Site Settings. They work the same way: a structured content type registered with a domain, with only one published record available at a time. The difference is that you can have as many as you need, each with its own class, editor and menu entry. Use them when a module, add-on or feature needs its own configuration (an API key, a default page size, a header message) and you don't want to change the tenant's Site Settings class to hold it. Custom Settings consist of: A class that inherits from BusinessObjectCustomSetting<T> A user-control that inherits from ToolControl<T> Once created, register the section under System Settings -> Data Sections -> Custom Settings. Unlike Site Settings, nothing has to be referenced in Domain Management: the section is assigned to its domains directly. Editors maintain the settings under Domain Settings -> Site Content. Once a domain has a Custom Settings section, Site Content becomes a submenu: Site Settings first, then one entry per Custom Settings section. Site Settings, Custom Settings or module Attributes? Use For Example Site Settings Things that describe the whole site Company name, logo, address Custom Settings Configuration for one feature, the same on every page of a domain A feed API key, a default page size, text shown with every content area Module Attributes Values that change per placement of a module on a page The content of one content area Usage Custom Settings can be accessed from: AdvantagePageTemplate AdvantageModule AdvantageModuleRewrite AdvantageHandler The API base classes (AdvantageApiControllerBase, AdvantageApiAuthorizeBase, AdvantageApiCorsPolicyProviderBase, AdvantageDelegatingHandlerBase) AdvantageModuleEngine Using the following method: this.CustomSettings<T>() -> returns the object defined by the developer, or null until a record has been published for the current domain and language. There is no dynamic CustomSettings() overload. Always use the typed call. Example Class (BusinessObjectCustomSettings) Settings for a Content Area module: optional text shown above and below every content area on the domain. Example Object Override Validate the same way as Site Settings to stop an editor publishing incomplete settings. Validate before publishusing AdvantageCMS.Core.Admin.Enums; using AdvantageCMS.Core.Admin.Event; public override bool Validate(eCMSActions action, ActionArgs e, out eCMSEngineEventStatus status, out string message) { status = eCMSEngineEventStatus.Success; message = string.Empty; if (action == eCMSActions.Publish && string.IsNullOrEmpty(HeaderText) && string.IsNullOrEmpty(FooterText)) message += "Enter a header or footer text before publishing.<br/>"; if (string.IsNullOrEmpty(message)) return true; status = eCMSEngineEventStatus.Exception; return false; } Control:ToolControl CustomSettings UserControl: ToolControl<T>: Where T BusinessObjectCustomSetting Overview A custom user-control for entering the settings. It's an ordinary detail editor: fill the form in LoadDataFromObject and copy it back to MyObject in SaveDataToObject. The admin opens the domain's single record in it, so no list control is needed. Note: Don't put <% %> code blocks in the editor's markup, not even <label for="<%= txtFoo.ClientID %>">. The tool control adds its toolbar to the control at runtime, and ASP.NET then throws "The Controls collection cannot be modified because the control contains code blocks". Use <asp:Label AssociatedControlID="txtFoo" /> for labels. Example Custom Setting Control ContentAreaSettingsControl.ascx<%@ Control Language="C#" AutoEventWireup="True" Inherits="ON211.ContentAreaSettingsControl" CodeFile="ContentAreaSettingsControl.ascx.cs" %> <fieldset> <legend>Content Area</legend> <div class="form-row"> <asp:Label runat="server" AssociatedControlID="txtHeaderText" Text="Header Text" /> <asp:TextBox runat="server" ID="txtHeaderText" MaxLength="500" /> </div> <div class="form-row"> <asp:Label runat="server" AssociatedControlID="txtFooterText" Text="Footer Text" /> <asp:TextBox runat="server" ID="txtFooterText" MaxLength="500" /> </div> </fieldset> ContentAreaSettingsControl.ascx.csusing AdvantageCMS.Core.Admin.BaseClasses; using AdvantageCMS.Core.Admin.Event; namespace ON211 { public partial class ContentAreaSettingsControl : ToolControl<ContentAreaSettings> { protected override void LoadDataFromObject(ActionArgs e) { txtHeaderText.Text = MyObject.HeaderText; txtFooterText.Text = MyObject.FooterText; } protected override void SaveDataToObject() { MyObject.HeaderText = txtHeaderText.Text.Trim(); MyObject.FooterText = txtFooterText.Text.Trim(); } } } Using Custom Settings In a module Read the settings once and expose them as a property. CustomSettings<T>() is already cached, so the property is just for readability and null-safety. ContentArea.ascx.csusing System; using AdvantageCMS.Core.Common.BaseClasses; namespace ON211 { public partial class Modules_ContentArea : AdvantageModule { /// <summary> /// Domain-level header/footer text; null until the Content Area custom settings are published. /// </summary> protected ContentAreaSettings Settings => CustomSettings<ContentAreaSettings>(); protected void Page_Load(object sender, EventArgs e) { litContent.Text = Attributes.GetString("Content"); } } } ContentArea.ascx<%@ Control Language="C#" AutoEventWireup="true" Inherits="ON211.Modules_ContentArea" CodeFile="ContentArea.ascx.cs" %> <%-- Header/footer come from the Content Area custom settings (Domain Settings > Site Content > Content Area). --%> <% if (!string.IsNullOrEmpty(Settings?.HeaderText)) { %> <div class="content-area-header"><%: Settings.HeaderText %></div> <% } %> <asp:Literal ID="litContent" runat="server"></asp:Literal> <% if (!string.IsNullOrEmpty(Settings?.FooterText)) { %> <div class="content-area-footer"><%: Settings.FooterText %></div> <% } %> In AdvantagePageTemplateContentAreaSettings settings = CustomSettings<ContentAreaSettings>(); if (!string.IsNullOrEmpty(settings?.HeaderText)) litBanner.Text = HttpUtility.HtmlEncode(settings.HeaderText); From ModuleEngineContentAreaSettings settings = moduleEngine.CustomSettings<ContentAreaSettings>(); Use <%: %> (HTML-encoded) for anything an editor typed. Use <%= %> only for trusted values such as numbers or CSPNonce. Never write secrets (API keys, passwords) into the markup or a script block. Use them in code-behind or on the server only. Registering The Section Open System Settings -> Data Sections -> Custom Settings and add a section. Display Name: the name editors see in the Site Content menu (for example Content Area). Object Name: choose the class (ON211.ContentAreaSettings). The list shows only classes that inherit BusinessObjectCustomSetting<T>. Custom Setting Control: choose the .ascx (~/ON211/ClientAdmin/ModuleSettings/ContentAreaSettingsControl.ascx). Tick the domains it applies to, then save. Editors then open Domain Settings -> Site Content -> Content Area, fill in the form and publish. Notes Permissions: Custom Settings use the domain's Site Settings permissions. Anyone who can view, save or publish Site Content can do the same to every Custom Settings section. They have no Security Roles of their own and don't appear in Roles. One record per class: each domain and language has one published record per class. Every module, handler or API that reads the class shares it. Caching: results are cached per class, for the request and in the web cache. Publishing clears only that class's cache entry, so don't add your own cache. Not a Site Setting: a BusinessObjectCustomSetting<T> class can't be registered as the domain's Site Settings. The Site Settings picker leaves these classes out, and the Custom Settings picker shows only them. Calling SiteSettings<T>() with a custom settings class is routed to the custom settings lookup, but prefer CustomSettings<T>(). Inherited properties: BusinessObjectCustomSetting<T> builds on BusinessObjectSiteSetting<T>, so IntelliSense also shows the site settings base properties (GoogleAnalyticsCode, SiteScripts, SocialLinks and so on). Ignore them in a custom settings class. Configuration sync: the section syncs like any other section. The settings values themselves aren't part of configuration sync.