Tuesday, July 3, 2018

Sitecore admin page in 9.0.2

Sitecore admin pages

Most Sitecore developers will probably know that Sitecore has some admin pages in /sitecore/admin.
Jammykam wrote a very good overview on it in 2016 as we did get an overview page of the admin tools in v8.1.

Sitecore admin pages in 9.0.2


Recently I installed the (now) latest version of Sitecore, 9.0.2. The admin tools are still there, but wow.. they did get an update. The page went from looks-like-this-is-made-by-a-developer to something that fits in the admin interface of Sitecore.



As you can see there are also a few new additions. I won't go over the existing ones (they have been explained in the aformentioned blog by Kamruz or also here by Kris).

New additions

One that was added with Sitecore 9 already was the extended show config:: Show Config Layers. It will bring you to /sitecore/admin/ShowConfigLayers.aspx and allows you to see your configuration based on layers and roles. Would have been nice if environments were able as well, but haven't found that yet.


But the one I really wanted to mention, is a new and very cool one: the Support Package generator.  Yes, no need to install this anymore - you can generate your support packages from the admin section of the site now! Very nice, and always up to date. Available at /sitecore/admin/supportpackage.



Friday, May 11, 2018

Sitecore context language based on Geo IP location

Sitecore context language


Sitecore uses a context language (Sitecore.Context.Language) to fetch the correct version of an item to display. There is an out-of-the-box logic to determine the context language - it uses:
  1. The "sc_lang" query string parameter
  2. The language prefix in the url
  3. The language cookie for the site
  4. The default language in the site config.
  5. The DefaultLanguage setting in the Sitecore config
There are many other functional scenario's to determine the language of a (new) visitor and they can be developed in a custom processor that is placed in the httpRequestBegin pipeline. Create a class that overrides HttpRequestProcessor, implement an override for the Process method containing any logic you need and adapt the Sitecore Context properties (Language) as needed.

What if you want visitors to be directed to a language based on their location?

Geo IP detection

In Sitecore you can setup Geo IP location detection. This will add information to the Tracker about the location of your visitor. This information can be used for several purposes, and possibly also for language detection.

Geo IP to set the context language

We had to set the language of the new visitor to en-US when the visitor came from the USA.
As we had languages enabled in the urls, we only had to take care of the homepage - all other urls had a defined language already.

Seems so easy! Just create the language resolver as mentioned above and set the language based on the country in the tracker. Done.

Issue 1 : the tracker

If you place your language resolver where you normally would (just after the one from Sitecore) you'll notice that your tracker is not et initialized. This is normal. If you want more information on the complete pipeline sequence in a request, you should read Martin Davies' blog
Solution: put your resolver elsewhere..  :
<startAnalytics>
   <processor patch:after="*[@type='Sitecore.Analytics.Pipelines.StartAnalytics.StartTracking, Sitecore.Analytics']" type="your-language-resolver, .." />
</startAnalytics>
As this is another pipeline, we'll also use another base class: my processor derives from RenderLayoutProcessor - the rest stays the same.

Issue 2: the Geo IP

On the first visit, you are most likely to find no geo ip data in the request pipeline. It's just too soon. The request has been made (async), but the results are not there yet. An article by Pavel Veller got us in the right direction. A new CreateVisitProcessor was made:
public class UpdateGeoIpData : CreateVisitProcessor
{
    public override void Process(CreateVisitArgs args)
    {
        var url = WebUtil.GetRawUrl();
        if (!url.Equals("/", StringComparison.OrdinalIgnoreCase))
        {
            return;
        }

        args.Interaction.UpdateGeoIpData(TimeSpan.FromMilliseconds(500));
    }
}

<pipelines>
    <createVisit>
        <processor type="Sitecore.Analytics.Pipelines.CreateVisits.UpdateGeoIpData, Sitecore.Analytics">
          <patch:delete/>
        </processor>
        <processor type="...UpdateGeoIpData, .." patch:after="processor[@type='Sitecore.Analytics.Pipelines.CreateVisits.XForwardedFor, Sitecore.Analytics']" />
    </createVisit>
</pipelines>

This will make sure that we wait a little longer, but only on the pages where needed.

Resolving

