Thursday, March 23, 2017

Identifying a contact in Sitecore via JavaScript

Its becoming more common these days to make use of JavaScript frameworks such as Angular, Knockout and React to build Sitecore web sites with great user experiences. However, as this is done there is less and less work being done inside the standard code behind (for web form sublayouts), and controllers (for MVC renderings). This is instead replaced by service calls asynchronously to Web API or standard WCF, which leaves one downside to the Sitecore developer.

In a standard service call you won't have access to the Sitecore context, which of course can be overcome by simply creating your own context to access the data. Yet when it comes to the tracker and the great features available with identified contacts, it might not work so cleanly. You can't fake access to the current tracker (and by association the current Contact). Using contact repositories involve getting a copy of that contact, attempting a lock and leaves many chances for error.

The cleanest and simplest way I have found to allow the front end to interact with the current contact is to make use of an MVC controller. Web forms users should not stop reading because this works in both MVC projects and web forms the same! Effectively we create an MVC controller which inherits off of Sitecore MVC controllers. This means that there is full access to the context of Sitecore and the tracker, and these controllers can be called the same way in AJAX as you would Web API or any other service.

So lets get down to an example, the following code is a controller which allows a contact to be identified by email address.

The controller

This is just a simple example showing that you have access to the current session/contact.
using System;
using System.Web.Mvc;
using Sitecore.Analytics;
using Sitecore.Mvc.Controllers;

namespace MyProject.Controllers
{
    /// <summary>
    /// MVC controller to allow Sitecore context access
    /// </summary>
    public class ContactController : SitecoreController
    {
        [HttpPost]
        public ActionResult IdentifyContact(string emailAddress)
        {
            var result = false;

            try
            {
                Tracker.Current.Session.Identify(emailAddress);
                result = true;
            }
            catch (Exception)
            {
                return Json(result);
            }

            return Json(result);
        }
    }
}

Registering the controller

The controller needs to be registered during the initialization pipeline in Sitecore.
using Sitecore.Pipelines;
using System.Web.Mvc;
using System.Web.Routing;

namespace MyProject
{
    class RegisterMvcRoute
    {
        public virtual void Process(PipelineArgs args)
        {
            Register();
        }

        public static void Register()
        {
            RouteTable.Routes.MapRoute("CustomContact", "Contact/IdentifyContact", new { controller = "Contact", action = "IdentifyContact" });
        }
    }
}
The patch file for this is:
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <initialize>
        <processor type="MyProject.RegisterMvcRoute, MyProject" patch:before="processor[@type='Sitecore.Mvc.Pipelines.Loader.InitializeRoutes, Sitecore.Mvc']" />   
      </initialize>
    </pipelines>
  </sitecore>
</configuration>

Calling the controller

It's as simple as making an AJAX request in your JavaScript.
$.ajax({
    type: "POST",
    url: "/Contact/IdentifyContact",
    data: JSON.stringify({'emailAddress': 'testing@email.com'}),
    contentType: "application/json; charset=utf-8",
    dataType: "json",
    success: function (result) { },
    error: function (request, status, error) { }
});
In this example the result object would be boolean, but obviously whole objects can be returned as desired.

Conclusion

This concept can be further extended to allow contact facets to be modified and accessed via JavaScript as well as registering goals and events.

Thursday, March 16, 2017

Sitecore registering a goal or event on any page programatically

Registering a page event/goal in Sitecore is simple enough when you have the page/site context, such as a call on the code behind of a web control. There are cases where this is not available, for example via a service or generic MVC route and in these cases the goal will not register against the correct page.

In the following code example, it shows how the page context can be modified before the page event/goal is registered. That way it appears against the correct page in analytics. The origianl source for these methods was Brian Pedersen.
public static void RegisterAjaxEvent(string eventName, ID itemId, string text, string data, string dataKey)
{
    if (!Tracker.Enabled)
        return;

    if (!Tracker.IsActive)
        Tracker.StartTracking();

    IPageContext currentPage = Tracker.Current.CurrentPage;
    if (currentPage == null)
        return;

    RegisterEventOnCurrentPage(eventName, text, data, dataKey, currentPage, itemId);
}

