Large number of items in the content tree – Part 2

  • May 07, 2015
  • Content Tree
  • EPiServer
  • EPiServer Find
  • Rest Store
  • DOJO

A while ago I wrote a blog about how to deal with large number of items in the content tree. In the blog I talked about the performance problems when EPiServer tries to load a lot of child pages and how you could solve this. I also build a solution for that problem as you can read in the blog. In the solution I prevent the child pages from being load in the content tree and added a custom view to EPiServer where pages can be searched. For the search functionality I built an easy solution with the IContentRepository. Now I’ve implemented EPiServer Find for the search functionality and added some nice filtering. Below the result.

 

If you compare the solution from my first blog and now you can see on the left side of the search results there are two facet categories, ‘Filter on month’ and ‘Filter on category’. Ofcourse the News item pages contains the standard category property and a news date property. On these two properties it’s now possible to filter. In this blog I will only explain what is changed after my first blog, so it’s best to read the first blog if you haven’t done that yet.

 

Rest store

 

In the store class I changed most of the functionality, because I’m using EPiServer Find now for the search functionality. The Get method now contains an extra parameter ‘selectedFilters’ this is a JSON that contains the selected filters (category/news date). Two methods are added to the class, first the GetNewsFilterCategories. This method initiates filter objects for the category and the date filter, later more about classes that I’ve used. These filter objects are updated when the results are retrieved from EPiServer Find. The second method that’s added is GetSearchResult. This method create objects for the search results that will be returned to the DOJO widget which will update the results in the HTML. Below the code of the SearchPageStore class.

[RestStore("searchpages")]
public class SearchPagesStore : RestControllerBase
{
    private readonly IClient _client; 
    private readonly IContentRepository _contentRepository;
    private readonly UrlResolver _urlResolver;
    private readonly FacetFilterBuilder _facetFilterBuilder;
    private readonly SearchResultItemBuilder _searchResultItemBuilder;
 
    /// <summary>
    /// Public constructor
    /// </summary>
    /// <param name="contentRepository"></param>
    public SearchPagesStore(IContentRepository contentRepository)
    {
        _contentRepository = contentRepository;
        _urlResolver = UrlResolver.Current;
        _client = Client.CreateFromConfig();
        _facetFilterBuilder = new FacetFilterBuilder();
        _searchResultItemBuilder = new SearchResultItemBuilder();
    }
 
    /// <summary>
    /// Get news filter categories
    /// </summary>
    /// <param name="selectedFilters"></param>
    /// <returns></returns>
    private IEnumerable<FacetFilterCategory> GetNewsFilterCategories(IEnumerable<SelectedFilter> selectedFilters)
    {
        SelectedFilter dateSelectedFilter = null;
        SelectedFilter categorySelectedFilter = null;
        if (selectedFilters != null)
        {
            dateSelectedFilter = selectedFilters.FirstOrDefault(s => s.Id == 1);
            categorySelectedFilter = selectedFilters.FirstOrDefault(s => s.Id == 2);
        }
 
        var dateFilter = new DateNewsFacetFilterCategory();
        dateFilter.Name = "Filter on month";
        dateFilter.Facets = null;
        dateFilter.Id = 1;
        dateFilter.SelectedValue = (dateSelectedFilter != null ? dateSelectedFilter.Value : string.Empty);
 
        var categoryFilter = new CategoryNewsFacetFilterCategory();
        categoryFilter.Name = "Filter on category";
        categoryFilter.Facets = null;
        categoryFilter.Id = 2;
        categoryFilter.SelectedValue = (categorySelectedFilter != null ? categorySelectedFilter.Value : string.Empty);
 
        var list = new List<FacetFilterCategory> {dateFilter, categoryFilter};
        return list;
    }
 
    /// <summary>
    /// Get search result
    /// </summary>
    /// <param name="result"></param>
    /// <param name="filters"></param>
    /// <returns></returns>
    private SearchResult GetSearchResult(IContentResult<NewsItemPage> result, IEnumerable<FacetFilterCategory> filters)
    {
        var searchResult = new SearchResult();
 
        var dateFilterItems = new List<FacetFilter>();
        dateFilterItems.Add(new FacetFilter { Key = "All", Value = "-1" });
 
        foreach (var dateFacet in result.HistogramFacetFor(x => x.NewsDate).Entries)
        {
            dateFilterItems.Add(_facetFilterBuilder.Create(dateFacet));
        }
        filters.First(f => f.Id == 1).Facets = dateFilterItems;
 
        var categoryFilterItems = new List<FacetFilter>();
        categoryFilterItems.Add(new FacetFilter { Key = "All", Value = "-1" });
 
        foreach (var categoryFacet in result.CategoriesFacet())
        {
            categoryFilterItems.Add(_facetFilterBuilder.Create(categoryFacet));
        }
        filters.First(f => f.Id == 2).Facets = categoryFilterItems;
 
        searchResult.Facets = filters;
        searchResult.Items = result.Select(_searchResultItemBuilder.Create);
 
        return searchResult;
    }
 
