Can I have different Section properties based on the Channel name?

nele.debrouwer April 15, 2024 6:57 AM

I have 2 website channels, both using the same sections. Is there a way I can differentiate the section properties based on the Channel I'm in?

The Visibility conditions only works with values entered into another input in the form.

I would like to do something like:

[DropDownComponent(Label = "Branding color", Order = 1, Options = "Primary;Primary\nSecondary;Secondary\nAccent;Accent")]
[VisibleIfEqualTo(nameof(WebsiteChannelName), "Channel1", StringComparison.OrdinalIgnoreCase)]
public string BrandingColor { get; set; }

or

public string ChannelProperties => WebsiteChannelName.Equals("Channel1") ? "Primary;Primary\nSecondary;Secondary\nAccent;Accent" : "text-bg-primary-1;Dark Blue\ntext-bg-primary-2;White\ntext-bg-primary-3;Sand";

[DropDownComponent(Label = "Branding color", Order = 1, Options = ChannelProperties)]
public string BrandingColor { get; set; }

Environment

Answers

🔗 Sean Wright (seangwright) April 23, 2024 6:27 PM

@nele.debrouwer

The VisibilityConditionAttribute type is actually in Kentico.Xperience.Admin.Base.Shared, which is a special assembly that is "shared" between Kentico.Xperience.WebApp and Kentico.Xperience.Admin.

The Kentico.Xperience.Admin.Base.Shared assembly is not published as a separate NuGet package and instead should be accessed through Kentico.Xperience.WebApp or Kentico.Xperience.Admin.

This means your VisibilityCondition and VisibilityConditionProperties classes would need to go in a separate "admin" .NET project, which is only included during administration deployments.

You would then need to create a "shared" Admin project that references the Kentico.Xperience.WebApp NuGet package and is itself referenced by your ASP.NET Core project and your "admin" .NET project.

This shared project would contain the VisibilityConditionAttribute class.

Shared .NET project

This project references the Kentico.Xperience.WebApp NuGet package.

public class ChannelVisibilityConditionAttribute(string channelName)
    : VisibilityConditionAttribute
{
    public string ChannelName { get; set; } = channelName ?? "";
}

ASP.NET Core project

This project references the Kentico.Xperience.WebApp NuGet package and the Shared project.

It also "conditionally" references the Admin project (only when performing a deployment including the administration).

public class MySectionProperties 
    : ISectionProperties
{
    [ChannelVisibilityCondition("DancingGoatPages")]
    [DropDownComponent(
		Label = "Pages Color", 
		Options = "Primary;Primary\nSecondary;Secondary")] 
    public string PagesColor { get; set; } = "";
}

Administration .NET project

This project references the Shared project and the Kentico.Xperience.Admin NuGet package.

By referencing the Shared project, the ChannelVisibilityConditionAttribute can be assigned to the ChannelVisibilityCondition class, gluing them together for the administration/Page Builder UI.

[VisibilityConditionAttribute(
    typeof(ChannelVisibilityConditionAttribute))]
public class ChannelVisibilityCondition(...)
    : VisibilityCondition<ChannelVisibilityConditionProperties>
{
    // ...
}

public class ChannelVisibilityConditionProperties 
    : VisibilityConditionProperties
{
    public string ChannelName { get; set; } = "";
}

I hope this makes sense! Deploying without the administration is possible but it requires a little more planning and architecture by your team.


🔗 Liam Goldfinch (liamgold) April 15, 2024 8:10 AM

I think you might need to build a custom visibility condition.

In the evaluate method you could inspect the current channel name and handle appropriately.

They also support dependency injection.


🔗 nele.debrouwer April 15, 2024 12:08 PM

Hey @Liam Thanks for pointing me to the custom visibility conditions.

I was able to create a custom condition and check the channel Name, however when calling CMS.Core.Service.Resolve<IWebPageDataContextRetriever>().WebsiteChannelName it always returns my first channel.

It seems that on the section it doesn't resolve the correct channel context...

Any idea how I can retrieve the correct channel context?

fyi, when I use IWebsiteChannelContext through depency injection in a ViewComponent, I do get the correct Channel Name.


🔗 Sean Wright (seangwright) April 15, 2024 9:32 PM

@nele.debrouwer

I can think of 2 options at the moment (but there are possibly others).


Create a custom visibility condition

public class ChannelVisibilityConditionAttribute(string channelName) 
	: VisibilityConditionAttribute
{
    public string ChannelName { get; set; } = channelName ?? "";
}