private static void RegisterEventOnCurrentPage(string eventName, string text, string data, string dataKey, IPageContext currentPage, ID itemId)
{
    PageEventData pageEvent = new PageEventData(eventName)
    {
        Text = text,
        Data = data ?? string.Empty,
        DataKey = string.IsNullOrEmpty(dataKey) ? string.Empty : dataKey
    };
    try
    {
        // Register the event on the correct page
        if (!ID.IsNullOrEmpty(itemId))
        {
            var db = global::Sitecore.Data.Database.GetDatabase("web");
            var item = db.Items.GetItem(itemId);

            if (item != null)
            {
                var relativePath = item.Paths.Path.Replace("/sitecore/content/Home", "");
                if (string.IsNullOrEmpty(relativePath))
                {
                    relativePath = "/";
                }

                currentPage.SetUrl(relativePath);
                currentPage.SetItemProperties(itemId.ToGuid(), item.Language.Name, item.Version.Number);
            }
        }

        var result = currentPage.Register(pageEvent);
    }
    catch (Exception exception)
    {
        Log.Error(string.Format("{0} pageevent not created in current Sitecore Instance: {1}", eventName, exception), "");
    }
}
The difference with this version of  method is an additional itemId parameter, which is the ID of the page that the event/goal should be registered to. Then, before the event/goal is registered, the currentPage variable is updated to use the correct item id and URL (path).

In Sitecore analytics the URLs for these events/goals appear as relative paths, which is why the string replace is taking out the root path for the site.

Sitecore contacts error with FlushContactToXdb method

Working with contacts and custom data, during the identification process in some cases the FlushContactToXdb method on ContactManager was giving the following error:
An unhandled exception of type 'System.StackOverflowException' occurred in mscorlib.dll
As with many of the community I was using the ExtendedContactRepository class from Brian Pedersen. It turns out that there was a possibility for infinite loop with the GetOrCreateContact method, which is detailed here. Effectively the repository was attempting to save a contact as new when it already exists with the given identifier.

An update to the repository appears to solve the issue in my case.

Monday, March 13, 2017

Sitecore personalization rules for custom contact facets

This post is part 5 of a series, the introduction is available here and includes a table of contents, along with the full source code.

The concept of contacts (and the experience profile) in Sitecore is a great way to store a lot of valuable information on visitors to a Sitecore web site. Out of the box features really do provide a complete overview of a given browser across many mediums (including print). But custom contact facets open the door to unique business logic and requirements of a given organisation. What better way to utilize this data than creating a custom rule to be used for personalization?

In this example we are going to go with a simple rule - User is an active customer. Extending on from the active customer flag that was added to the custom data contact facet. This will allow Sitecore web pages to change based on whether or not a user has been flagged as an active customer. The use case here would be an incentive to purchase for non-customers and perhaps an up-sell for existing customers. By combining with another rule that checks the product purchase history of the user, you can see how targeted you could make web pages with Sitecore and minor customization.

The custom rule code

This is a simple rule that doesn't make use of conditions (greater than, less than, etc.), you'll notice that it includes TrueCondition rather than something more complex such as StringOperatorCondition. It simple returns whether or not the current user has been marked as an active customer or not. Sitecore will handle the inverse if the user selects where not in the rule builder.

using ContactFacet.Custom;
using Sitecore.Rules;
using Sitecore.Rules.Conditions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace ContactFacet.Rules
{
    public class IsActiveCustomerRule<T> : TrueCondition<T> where T : RuleContext
    {
        protected override bool Execute(T ruleContext)
        {
            try
            {
                var contact = Sitecore.Analytics.Tracker.Current.Contact;
                ICustomDataFacet facet = contact.GetFacet<ICustomDataFacet>(CustomDataFacet.FacetName); // Get the facet

                return facet.ActiveCustomer;
            }
            catch (Exception)
            {
                return false;
            }
        }
    }
}

Registering the rule in Sitecore

This is the relatively easy part.
  1. Log into Sitecore
  2. Browse to the elements item (where rules definitions are defined) - /sitecore/system/Settings/Rules/Definitions/Elements/
  3. Insert a new Element Folder and call is Contact
  4. Under the elment folder expand Tags and edit the Default item
    1. Select conditional renderings on the tags field - this allows the rule to be used when building page components
    2. Save the item
  5. Under the Contact element folder, Insert a new Condition item and give it a name of Is active customer
  6. Set the text to Where the user is an active customer. Usually this would be built up using operators and would appear a bit more advanced.
  7. Set the type to your class followed by DLL - ContactFacet.Rules.IsActiveCustomerRule, ContactFacet for example
  8. The rule has been created