    /// <summary>
    /// Do search
    /// </summary>
    /// <param name="id"></param>
    /// <param name="value"></param>
    /// <param name="selectedFilters"></param>
    /// <returns></returns>
    public RestResult Get(int id, string value, string selectedFilters)
    {
        var sFilters = Enumerable.Empty<SelectedFilter>();
        if (!string.IsNullOrEmpty(selectedFilters))
        {
            sFilters = JsonConvert.DeserializeObject<IEnumerable<SelectedFilter>>(selectedFilters);
        }
         
        var searchCriteria = string.Empty;
        if (!string.IsNullOrEmpty(value))
        {
            searchCriteria = value;
        }
        var filters = GetNewsFilterCategories(sFilters).ToList();
 
        var search = _client.Search<NewsItemPage>()
            .Filter(p => p.Name.AnyWordBeginsWith(searchCriteria))
            .Filter(p => p.Ancestors().Match(id.ToString()));
 
        foreach (var filter in filters)
        {
            search = filter.SetFilter(search, filter.SelectedValue);
            search = filter.SetFilterFacet(search);
        }
 
        var result = search.Take(1000).GetContentResult();
 
        var searchResult = GetSearchResult(result, filters);
 
        return Rest(searchResult);
    }
}
SearchResult class

 

An object of the SearchResult class is returned to the DOJO widget. This is a custom class and has a dependencies to other custom classes. You can find the full source code on my GitHub, but below a class diagram of the SearchResult class and his dependencies.

 

As you can see the SearchResult class contains two properties, a collection of SearchResultItem classes and a collection of FacetFilterCategory classes. The SearchResultItem represent a found page by EPiServer Find. The FacetFilterCategory is an abstract class. There are two implementation for this class the CategoryNewsFacetFilterCategory and the DateNewsFacetFilterCategory class. These two classes are responsible for setting a filter action on the ITypeSearch type and set the Facet for the search. Below the code snipped for the DateNewsFacetFilterCategory class.

public class DateNewsFacetFilterCategory : FacetFilterCategory
{
    public override ITypeSearch<NewsItemPage> SetFilter(ITypeSearch<NewsItemPage> search, string selectedValue)
    {
        if (string.IsNullOrEmpty(selectedValue) || selectedValue == "-1")
        {
            return search;
        }
        return search.Filter(f => f.NewsDate.MatchMonth(2015, int.Parse(selectedValue)));
    }
 
    public override ITypeSearch<NewsItemPage> SetFilterFacet(ITypeSearch<NewsItemPage> search)
    {
        return search.HistogramFacetFor(x => x.NewsDate, DateInterval.Month);
    }
}

The SetFilter method calls the Filter method on the ITypeSearch type for doing the actually filtering on month. The SetFilterFact sets a facet for the NewsDate property.

 

DOJO search view

 

There have been a lot of changes since my first blog about this solution. Again because of the implementation of EPiServer Find and the added filter functionality. The _buildFilters method render the filters on the left side of the search results. I’m using the ‘dom-construct’ class for creating DOM nodes. The _buildFilters method accepts a parameter that contains a collection of the filters. This is actually a JSON of the Facets property from the SearchResult class. The Store which I discussed earlier returns an instance of the SearchResult class which contains the filter collection. The SearchResult class is automatically serialized to JSON by EPiServer so it’s usable in the DOJO widget. The _getResult method creates a JSON of the selected filter and send a request to the Store for retrieving search results. The _buildResult method is called because it’s defined as the callback method. Below the code snippet of the DOJO widget.

