Jitendra Chilate, Author at Perficient Blogs https://blogs.perficient.com/author/jchilate/ Expert Digital Insights Tue, 05 Dec 2023 17:57:18 +0000 en-US hourly 1 https://blogs.perficient.com/files/favicon-194x194-1-150x150.png Jitendra Chilate, Author at Perficient Blogs https://blogs.perficient.com/author/jchilate/ 32 32 30508587 Automate Endpoints Creation in Optimizely Configured Commerce with PowerShell https://blogs.perficient.com/2023/12/05/automate-endpoints-creation-in-optimizely-configured-commerce-with-powershell/ https://blogs.perficient.com/2023/12/05/automate-endpoints-creation-in-optimizely-configured-commerce-with-powershell/#comments Tue, 05 Dec 2023 17:57:18 +0000 https://blogs.perficient.com/?p=350085

Creating custom storefront APIs in the backend often involves a time-consuming process of crafting multiple files. Our PowerShell script automates endpoint creation to streamline this, generating the necessary classes and interfaces effortlessly. Let’s explore in this blog how to automate endpoint creation in Optimizely Configured Commerce with PowerShell.

The Endpoints

Optimizely Configured Commerce interacts with the Configured Commerce data through RESTful services.

Based on the feature/functionality’s requirements, there is always a need to create custom storefront APIs in Optimizely Commerce-based projects to post/get/update the specific data.

There is a specific flow of code execution that occurs from the client-side request to the server side and back to the client-side in response.

This HTTP request typically includes information about the action being performed, such as requested resources, parameters, headers, etc.

Whenever any page loads on the browser, it renders out its associated widgets having endpoints which could be responsible for retrieving product information, order processing, managing the shopping cart or billing, etc.

The Flow

These endpoints / APIs maps to their respective controllers in the backend, and the request goes to the specific controller method along with the client-side request object which holds the client-side parameters.

E.g., The endpoint “api/v1/products/{productId}” maps to one of the ProductsV1Controller.Get methods and returns data for a specific product.

  • HTTP Request routes to the respective controller class’s action method.
  • Through the controller method, the code execution goes to the Mapper class’s MapParameter() method for mapping client-side request object parameter with server-side parameter class along with query string e.g. filter and expander.
  • After parameter mapping, execution goes to the service class where based on the server-side parameter and result classes, the execute() method of the actual handler class executes and returns the server-side result
  • Again the execution goes to the Mapper class and executes the MapResult() method where all the server-side result parameter values get mapped with client-side object parameters.
  • Finally, the execution goes back to the controller method and the result model to the client browser.
Flow

Web API Request Flow in Optimizely Configured Commerce

Creating a single endpoint involves generating approximately 10 files containing classes and interfaces in the backend, requiring considerable manual effort.

The Implementation

Below are the files we need to create manually to create an endpoint.

ApiModels

Model.cs

Parameter.cs

Controller

Controller.cs

Services

IService.cs

Service.cs

Services/Parameters

ServiceParameter.cs

Services/Results

Result.cs

Mappers

IMapper.cs

Mapper.cs

Handlers

Handler.cs

Hence, to reduce manual intervention and simplify the creation of new endpoints, we’ve created a PowerShell utility that needs an endpoint name and path where we need to create a new API, when running the script, it auto-creates the entire 10 files in the backend with sample code for the API.

Later, a developer can extend the code in the classes like client-side request parameter, response, mapper, service, service parameter, result, handler, and controller as per their need.

To Automate Endpoints in Optimizely Configured Commerce with PowerShell Script

$apiName = Read-Host "Enter the API Name"
if ($apiName -eq "") {
    Write-Host "You did not enter the API Name!"
    exit
}

$apiPath = Read-Host "Enter the API Path"
if ($apiPath -eq "") {
    Write-Host "You did not enter the API Path!"
    exit
}