Using the rule

Now when you are using the personalize section of the rule editor the new rule will be visible.

Which can then be used to build up the page to appear differently to users who are/are not active customers.



This is just a simple example to get the ball rolling, but illiterates how data stored on a custom contact facet can be used to deliver different experiences to different users.

Saturday, March 11, 2017

Sitecore merging contacts with custom facets

This post is part 4 of a series, the introduction is available here and includes a table of contents, along with the full source code.

When there are two contact records in the xDB that are the same person, they can be merged. This will happen automatically when you identify a contact and can be called programatically when business logic dictates a merge. This also introduces the concept of the dying contact (the one being merged) and the surviving contact (the one being merged into).

Sitecore has a post which goes over the merge rules in detail, but custom contact facet data in particular will only copy over from the dying contact if the surviving contact has no data in that facet. It is however possible to create a custom pipeline that runs during a merge which can handle the custom contact facet merge to ensure data is brought across. Effectively you will have access in code to both contacts and can use custom logic to decide what to do with the data. It may be as simple as a full replacement, or you may need to decide on a field by field basis.

The pipeline

This is a simple example where we are effectively overwriting the customer data of the surviving contact and bringing in the product purchases. There would likely be a lot more rules around this copy in a real world example.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Sitecore.Analytics.Pipelines.MergeContacts;
using ContactFacet.Custom;

namespace ContactFacet.Pipeline
{
    public class MergeCustomData : MergeContactProcessor
    {
        public override void Process(MergeContactArgs args)
        {
            var dyingContactFacet = args.DyingContact.GetFacet<ICustomDataFacet>(CustomDataFacet.FacetName);
            var survivingContactFacet = args.SurvivingContact.GetFacet<ICustomDataFacet>(CustomDataFacet.FacetName);
            
            if (dyingContactFacet.IsEmpty)
            {
                return; // No data to merge
            }

            survivingContactFacet.CrmId = dyingContactFacet.CrmId; // Take over the CRM ID
            survivingContactFacet.ActiveCustomer = dyingContactFacet.ActiveCustomer; // Take over active customer flag

            //Copy over products
            if (dyingContactFacet.ProductPurchases != null && dyingContactFacet.ProductPurchases.Any())
            {
                foreach (var productPurcahse in dyingContactFacet.ProductPurchases)
                {
                    var newPurchase = survivingContactFacet.ProductPurchases.Create();
                    newPurchase.PurchaseDate = productPurcahse.PurchaseDate;
                    newPurchase.ProductId = productPurcahse.ProductId;
                }
            }
        }
    }
}

Configuration

<configuration>
  <sitecore>
    <pipelines>
      <mergeContacts>
        <processor type="ContactFacet.Pipeline.MergeCustomData" />
      </mergeContacts>
    </pipelines>
  </sitecore>
</configuration>

It's as simple is that. Part five: Personalization rules for contact facets, gives an example personalization rule that utilities the custom contact facet data.

Sitecore showing custom contact facet data in Experience Profiles

This post is part 3 of a series, the introduction is available here and includes a table of contents, along with the full source code.

Add the custom data tab to the Experience Profile

Adding the tab to Sitecore's Experience Profile area, requires creating items in the Core database. This task cannot be done via the standard web interface and instead will have to be done through Sitecore Rocks.

This task is similar to building up a standard page in Sitecore. A base item is created which contains a layout and a number of sublayouts/renderings which then build up what is displayed on the page. However instead of it being a standalone page, it's another tab which appears inside the experience profile.