define([
     "dijit",
     "dojo",
     "dojo/_base/declare",
     "dojo/when",
     "dojo/dom-construct",
     "dojo/_base/array",
     "dojo/_base/lang",
     "dojo/query",
     "dojo/dom-class",
     "dojo/dom-attr",
     "dojo/on",
     "dojo/json",
     "dojo/text!./templates/searchcontenttemplate.html",
 
     "dijit/_Widget",
     "dijit/_TemplatedMixin",
     "dijit/_WidgetsInTemplateMixin",
 
     "epi/dependency",
     "epi/routes"
], function (
    dijit,
 
    dojo,
    declare,
    when,
    domConstruct,
    array,
    lang,
    query,
    domClass,
    domAttr,
    on,
    JSON,
    template,
 
    _Widget,
    _TemplatedMixin,
    _WidgetsInTemplateMixin,
 
    dependency,
    routes
) {
    return declare("app.editors.searchcontentview", [
        _Widget, _TemplatedMixin, _WidgetsInTemplateMixin
    ], {
        _tableNode: null,
        templateString: template,
        intermediateChanges: false,
        store: null,
        contentStore: null,
        currentContentId: 1,
 
        postCreate: function() {
            this.tableNode = domConstruct.create("table", {
                className: "pagesTable",
                id: "pagesTable"
            });
 
            var colgroup = domConstruct.create("colgroup");
            colgroup.appendChild(domConstruct.create("col", {
                width: "400"
            }));
            colgroup.appendChild(domConstruct.create("col", {
                width: "150"
            }));
            colgroup.appendChild(domConstruct.create("col", {
                width: "150"
            }));
            this.tableNode.appendChild(colgroup);
 
            var tableHead = domConstruct.create("thead");
            var tableBody = domConstruct.create("tbody");
            var headerRow = domConstruct.create("tr");
            headerRow.appendChild(domConstruct.create("th", { innerHTML: "Page" }));
            headerRow.appendChild(domConstruct.create("th", { innerHTML: "Created by" }));
            headerRow.appendChild(domConstruct.create("th", { innerHTML: "Created" }));
 
            tableHead.appendChild(headerRow);
            this.tableNode.appendChild(tableHead);
            this.tableNode.appendChild(tableBody);
            this.resultList.appendChild(this.tableNode);
 
            this.inputWidget.set("intermediateChanges", true);
 
            var registry = dependency.resolve("epi.storeregistry");
            registry.create("app.searchpages", routes.getRestPath({ moduleArea: "app", storeName: "searchpages" }));
            this.store = registry.get("app.searchpages");
 
            this.contentStore = registry.get("epi.cms.content.light");
 
            this.connect(this.inputWidget, "onChange", this._onInputWidgetChanged);
            this.connect(this.showAllLink, "onclick", this._onShowAllLinkClicked);
 
            var contextService = epi.dependency.resolve("epi.shell.ContextService");
            var currentContext = contextService.currentContext;
            var res = currentContext.id.split("_");
 
            this.currentContentId = res[0];
 
            this._getResults();
        },
 
        _onShowAllLinkClicked: function(event) {
            event.preventDefault();
            this.inputWidget.value = "";
            this._getResults();
        },
 
        // Event handler for the changed event of the input widget         
        _onInputWidgetChanged: function(value) {
            this._getResults();
        },
 
        _filterClicked: function (e) {
            query("li a", e.currentTarget.parentElement.parentElement).removeClass("active-filter");
            domClass.toggle(e.currentTarget, "active-filter");
 
            this._getResults();
        },
 
        _getResults: function() {
            var selectedFilters = [];
            query(".filter-category").forEach(function (filterCat) {
                var filters = query("li a.active-filter", filterCat);
                if (filters.length > 0) {
                    filters.forEach(function (filter) {
                        selectedFilters.push({ Id: filterCat.id, Value: dojo.attr(filter, "data-val") });
                    });
                }
            });
            dojo.when(this.store.query(
            {
                id: this.currentContentId,
                value: this.inputWidget.value,
                selectedFilters: JSON.stringify(selectedFilters)
            }), lang.hitch(this, this._buildResult));
        },
 
        _buildResult: function (result) {
            this._buildRows(result.items);
            this._buildFilters(result.facets);
        },
 
        _buildFilters: function (filters) {
            this._clearFilters();
            var ulFilters = domConstruct.create("ul");
 
            array.forEach(filters, function (item) {
                var filterDiv = domConstruct.create("div", {
                    className: "filter-category",
                    id: item.id
                });
                var filterTitle = domConstruct.create("strong", {
                    innerHTML: item.name
                });
 
                var ul = domConstruct.create("ul");
 
                array.forEach(item.facets, function (filterItem) {
                    var li = domConstruct.create("li");
                    var a = domConstruct.create("a", {
                        href: "#",
                        'data-val': filterItem.value
                    });
                    if (filterItem.value == "-1") {
                        a.innerHTML = filterItem.key;
                    } else {
                        a.innerHTML = filterItem.key + " (" + filterItem.count + ")";
                    }
                    if (item.selectedValue == filterItem.value || (filterItem.value == "-1" && item.selectedValue == "")) {
                        a.className = "active-filter";
                    }
 
                    on(a, "click", lang.hitch(this, this._filterClicked));
                    li.appendChild(a);
                    ul.appendChild(li);
                }, this);
 
                filterDiv.appendChild(filterTitle);
                filterDiv.appendChild(ul);
 
                this.facetsContainer.appendChild(filterDiv);
            }, this);
        },
 
        _buildRows: function (items) {
            this._clearTableBody();
 
            var tableNode = dojo.byId("pagesTable");
            var tableBody = query("tbody", tableNode)[0];
 
            array.forEach(items, function (item) {
                var newRow = domConstruct.create("tr");
                var pageColumn = domConstruct.create("td", {
                    innerHTML: "<a class="page-item-link" href="&quot; + item.url + &quot;">" + item.name + "</a>"
                });
                var createdByColumn = domConstruct.create("td", {
                    innerHTML: item.createdBy
                });
                var createdAtColumn = domConstruct.create("td", {
                    innerHTML: item.created
                });
 
                newRow.appendChild(pageColumn);
                newRow.appendChild(createdByColumn);
                newRow.appendChild(createdAtColumn);
 
                tableBody.appendChild(newRow);
            }, this);
        },
 
        _clearTableBody: function() {
            var tableBody = query("tbody", this.tableNode)[0];
 
            domConstruct.empty(tableBody);
        },
 
        _clearFilters: function () {
            var filterContainer = query(".filter-container")[0];
 
            domConstruct.empty(filterContainer);
        }
    });
});

You can find the full source code on my GitHub account.

Share this article