if (Tracker.Current.Interaction.HasGeoIpData)
{
    if (Tracker.Current.Interaction.GeoData.Country.Equals("US", StringComparison.OrdinalIgnoreCase))
    {
        return "en-US";
    }
}

Our final logic looked something as above. We test the country from the GeoData and handle accordingly.

For the moment we are running some performance tests, but so far it seems ok.

Monday, April 30, 2018

SUGCON 2018 - Sitecore User group conference in Berlin


The Sitecore User Group Conference (SUGCON) is getting bigger and better every year. The 2018 edition in Berlin was definitely a success. The sessions were great. We had plenty of good food and drinks - not enough tables but that made us join and meet new people (or bump into old acquaintances) which brings me to the most important part, the attendees: 600 awesome Sitecore community people that make this conference to the annual high mass for all Sitecore users, developers, marketers, .... With a 'little' help of an amazing group of organizers (also community people) who did a tremendous job putting this all together.
A warm applause for the community and a big thanks for the organizers!

Day 1 sessions


Day 1 started with an opening keynote, highlighting three community pilars (Sitecore community forum - Sitecore Slack - Sitecore StackExchange) and a surprise announcement on Sitecore's release cadence. From now on Sitecore will release:

  • a "Software release" twice a year with new features
  • "Update releases" with fixes but no breaking changes when needed. 


Jason Wilkerson and Richard Seal got the conference really started with a trip to "SitecoreLand". A fictive amusement park with bracelets, fast lanes, and a Sitecore driven website and mobile app. They used the whole Sitecore spectrum: SXA (Sitecore Experience Accelerator), Sitecore Commerce, Marketing automation and xConnect with a special sauce of IoT (Internet of Things) to accomplish a proof of concept for an all-in customer experience. It was a call to all of us to dream.. to use our imagination..  because lots of things are possible.

Kam Figy went on stage to show us "Uber-modern API's for Sitecore". The rabbit out of his hat (can a Unicorn wear a hat?) was called GraphQL. This is all still in preview, but the demo's were cool (and working). He showed the graph browser, an alternative database browser and the integration in JSS. It was amazing, overwhelming, cool, and a little bit "wtf".. (but javascript is not my usual playground).


Next on stage was Alexei Veshalovich. He showed us an omni-channel demo for an unrecognizable car brand using (amongst others) Outlook Plugin, Xamarin, and from the Sitecore platform JSS, ExM, xConnect and the Marketing automation engine. And still no angry demo gods :)

My next session was Commerce minded: Sumith Damodaran presented the architecture of the new Sitecore Commerce 9 platform with an emphasis on plugins.
We learned about inventory sets and sellable items, the storage roles and the application roles like Shops, Authoring and Minions. Commerce Plugins are a (or the) way to extend and enhance the solution, knowing that the product itself also implements it's core features via plugins. And a final note on SXA Storefront for building B2C e-commerce solutions using Sitecore Experience Accelerator.

Evening entertainment

Sugcon is also the place where the award ceremony for the Sitecore Hackathon and the MVP's take place.

For the first time the hackathon results were not announced up front, so the tension with the participants was a little higher when Akshay Sura presented the winners. As our team had to withdraw due to illness 😞 we didn't renew our title but we're still proud to see our name -No Weekend 4 Us- on the slide with past winners. But: congratulations to all 2018 winners! We know they did an amazing job. And thanks to Akshay and all judges for investing their time to make this happen!



During the MVP ceremony the many mvp's, who spend quite some time making the Sitecore community as good as it is, are being celebrated and their physical award is handed out. And of course, this is picture time.

The entertainment this year was a "Community Feud" with Akshay Sura and Robin Hermanussen as show hosts and Pete Navarra and Jason St-Cyr as team captains.
It was fun.. and our team (with Pete as captain) won - the prize money was donated to the non-profit Girls in Tech. The questions and answers were based upon a questionaire filled in by community members some time ago. "What drink would Pieter Brinkman and Mark Frost have in a pub?" - I think Pete learned that we have quite some trolls, as "water" was a top answer πŸ™Š





Day 2 sessions

Day 2 started with two Honorary MVP's (Todd Mitchell & Lars Petersen) showing us the power of connected data - how to bring all sorts of data in xDB through xConnect and get it out again in a meaningful way. Amongst others they used a calculated facet as a way to capture data instead of keeping and counting a number of events.

