Monday, November 28, 2016

Sitecore Data Exchange Framework - Sql Server provider

As you have probably heard Sitecore team has released a new module for integration Sitecore application with other systems. I have written an import tool of my own, and was very curious to see what Sitecore developers came up with. Out of the box, Data Exchange Framework module comes with Sitecore provider, which is great because it can be used with Sitecore to Sitecore migrations or for providers that need to write to Sitecore. However, I was more interested in using this module for importing content from non-Sitecore systems where the data often is stored in a SQL database.

I have to say that documentation for this module is written very well, and implementing a new provider was a very simple task just by following instructions from Developer Guide (http://integrationsdn.sitecore.net/DataExchangeFramework/v1.1/). I am not going to try to re-write the documentation, but will provide the code I arrived at after I had implemented everything that the developer guide had specified.

The code that you can find at https://github.com/jcore/Sitecore.DataExchange.Providers.SqlServer contains classes and a Sitecore package for provider. The package does not include the template for items that would be created by import process. It would be different for each solution, so I decided to omit it.

Here is screenshot of the project structure. As you can see it is pretty slim. It took me a couple of hours to implement this version of provider, which I thought was great. I really liked how easy it was to create new provider to fit what I needed to do. 

There are more to explore with this module. I am especially interested in processing of sql values before they are stored in Sitecore.

Sunday, October 9, 2016

How to create a custom Coveo Facet component in Sitecore application

If you use Coveo renderings in your solution and facet controls that Coveo provides out-of-the-box don’t match the requirements you have, you can create a custom JavaScript component that would behave according to your specifications. In this blog I’ll demonstrate how to go about creating a custom dropdown facet.
  1. First I created a copy of Coveo Facet View rendering in Sitecore and a view file in my solution. The code in the view file was almost exact duplicate of out-of-the-box FacetView.cshtml. I changed the css class on the div element for my facet to ‘CoveoFacetDropdown’ and added data-default-caption='@Model.DefaultCaption' attribute.

    @Html.Coveo().RenderErrorSummary(Model.ValidateModel())
    
    <script type="text/javascript" src="/Coveo/js/CustomCoveoJsSearch.js"></script>
    
    @if (Model.IsConfigured) {
        <div>
            @if (Model.IconProperties != null) {
                <style>
                    @(Html.Raw(Model.GetIconCss()))
                </style>
            }
    
            <div id='@Model.Id'
                 class="CoveoFacetDropdown"
                 data-title='@Model.Title'
                 data-default-caption='@Model.DefaultCaption'
                 data-field='@Model.Field'
                 data-number-of-values='@Model.NumberOfValues'
                 data-id='@Model.Id'
                 data-enable-collapse='@Model.EnableCollapse'
                 data-enable-more-less='@Model.EnableMoreLess'
                 data-enable-settings='@Model.EnableSettings'
                 data-lookup-field='@Model.LookupField'
                 data-sort-criteria='@Model.Sort'
                 data-is-multi-value-field='@Model.IsMultiValueField'
                 data-show-icon='@Model.ShowIcon'
                 data-header-icon='no-icon'
                 data-computed-field='@Model.ComputedField'
                 data-computed-field-operation='@Model.ComputedFieldOperation'
                 data-computed-field-format='@Model.ComputedFieldFormat'
                 data-computed-field-caption='@Model.ComputedFieldCaption'
                 data-include-in-breadcrumb='@Model.IncludeInBreadcrumb'
                 data-number-of-values-in-breadcrumb='@Model.NumberOfValuesInBreadcrumb'
                 data-include-in-omnibox='@Model.IncludeInOmnibox'
                 data-enable-facet-search='@Model.EnableFacetSearch'
                 data-number-of-values-in-facet-search='@Model.NumberOfValuesInFacetSearch'
                 data-enable-toggling-operator='@Model.EnableTogglingOperator'
                 data-use-and='@Model.UseAnd'
                 data-page-size='@Model.MorePageSize'
                 data-injection-depth='@Model.InjectionDepth'
                 data-available-sorts='@String.Join(",", Model.AvailableSorts)'></div>
        </div>
    }
    
    
  2. Next step was to implement this facet in JavaScript. For that I created a new CustomCoveoJsSearch.js file in my solution and placed my dropdown facet implementation there. The entry point for facet lives in the following function:

    var Coveo;
    (function (Coveo) {
        var FacetDropdown = (function (_super) {
            __extends(FacetDropdown, _super);
            function FacetDropdown(element, options, bindings) {
                _super.call(this, element, Coveo.ComponentOptions.initComponentOptions(element, FacetDropdown, options), bindings, FacetDropdown.ID);
                this.element = element;
                this.options.enableFacetSearch = false;
                this.options.enableSettings = false;
                this.options.includeInOmnibox = false;
                this.options.enableMoreLess = false;
                this.options.defaultCaption = element.getAttribute("data-default-caption");
            }
    
            FacetDropdown.prototype.initFacetQueryController = function () {
                this.facetQueryController = new Coveo.FacetDropdownQueryController(this);
            };
    
            FacetDropdown.prototype.initFacetValuesList = function () {
                this.facetValuesList = new Coveo.FacetDropdownValuesList(this, Coveo.DropdownFacetSearchValueElement);
                Coveo.$(this.element).append(this.facetValuesList.build());
            };
    
            FacetDropdown.ID = 'FacetDropdown';
            FacetDropdown.parent = Coveo.Facet;
            FacetDropdown.options = {
                dateField: Coveo.ComponentOptions.buildBooleanOption({ defaultValue: false })
            };
            return FacetDropdown;
        })(Coveo.Facet);
        Coveo.FacetDropdown = FacetDropdown;
        Coveo.CoveoJQuery.registerAutoCreateComponent(FacetDropdown);
    })(Coveo || (Coveo = {}));

    Notice that the name of the object in JavaScript is "FacetDropdown" while css class in html is "CoveoFacetDropdown". It turns out that Coveo component match with corresponding css classes are done by appending "Coveo" to css class. In other words if you create a component with the name "FacetDropdown", css class has to be "CoveoFacetDropdown".
  3. To complete implementation of this custom control I added methods to instantiate component options, controller and option list methods. So at the end I arrived at the following:
    var Coveo;
    (function (Coveo) {
        var FacetDropdownQueryController = (function (_super) {
            __extends(FacetDropdownQueryController, _super);
            function FacetDropdownQueryController(facet) {
                _super.call(this, facet);
                this.facet = facet;
            }
            return FacetDropdownQueryController;
        })(Coveo.FacetQueryController);
        Coveo.FacetDropdownQueryController = FacetDropdownQueryController;
    })(Coveo || (Coveo = {}));
    
    var Coveo;
    (function (Coveo) {
        var DropdownFacetSearchValueElement = (function (_super) {
            __extends(DropdownFacetSearchValueElement, _super);
            function DropdownFacetSearchValueElement(facet, facetValue, keepDisplayedValueNextTime) {
                _super.call(this, facet, facetValue, keepDisplayedValueNextTime);
                this.facet = facet;
                this.facetValue = facetValue;
                this.keepDisplayedValueNextTime = keepDisplayedValueNextTime;
            }
            DropdownFacetSearchValueElement.prototype._handleExcludeClick = function (eventBindings) {
                this.facet.open(this.facetValue);
                _super.prototype.handleExcludeClick.call(this, eventBindings);
            };
            DropdownFacetSearchValueElement.prototype.build = function () {
                this.renderer = new Coveo.DropdownValueElementRenderer(this.facet, this.facetValue).build();
                return this;
            };
            DropdownFacetSearchValueElement.prototype.handleEventForValueElement = function (eventBindings) {
                var _this = this;
                return this;
            };
            
            return DropdownFacetSearchValueElement;
        })(Coveo.FacetValueElement);
        Coveo.DropdownFacetSearchValueElement = DropdownFacetSearchValueElement;
    })(Coveo || (Coveo = {}));
    
    var Coveo;
    (function (Coveo) {
        var FacetDropdownValuesList = (function (_super) {
            __extends(FacetDropdownValuesList, _super);
            function FacetDropdownValuesList(facet) {
                _super.call(this, facet, Coveo.DropdownFacetSearchValueElement);
                this.facet = facet;
            }
            FacetDropdownValuesList.prototype.build = function () {
                this.valueContainer = Coveo.$('<select class="coveo-facet-values"/>');
                Coveo.Component.pointElementsToDummyForm(this.valueContainer);
                this.bindEvent({ displayNextTime: false, pinFacet: this.facet.options.preservePosition });
                return this.valueContainer;
            };
            FacetDropdownValuesList.prototype.select = function (value) {
                var valueElement = this.get(value);
                valueElement.select();
                return valueElement;
            };
            FacetDropdownValuesList.prototype._getValuesToBuildWith = function () {
                var values = this.facet.values.getAll();
                if (values && values.length > 0 && values[0].value !== this.facet.options.defaultCaption) {
                    var emptyValue = new Coveo.FacetValue();
                    emptyValue.value = this.facet.options.defaultCaption;
                    emptyValue.lookupValue = this.facet.options.defaultCaption;
                    values.unshift(emptyValue);
                }
                return this.facet.facetSort.reorderValues(values);
            };
            FacetDropdownValuesList.prototype.toggleSelect = function (value) {
                var valueElement = this.get(value);
                if (valueElement.facetValue.selected) {
                    valueElement.unselect();
                }
                else {
                    valueElement.select();
                }
                return valueElement;
            };
            FacetDropdownValuesList.prototype.handleEventForSelectChange = function (eventBindings) {
                var _this = this;
                this.valueContainer.change(function (event) {
                    if (eventBindings.omniboxObject) {
                        _this.omniboxCloseEvent(eventBindings.omniboxObject);
                    }
                    _this.handleSelectValue(eventBindings);
                    if (Coveo.DeviceUtils.isMobileDevice() && !_this.facet.searchInterface.isNewDesign() && _this.facet.options.enableFacetSearch) {
                        Coveo.Defer.defer(function () {
                            Coveo.ModalBox.close(true);
                            _this.facet.facetSearch.completelyDismissSearch();
                        });
                    }
                });
            };
            FacetDropdownValuesList.prototype.bindEvent = function (eventBindings) {
                if (!Coveo.Utils.isNullOrUndefined(eventBindings.omniboxObject)) {
                    this.isOmnibox = true;
                }
                else {
                    this.isOmnibox = false;
                }
                this.handleEventForSelectChange(eventBindings);
            };
            FacetDropdownValuesList.prototype.handleSelectValue = function (eventBindings) {
                var _this = this;
                this.facet.keepDisplayedValuesNextTime = eventBindings.displayNextTime && !this.facet.options.useAnd;
                var actionCause;
                Coveo.$(this.facet.values.values).each(function (i) {
                    _this.facet.values.values[i].selected = false;
                });
                var selectedIndex = this.valueContainer[0].selectedIndex;
                this.facetValue = this.facet.values.values[selectedIndex];
                if (this.facetValue.value !== this.facet.options.defaultCaption) {
                    this.facetValue.selected = true;
                } 
                if (this.facetValue.excluded) {
                    actionCause = Coveo.AnalyticsActionCauseList.facetUnexclude;
                    this.facet.unexcludeValue(this.facetValue);
                }
                else {
                    if (this.facetValue.selected) {
                        actionCause = Coveo.AnalyticsActionCauseList.facetDeselect;
                    }
                    else {
                        actionCause = Coveo.AnalyticsActionCauseList.facetSelect;
                    }
                }
                if (this.isOmnibox) {
                    actionCause = Coveo.AnalyticsActionCauseList.omniboxFacet;
                }
                this.facet.triggerNewQuery(function () { return _this.facet.usageAnalytics.logSearchEvent(actionCause, _this.getAnalyticsFacetMeta()); });
            };
            FacetDropdownValuesList.prototype.getAnalyticsFacetMeta = function () {
                return {
                    facetId: this.facet.options.id,
                    facetValue: this.facetValue.value,
                    facetTitle: this.facet.options.title
                };
            };
            return FacetDropdownValuesList;
        })(Coveo.FacetValuesList);
        Coveo.FacetDropdownValuesList = FacetDropdownValuesList;
    })(Coveo || (Coveo = {}));
    
    var Coveo;
    (function (Coveo) {
        var DropdownValueElementRenderer = (function () {
            function DropdownValueElementRenderer(facet, facetValue) {
                this.facet = facet;
                this.facetValue = facetValue;
            }
            DropdownValueElementRenderer.prototype.withNo = function (element) {
                if (Coveo._.isArray(element)) {
                    Coveo._.each(element, function (e) {
                        if (e) {
                            e.remove();
                        }
                    });
                }
                else {
                    if (element) {
                        element.remove();
                    }
                }
                return this;
            };
            DropdownValueElementRenderer.prototype.build = function () {
                var _this = this;
                this.buildOptionValue();
                return this;
            };
            DropdownValueElementRenderer.prototype.buildOptionValue = function () {
                var caption = this.facet.getValueCaption(this.facetValue);
                this.listElement = Coveo.$('<option class="coveo-facet-value coveo-facet-selectable"/>')
                    .attr("value", this.facetValue.lookupValue)
                    .text(caption);
                if (caption === this.facetValue.value && this.facetValue.selected) {
                    this.listElement.attr("selected",true);
                }
            };
            DropdownValueElementRenderer.prototype.setCssClassOnListValueElement = function () {
                this.listElement.toggleClass('coveo-selected', this.facetValue.selected);
            };
            return DropdownValueElementRenderer;
        })();
        Coveo.DropdownValueElementRenderer = DropdownValueElementRenderer;
    })(Coveo || (Coveo = {}));
    
    
    var Coveo;
    (function (Coveo) {
        var FacetDropdown = (function (_super) {
            __extends(FacetDropdown, _super);
            function FacetDropdown(element, options, bindings) {
                _super.call(this, element, Coveo.ComponentOptions.initComponentOptions(element, FacetDropdown, options), bindings, FacetDropdown.ID);
                this.element = element;
                this.options.enableFacetSearch = false;
                this.options.enableSettings = false;
                this.options.includeInOmnibox = false;
                this.options.enableMoreLess = false;
                this.options.defaultCaption = element.getAttribute("data-default-caption");
            }
    
            FacetDropdown.prototype.initFacetQueryController = function () {
                this.facetQueryController = new Coveo.FacetDropdownQueryController(this);
            };
    
            FacetDropdown.prototype.initFacetValuesList = function () {
                this.facetValuesList = new Coveo.FacetDropdownValuesList(this, Coveo.DropdownFacetSearchValueElement);
                Coveo.$(this.element).append(this.facetValuesList.build());
            };
    
            FacetDropdown.ID = 'FacetDropdown';
            FacetDropdown.parent = Coveo.Facet;
            FacetDropdown.options = {
                dateField: Coveo.ComponentOptions.buildBooleanOption({ defaultValue: false })
            };
            return FacetDropdown;
        })(Coveo.Facet);
        Coveo.FacetDropdown = FacetDropdown;
        Coveo.CoveoJQuery.registerAutoCreateComponent(FacetDropdown);
    })(Coveo || (Coveo = {}));
    

Sunday, September 4, 2016

Search, Content Tagging and Pages with search results

Every web application that is being built these days has some sort of search functionality. I have seen different implementations. Some of them are simple and easy to maintain, some not so much so. I would like to share with you, readers of my blog, how I have implemented such functionality. I am not saying only my implementation is correct, but served me well in last three applications I have worked on.

Assumptions

  1. Lets assume you have content that might belong to one or more categories
  2. Second assumption is that you have category landing pages that have listing modules that display content that is tagged to this category.
  3. You might have listing/search results modules on other pages of your website.

Key Concepts

To be able to produce search results based on category, all articles, news and press release page items as well as any other content needs to be "tagged" to this category.

To accomplish that I have implemented a base template with  tree list "Category" field. The source of this category needs to point to a location in the Sitecore tree that represents the list of categories. All templates that can be tagged to category should inherit this template. So now, if an articles is tagged to a category or categories, it would show up in search results when you specify selected category value. Nothing remarkable so far.

The next part is landing pages. Lets say I would like to have three modules on my Category One landing page: news, articles, and press releases. Each of these modules should only display the content that is tagged to the Category One. There are two options - either implement a rendering data source that would have the category selection specified or have the same base template with Category field inherited by the landing page. When a value is selected in Category field on Category One landing page, it can be used to produce search results for all modules.

This approach might not be perfect, but seems to be pretty simple for editors to understand and for developers to implement. It allows tagging content to multiple categories/tags, reusing the same template for tagging and producing consistent results.