This post is based heavily on the example/steps provided by Adam Conn, in this post. The primary addition is the option to display listed data (in a table) as opposed to single strings, along with some changes in rendering names.
  1. Expand the core database and locate the PageSettings item: /sitecore/client/Applications/ExperienceProfile/Contact/PageSettings
  2. Insert a new item under PageSettings of the type Page-Stylesheet-File an
    1. Set the name to CustomDataTab
    2. Set the Stylesheet field to /sitecore/shell/client/Applications/ExperienceProfile/Contact/CustomData/CustomDataTab.css - this file will be created in a later step.
  3. Locate the Tabs item: /sitecore/client/Applications/ExperienceProfile/Contact/PageSettings/Tabs
  4. Insert a new item of template tab and call it CustomData


  5. On this newly created item, change the header fields text to Custom Data
  6. Insert another item of type tab under the CustomData item and all this CustomDataPanel
  7. Right click on the CustomDataPanel item and select Tasks Design Layout


  8. Set the layout field to the type Speak-EmptyLayout

  9.  
  10. Right click on the CustomData item and select Tasks > Design Layout
  11. Add a new rendering of type text
    1. Edit this item and set IsVisible to false
    2. Set the ID to CustomDataTabId
    3. Set the Text field to be the item ID of the CustomData item



  12. Add a border rendering and set the ID to CustomDataTabBorder
  13. Add a LoadOnDemandPanel rendering and edit the following fields
    1. Set PlaceholderKey to CustomDataTabBorder.Content
    2. Set Database to core
    3. Set ItemId to the item ID of the CustomDataPanel
    4. Set ID to CustomDataLoadOnDemandPanel


  14. Add an image rendering and edit the following fields
    1. Set width to 60px
    2. Set height to 60px
    3. Set IsVisible to {Binding CustomDataLoadOnDemandPanel.isLoaded, Converter=Not}
    4. Set PlaceholderKey to CustomDataTabBorder.Content
    5. Set ID to CustomDataIndicatorImage
  15. Add a ProgressIndicator rendering and edit the following fields
    1. Set ID to CustomDataProgressIndicator
    2. Set IsBusy to {Binding CustomDataLoadOnDemandPanel.isBusy}
    3. Set DataSource to {88134D50-FA78-406D-8C52-8286EB12BB25} - this is a system item for the progress indicator
    4. Set PlaceholderKey to CustomDataTabBorder.Content
    5. Set TargetControl to CustomDataTabBorder
  16. The design layout for CustomData should now appear the same as the following:


  17. Insert under the CustomDataPanel item a new item of type AdvancedExpander Parameters (may be called Accordion Parameters in older versions). Call this item, CustomDataAccordion.
    1. Set the header field of this item to Custom Data.


  18. Insert under the CustomDataPanel item a new item of type Text Parameters, and call this item DataNotFound.
    1. Set the text field of this new item to be "No custom data has been found". This is the message that will display when a given contact has no data in the custom contact facet.
  19. Right click on the CustomDataPanel item and select Tasks Design Layout
  20. Add a new rendering of type SubPageCode.
    1. Ensure the PlaceholderKey is Page.Body
    2. Set PageCodeScriptFileName to "/sitecore/shell/client/Applications/ExperienceProfile/Contact/CustomData/CustomDataTab.js" - this file will be created in a later step.
  21. Add a new rendering of type border.
    1. Give it an id of CustomDataTabBorder.
    2. Ensure the PlaceholderKey is Page.Body
  22. Add a new rendering of type MessageBar.
    1. Give it an id of ExternalDataTabMessageBar
    2. Ensure the PlaceholderKey is Page.Body
  23. Add a new rendering of type text.
    1. Give it an id of NoCustomData.
    2. Set IsVisible to false.
    3. Set the text to No Custom Data Found
    4. Set the PlaceholderKey to CustomDataTabBorder.Content
  24. Add a new rendering of type GenericDataProvider
    1. Set the PlaceholderKey to CustomDataTabBorder.Content
    2. Give it an id of CustomDataProvider
  25. Add a new rendering of type Border
    1. Set the PlaceholderKey to CustomDataTabBorder.Content
    2. Give it an id of CustomDataBorder
  26. Add a new rendering of type AdvancedExpander (in older versions of Sitecore it will be accordion)
    1. Set the PlaceholderKey to CustomDataBorder.Content
    2. Give it an id of CustomDataExpander
    3. Set the datasource to the item id of the CustomDataAccordion item created earlier.
  27. Add a new rendering of type border
    1. Set the UsePadding field to True
    2. Set the PlaceholderKey to CustomDataExpander.Body
    3. Set the id to CrmIdBorder
  28. Add a new rendering of type text
    1. Set the text value to CRM ID: 
    2. Set the PlaceholderKey to CrmIdBorder.Content
    3. Give it an id of CrmIdLabel
  29. Add a new rendering of type text 
    1. Set the PlaceholderKey to CrmIdBorder.Content
    2. Give it an id of CrmIdValue
  30. Under the CustomDataPanel item insert a new item of type ListControl
    1. Give it an id of ProductPurchaseList
  31. Under the ProductPurchaseList insert a new item of ColumnField type
    1. Give it an id of Product
    2. Set the header text to Product ID
    3. Set the data field to Product
  32. Under the ProductPurchaseList insert a new item of ColumnField type
    1. Give it an id of Date
    2. Set the header text to Purchase Date
    3. Set the data field to Date
  33. Add a new rendering of type border
    1. Set the UsePadding field to True
    2. Set the PlaceholderKey to CustomDataExpander.Body
    3. Set the id to ProductPurchaseBorder
  34. Add a new rendering of type text
    1. Set the text value to Purchase History: 
    2. Set the PlaceholderKey to ProductPurchaseBorder.Content
    3. Give it an id of ProductPurchaseLabel
  35. Add a new rendering of type SearchDataSource
    1. Set the id to ProductPurchaseDataSource
    2. Set the PlaceholderKey to ProductPurchaseBorder.Content
  36. Add a new rendering of type ListControl
    1. Set the id of to ProductPurchaseListControl
    2. Set the PlaceholderKey to ProductPurchaseBorder.Content
    3. Set the DataSource to the id of the ProductPurchaseList item created earlier
    4. Set the value of the Binding field to {Binding ProductPurchaseDataSource.Items}