For my next session I had the heartbreaking choice between the "EXM Live!" session by the magnificant Pete Navarra or the xConnect evolution by xDB guru Dmytro Shevchenko.

I decided to go for Dmytro and the underlying mechanics of Sitecore's new scalable architecture. This means I will have to watch Pete in Orlando ;) The xConnect session was interesting and a good follow-up of the session of last year. The differences between the locking mechanism in Sitecore 8 and 9 was explained - we went from pessimistic to optimistic - and code was shown to handle this. We're looking forward to Dmytro's posts on this ;)

Thomas Eldblom went on the main stage to give his last session as Sitecore employee (as we would learn later 😞).  
Using SIF -the Sitecore Install Framework- live on stage installing several Sitecore architectures might be a challenge but he was well prepared and the demo's went very smooth. He showed the different setups provided by Sitecore and how to tweak SIF to install the ones that are not out-of-the-box. Even installing modules like the Publishing Service can be done with SIF.

As I missed Mark Stiles' session on cognitive services last year, I decided to join it now. It was a good overview of what companies like Google and Microsoft are offering on AI and how this can be leveraged. It made me curious to see where this is going:

I don't think Mikkel RΓΈmer expected so many people in his "White hat hacker's guide to the internet". The break-out room was fully packed and heard that he tested 3K+ Sitecore sites on some known issues like the Telerik and the PushSession vulnerabilities and faulty configurations like open logins with or without the default password. As the results were quite astonishing - meaning too many sites were not ok - this was an eye opener for a lot of people. Patch your solutions! Read and act by the hardening guides! And to be honest, I'm not sure if I was worried most by his results or by the questions afterwards as some of those made clear there is still a lot of work to make all developers aware of security risks... 

Up to George Chang to give us some thoughts on Identity in Sitecore 9.
The new Federated Authentication feature in Sitecore 9 looks very good. I already read some blog posts on it and this session confirmed my feelings. Quite sure I will be trying this one out in the near future.






Back to the the main stage for two well-known community trolls telling us the story behind the new Forms: Kamruz Jaman and Mike Reynolds (or was it Reybolds?). I must admit I had expected a bit more customizations and code but that might be because I already saw quite a bit about Forms already (and presented a session on this topic myself on the local user group). But the session was a good overview of the features, the possibilities and the missing pieces of a Sitecore module that a lot of users have been waiting for. And.. they managed to get all of us on Slack -during the session (so I might have missed a bit)- by posting Forms data on Slack:

The last session slot was for Sitecore's X-team -represented by Alex Shyba & Adam Weber- to demonstrate the current status of the Sitecore Javascript Services (JSS). Heavy stuff after 2 days of conference...


Pieter Brinkman had the honour to close the conference and announce the next Sugcon Europe: mark April 4th/5th 2019 in your agenda and start learning English because the community is going to London (in Brexit-country).


Final thoughts

Sugcon is truly the place and time to meet all those wonderful community members you got to know online in real life. Have a chat with the guy (or girl) who helped you solve an issue, or vice versa. Meet the people behind the modules you might be using. Sadly -as I am used to arriving the evening before the conference and leaving right after- it's too short to meet you all so I surely missed quite a few people. Let's catch up in Orlando ;)  

But still it was good to see some old and new prominent SSE folks (good to wear your 5K-shirt, Chris), people I've worked with in the past, people I will work with in the near future... many known faces from Belgium (SugBelux), and (un)known faces from all around the globe.

Thanks to all organizers for creating this opportunity for the tremendous Sitecore community to meet and learn in person. Hey, we are omni-channel! 



Tuesday, March 6, 2018

Sitecore 9 configuration roles: content management, reporting, processing

Sitecore 9 configuration roles

A while ago I had to setup a few Sitecore servers with a topology as:
  • Content Delivery
  • Content Management (and all the rest)
In Sitecore 9 things have changed a little bit when setting up server roles. As we could read in the documention we have these roles at our disposal:
  • ContentDelivery
  • ContentManagement
  • Processing
  • Reporting
  • Standalone
