How to extend the view of the contact list to show extra facets instead of the default ones


Description

Users might face difficulties with extending List Manager so it shows extra facets of contacts. The article describes the algorithm for adding columns to the Contacts table.

Solution

Back up your solution before applying any changes.
  1. Create and deploy a custom model that wraps a custom facet, as described in the following articles:
    Create a model
    Deploy a custom model
  2. Add a column for a custom facet to the Contacts table:
    1. Switch to the Core database.
    2. Add a new ColumnField item under the /sitecore/client/Applications/List Manager/Lists/Contact list item.
    3. In the newly created item, fill out the following fields:
    • EmptyText (sets a default text if a cell does not contain any data)
    • HeaderText (a name of the column)
    • DataField (a name of the custom facet, used as a data source).
    4. Save the changes. The new column then appears in the List Manager app.
  3. Configure the Import contacts wizard dialog to include custom contact facets as described in this article: Configure the Import contacts wizard to include custom contact facets.
  4. Create a custom contacts controller.
    1. Create your custom ContactDataModel to contain all the required facets. For example:
    public class ContactDataModel : Sitecore.ListManagement.Services.Model.ContactDataModel
    {
    public string FavouriteBrand { get; set; }
    }
    2. Data is mapped by the Sitecore.ListManagement.Services.Repositories.ListSubscriptionsStore class (retrieve the code using IL Spy, .NET Reflector, or any other reverse engineering tool). Create your own class that has a GetSubscribers method returning IEnumerable of your custom model as follows:
    public IEnumerable<CustomNamespace.ContactDataModel> GetSubscribers(Guid listId, string searchFilter, int pageIndex, int pageSize)
    {
    ContactList contactList = this.EnsureList(listId, null);
    ContactSearchResult result = this._contactProvider.GetFilteredContacts(contactList, searchFilter, pageIndex, pageSize);
    List<CustomNamespace.ContactDataModel> source = result.Contacts.Select<Contact, CustomNamespace.ContactDataModel>(new Func<Contact, CustomNamespace.ContactDataModel>  (this.MapEntity)).ToList<CustomNamespace.ContactDataModel>();
    if (source.Any<ContactDataModel>())
    {
      source[0].Count = result.Count;
    }
    return source;
    }
    Note: The data is retrieved from _contactProvider and converted to the specified model using the MapEntity method.
    Next, inject the custom mapping logic in the MapEntity method:
    CustomNamespace.ContactDataModel model1 = new CustomNamespace.ContactDataModel();
    Guid? id = entity.Id;
    model1.Id = (id.HasValue ? id.GetValueOrDefault() : Guid.Empty).ToString();
    model1.Email = (entity.Emails()?.PreferredEmail == null) ? null : ((entity.Emails() == null) ? null : (entity.Emails().PreferredEmail)).SmtpAddress;
    model1.FirstName = entity.Personal()?.FirstName;
    model1.LastName = entity.Personal()?.LastName;
    model1.FavouriteBrand = entity.GetFacet<MarketingDataFacet>().FavouriteBrand;
    ContactIdentifier identifier = entity.Identifiers.FirstOrDefault<ContactIdentifier>(x => (x.Source == "ListManager")) ?? entity.Identifiers.FirstOrDefault<ContactIdentifier>();
    model1.Identifier = identifier?.Identifier;
    model1.IdentifierSource = identifier?.Source;
    return model1;
    The _contactProvider.GetFilteredContacts method returns only three facets (ListSubscriptions, Personal, Emails). So, add the custom ones to the list:
    IContactSource contactSource = this._contactSourceFactory.GetSource(contactList);
    FilteringSource source2 = new FilteringSource(contactSource, searchFilter);
    string[] facets = new string[] { "ListSubscriptions", "Personal", "Emails", "MarketingDataFacet" };
    return new ContactSearchResult(this._segmentationService.GetContacts(source2, pageSize * pageIndex, pageSize, facets), (int)this._segmentationService.GetCount(contactSource));
    3. The default ListSubscriptionsStore is used by the Sitecore.ListManagement.Services.Controllers.ContactsController controller. Create your custom controller, which performs the same actions and changes the GetEntries method to use the custom ListSubscriptionsStore.
     public class CustomContactsController : ServicesApiController
    {
    //  Add a parametreless constructor to your controller
    public CustomContactsController()
    {
      var clp = Sitecore.DependencyInjection.ServiceLocator.ServiceProvider.GetService<IContactListProvider>();
      var sp = Sitecore.DependencyInjection.ServiceLocator.ServiceProvider.GetService<ISubscriptionService>();
      var csf = Sitecore.DependencyInjection.ServiceLocator.ServiceProvider.GetService<IContactSourceFactory>();
      var sss = Sitecore.DependencyInjection.ServiceLocator.ServiceProvider.GetService<ISegmentationService>();
      var cp = new Sitecore.Support.ListManagement.XConnect.ContactProvider(sss, csf); // create an object of the custom ContactProvider class
      var or = Sitecore.DependencyInjection.ServiceLocator.ServiceProvider.GetService<IListOperationRepository>();
      var lss = Sitecore.DependencyInjection.ServiceLocator.ServiceProvider.GetService<IListSubscriptionsStore<ContactDataModel>>();
      int batchSize = Sitecore.Configuration.Settings.GetIntSetting("ListManagement.BatchSize", 250);
      this._listSubscriptionsStore = new CustomListSubscriptionsStore(clp, sp, cp, or, batchSize); // create your custom store
    }
    ..........
            
    [Route]
    [HttpGet]
    [ActionName("MyDefaultAction")]
     public virtual IEnumerable<CustomNamespace.ContactDataModel> GetEntities(Guid listId, string filter = "", int pageIndex = 0, int pageSize = 20)
    {
      try
      {
        return this._listSubscriptionsStore.GetSubscribers(listId, filter, pageIndex, pageSize); // your custom store must be used here
      }
      catch (Exception exception) when (!(exception is ContactListNotFoundException))
      {
        this._log.Error(string.Format(CultureInfo.InvariantCulture, "[ListManagement]: Failed to get count of contacts for {0} contact list. The error has been occurred: {1}", listId, exception.Message), exception, this);
        return Enumerable.Empty<ListManagmentViewFacets.ContactDataModel>();
      }
    }
    }        
    4. Change the RoutePrefix attribute value of the controller to sitecore/api/customlists/{listId}/contacts.
    5. Create a processor that registers your control:
    public class RegisterHttpRoutes
    {
      public void Process(PipelineArgs args)
      {
        GlobalConfiguration.Configure(Configure);
      }

    protected void Configure(HttpConfiguration configuration)
      {
      var routes = configuration.Routes;
       routes.MapHttpRoute("CustomContacts", "sitecore/api/customlists/{listId}/contacts", new
        {
            controller = "CustomContacts",
            action = "MyDefaultAction", // Name of the action in ActionName attribute         
       });
     }
    }
    6. Build the assembly and put it in the bin folder of your site.
    7. Patch the "RegisterHttpRoutes" processor right after "<processor type="Sitecore.Mvc.Pipelines.Loader.InitializeRoutes, Sitecore.Mvc" /> " (<initialize> pipeline).
    8. Open your Core database in the Content Editor and navigate to the /sitecore/client/Applications/List Manager/Global Settings/ListTaskPageSettings/ContactsDataSource Parameters item. Change its URL field to "/sitecore/api/customlists".

After performing all these steps, you can upload a CSV list containing custom facets.

Note: The given algorithm is intended as an example and can be extended and changed according to your requirements.