The CustomDataPanel design layout will now look like the following.


The item tree for the custom tab will look like this.


Create the query pipeline

In order to display the data from the Custom Data facet in the experience profile a query pipeline processor needs to be created which will query xDB to retrieve the data.

using Sitecore.Cintel.Reporting;
using Sitecore.Cintel.Reporting.Processors;
using Sitecore.Cintel.Reporting.ReportingServerDatasource;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace ContactFacet.Reporting
{
    public class GetCustomData : ReportProcessorBase
    {
        public override void Process(ReportProcessorArgs args)
        {
            var queryExpression = this.CreateQuery().Build();
            var table = base.GetTableFromContactQueryExpression(queryExpression, args.ReportParameters.ContactId, null);
            args.QueryResult = table;
        }
        protected virtual QueryBuilder CreateQuery()
        {
            var builder = new QueryBuilder
            {
                collectionName = "Contacts"
            };
            builder.Fields.Add("_id");
            builder.Fields.Add("CustomData_CrmId");
            builder.Fields.Add("CustomData_ActiveCustomer");
            builder.QueryParms.Add("_id", "@contactid");
            return builder;
        }
    }
}

Create the contact view pipeline

The next step is to create the pipeline processor that will be used by the experience profile to execute a query and place the results into a data object. In this case the data will be stored inside a System.Data.DataTable which will ultimately be serialized into an array to be used via the display JavaScript file.

The first step is to define the results data table structure.

using Sitecore.Cintel.Reporting;
using Sitecore.Cintel.Reporting.Processors;
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Web;

namespace ContactFacet.Reporting
{
    public class ConstructCustomDataTable : ReportProcessorBase
    {
        public override void Process(ReportProcessorArgs args)
        {
            args.ResultTableForView = new DataTable();

            var viewField = new ViewField<string>("CrmId");
            args.ResultTableForView.Columns.Add(viewField.ToColumn());

            var customViewField = new ViewField<bool>("ActiveCustomer");
            args.ResultTableForView.Columns.Add(customViewField.ToColumn());

            var productViewField = new ViewField<DataTable>("ProductPurchases");
            args.ResultTableForView.Columns.Add(productViewField.ToColumn());
        }
    }
}

The next step is to create the pipeline which will execute the query and populate the data table defined above.

using ContactFacet.Custom;
using Sitecore.Cintel;
using Sitecore.Cintel.Reporting;
using Sitecore.Cintel.Reporting.Processors;
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Web;

