Try our conversational search powered by Generative AI!

Jonas Bergqvist
Jan 8, 2016
  5196
(1 votes)

Automatic landing page in Quicksilver using the search provider system

My last blog post was an overview over “automatic landing pages”. This blog post will show how we can implement this on Quicksilver, using the search provider system. We will change the current facet navigation in Quicksilver to instead use url-based facet navigation.

Image greenShoes.PNG'

Image greenShoesHtml.PNG

Hackday project

This is a hackday project, which was very quickly developed. There is alot of improvement possibilities that I see now directly when I'm writing this blog post. You can easily take the code and prettify it.

Adding nuget package or copy files from github

We will start with adding the nuget package “brilliantcut.automaticlandingpage” to quicksilver. The easiest way to install the packages are to create a new package source in visual studio that points to "http://nuget.jobe.employee.episerver.com/" (https://docs.nuget.org/consume/Package-Manager-Dialog). Now it's possible to install the packages by writing "install-package brilliantcut.automaticlandingpage" in the "Package manager console".

Another alternative to install the nuget package is to copy the c# classes from github: https://github.com/jonasbergqvist/BrilliantCut.AutomaticLandingPage

Now we have some nice classes that we can use to change the current facet navigation on the site.

Disable the default partial route

We will disable the default partial route in “SiteInitialization.cs”. The following line should be removed in the class: CatalogRouteHelper.MapDefaultHierarchialRouter(RouteTable.Routes, false);

New feature folder

Create a new folder under the “Features” folder called “AutomaticLandingPage”.

Configurable module

Create a new configurable module by creating a new class that implements “IConfigurableModule” in the new folder. Set a module dependency to the site initialization by adding the following attribute on the class: [ModuleDependency(typeof(SiteInitialization))].

We will now add a new partial route using the service “CatalogContentRouteRegistration” in “BrilliantCut.AutomatedLandingPage”. Add the following line in the initialization method: context.Locate.Advanced.GetInstance<CatalogContentRouteRegistration>().RegisterDefaultRoute();

Let the “ConfigureContainer” method be empty for now. We will come back to this soon.

[ModuleDependency(typeof(SiteInitialization))]
public class AutomaticLandingPageInitializationModule : IConfigurableModule
{
    public void ConfigureContainer(ServiceConfigurationContext context)
    {
    }

    public void Initialize(InitializationEngine context)
    {
        context.Locate.Advanced.GetInstance<CatalogContentRouteRegistration>().RegisterDefaultRoute();
    }

    public void Uninitialize(InitializationEngine context)
    {
    }
}

New FilterOptionFormModelBinder implementation

The default FilterOptionFormModelBinder in Quicksilver needs to be overridden to make the automated landing page work. We will create a class called “AutomaticLandingPageFilterOptionFormModelBinder”, which will get the values caught in the partial route, and add those to the FilterOptionFormModel:

public class AutomaticLandingPageFilterOptionFormModelBinder : FilterOptionFormModelBinder
{
    private readonly DefaultModelBinder _defaultModelBinder;
    private readonly IContentLoader _contentLoader;

    public AutomaticLandingPageFilterOptionFormModelBinder(IContentLoader contentLoader, LocalizationService localizationService, Func<CultureInfo> preferredCulture, DefaultModelBinder defaultModelBinder) 
        : base(contentLoader, localizationService, preferredCulture)
    {
        _defaultModelBinder = defaultModelBinder;
        _contentLoader = contentLoader;
    }

    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        var model = (FilterOptionFormModel)_defaultModelBinder.BindModel(controllerContext, bindingContext);
        if (model == null)
        {
            return model;
        }

        AddFacets(controllerContext, model);

        var contentLink = controllerContext.RequestContext.GetContentLink();
        IContent content = null;
        if (!ContentReference.IsNullOrEmpty(contentLink))
        {
            content = _contentLoader.Get<IContent>(contentLink);
        }

        var query = controllerContext.HttpContext.Request.QueryString["q"];
        var sort = controllerContext.HttpContext.Request.QueryString["sort"];
        SetupModel(model, query, sort, null, content);
        return model;
    }

    private static void AddFacets(ControllerContext controllerContext, FilterOptionFormModel model)
    {
        var routeFacets = controllerContext.RouteData.Values[FacetUrlService.RouteFacets] as ConcurrentDictionary<RouteFacetModel, HashSet<object>>;
        if (routeFacets != null)
        {
            model.FacetGroups = new List<FacetGroupOption>();
            foreach (var routeFacet in routeFacets.Keys)
            {
                var facetGroupOption = new FacetGroupOption
                {
                    GroupName = routeFacet.FacetName,
                    GroupFieldName = routeFacet.FacetPath,
                    Facets = new List<FacetOption>()
                };

                foreach (var facetValue in routeFacets[routeFacet].Select(x => x.ToString()))
                {
                    facetGroupOption.Facets.Add(new FacetOption
                    {
                        Name = facetValue,
                        Key = facetValue,
                        Selected = true
                    });
                }

                model.FacetGroups.Add(facetGroupOption);
            }
        }
    }
}