#Handlers
$handlerFolder = $apiPath+"\Handlers"
if (!(Test-Path -Path $handlerFolder -PathType Container)){ New-Item -Path $handlerFolder -ItemType Directory }
$handlerNamespace = $handlerFolder -split [regex]::Escape("src\")
$handlerNamespace = $handlerNamespace[1] -replace [regex]::Escape("\"), "."
$handlerNamespace = $handlerNamespace -replace [regex]::Escape(".."), "."
$handlerFolder = $handlerFolder + "\" + $apiName+"Handler.cs"
$serviceNamespace = $handlerNamespace -replace [regex]::Escape("Handler"), "Services.Parameter"
$resultNamespace = $handlerNamespace -replace [regex]::Escape("Handler"), "Services.Result"
$handlerName = $apiName + "Handler"
$serviceParameter = $apiName + "ServiceParameter"
$serviceResult = $apiName + "Result"
$csHandlerContent = @"
using Insite.Core.Interfaces.Dependency;
using Insite.Core.Services.Handlers;
using Insite.Core.Interfaces.Data;
using System;
using $serviceNamespace;
using $resultNamespace;

namespace $handlerNamespace
{

    [DependencyName("$apiName")]
    public sealed class $handlerName
    : HandlerBase<$serviceParameter, $serviceResult>
    {
        public override int Order => 100;
        
        public override $serviceResult Execute(IUnitOfWork unitOfWork, $serviceParameter parameter, $serviceResult result)
        {
            result.SampleProperty = "This the sample proerpty!";
            return this.NextHandler.Execute(unitOfWork, parameter, result);
        }

    }
}
"@

#ApiModels
$apiModelsFolder = $apiPath+"\ApiModels"
if (!(Test-Path -Path $apiModelsFolder -PathType Container)){ New-Item -Path $apiModelsFolder -ItemType Directory }
$apiModelsNamespace = $apiModelsFolder -split [regex]::Escape("src\")
$apiModelsNamespace = $apiModelsNamespace[1] -replace [regex]::Escape("\"), "."
$apiModelsNamespace = $apiModelsNamespace -replace [regex]::Escape(".."), "."
$apiModelsFolderModel = $apiModelsFolder + "\" + $apiName+"Model.cs"
$apiModelsFolderParameter = $apiModelsFolder + "\" + $apiName+"Parameter.cs"
$apiModelName = $apiName + "Model"
$lApiModelName = $apiModelName.Substring(0, 1).ToLower() + $apiModelName.Substring(1)
$apiParameterName = $apiName + "Parameter"
$lApiParameterName = $apiParameterName.Substring(0, 1).ToLower() + $apiParameterName.Substring(1)

$modelContent = @"
using Insite.Core.Plugins.Search.Dtos;
using Insite.Core.WebApi;
using Newtonsoft.Json;
using System.Collections.Generic;

namespace $apiModelsNamespace
{

    public class $apiModelName : BaseModel
        
    {
        [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
        public PaginationModel Pagination { get; set; }

        [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
        public string SampleProperty { get; set; }
       
    }
}
"@

$parameterContent = @"
using Insite.Core.WebApi;
using System;
using System.Collections.Generic;

namespace $apiModelsNamespace
{

    public class $apiParameterName : BaseParameter
        
    {
        public string SampleProperty { get; set; }
        public int? Page { get; internal set; }
    }
}
"@

#Services
$servicesFolder = $apiPath+"\Services"
if (!(Test-Path -Path $servicesFolder -PathType Container)){ New-Item -Path $servicesFolder -ItemType Directory }
$servicesNamespace = $servicesFolder -split [regex]::Escape("src\")
$servicesNamespace = $servicesNamespace[1] -replace [regex]::Escape("\"), "."
$servicesNamespace = $servicesNamespace -replace [regex]::Escape(".."), "."
$servicesFolderClass = $servicesFolder + "\" + $apiName+"Service.cs"
$servicesFolderInterface = $servicesFolder + "\I" + $apiName+"Service.cs"
$serviceName = $apiName + "Service"
$iServiceName = "I" + $serviceName
$serviceResultName = $apiName + "Result"
$serviceMethodName = "Get" + $apiName + "Collection"
$servicesParameterName = $apiName + "ServiceParameter"

$servicesParameterFolder = $apiPath+"\Services\Parameters"
if (!(Test-Path -Path $servicesParameterFolder -PathType Container)){ New-Item -Path $servicesParameterFolder -ItemType Directory }
$servicesParametersNamespace = $servicesParameterFolder -split [regex]::Escape("src\")
$servicesParametersNamespace = $servicesParametersNamespace[1] -replace [regex]::Escape("\"), "."
$servicesParametersNamespace = $servicesParametersNamespace -replace [regex]::Escape(".."), "."
$servicesParameterFolder = $servicesParameterFolder + "\" + $apiName+"ServiceParameter.cs"

$servicesResultFolder = $apiPath+"\Services\Results"
if (!(Test-Path -Path $servicesResultFolder -PathType Container)){ New-Item -Path $servicesResultFolder -ItemType Directory }
$servicesResultNamespace = $servicesResultFolder -split [regex]::Escape("src\")
$servicesResultNamespace = $servicesResultNamespace[1] -replace [regex]::Escape("\"), "."
$servicesResultNamespace = $servicesResultNamespace -replace [regex]::Escape(".."), "."
$servicesResultFolder = $servicesResultFolder + "\" + $apiName+"Result.cs"



$iServiceContent = @"
using Insite.Core.Interfaces.Dependency;
using Insite.Core.Services;
using $servicesResultNamespace;
using $servicesParametersNamespace;

namespace $servicesNamespace
{

    [DependencyInterceptable]
    public interface $iServiceName :
        IDependency, 
        ISettingsService
    {
       $serviceResultName $serviceMethodName($servicesParameterName parameter);
    }
}
"@

$serviceContent = @"
using Insite.Core.Interfaces.Data;
using Insite.Core.Interfaces.Dependency;
using Insite.Core.Services;
using Insite.Core.Services.Handlers;
using $resultNamespace;
using System;
using $servicesParametersNamespace;

namespace $servicesNamespace
{

    public class $serviceName :
        ServiceBase,
        $iServiceName,
        IDependency, 
        ISettingsService
    {
        protected readonly IHandlerFactory HandlerFactory;

        public $serviceName(IUnitOfWorkFactory unitOfWorkFactory, IHandlerFactory handlerFactory)
      : base(unitOfWorkFactory)
        {
            this.HandlerFactory = handlerFactory;
        }

         [Transaction]
        public $serviceResultName $serviceMethodName($servicesParameterName parameter)
        {
            $serviceResultName result = ($serviceResultName)null;
            this.UnitOfWork.ExecuteWithoutChangeTracking((Action)(() => result = this.HandlerFactory.GetHandler<IHandler<$servicesParameterName, $serviceResultName>>().Execute(this.UnitOfWork, parameter, new $serviceResultName())));
            return result;
        }
       
    }
}
"@

#Services/Parameters
$serviceParameterContent = @"
using Insite.Core.Context;
using Insite.Core.Extensions;
using Insite.Core.Interfaces.EnumTypes;
using Insite.Core.Plugins.Pricing;
using System;
using System.Collections.Generic;
using System.Linq;
using $apiModelsNamespace;
using Insite.Core.WebApi;
using Insite.Core.Services;

namespace $servicesParametersNamespace
{

    public class $servicesParameterName : PagingParameterBase
        
    {
       public $servicesParameterName(
          $apiParameterName $lApiParameterName)
        {
            if ($lApiParameterName == null)
                return;
            this.SampleProperty = $lApiParameterName.SampleProperty;
        }

        public string SampleProperty { get; set; }
    }
}
"@

#Services/Result
$lServiceResultName = $serviceResultName.Substring(0, 1).ToLower() + $serviceResultName.Substring(1)
$serviceResultContent = @"
using Insite.Core.Plugins.Search.Dtos;
using Insite.Core.Services;
using Insite.Data.Entities;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;

namespace $servicesResultNamespace
{

    public class $serviceResultName : PagingResultBase
        
    {
        public virtual string SampleProperty { get; set; }
        public virtual ReadOnlyCollection<SortOrderDto> SortOptions { get; set; }
        public virtual string SortOrder { get; set; }
        public virtual bool ExactMatch { get; set; }
    }
}
"@



#Mappers
$mapperFolder = $apiPath+"\Mappers"
if (!(Test-Path -Path $mapperFolder -PathType Container)){ New-Item -Path $mapperFolder -ItemType Directory }
$mappersNamespace = $mapperFolder -split [regex]::Escape("src\")
$mappersNamespace = $mappersNamespace[1] -replace [regex]::Escape("\"), "."
$mappersNamespace = $mappersNamespace -replace [regex]::Escape(".."), "."

$mapperFolderClass = $mapperFolder + "\" + $apiName+"Mapper.cs"
$mapperFolderInterface = $mapperFolder + "\I" + $apiName+"Mapper.cs"

$mapperName = $apiName + "Mapper"
$imapperName = "I" + $mapperName

$iMapperContent = @"
using Insite.Core.Interfaces.Dependency;
using Insite.Core.WebApi.Interfaces;
using $apiModelsNamespace;
using $servicesNamespace;
using $servicesResultNamespace;
using $servicesParametersNamespace;

namespace $mappersNamespace
{

    [DependencyInterceptable]
    public interface $imapperName :
        IWebApiMapper<$apiParameterName, $servicesParameterName, $serviceResultName, $apiModelName>,
        IDependency, 
        IExtension
    {
       
    }
}
"@


$mapperContent = @"
using Insite.Core.Interfaces.Dependency;
using Insite.Core.Plugins.Search.Dtos;
using Insite.Core.Plugins.Utilities;
using Insite.Core.Services;
using Insite.Core.WebApi;
using Insite.Core.WebApi.Interfaces;
using Insite.Core.Extensions;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Net.Http;
using $apiModelsNamespace;
using $servicesNamespace;
using $servicesResultNamespace;
using $servicesParametersNamespace;

namespace $mappersNamespace
{

    public class $mapperName : 
    $imapperName,
    IWebApiMapper<$apiParameterName, $servicesParameterName, $serviceResultName, $apiModelName>,
    IDependency,
    IExtension
    {
        public $mapperName(
        IUrlHelper urlHelper,
        IObjectToObjectMapper objectToObjectMapper)
        {
        }

        public $servicesParameterName MapParameter($apiParameterName apiParameter, HttpRequestMessage request)
        {
            $servicesParameterName parameter = new $servicesParameterName(apiParameter);
            if (apiParameter != null)
                parameter.Page = new int?(apiParameter.Page ?? 1);
            string queryString = request.GetQueryString("filter");
            if (!queryString.IsBlank())
            {
                string[] source2 = queryString.ToLower().Split(',');
                parameter.PageSize = ((IEnumerable<string>)source2).Contains<string>("pagesize")?20:10;
            }
            return parameter;
        }

        public $apiModelName MapResult($serviceResultName serviceResult, HttpRequestMessage request)
        {
            $apiModelName $lApiModelName = new $apiModelName();
            if (serviceResult != null)
            {
                $lApiModelName.SampleProperty = serviceResult.SampleProperty;
                $lApiModelName.Pagination = this.MakePaging(request, serviceResult);
            }
            return $lApiModelName;
        }

        public virtual PaginationModel MakePaging(
          HttpRequestMessage httpRequestMessage,
          $serviceResultName $lServiceResultName)
        {
            PaginationModel paginationModel = new PaginationModel((PagingResultBase)$lServiceResultName);
            if (paginationModel.NumberOfPages > 1 && paginationModel.Page < paginationModel.NumberOfPages)
            {
                var routeValues = new
                {
                    page = paginationModel.Page + 1,
                    query = httpRequestMessage.GetQueryString("query"),
                    pageSize = httpRequestMessage.GetQueryString("pageSize"),
                    categoryId = httpRequestMessage.GetQueryString("categoryId"),
                    sort = httpRequestMessage.GetQueryString("sort"),
                    expand = httpRequestMessage.GetQueryString("expand")
                };
            }
            if (paginationModel.Page > 1)
            {
                var routeValues = new
                {
                    page = paginationModel.Page - 1,
                    query = httpRequestMessage.GetQueryString("query"),
                    pageSize = paginationModel.PageSize,
                    categoryId = httpRequestMessage.GetQueryString("categoryId"),
                    sort = httpRequestMessage.GetQueryString("sort"),
                    expand = httpRequestMessage.GetQueryString("expand")
                };
            }
            if ($lServiceResultName.SortOptions != null)
            {
                paginationModel.SortOptions = $lServiceResultName.SortOptions.Select<SortOrderDto, SortOptionModel>((Func<SortOrderDto, SortOptionModel>)(o => new SortOptionModel()
                {
                    DisplayName = o.DisplayName,
                    SortType = o.SortType
                })).ToList<SortOptionModel>();
                paginationModel.SortType = $lServiceResultName.SortOrder;
            }
            return paginationModel;
        }
            
    }
}
"@

#Controllers
$controllerFolder = $apiPath+"\Controller"
if (!(Test-Path -Path $controllerFolder -PathType Container)){ New-Item -Path $controllerFolder -ItemType Directory }
$controllerNamespace = $controllerFolder -split [regex]::Escape("src\")
$controllerNamespace = $controllerNamespace[1] -replace [regex]::Escape("\"), "."
$controllerNamespace = $controllerNamespace -replace [regex]::Escape(".."), "."
$controllerFolder = $controllerFolder + "\" + $apiName+"V1Controller.cs"
$controllerName = $apiName + "Controller"
$lControllerName = $controllerName.Substring(0, 1).ToLower() + $controllerName.Substring(1)
$lMapperName = $mapperName.Substring(0, 1).ToLower() + $mapperName.Substring(1)
$lserviceName = $serviceName.Substring(0, 1).ToLower() + $serviceName.Substring(1)
$routeName = $apiName + "V1"
$csControllerContent = @"
using Insite.Core.Plugins.Utilities;
using Insite.Core.WebApi;
using System.Threading.Tasks;
using System.Web.Http;
using System.Web.Http.Description;
using $servicesNamespace;
using $mappersNamespace;
using $apiModelsNamespace;
using $servicesResultNamespace;
using $servicesParametersNamespace;
using System;

namespace $controllerNamespace
{

    [RoutePrefix("api/v1/$apiName")]
    public class $controllerName : BaseApiController
    {
        private readonly $imapperName $lMapperName;
        private readonly $iserviceName $lServiceName;

        public $controllerName(ICookieManager cookieManager,
        $imapperName $lMapperName,
        $iserviceName $lServiceName)
        : base(cookieManager)
        {
            this.$lMapperName = $lMapperName;
            this.$lServiceName = $lServiceName;
        }

       
        [Route("", Name = "$routeName")]
        [ResponseType(typeof($apiModelName))]
        public async Task<IHttpActionResult> Get([FromUri] $apiParameterName model)
        {
            $controllerName $lControllerName = this;
            return await $lControllerName.ExecuteAsync<$imapperName,
                $apiParameterName,
                $servicesParameterName,
                $serviceResultName,
                $apiModelName>($lControllerName.$lMapperName,
                new Func<$servicesParameterName, $serviceResultName>
                ($lControllerName.$lServiceName.$serviceMethodName),
                model);
        }

    }
}
"@

$csControllerContent | Set-Content -Path $controllerFolder
$csHandlerContent | Set-Content -Path $handlerFolder
$modelContent | Set-Content -Path $apiModelsFolderModel
$parameterContent | Set-Content -Path $apiModelsFolderParameter
$iServiceContent | Set-Content -Path $servicesFolderInterface
$serviceContent | Set-Content -Path $servicesFolderClass
$serviceParameterContent | Set-Content -Path $servicesParameterFolder
$serviceResultContent | Set-Content -Path $servicesResultFolder
$iMapperContent | Set-Content -Path $mapperFolderInterface
$mapperContent | Set-Content -Path $mapperFolderClass

 

You can access the utility script by downloading it from this GitHub repository: jitendrachilate16/Optimizely-Utility-Scripts (github.com)

The Demo

For the demo, we created an endpoint “GetSampleData” in the “Catalog” module using the script.

Step 1:

Execute the PowerShell script “CreateEndpoint.ps1”. PowerShell command prompt will ask to enter the name of the API.

Supply the endpoint name, as an example, we have furnished: GetSampleData.

Hit enter!

Step 2:

Following that, it prompts you to input the path where you wish to create the API.

In my case I’ve provided: C:\Insite\OptimizelyB2BCommerce-master\src\Extensions\Catalog\

Hit enter!

Step 3:

Subsequently, navigate to the solution, verifying the creation of essential classes and interface files within the designated project and path. Then, proceed to build the solution.

Step 4:

After successfully building the solution, proceed to browse the API – and there you have it. Your API is now ready for use!

You can of course make necessary updates to extend your endpoint as per the requirement.

Please view the below gif to demonstrate its overall working.

CreateEndpoint

Demo to create endpoint using script.

Conclusion

In this blog, we explored a simplified way to create custom storefront APIs in Optimizely Configured Commerce. Instead of the usual manual effort, we introduced a handy PowerShell script that automates the process. The script generates all the necessary files with just a few inputs, making it much easier for developers. We walked through the web API request flow and even demonstrated the script’s use by creating an endpoint called “GetSampleData.” You can find and use the script on GitHub to simplify your endpoint creation and updates based on your project needs.

Thank you for your time! I believe the automation insights shared in this blog will prove to be a valuable time-saver. 🙂

]]>
https://blogs.perficient.com/2023/12/05/automate-endpoints-creation-in-optimizely-configured-commerce-with-powershell/feed/ 1 350085
Switch SXA Themes Based on Cookie Value https://blogs.perficient.com/2023/09/14/switch-sxa-themes-based-on-cookie-value/ https://blogs.perficient.com/2023/09/14/switch-sxa-themes-based-on-cookie-value/#respond Thu, 14 Sep 2023 08:39:28 +0000 https://blogs.perficient.com/?p=344433

The Requirement

There was a requirement to give users the ability to switch the themes of the site.
We can have multiple use cases to switch the theme of the site as mentioned below and provide a better user experience.

  • We can show a pop-up on the home page with a message like “Do you want to experience a new design, please click here”. This option would change the theme.
  • Change the theme based on the seasons, festivals, or special days.
  • The theme changes during the day and night times.

We can use a cookie value to change the themes of the site. For example, “0” for the default theme and “1” for the new theme.

Before going straight to the investigation and implementation, let’s first understand the SXA Themes

The SXA Themes

The sites’ look and feel are defined by themes which are comprised of styles, scripts, images, and fonts.

Sitecore SXA comes with OOTB SXA themes and base themes. We can create our own custom theme by inheriting base themes. These themes can be applied to sites.

Themes in the content editor tree can be found at sitecore/Media Library/Themes & sitecore/Media Library/Base Themes as shown in the below image.

For more information on themes, please visit The SXA themes | Sitecore Documentation.

Sxa Themes

Sxa Themes

The Investigation

When the Sitecore SXA page item renders out on the browser, behind the scenes the pipeline “assetService” has an “AddTheme” processor of assembly Sitecore.XA.Foundation.Theming executes and returns the Sitecore SXA theme item based on the theme which is set on the Sitecore Page Design item or design mapping and gets applied on the SXA page.

We can see in the below screenshot of showconfig.aspx the “assetService” and “AddTheme” processor.

showconfig.aspx

Existing assetService, AddTheme processor

When peeking into Sitecore.XA.Foundation.Theming.dll, found that the AddTheme processor adds the theme item which gets through calling the ThemingContext service highlighted in the below image.

AddTheme Processor

Sitecore.XA.Foundation.Theming.dll –> AddTheme Processor

When investigated on ThemeContext, found that a function “DoGetThemeItem” gets called which returns the theme that gets applied to the SXA page.

And that(“DoGetThemeItem”) is the place where we need to write our custom code to switch the themes based on cookie value.

Hence, now we need to override the “DoGetThemeItem” function by creating a custom class that gets inherited by the ThemingContext class of Sitecore.XA.Foundation.Theming like shown below.

The Implementation

using System.Web;
using Sitecore.Data;
using Sitecore.XA.Foundation.Theming;
using Sitecore.Data.Items;

namespace ThemeSwitcher
{
    public class CustomThemingContext : ThemingContext
    {
        private const string CookieName = "Default-Theme";

        protected override Item DoGetThemeItem(Item item, DeviceItem device)
        {
            HttpCookie cookie = null;
            cookie = cookie ?? HttpContext.Current.Request.Cookies.Get(CookieName);
            Item defaultThemeItem = Sitecore.Context.Database.GetItem(new ID(Templates.Themes.DefaultTheme));
            Item newThemeItem = Sitecore.Context.Database.GetItem(new ID(Templates.Themes.NewTheme));
            return (cookie?.Value == "1") ? newThemeItem : defaultThemeItem;
              
        }
    }
}

Here, we need to create a project wherein we have a class called “CustomThemingContext” and write our custom logic for theme switching as shown in above code block.

And we need to replace the existing ThemeContext service with our new custom CustomThemingContext by patching the configuration as shown in below image.

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:set="http://www.sitecore.net/xmlconfig/set/" xmlns:role="http://www.sitecore.net/xmlconfig/role/">
    <sitecore>
        <mvc>
            <precompilation>
                <assemblies>
                    <assemblyIdentity name="ThemeSwitcher" />
                </assemblies>
            </precompilation>
        </mvc>

        <services>
            <register serviceType="Sitecore.XA.Foundation.Theming.IThemingContext, Sitecore.XA.Foundation.Theming"
                      implementationType="ThemeSwitcher.CustomThemingContext, ThemeSwitcher"
                      lifetime="Singleton"
                      patch:instead="*[@implementationType='Sitecore.XA.Foundation.Theming.ThemingContext, Sitecore.XA.Foundation.Theming']"/>
        </services>
    </sitecore>
</configuration>

The SXA theme contains styles, javascripts and images and their references get injected in the SXA pages. By default, the SXA pages use the MVC layout which is found at /sitecore/layout/Layouts/Foundation/Experience Accelerator/MVC/MVC Layout having the view path /Views/SxaLayout/SxaLayout.cshtml which is an Out-Of-the-Box(OOTB) razor view included in the Sitecore SXA as shown in below image.

Sxa Theme Image011

If we see line no. 11 from the above image

GenerateLinks()

Calling GenerateLinks() function

GenerateLinks() sorts through all the Sitecore themes and returns the requires styles and scripts links which gets added in the <head> tag of the razor view.

Again, peeking into Sitecore.XA.Foundation.Theming.dll and checking into AssetLinksGenerator class, we found that GenerateLinks() function which again calls to GenerateAssetLinks() function where the little customization needed and hence the cookie value will be added in the existing cache key. This will ensure that whenever there is a change in the cookie value to change the theme, the cache key will also get changed which will call the “assetService” pipeline to get the asset links of styles and scripts instead of returning the previous cached asset links.

Here, we need to create a custom AssetLinkGenerator class and add it into our project and update the GenerateAssetLinks() function as highlighted below.

To view the complete code file, please visit to ThemeSwitcher/Service/CustomAssetLinksGenerator.cs at master · jitendrachilate/ThemeSwitcher (github.com)

public virtual AssetLinks GenerateAssetLinks(IThemesProvider themesProvider)
       {
           HttpCookie cookie = null;
           cookie = cookie ?? HttpContext.Current.Request.Cookies.Get(CookieName);
           if (!Sitecore.SecurityModel.License.License.HasModule("Sitecore.SXA"))
           {
               HttpContext.Current.Response.Redirect(Settings.NoLicenseUrl + "?license=Sitecore.SXA");
               return (AssetLinks)null;
           }

           string str = string.Format("{0}#{1}#{2}#{3}#{4}", (object)this.Context.Item.ID, (object)this.Context.Device.ID, 
               (object)this.Context.Database.Name, (object)this._configuration.RequestAssetsOptimizationDisabled, (object)cookie?.Value);
           string cacheKey1;
           if (HttpContext.Current.Cache[str] != null && HttpContext.Current.Cache[cacheKey1 = this.GenerateCacheKey((int)HttpContext.Current.Cache[str])] != null)
           {
               Sitecore.Diagnostics.Log.Info("GenerateAssetLinks: returning cache value", this);
               return HttpContext.Current.Cache[cacheKey1] as AssetLinks;
           }
           AssetsArgs args = new AssetsArgs();
           CorePipeline.Run("assetService", (PipelineArgs)args);
           int hashCode = args.GetHashCode();
           string cacheKey2 = this.GenerateCacheKey(hashCode);
           if (!(HttpContext.Current.Cache[cacheKey2] is AssetLinks result) || this._configuration.RequestAssetsOptimizationDisabled)
           {
               result = new AssetLinks();
               if (!args.AssetsList.Any<AssetInclude>())
                   return result;
               args.AssetsList = (IList<AssetInclude>)args.AssetsList.OrderBy<AssetInclude, int>((Func<AssetInclude, int>)(a => a.SortOrder)).ToList<AssetInclude>();
               foreach (AssetInclude assets in (IEnumerable<AssetInclude>)args.AssetsList)
               {
                   switch (assets)
                   {
                       case ThemeInclude _:
                           this.AddThemeInclude(assets as ThemeInclude, result, themesProvider);
                           continue;
                       case UrlInclude _:
                           this.AddUrlInclude(assets as UrlInclude, result);
                           continue;
                       case PlainInclude _:
                           this.AddPlainInclude(assets as PlainInclude, result);
                           continue;
                       default:
                           continue;
                   }
               }
               this.CacheLinks(cacheKey2, result, this.DatabaseRepository.GetContentDatabase().Name.ToLowerInvariant().Equals("master", StringComparison.Ordinal) ? AssetContentRefresher.MasterCacheDependencyKeys : AssetContentRefresher.WebCacheDependencyKeys);
               this.CacheHash(str, hashCode);

           }
           return result;
       }

Custom View (for MVC Layout item)

An important thing to notice here is that this Razor view file gets updated whenever there is a change in the theme and dynamically the asset links get added in the <head> tag of the SxaLayout.cshtml. However, a developer will notice that the changes may not reflect especially after deployment in the servers because SXA razor views are precompiled into DLLs for better performance and any changes on razor view on runtime like adding theme assets links won’t show up with the changes.

To make razor view changes renders we can locate to the App_Config/Sitecore/Mvc/Sitecore.Mvc.config file, there is a setting called “Mvc.UsePhysicalViewsIfNewer”, change its default value “false” to “true”. This change will allow Sitecore’s view engine to check and compare the modified dates of the DLLs and chtmls files and based on it Sitecore will choose either the precompiled DLLs or physical view for rendering.

For more details, please refer to this blog post on making these configuration changes.

https://sitecorewithraman.wordpress.com/2023/08/06/overriding-precompiled-sxa-razor-views-in-sitecore/

Creating a New View

However, it’s recommended not to make any changes to OOTB razor views, and better to create a new custom razor view cshtml file and associate this view with the MVC Layout.

Hence, we’ll be creating a new view by copying the existing SxaLayout.cshtml razor view and let’s say name CustomSxaLayout.cshtml and call the GenerateLinks() function as shown in the below code block.

@using Sitecore.Mvc
@using Sitecore.XA.Foundation.MarkupDecorator.Extensions
@using Sitecore.XA.Foundation.SitecoreExtensions.Extensions
@using Sitecore.XA.Foundation.Grid.Extensions
@using Sitecore.XA.Foundation.Theming.Bundler

@model Sitecore.Mvc.Presentation.RenderingModel

@{
    AssetLinks assetLinks =  ThemeSwitcher.CustomAssetLinksGenerator.GenerateLinks(new ThemesProvider());
}

<!DOCTYPE html>
<!--[if lt IE 7]>      <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
<!--[if IE 7]>         <html class="no-js lt-ie9 lt-ie8"> <![endif]-->
<!--[if IE 8]>         <html class="no-js lt-ie9"> <![endif]-->
<!--[if gt IE 8]><!-->
<html class="no-js" lang="@Model.Item.Language.Name">
<!--<![endif]-->
<head>
    <meta charset="utf-8">
    @foreach (string style in assetLinks.Styles)
    {
        @Html.Raw(style)
    }

    @Html.Sxa().VisitorIdentification()
    @Html.Sxa().Placeholder("head")
</head>
<body class="body-background" @Html.Sxa().Body().Decorate()>
    @Html.Sitecore().Placeholder("body-top")
    @Html.Sxa().GridBody()
    @Html.Sitecore().Placeholder("body-bottom")
    @foreach (string script in assetLinks.Scripts)
    {
        @Html.Raw(script)
    }
    <!-- /#wrapper -->
</body>
</html>

At line no. 10 we’re calling the CustomAssetlinksGenerator’s GenerateLinks() function to get the corresponding theme item based on the cookie value.

Finally, navigate to /sitecore/layout/Layouts/Foundation/Experience Accelerator/MVC/MVC Layout and change the existing view path of SxaLayout.cshtml to CustomSxaLayout.cshtml as shown in the below image.

MVC Layout Item

MVC Layout Item

Please do remember to apply the unicorn serialization rule to the above highlighted “MVC Layout” item so as to prevent the changes from being overwritten by any upcoming deployment, else the changes made on the ‘Path’ field will be reverted.

Please find the code implementation at Git Repo: https://github.com/jitendrachilate/ThemeSwitcher.git

The Demo:

We see in the below gif, that there are two themes “DefaultTheme” which has a background color gray, and “NewTheme” which has a background color light pink.

When changing the cookie “Default-Theme” value to 1 from 0 the background color turns to lightpink and when changed back the cookie value to 0 from 1 the background color turns to gray.

Theme Switcher Demo

Theme Switcher Demo

Thank you for reading! Hope this blog helps you to implement theme switching in your sites. 😊

 

]]>
https://blogs.perficient.com/2023/09/14/switch-sxa-themes-based-on-cookie-value/feed/ 0 344433