[VisibilityConditionAttribute(typeof(ChannelVisibilityConditionAttribute))]
public class ChannelVisibilityCondition(IWebsiteChannelContext websiteChannelContext) 
	: VisibilityCondition<ChannelVisibilityConditionProperties>
{
    private readonly IWebsiteChannelContext websiteChannelContext = websiteChannelContext;

    public override bool Evaluate(IFormFieldValueProvider formFieldValueProvider)
    {
        return string.Equals(
			websiteChannelContext.WebsiteChannelName,
			Properties.ChannelName, 
			StringComparison.OrdinalIgnoreCase);
    }
}

public class ChannelVisibilityConditionProperties 
	: VisibilityConditionProperties
{
    public string ChannelName { get; set; } = "";
}

You can use it in your component properties like this:

public class MySectionProperties : ISectionProperties
{
    [ChannelVisibilityCondition("DancingGoatPages")]
    [DropDownComponent(
		Label = "Pages Color", 
		Options = "Primary;Primary\nSecondary;Secondary")] 
    public string PagesColor { get; set; } = "";

    [ChannelVisibilityCondition("DancingGoatNew")]
    [DropDownComponent(
		Label = "New Color", 
		Options = "Accent;Accent\nSpecial;Special")] 
    public string NewColor { get; set; } = "";
}

This will only show each property for the specified channel.


Create a custom options provider

Another option is to create a custom IDropDownOptionsProvider, which can populate the dropdown based on the context of the request, which in this case would be the IWebsiteChannelContext.

You'd need to create a custom object type, which I've called PageBuilderColorInfo in the example below:

public class ChannelColorDropDownOptionsProvider(
    IWebsiteChannelContext websiteChannelContext, 
    IInfoProvider<PageBuilderColorInfo> colorInfoProvider) 
	: IDropDownOptionsProvider
{
    private readonly IWebsiteChannelContext websiteChannelContext = websiteChannelContext;
    private readonly IInfoProvider<PageBuilderColorInfo> colorInfoProvider = colorInfoProvider;

    public async Task<IEnumerable<DropDownOptionItem>> GetOptionItems()
    {
        var options = await colorInfoProvider.Get()
            .WhereEquals(
				nameof(PageBuilderColorInfo.PageBuilderColorWebsiteChannelID), 
				websiteChannelContext.WebsiteChannelID)
            .GetEnumerableTypedResultAsync();

        return options.Select(i => new DropDownOptionItem() 
		{ 
			Text = i.PageBuilderColorDisplayName, 
			Value = i.PageBuilderColorName 
		});
    }
}

You would use this custom provider in your component properties like this:

public class MySectionProperties : ISectionProperties
{
    [DropDownComponent(
		Label = "Pages Color", 
		DataProviderType = typeof(ChannelColorDropDownOptionsProvider))] 
    public string PagesColor { get; set; } = "";
}

This approach has a more complex setup, but simplifies the component properties and allows you to dynamically manage the options in the administration UI.

You can see an example of a custom IDropDownOptionsProvider in the Quickstart Guides.


🔗 nele.debrouwer April 16, 2024 10:46 AM

Hey @seangwright, Thanks for the given options.

I implemented the first option (Visibility condition), but as mentioned in my previous comment, the website channel context always returns my first channel, unless I acces Channel_B via it's own URL.

We'll have content editors managing both website channels, so they won't always change the URL when they need to switch between website channels.

It seems the context does not get properly resolved in the Visibility condition.

Any idea how I can retrieve the correct channel context?


🔗 Sean Wright (seangwright) April 16, 2024 12:07 PM

@nele.debrouwer

Hm, I'm guessing you are accessing the administration through one of the website channel domains?

If so, don't do this 😉.

The administration is not a website channel. It should be accessed by a non-channel domain that is only used for the administration.

If you look at the Community Portal project you can see we have this separation in the local environment. The Community Portal is deployed to SaaS which enforces this separation.

Here's an example of the system domains in the SaaS environment (which are used to access the administration UI):

System domains list in SaaS

You can also see the error that is displayed if you try to access a website channel from the system (administration) domain:

Website channel access of administration domain


Update #1

I just realized the use-case I was testing for this solution didn't match your use-case. So, in addition to my example solutions and my guidance above (which still applies), I also acknowledge we have a gap in the product right now.

You can't use IWebsiteChannelContext or IPageBuilderDataContextRetriever in Page Builder UI Form Components or related types (visibility conditions, custom options providers).

We have some internal services to access this context, but they aren't available for you to use. We'll look at exposing this information in the future.

In the meantime, here's a ... "workaround" that you can use 😁:

public interface IPageBuilderUtilityContextRetriever
{
    Task<PageBuilderUtilityContext> Retrieve();
}

public record PageBuilderUtilityContext(
	string WebsiteChannelName, 
	int WebsiteChannelID, 
	IWebPageFieldsSource Webpage);