namespace ContactFacet.Reporting
{
    public class PopulateCustomData : ReportProcessorBase
    {
        public override void Process(ReportProcessorArgs args)
        {
            ICustomDataFacet fullFacet = (ICustomDataFacet)CustomerIntelligenceManager.ContactService.GetFacet(args.ReportParameters.ContactId, "CustomData");
            var result = args.QueryResult;
            var table = args.ResultTableForView;

            foreach (DataRow row in result.AsEnumerable())
            {
                var targetRow = table.NewRow();

                // CRM ID
                var id = row["CustomData_CrmId"];

                if (id == null || string.IsNullOrEmpty(id.ToString()))
                {
                    continue;
                }
                
                targetRow["CrmId"] = id;

                // Active Customer
                var activeCustomer = row["CustomData_ActiveCustomer"];

                if (activeCustomer == null || string.IsNullOrEmpty(id.ToString()))
                {
                    continue;
                }

                targetRow["ActiveCustomer"] = Convert.ToBoolean(activeCustomer);

                // Product Purchases
                DataTable productPurchases = new DataTable();
                productPurchases.Columns.Add("Product", typeof(string));
                productPurchases.Columns.Add("Date", typeof(DateTime));

                if (fullFacet.ProductPurchases != null && fullFacet.ProductPurchases.Any())
                {
                    foreach (var purchase in fullFacet.ProductPurchases)
                    {
                        productPurchases.Rows.Add(purchase.ProductId, purchase.PurchaseDate);
                    }
                }

                targetRow["ProductPurchases"] = productPurchases;

                table.Rows.Add(targetRow);
            }

            args.ResultSet.Data.Dataset[args.ReportParameters.ViewName] = table;
        }
    }
}

Register the pipelines

The following configuration file will regster the three pipelines above inside Sitecore.

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <group groupName="ExperienceProfileContactDataSourceQueries">
        <pipelines>
          <custom-query>
            <processor type="ContactFacet.Reporting.GetCustomData, ContactFacet" />
          </custom-query>
        </pipelines>
      </group>
      <group groupName="ExperienceProfileContactViews">
        <pipelines>
          <custom>
            <processor type="ContactFacet.Reporting.ConstructCustomDataTable, ContactFacet" />
            <processor type="Sitecore.Cintel.Reporting.Processors.ExecuteReportingServerDatasourceQuery, Sitecore.Cintel">
              <param desc="queryName">custom-query</param>
            </processor>
            <processor type="ContactFacet.Reporting.PopulateCustomData, ContactFacet" />
          </custom>
        </pipelines>
      </group>
    </pipelines>
  </sitecore>
</configuration>

Create JavaScript to call the pipelines and display data

Earlier on a JavaScript file was referenced with the following path "/sitecore/shell/client/Applications/ExperienceProfile/Contact/CustomData/CustomDataTab.js" in the code solution create this file with that folder path.
define(
  ["sitecore",
    "/-/speak/v1/experienceprofile/DataProviderHelper.js",
    "/-/speak/v1/experienceprofile/CintelUtl.js"
  ],
  function (sc, providerHelper, cintelUtil, ExternalDataApiVersion) {
      var cidParam = "cid";
      var intelPath = "/intel";

      var app = sc.Definitions.App.extend({
          initialized: function () {
              $('.sc-progressindicator').first().show().hide();
              var contactId = cintelUtil.getQueryParam(cidParam);
              var tableName = "";
              var baseUrl = "/sitecore/api/ao/v1/contacts/" + contactId + "/intel/custom";

              providerHelper.initProvider(this.CustomDataProvider,
                tableName,
                baseUrl,
                this.ExternalDataTabMessageBar);

              providerHelper.getData(this.CustomDataProvider,
                $.proxy(function (jsonData) {
                    var dataSetProperty = "Data";
                    if (jsonData.data.dataSet != null && jsonData.data.dataSet.custom.length > 0) {
                        // Data present set value content
                        var dataSet = jsonData.data.dataSet.custom[0];
                        this.CustomDataProvider.set(dataSetProperty, jsonData);
                        this.CrmIdValue.set("text", dataSet.CrmId);
                        this.ProductPurchaseDataSource.set('items', dataSet.ProductPurchases);
                    } else {
                        // No data hide the labels
                        this.CrmIdLabel.set("isVisible", false);
                        this.ProductPurchaseLabel.set("isVisible", false);
                        this.ExternalDataTabMessageBar.addMessage("notification", this.NoCustomData.get("text"));
                    }
                }, this));
          }
      });
      return app;
  });

Create the CSS styles

By default the tab will not look the greatest in terms of padding. Below is a basic starter to show how you can style he custom data tab.

div[data-sc-id=CustomDataExpander] {
  margin-top: 30px;
}

div[data-sc-id=CustomDataExpander] .sc-advancedExpander-body {
    padding:10px;
}

div[data-sc-id=CustomDataExpander] .sc-border {
    margin-top:10px;
    margin-bottom:10px;
}

The end result

Now the custom data tab will appear inside the experience profile and star displaying as expected.


Part four: Sitecore merging contacts with custom facets goes over how this custom facet data is handled when two contacts are merged.