For our CD server, the choice was easy: we set the server to the ContentDelivery role.
For our CM server, it was not that obvious. As the documentation mentioned combining roles is possible and it even mentions "ContentManagement, Processing, Reporting" as an example we though this would be a good idea. 

ContentManagement, Processing, Reporting

Unfortunately immediately after setting these roles, the server crashed. We noticed that we had to set remote settings for processing and reporting server which seemed very weird as our processing and reporting was not remote. 

I ended up asking this on SSE and also to Sitecore Support. Support logged this as a bug and gave us the solution - details on this can be found on SSE, so no need to copy them here.

With these changes, we had no need to set any remote settings. And the site worked!

But a few wise men asked me: "why are you doing this?". The only answer I had at that time was: "because I can"..  which is maybe not the best answer when explaining a server setup :)


Standalone

Before Sitecore 9 and the configuration roles we would have had a setup for this CM server that is now similar to a Standalone. Because that was easy. And disabling and enabling files was not...

So why not use standalone role now? Well, we assumed that as this server is not a content delivery one we would benefit from defining the roles. But just assuming is as wicked as using roles just because you can...

Compare configs

So let's compare the resulting configs.

There were quite some differences actually. I won't go into all the details as that is probably just boring but let's focus on some main points: [SO = standalone, CMPR = ContentManagement,Processing,Reporting]
  • databases: in a SO setup, core and master are set as default databases, in a CMPR setup these are all set to web
  • tracking: in a CMPR setup entries are removed regarding RobotDetection, form dropouts, session commits, content testing,..  
  • EXM: a lot of entries removed in CMPR, especially on tracking
So without going into details, I would assume that when not running in Standalone the website running on the ContentManagement server might not be doing your tracking and testing correctly and if entrying from a mail you'll also miss out.

But.. is this important? Will it make the server more performant? 
I think not. 

So what do you need to do? Well, you have a choice..  Also consider if you are using the server as a true test environment (using an extra publishing target perhaps) you probably do want ContentDelivery enabled, meaning a standalone setup.

The easy choice is: set it to standalone.
But.. if you would have separate Processing and/or Reporting roles, would you add ContentDelivery as a role to the ContentManagement server? Probably/maybe not.. so why do that when those roles get combined?

Because it's easy... :)

via GIPHY

Wednesday, February 7, 2018

Displaying extra data on a Sitecore 9 Goal

Goals in Sitecore 9

In our project we had goals set when a whitepaper was downloaded. This works fine, but it would be much nicer if we could show which whitepaper was actually downloaded. All the whitepapers have an identifier code, so the challenge was to add this identifier to the goal data.

Capturing custom goal data

Capturing the data is very well explained in the official documentation of Sitecore. We used the "Data" and "DataKey" properties of the goal:
  • DataKey: a key that identifies the contents of  "Data" - e.g. 'Whitepaper code'
  • Data : any data collected as part of triggering the event - in our case the whitepaper identifier
This all works fine and we can see the data in the database (xDB shards) as part of the events.

Displaying custom goal data

Data is nice, but you need a way to show it. It wasn't immediatly clear how to do this, so I asked it on Sitecore Stack Exchange.
Jarmo Jarvi pointed me in the good direction by mentioning a tremendous blog post by Jonathan Robbins. The blog post was based on Sitecore 8 - the idea behind my post here is to show the differences when doing this in Sitecore 9 and xConnect. The result will be the same:
an extra column in the goals section of the activity tab of a contact in the Experience Profile

ExperienceProfileContactViews

The ExperienceProfileContactViews pipeline is where all the magic happens. Adding the extra column to the results table is identical to the description for Sitecore 8. Fetching the data (in GetGoals) however is quite different as we have to use xConnect now:
public class AddGoalDataColumn : ReportProcessorBase
{
  public override void Process(ReportProcessorArgs args)
  {
    args.ResultTableForView?.Columns.Add(Schema.GoalData.ToColumn());
  }
}

public static class Schema
{
  public static ViewField GoalData = new ViewField("GoalData");
}