public class PageBuilderUtilityContextRetriever(
    IHttpContextAccessor contextAccessor,
    IInfoProvider<ChannelInfo> channelInfoProvider,
    IContentQueryExecutor queryExecutor) 
	: IPageBuilderUtilityContextRetriever
{
    private readonly IHttpContextAccessor contextAccessor = contextAccessor;
    private readonly IInfoProvider<ChannelInfo> channelInfoProvider = channelInfoProvider;
    private readonly IContentQueryExecutor queryExecutor = queryExecutor;

    private PageBuilderUtilityContext? context = null;

    public async Task<PageBuilderUtilityContext> Retrieve()
    {
        if (context is not null)
        {
            return context;
        }

        int websiteChannelID = 0;
        int webPageID = 0;

        var httpContext = contextAccessor.HttpContext;

        string path = httpContext.Request.Form["path"].FirstOrDefault() ?? "";
        string pattern = @"webpages-(\d+)/([^_/]+)_(\d+)";
        var match = Regex.Match(path, pattern);
        if (match.Success)
        {
            websiteChannelID = int.TryParse(match.Groups[1].Value, out int channelID) ? channelID : 0;
            webPageID = int.TryParse(match.Groups[3].Value, out int pageID) ? pageID : 0;
        }

        var channels = await channelInfoProvider.Get()
            .Source(s => s.Join<WebsiteChannelInfo>(nameof(ChannelInfo.ChannelID), nameof(WebsiteChannelInfo.WebsiteChannelChannelID)))
            .Where(w => w.WhereEquals(nameof(WebsiteChannelInfo.WebsiteChannelID), websiteChannelID))
            .Columns(nameof(ChannelInfo.ChannelName))
            .GetEnumerableTypedResultAsync();

        string websiteChannelName = channels
            .Select(s => s.ChannelName)
            .FirstOrDefault() ?? "";

        var query = new ContentItemQueryBuilder()
            .ForContentTypes(q => q.ForWebsite([webPageID], false));

        var pages = await queryExecutor.GetMappedWebPageResult<IWebPageFieldsSource>(query);

        var webPage = pages.FirstOrDefault();

        context = new(websiteChannelName, websiteChannelID, webPage);

        return context;
    }
}

Register this service with DI:

services.AddScoped<
	IPageBuilderUtilityContextRetriever, 
	PageBuilderUtilityContextRetriever>();

And then inject into your visibility condition class constructor.

You'll probably want to add better guards in the Retrieve() method or maybe convert it to TryRetrieve() in case the context can't be retrieved.


🔗 nele.debrouwer April 19, 2024 7:09 AM

Hey @seangwright, Thanks for the possible workaround. This works as expected.

We however think we experienced another gap when using a custom visibility condition as a section property.

When we deployed to our Staging environment we received the following error during the deploy to our 'live' environment:

Run dotnet publish -c Release -p:AdminAttached=false -o C:\Users\runneradmin\AppData\Local\Microsoft\dotnet/live
Error: D:\a\Conditions\ChannelVisibilityCondition.cs(10,102): error CS0246: The type or namespace name ‘VisibilityCondition<>’ could not be found (are you missing a using directive or an assembly reference?)
Error: D:\a\Conditions\ChannelVisibilityCondition.cs(9,6): error CS0246: The type or namespace name ‘VisibilityConditionAttributeAttribute’ could not be found (are you missing a using directive or an assembly reference?)
Error: D:\a\Conditions\ChannelVisibilityCondition.cs(9,6): error CS0246: The type or namespace name ‘VisibilityConditionAttribute’ could not be found (are you missing a using directive or an assembly reference?)
Error: D:\a\ChannelVisibilityCondition.cs(14,39): error CS0246: The type or namespace name ‘IFormFieldValueProvider’ could not be found (are you missing a using directive or an assembly reference?)
Error: D:\a\Conditions\ChannelVisibilityConditionProperties.cs(5,57): error CS0246: The type or namespace name ‘VisibilityConditionProperties’ could not be found (are you missing a using directive or an assembly reference?)
Error: Process completed with exit code 1.

To our live environment we deploy withouth the admin attached as explained on Deploy without the administration

However, the VisibilityConditionProperties an VisibilityConditionAttributeAttribute classes are part of the Assembly Kentico.Xperience.Admin.Base which does not get deployed when you deploy without the administration.

At the moment we've added a reference in our solution to the Kentico.Xperience.Admin.Base assembly, but this exposes the administration through the website channel domain, which we don't want.

Any idea on how we can resolve this issue?


🔗 Sean Wright (seangwright) April 19, 2024 11:56 AM

@nele.debrouwer

I took this to the development team and I'll update this thread when I get a response.


🔗 nele.debrouwer April 19, 2024 12:56 PM

@seangwright, Thanks for following up so quickly.


🔗 nele.debrouwer April 29, 2024 7:17 AM

Hey @seangwright,

Thanks for checking with the developmetn team and the clarification.


To answer this question, you have to login first.