Sitecore accessing custom contact facets

This post is part 2 of a series, the introduction is available here and includes a table of contents, along with the full source code.


Setting the facet data

For this example, a test aspx web page has been created which identifies a contact, and enters data into each of the fields on the newly created contact facets.
using ContactFacet.Custom;
using Sitecore.Analytics.Model;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace ContactFacet
{
    public partial class GetFacet : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            var identifyUser = IdentifyUser(); // Identify the user

            if (identifyUser)
            {
                var contact = Sitecore.Analytics.Tracker.Current.Contact;
                ICustomDataFacet facet = contact.GetFacet<ICustomDataFacet>(CustomDataFacet.FacetName); // Get the facet

                if (facet != null)
                {
                    facet.ActiveCustomer = true;
                    facet.CrmId = "1234567A";

                    // Product Purchases
                    var productPurchasesField = facet.ProductPurchases.Create();
                    productPurchasesField.ProductId = "PID1";
                    productPurchasesField.PurchaseDate = DateTime.Now;

                    var productPurchasesField2 = facet.ProductPurchases.Create();
                    productPurchasesField2.ProductId = "PID3";
                    productPurchasesField2.PurchaseDate = DateTime.Now;
                }
            }

            Session.Abandon(); // Push data through to MongoDB
        }

        private bool IdentifyUser()
        {
            var identifiers = Sitecore.Analytics.Tracker.Current.Contact.Identifiers;

            if (identifiers.IdentificationLevel != ContactIdentificationLevel.Known)
            {
                Sitecore.Analytics.Tracker.Current.Session.Identify("test@email.com"); // Statically bound email :)
                return true;
            }

            return false;
        }
    }
}
It's relatively simple, the flow of the code is as follows:
  1. Identify the user by an email address - this data can be saved to anonymous contacts who can be identified at a later stage.
  2. Get the custom contact facet
  3. Populate the simple fields on the facet (active customer and CRM ID).
  4. Each of the product purchase elements are instantiated via the create method - remember that this type only had a get and not set inside the interface.
  5. The session is abandoned to push the data through to MongoDB - this is for testing purposes and in the real world would happen 20 minutes after the users last interaction with the Sitecore website. 
Now we can use a tool such as Robomongo to query MongoDB and ensure that our data has been saved. The query that can be used is: db.getCollection('Contacts').find({"Identifiers.Identifier":"test@email.com"}).

Robomongo showing the custom contact facet data


Conclusion

In the example above we show how to set the data for a custom contact facet in Sitecore. Accessing the data is covered in part 3, where the Experience Profile is updated to display the custom facet data.

Sitecore creating custom contact facets

This post is part 1 of a series, the introduction is available here and includes a table of contents, along with the full source code.


The data structure


The facet will be named CustomData.
  1. CRM ID - string
  2. Active Customer - boolean
  3. Product Purchases - List of custom object
    1. Product ID - string
    2. Purchase Date - datetime

The interface

The first step is to create the interface for the custom contact facet. The primary object is the facet and this can contain many elements.
using Sitecore.Analytics.Model.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace ContactFacet.Custom
{
    public interface ICustomDataFacet : IFacet
    {
        string CrmId { get; set; }
        bool ActiveCustomer { get; set; }
        IElementCollection<IProductPurchaseElement> ProductPurchases { get; }
    }

    public interface IProductPurchaseElement : IElement
    {
        string ProductId { get; set; }
        DateTime PurchaseDate { get; set; }
    }
}
Any facet field which is a list of an object (even a list of single strings) must be based on IElementCollection with an IElement.

The implementation