public class FillGoalData : ReportProcessorBase
{
  public override void Process(ReportProcessorArgs args)
  {
    var resultTableForView = args.ResultTableForView;
    Assert.IsNotNull(resultTableForView, "Result table for {0} could not be found.", args.ReportParameters.ViewName);
    var i = 0;
    foreach (var row in resultTableForView.AsEnumerable())
    {
      var goalData = args.QueryResult.Rows[i].ItemArray[4];
      if (goalData != null)
      {
        row[Schema.GoalData.Name] = goalData;
      }

      i++;
    }
  }
}
public class GetGoals : ReportProcessorBase
{
  public override void Process(ReportProcessorArgs args)
  {
    var goalsDataXconnect = GetGoalsDataXconnect(args.ReportParameters.ContactId);
    args.QueryResult = goalsDataXconnect;
  }

  private static DataTable GetGoalsDataXconnect(Guid contactId)
  {
    var goalsTableWithSchema = CreateGoalsTableWithSchema();
    var contactExpandOptions = new ContactExpandOptions(Array.Empty<string>())
    {
      Interactions = new RelatedInteractionsExpandOptions("WebVisit")
      {
        StartDateTime = DateTime.MinValue
      }
    };

    FillRawTable(GetContactByOptions(contactId, contactExpandOptions).Interactions.Where(p => p.Events.OfType<Goal>().Any()), goalsTableWithSchema);
    return goalsTableWithSchema;
  }

  private static DataTable CreateGoalsTableWithSchema()
  {
    var dataTable = new DataTable();
    dataTable.Columns.AddRange(new[]
    {
      new DataColumn("_id", typeof(Guid)),
      new DataColumn("ContactId", typeof(Guid)),
      new DataColumn("Pages_PageEvents_PageEventDefinitionId", typeof(Guid)),
      new DataColumn("Pages_PageEvents_DateTime", typeof(DateTime)),
      new DataColumn("Pages_PageEvents_Data", typeof(string)),
      new DataColumn("Pages_Url_Path", typeof(string)),
      new DataColumn("Pages_Url_QueryString", typeof(string)),
      new DataColumn("Pages_PageEvents_Value", typeof(int)),
      new DataColumn("Pages_Item__id", typeof(Guid)),
      new DataColumn("SiteName", typeof(string))
    });

    return dataTable;
  }

  private static void FillRawTable(IEnumerable<Interaction> goalsInteractions, DataTable rawTable)
  {
    foreach (var goalsInteraction in goalsInteractions)
    {
      foreach (var goal in goalsInteraction.Events.OfType<Goal>())
      {
        var currentEvent = goal;
        var row = rawTable.NewRow();
        row["_id"] = goalsInteraction.Id;
        row["ContactId"] = goalsInteraction.Contact.Id;
        row["Pages_PageEvents_PageEventDefinitionId"] = currentEvent.DefinitionId;
        row["Pages_PageEvents_DateTime"] = currentEvent.Timestamp;
        row["Pages_PageEvents_Data"] = currentEvent.Data;
        row["Pages_PageEvents_Value"] = currentEvent.EngagementValue;
        var dataRow = row;
        const string index = "SiteName";
        var webVisit = goalsInteraction.WebVisit();
        var str = webVisit?.SiteName ?? string.Empty;
        dataRow[index] = str;
        if (currentEvent.ParentEventId.HasValue)
        {
          if (goalsInteraction.Events.FirstOrDefault(p => p.Id == currentEvent.ParentEventId.Value) is PageViewEvent pageViewEvent)
          {
            row["Pages_Item__id"] = pageViewEvent.ItemId;
            var urlString = new UrlString(pageViewEvent.Url);
            row["Pages_Url_Path"] = urlString.Path;
            row["Pages_Url_QueryString"] = urlString.Query;
          }
        }

        rawTable.Rows.Add(row);
      }
    }
  }

  private static Contact GetContactByOptions(Guid contactId, ExpandOptions options = null)
  {
    using (var client = SitecoreXConnectClientConfiguration.GetClient())
    {
      if (options == null)
      {
        options = new ContactExpandOptions(Array.Empty<string>())
        {
          Interactions = new RelatedInteractionsExpandOptions("IpInfo", "WebVisit")
        };
      }

      var contactReference = new ContactReference(contactId);
      var contact = client.Get(contactReference, options);
      if (contact == null)
      {
        throw new ContactNotFoundException($"No Contact with id [{contactId}] found");
      }

      return contact;
    }
  }
}

