Autofill Episerver Forms fields from Profile Store
As a user, it can be frustrating to repeatedly fill in the same information, especially if the system already has that data recorded. So why not use all the info from Profile Store to autofill forms and save some time?
The Episerver Profile Store can be a great option for tracking visitor behaviour and getting personalisation going quickly. Once you combine the Profile Store with Insight, Forms and David Knipe's Insight Form Field Mapper, you can easily extract a great deal of value from the service.
With this idea in mind I started digging around to see how we could achieve exactly that. Fortunately, I found the developer guides for the Forms Autofill API and even though it’s still in beta, it doesn't look like there has been much new development going on around it. I decided to give it a go.
For this demo we show a user providing their details in a standard custom form during checkout. That info is then stored to the Profile Store and retrieved to autofill the Forms elements on the homepage. Sharing information between different forms is also supported as long as they are mapped to push and pull data from the Profile Store in the CMS.
All we really need to do to get the Forms elements hooked up to our data source (Profile Store) is implement IExternalSystem
and IAutofillProvider
.
Implementing the interfaces
Both interfaces can be implemented in the same class as there is some overlap between them. I've called my implementing class ProfileStoreAutofillFields
. The IExternalSystem
interface is used to register a custom data source and drives the CMS drop-down list.
public virtual string Id
{
get { return "ProfileStoreAutofillFields"; }
}
public virtual IEnumerable<IDatasource> Datasources
{
get
{
// Register the Profile Store as a data source
var profileStoreDataSource = new Datasource()
{
Id = "ProfileStoreDataSource",
Name = "Profile Store Data Source",
OwnerSystem = this,
Columns = new Dictionary<string, string> {
// "Name of mapped field", "friendly name in CMS"
{ "profilestoreemail", "Email" },
{ "profilestorename", "Name" },
{ "profilestorecity", "City" },
{ "profilestoremobile", "Mobile" },
{ "profilestorephone", "Phone" }
}
};
return new[] { profileStoreDataSource };
}
}
Up next, the IAutofillProvider
is where the magic happens and we use GetSuggestedValues
to provide our "suggested" values from the Profile Store based on the device GUID as set in the Profile Store Cookie. The drop-down list in the CMS needs to match here for a value to be suggested.
/// <summary>
/// Returns a list of suggested values by field mapping key. This will be called automatically by the GetAutofillValues() function in DataElementBlockBase for each field
/// </summary>
/// <returns>Collection of suggested values</returns>
public virtual IEnumerable<string> GetSuggestedValues(IDatasource selectedDatasource, IEnumerable<RemoteFieldInfo> remoteFieldInfos,
ElementBlockBase content, IFormContainerBlock formContainerBlock, HttpContextBase context)
{
if (selectedDatasource == null || remoteFieldInfos == null)
return Enumerable.Empty<string>();
// Make sure the Data sources are for this system
if (!this.Datasources.Any(ds => ds.Id == selectedDatasource.Id)
|| !remoteFieldInfos.Any(mi => mi.DatasourceId == selectedDatasource.Id))
{
return Enumerable.Empty<string>();
}
// We also need to make sure that we have some tracking info to auto fill
// _madid is the default Episerver Profile Store tracking cookie, see https://world.episerver.com/documentation/developer-guides/tracking/episerver-cookies/
var userDeviceId = context.Request.Cookies["_madid"]?.Value;
// Because this gets called with EVERY FIELD it is suggested to cache the response elsewhere
var userProfile = ProfileStoreApiService.GetProfileByDeviceId(userDeviceId);
if (userProfile == null)
{
return Enumerable.Empty<string>();
}
// Unpack the info object
var info = userProfile["Info"];
// Get the field details
var activeRemoteFieldInfo = remoteFieldInfos.FirstOrDefault(mi => mi.DatasourceId == selectedDatasource.Id);
switch (activeRemoteFieldInfo.ColumnId)
{
// Suggest the data from the Profile Store user profile
case "profilestoreemail":
return new List<string> {
(string)(info["Email"] ?? info["Email"]?.ToString())
};
case "profilestorename":
return new List<string> {
(string)(userProfile["Name"] ?? userProfile["Name"]?.ToString())
};
case "profilestorecity":
return new List<string>{
(string)(info["City"] ?? info["City"]?.ToString())
};
case "profilestorephone":
return new List<string>{
(string)(info["Phone"] ?? info["Phone"]?.ToString())
};
case "profilestoremobile":
return new List<string>{
(string)(info["Mobile"] ?? info["Mobile"]?.ToString())
};
default:
return Enumerable.Empty<string>();
}
}
I have only added a handful of fields to demonstrate how it works. To add more, simply update the implementation of both interfaces with the fields available in the Profile Store API - all of which you can see here.
Getting the user profile from the Profile Store
We get all the user info by querying the Profile Store API. There are some ongoing improvements to provide a wrapper instead of manually having to implement the API calls, such as the Episerver Profile Store .NET client; however, to keep things flexible I've provided a (mostly unchanged) version of the one call we need here from the client by David Knipe.
using Newtonsoft.Json.Linq;
using RestSharp;
public static class ProfileStoreApiService
{
// This should come from app settings really
private static readonly string apiRootUrl = "https://somesecret.profilestore.episerver.net"; // Get from Insight / Profile store developer portal
private static readonly string subscriptionKey = "somesecret"; // Get from Insight / Profile store developer portal
public static JToken GetProfileByDeviceId(string deviceId)
{
// Set up the request
var client = new RestClient(apiRootUrl);
var request = new RestRequest("api/v1.0/Profiles", Method.GET);
request.AddHeader("Ocp-Apim-Subscription-Key", subscriptionKey);
// Filter the profiles based on the current device id
request.AddParameter("$filter", "DeviceIds eq " + deviceId);
// Execute the request to get the profile
var getProfileResponse = client.Execute(request);
var getProfileContent = getProfileResponse.Content;
// Get the results as a JArray object
if (!string.IsNullOrWhiteSpace(getProfileContent))
{
var profileResponseObject = JObject.Parse(getProfileContent);
var profileArray = (JArray)profileResponseObject["items"];
// Expecting an array of profiles with one item in it
return profileArray.First;
}
return null;
}
}
It should be noted that in production you would want to build out this ProfileStoreApiService
to include some form of response caching. The GetSuggestedValues
function we have calls GetProfileByDeviceId
(aka the Profile Store API) for every Forms element we have mapped. Personally, I would parse the response JToken object and cache a strongly typed user profile object, but there are various ways to go about it.
With all that done it's time to spin up the site and go update some Forms.
Forms configuration
First, you need to update the Form Container to map our new data source.
Next, we need to edit the Form Elements to link the mappings.
Make sure it's mapped to push responses to the correct Profile Store/Insight fields.
And finally, map to the Profile Store data source to retrieve the stored fields.
That's all there is to it! You should now have Forms elements that fill themselves out as the user information gets updated.
Future work
This should be seen as more of a POC to get things going. You need to decide if and when you want to "trust" that a user profile is actually who we think they are (such as only for logged-in users) to make sure you don't expose personal user data.
Useful resources
Check out the Forms demo project and the Episerver Insight Form Field Mapper to see examples of what else is currently possible. The sample code for this can be found on Github
Nice write up - thanks for sharing :)!