Update Configurable module

The configurable module can now be updated to set the “AutomaticLandingPageFilterOptionFormModelBinder” as the implementation for “FilterOptionFormModelBinder”:

[ModuleDependency(typeof(SiteInitialization))]
public class AutomaticLandingPageInitializationModule : IConfigurableModule
{
    public void ConfigureContainer(ServiceConfigurationContext context)
    {
        context.Container.Configure(c => c.For<FilterOptionFormModelBinder>().Use<AutomaticLandingPageFilterOptionFormModelBinder>());
    }

    public void Initialize(InitializationEngine context)
    {
        context.Locate.Advanced.GetInstance<CatalogContentRouteRegistration>().RegisterDefaultRoute();
    }

    public void Uninitialize(InitializationEngine context)
    {
    }
}

_Facet.cshtml

We need to make changes to _Facet.cshtml to use the HtmlHelper in BrilliantCut.AutomaticLandingPage. The following line can be used in the <li> tags:

<a href="@Html.FacetContentUrl(ViewContext.RequestContext.GetRoutedData<CatalogContentBase>(), typeof(string).FullName, facetGroup.GroupFieldName, facetGroup.GroupName, facet.Key)">@facet.Key (@facet.Count)</a>

The whole file:

@using System.Web.Mvc.Html
@using BrilliantCut.AutomaticLandingPage
@using EPiServer.Commerce.Catalog.ContentTypes
@using EPiServer.Shell.Web.Mvc.Html
@using EPiServer.Web.Routing

@model EPiServer.Reference.Commerce.Site.Features.Search.Models.FilterOptionFormModel
@{
    Layout = null;
}
<div class="col-sm-3 facets-wrapper jsSearchFacets">
    @if (Model.FacetGroups.Any(x => x.Facets.Any(y => y.Selected)))
    {
        <div class="well facets-summary product-filtering choices">
            <h3>@Html.Translate("/Category/Filters")</h3>
            <ul class="nav">
                @for (var i = 0; i < Model.FacetGroups.Count; i++)
                {
                    var facetGroup = Model.FacetGroups[i];
                    for (var j = 0; j < facetGroup.Facets.Count; j++)
                    {
                        var facet = facetGroup.Facets[j];
                        if (!facet.Selected)
                        {
                            continue;
                        }
                        <li class="facet-active">
                            <a href="@Html.FacetContentUrl(ViewContext.RequestContext.GetRoutedData<CatalogContentBase>(), typeof(string).FullName, facetGroup.GroupFieldName, facetGroup.GroupName, facet.Key)">@facet.Key (@facet.Count)</a>
                        </li>
                    }
                }
                <li class="facets-amount">
                    @Html.Translate("/Facet/Choices") <strong>@Model.TotalCount</strong>
                </li>
            </ul>
            <a href="@Html.ContentUrl(ViewContext.RequestContext.GetRoutedData<CatalogContentBase>())">Remove all</a>          
        </div>
    }

    <ul class="nav">
        <li>
            <h3>@Html.Translate("/Category/SortBy")</h3>
            @Html.DropDownList("FormModel.Sort", Model.Sorting, new { @class = "form-control jsSearchSort" })<br />
        </li>
    </ul>

    @for (var i = 0; i < Model.FacetGroups.Count; i++)
    {
        var facetGroup = Model.FacetGroups[i];
       
        <ul class="nav facet-group">
            <li>
                <h3>@facetGroup.GroupName</h3>
                @Html.TextBox(string.Format("FormModel.FacetGroups[{0}].GroupFieldName", i), facetGroup.GroupFieldName, new { @hidden = "true" })
            </li>
            @for (var j = 0; j < facetGroup.Facets.Count; j++)
            {
                var facet = facetGroup.Facets[j];
                if (facet.Selected)
                {
                    continue;
                }
                <li>
                    <a href="@Html.FacetContentUrl(ViewContext.RequestContext.GetRoutedData<CatalogContentBase>(), typeof(string).FullName, facetGroup.GroupFieldName, facetGroup.GroupName, facet.Key)">@facet.Key (@facet.Count)</a>
                </li>
            }
        </ul>
    }