One more change is the configuration. It is almost the same as in Sitecore 8, but we don't need to change the goals-query anymore - instead we have to patch the GetGoals in the same ExperienceProfileContactViews pipeline.
<sitecore>
  <pipelines>
    <group groupName="ExperienceProfileContactViews">
      <pipelines>
        <goals>
          <processor patch:after="*[@type='Sitecore.Cintel.Reporting.Contact.Goal.Processors.ConstructGoalsDataTable, Sitecore.Cintel']"
             type="MyNamespace.AddGoalDataColumn, MyProject" />
          <processor patch:after="*[@type='Sitecore.Cintel.Reporting.Contact.Goal.Processors.PopulateGoalsWithXdbData, Sitecore.Cintel']"
             type="MyNamespace.FillGoalData, MyProject" />
          <processor patch:instead="*[@type='Sitecore.Cintel.Reporting.ReportingServerDatasource.Goals.GetGoals, Sitecore.Cintel']"
             type="MyNamespace.GetGoals, MyProject" />
        </goals>
      </pipelines>
    </group>
  </pipelines>
</sitecore>

Final step - core database

As Jonathan mentioned, we do need to add a new item in the core database to display our new column in the list of Goals. Create this item under the path /sitecore/client/Applications/ExperienceProfile/Contact/PageSettings/Tabs/Activity/Activity Subtabs/Goals/GoalsPanel/Goals and set a descriptive HeaderText and a DataField value that matches the schema name.

Your result should look like this:



I woud like to thank Jonathan Robbins for his original blog post and Jarmo Jarvi for his answer on SSE.

Monday, February 5, 2018

Display custom contact facets in Sitecore 9 Experience Profile

Custom contact facets

Creating custom facets is something that pops up quite often when customers start using Sitecore xDB and contact data. Sitecore 9 changed lots of things for developers working with xDB. The new xConnect layer is a wonderfull thing but when upgrading to Sitecore 9, the odds that you will need to rewrite some/all your code regarding xDB are not looking good.

Although.. you might/should see this as an opportunity to re-think what has been implemented before.

I want to focus on one particular part in this post. We had some custom facets implemented. Doing this with xConnect in Sitecore 9 is well documented on the official Sitecore documentation site.

Experience Profile

But when you have custom facets you usually also want to display this information in the Experience Profile. We used to do this based on some blog post from Adam Conn and/or Jonathan Robbins.
When upgrading to Sitecore 9, we had to made some small changes to this code. Especially (and obviously) the part where we actually do a query to fetch the data from xDB.

Contact View pipeline

As you can read in the previously mentioned blog posts the contact view pipeline is where alle the magic happens in 3 steps:
  1. Creating a data structure to hold the results of the query
  2. Executing the query
  3. Populating the data structure with the results of the query
The part where our changes are located can be found in the execution phase. Our processor will be in a pipeline referred to by a build-in Sitecore processor:
<group groupName="ExperienceProfileContactDataSourceQueries">
  <pipelines>
    <my-custom-query>
      <processor type="MyNamespace.MyDataProcessor, MyNamespace" />
    </my-custom-query>
  </pipelines>
</group>

<group groupName="ExperienceProfileContactViews">
<pipelines>
  <demo>
    ...
    <processor type="Sitecore.Cintel.Reporting.Processors.ExecuteReportingServerDatasourceQuery, Sitecore.Cintel">
      <param desc="queryName">my-custom-query</param>
    </processor>
    ...
  </demo>
</pipelines>
</group>
Let's focus on getting the data.


Query Pipeline Processors

The query pipeline processor is the part that executes the query - gets the data. It is (still) a processor based on ReportProcessorBase.

We will be using the model that was created with the custom facet. For the example we have:
  • custom facet: "CustomFacet"
  • property "Company" in this facet (type string)
The code for MyNamespace.MyDataProcessor:

public override void Process(ReportProcessorArgs args)
{
  var table = CreateTableWithSchema();
  GetTableFromContact(table, args.ReportParameters.ContactId);
  args.QueryResult = table;
}

private static DataTable CreateTableWithSchema()
{
  var dataTable = new DataTable() { Locale = CultureInfo.InvariantCulture };
  dataTable.Columns.AddRange(new[]
  {
    new DataColumn(XConnectFields.Contact.Id, typeof(Guid)),
    new DataColumn("Custom_Company", typeof(string))
  });

  return dataTable;
}