The next step is to create the implementation of the interface. You will notice that the classes are all serializable.
using Sitecore.Analytics.Model.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace ContactFacet.Custom
{
    [Serializable]
    public class CustomDataFacet : Facet, ICustomDataFacet
    {
        public static readonly string FacetName = "CustomData";
        private const string _PRODUCT_PURCHASES_NAME = "ProductPurchases";
        private const string _CRM_ID_NAME = "CrmId";
        private const string _ACTIVE_CUSTOMER_NAME = "ActiveCustomer";

        public CustomDataFacet()
        {
            EnsureCollection<IProductPurchaseElement>(_PRODUCT_PURCHASES_NAME);
        }

        public IElementCollection<IProductPurchaseElement> ProductPurchases
        {
            get
            {
                return GetCollection<IProductPurchaseElement>(_PRODUCT_PURCHASES_NAME);
            }
        }

        public bool ActiveCustomer
        {
            get
            {
                return GetAttribute<bool>(_ACTIVE_CUSTOMER_NAME);
            }
            set
            {
                SetAttribute(_ACTIVE_CUSTOMER_NAME, value);
            }
        }

        public string CrmId
        {
            get
            {
                return GetAttribute<string>(_CRM_ID_NAME);
            }
            set
            {
                SetAttribute(_CRM_ID_NAME, value);
            }
        }
    }

    [Serializable]
    public class ProductPurchaseElement : Element, IProductPurchaseElement
    {
        private const string _PRODUCT_ID = "ProductId";
        private const string _PURCHASE_DATE = "PurchaseDate";

        public ProductPurchaseElement()
        {
            EnsureAttribute<string>(_PRODUCT_ID);
            EnsureAttribute<DateTime>(_PURCHASE_DATE);
        }

        public string ProductId
        {
            get
            {
                return GetAttribute<string>(_PRODUCT_ID);
            }
            set
            {
                SetAttribute(_PRODUCT_ID, value);
            }
        }

        public DateTime PurchaseDate
        {
            get
            {
                return GetAttribute<DateTime>(_PURCHASE_DATE);
            }
            set
            {
                SetAttribute(_PURCHASE_DATE, value);
            }
        }
    }
}

Registering the contact facet in Sitecore

Now that the facet has been defined, we need to register it inside the Sitecore.Analytics.Model.config file. The following XML file will achieve this.
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <model>
      <elements>
        <element interface="ContactFacet.Custom.IProductPurchaseElement, ContactFacet" implementation="ContactFacet.Custom.ProductPurchaseElement, ContactFacet" />
        <element interface="ContactFacet.Custom.ICustomDataFacet, ContactFacet" implementation="ContactFacet.Custom.CustomDataFacet, ContactFacet" />
      </elements>
      <entities>
        <contact>
          <facets>
            <facet name="CustomData" contract="ContactFacet.Custom.ICustomDataFacet, ContactFacet" />
          </facets>
        </contact>
      </entities>
    </model>
  </sitecore>
</configuration>

Conclusion

That's it, the custom contact facet is now available to be accessed for a given site browser (identified or anonymous contact).Part two: Sitecore accessing custom contact facets will go over the steps to access this facet at a contact level.

Sitecore custom contact facets with data access and display in Experience Profiles

In Sitecore, a contact is a person who interacts with a given organization. These contacts and all of the information collected about them is stored in the Experience Database (xDb) and can be viewed via the Experience Profiles section of Sitecore. By default a large amount of data is collected about the user's interactions with an organization across all devices and mediums (including print and email).

Sitecore Experience Profile view of a contact

The scope of this post series is the concept of creating a custom contact facet (which means the ability to store custom data against a contact). The next step is the ability to access this data (read/write) via code. Finally add a custom tab to the Experience Profile to allow users to view this custom data for any given contact. Then a look at how merging contacts works, when custom data is in play and how to personalize based on the data iteself.

Table of contents

  1. Part One: Creating a custom contact facet
  2. Part Two: Custom contact facet data access
  3. Part Three: Adding contact facet to Experience Profiles
  4. Part Four: Handling custom facets when merging contacts
  5. Part Five: Personalization rules for contact facets
This series has been developed on Sitecore version 8.2, and there will be minor differences from earlier versions. Take for example the accordion control which has been renamed to an... Advanced Expander!

Prerequisites

Apart from the standard Sitecore instance (including MongoDB), along with Visual Studio. The only other requirement for following this series is Sitecore Rocks (either the Visual Studio plugin or stand-alone version). Sitecore Rocks is required for adding the contact facet to Experience Profiles - as this cannot be done via the standard web interface.

The scenario

To keep it simple, only 3 fields are going to be stored in the custom contact facet. The facet will be named CustomData.
  1. CRM ID - string
  2. Active Customer - boolean
  3. Product Purchases - List of custom object
    1. Product ID - string
    2. Purchase Date - datetime
These fields aren't chosen as they related well to real world examples, but more to showcase the various types of data that can be stored inside a contact facet.

The source code

The full source code for this series is available on GitHub, which includes Sitecore package for the items created. There is a test page for setting the facet data and another to abandon the session (pushes the data through to MongoDB and ultimately experience profile viewer faster).