</div>

The whole implementation

AutomaticLandingPageInitializationModule

[ModuleDependency(typeof(SiteInitialization))]
public class AutomaticLandingPageInitializationModule : IConfigurableModule
{
    public void ConfigureContainer(ServiceConfigurationContext context)
    {
        context.Container.Configure(c => c.For<FilterOptionFormModelBinder>().Use<AutomaticLandingPageFilterOptionFormModelBinder>());
    }

    public void Initialize(InitializationEngine context)
    {
        context.Locate.Advanced.GetInstance<CatalogContentRouteRegistration>().RegisterDefaultRoute();
    }

    public void Uninitialize(InitializationEngine context)
    {
    }
}

AutomaticLandingPageFilterOptionFormModelBinder

public class AutomaticLandingPageFilterOptionFormModelBinder : FilterOptionFormModelBinder
{
    private readonly DefaultModelBinder _defaultModelBinder;
    private readonly IContentLoader _contentLoader;

    public AutomaticLandingPageFilterOptionFormModelBinder(IContentLoader contentLoader, LocalizationService localizationService, Func<CultureInfo> preferredCulture, DefaultModelBinder defaultModelBinder) 
        : base(contentLoader, localizationService, preferredCulture)
    {
        _defaultModelBinder = defaultModelBinder;
        _contentLoader = contentLoader;
    }

    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        var model = (FilterOptionFormModel)_defaultModelBinder.BindModel(controllerContext, bindingContext);
        if (model == null)
        {
            return model;
        }

        AddFacets(controllerContext, model);

        var contentLink = controllerContext.RequestContext.GetContentLink();
        IContent content = null;
        if (!ContentReference.IsNullOrEmpty(contentLink))
        {
            content = _contentLoader.Get<IContent>(contentLink);
        }

        var query = controllerContext.HttpContext.Request.QueryString["q"];
        var sort = controllerContext.HttpContext.Request.QueryString["sort"];
        SetupModel(model, query, sort, null, content);
        return model;
    }

    private static void AddFacets(ControllerContext controllerContext, FilterOptionFormModel model)
    {
        var routeFacets = controllerContext.RouteData.Values[FacetUrlService.RouteFacets] as ConcurrentDictionary<RouteFacetModel, HashSet<object>>;
        if (routeFacets != null)
        {
            model.FacetGroups = new List<FacetGroupOption>();
            foreach (var routeFacet in routeFacets.Keys)
            {
                var facetGroupOption = new FacetGroupOption
                {
                    GroupName = routeFacet.FacetName,
                    GroupFieldName = routeFacet.FacetPath,
                    Facets = new List<FacetOption>()
                };

                foreach (var facetValue in routeFacets[routeFacet].Select(x => x.ToString()))
                {
                    facetGroupOption.Facets.Add(new FacetOption
                    {
                        Name = facetValue,
                        Key = facetValue,
                        Selected = true
                    });
                }

                model.FacetGroups.Add(facetGroupOption);
            }
        }
    }
}

_Facet.cshtml

@using System.Web.Mvc.Html
@using BrilliantCut.AutomaticLandingPage
@using EPiServer.Commerce.Catalog.ContentTypes
@using EPiServer.Shell.Web.Mvc.Html
@using EPiServer.Web.Routing