private static void GetTableFromContact(DataTable rawTable, Guid contactId)
{
  string[] facets = { CustomFacet.DefaultFacetKey };
  var contact = GetContact(contactId, facets);
  var row = rawTable.NewRow();
  row[XConnectFields.Contact.Id] = contactId;

  if (contact.Facets.TryGetValue(CustomFacet.DefaultFacetKey, out var customFacet))
  {
    row["Custom_Company"] = ((CustomFacet)customFacet)?.Company;
  }

  rawTable.Rows.Add(row);
}

private static Contact GetContact(Guid contactId, string[] facets)
{
  using (var client = SitecoreXConnectClientConfiguration.GetClient())
  {
    var contactReference = new ContactReference(contactId);
    var contact = facets == null || facets.Length == 0 ? client.Get(contactReference, new ContactExpandOptions(Array.Empty<string>())) : client.Get(contactReference, new ContactExpandOptions(facets));
    if (contact == null)
    {
      throw new ContactNotFoundException(FormattableString.Invariant($"No Contact with id [{contactId}] found"));
    }
   
  return contact;
  }
}

When comparing this code to what we had in Sitecore 8, we have a bit more code but it all seems to make sense. Especially when  you get familiar with coding with xConnect.
What happens here in short:
  1. Create a dataTable
  2. Fetch the contact from xConnect with the necessary facet(s)
  3. Read the data from the contact and add it to the dataTable
  4. Set the dataTable in the pipeline arguments

Special thanks to Nico Geeroms who did the actual coding and testing on this one.

Monday, January 22, 2018

Sitecore Forms Send Email Campaign message

Sitecore Forms 9.0 Update-1 rev. 171219

Sitecore release it's Update-1 version of the platform. In this version they included EXM (Email Experience Manager) as out-of-the-box part of the product. No more separate module..  like they did with Forms before. This change also had some (expected) changes to the Forms module. In the initial release version there was no submit action that could send an email. We expected this to be introduced together with EXM and that was a correct assumption.



So.. we have a "Send Email Campaign message" submit action now.

Send Email Campaign message submit action


I tried to do a quick test on a vanilla install of the platform:
  1. Create an automated campaign in EXM (how-to)
  2. Test the campaign - yes, the test mail arrived perfectly.
  3. Create a form (any form will do), added a few fields and all wanted save actions, amongst which the "Send Email Campaign Message" - place it before the redirect ;)
    The save action will allow you to select a campaign - my newly created campaign was in the list so I selected that one.
  4. Add the form to a page (we had to create/generate an mvc layout for this*)
  5. Publish everything and let's try this...
Submit the form with some test data.. and.. damn..  "Failed to send email!
In the Sitecore logs I found more information:  ERROR Contact id is null.

As I am not that familiar with EXM (yet), as probably quite a lot of others, I was not aware that I could only send a mail to a known contact. Sitecore Support helped me on this one, so now I figured out that I do need to identify my contact first.
To send an email with the "Send Email Campaign Message" action, your contact needs to be identified.
Sound reasonable, but the (first) problem is that there is no out-of-the-box submit action to do this. Luckily all you need to do this yourself has been documented on the official doc site.  (still weird that they can document it, but not put it in the product...).

Documentation on the submit action could have saved me some time, so hopefully this small post will help someone.

Further usage

We did not find a way to send the form data in the message (without customizing the save action) - unless the form data is all in the contact data, which probably is not the case.

We also haven't found a solution to send the email to someone else - not the person who submitted the form. The mail will be send to the identified contact.

Conclusion

I must admit I was hoping Update-1 would have more impact on the Forms part of the product. I was also hoping the "Send Email" functionality would be in there. One could say it is, but without custom code is useless. Let's get our hopes up for Update-2...

Questions?

For questions on the topic, please find me (and many other Sitecore folks) on Sitecore Slack or Stack Exchange.

Is your custom automated campaign is not showing in the Send Email Campaign Message Action? See https://sitecore.stackexchange.com/a/11511/237


* Sitecore Forms is MVC only - and the vanilla setup of Sitecore still comes with a default WebForms homepage 😞