@model EPiServer.Reference.Commerce.Site.Features.Search.Models.FilterOptionFormModel
@{
    Layout = null;
}
<div class="col-sm-3 facets-wrapper jsSearchFacets">
    @if (Model.FacetGroups.Any(x => x.Facets.Any(y => y.Selected)))
    {
        <div class="well facets-summary product-filtering choices">
            <h3>@Html.Translate("/Category/Filters")</h3>
            <ul class="nav">
                @for (var i = 0; i < Model.FacetGroups.Count; i++)
                {
                    var facetGroup = Model.FacetGroups[i];
                    for (var j = 0; j < facetGroup.Facets.Count; j++)
                    {
                        var facet = facetGroup.Facets[j];
                        if (!facet.Selected)
                        {
                            continue;
                        }
                        <li class="facet-active">
                            <a href="@Html.FacetContentUrl(ViewContext.RequestContext.GetRoutedData<CatalogContentBase>(), typeof(string).FullName, facetGroup.GroupFieldName, facetGroup.GroupName, facet.Key)">@facet.Key (@facet.Count)</a>
                        </li>
                    }
                }
                <li class="facets-amount">
                    @Html.Translate("/Facet/Choices") <strong>@Model.TotalCount</strong>
                </li>
            </ul>
            <a href="@Html.ContentUrl(ViewContext.RequestContext.GetRoutedData<CatalogContentBase>())">Remove all</a>          
        </div>
    }

    <ul class="nav">
        <li>
            <h3>@Html.Translate("/Category/SortBy")</h3>
            @Html.DropDownList("FormModel.Sort", Model.Sorting, new { @class = "form-control jsSearchSort" })<br />
        </li>
    </ul>

    @for (var i = 0; i < Model.FacetGroups.Count; i++)
    {
        var facetGroup = Model.FacetGroups[i];
       
        <ul class="nav facet-group">
            <li>
                <h3>@facetGroup.GroupName</h3>
                @Html.TextBox(string.Format("FormModel.FacetGroups[{0}].GroupFieldName", i), facetGroup.GroupFieldName, new { @hidden = "true" })
            </li>
            @for (var j = 0; j < facetGroup.Facets.Count; j++)
            {
                var facet = facetGroup.Facets[j];
                if (facet.Selected)
                {
                    continue;
                }
                <li>
                    <a href="@Html.FacetContentUrl(ViewContext.RequestContext.GetRoutedData<CatalogContentBase>(), typeof(string).FullName, facetGroup.GroupFieldName, facetGroup.GroupName, facet.Key)">@facet.Key (@facet.Count)</a>
                </li>
            }
        </ul>
    }
</div>
 
Jan 08, 2016

Comments

tobias.gladh@knowit.se
tobias.gladh@knowit.se Feb 23, 2016 12:51 PM

Applying this to QuickSilver based soultion causes the Episerver UI to be unreachable (if not already logged in). Seems like it affects the routing for functions that don't have a page setup in EPiServer. Regular pages works while functions like /BackendLogin, /Login, /Cart/AddToCart or /Identity/Index does not work. It's the CatalogContentRouteRegistration that's causing the issue. Any ideas on why it causes this? 

Apr 26, 2016 11:30 AM

I will look at it, thanks

Please login to comment.
Latest blogs
Optimizely and the never-ending story of the missing globe!

I've worked with Optimizely CMS for 14 years, and there are two things I'm obsessed with: Link validation and the globe that keeps disappearing on...

Tomas Hensrud Gulla | Apr 18, 2024 | Syndicated blog

Visitor Groups Usage Report For Optimizely CMS 12

This add-on offers detailed information on how visitor groups are used and how effective they are within Optimizely CMS. Editors can monitor and...

Adnan Zameer | Apr 18, 2024 | Syndicated blog

Azure AI Language – Abstractive Summarisation in Optimizely CMS

In this article, I show how the abstraction summarisation feature provided by the Azure AI Language platform, can be used within Optimizely CMS to...

Anil Patel | Apr 18, 2024 | Syndicated blog

Fix your Search & Navigation (Find) indexing job, please

Once upon a time, a colleague asked me to look into a customer database with weird spikes in database log usage. (You might start to wonder why I a...

Quan Mai | Apr 17, 2024 | Syndicated blog

The A/A Test: What You Need to Know

Sure, we all know what an A/B test can do. But what is an A/A test? How is it different? With an A/B test, we know that we can take a webpage (our...

Lindsey Rogers | Apr 15, 2024

.Net Core Timezone ID's Windows vs Linux

Hey all, First post here and I would like to talk about Timezone ID's and How Windows and Linux systems use different IDs. We currently run a .NET...

sheider | Apr 15, 2024