Home » 2018 » December

Monthly Archives: December 2018

Allow Text for Empty List Items from Dynamic Datasources in Sitecore Experience Forms

Last week, when building a form on Sitecore Experience Forms, I noticed an issue where you cannot associate text with an empty list option for List fields.

For example, suppose you have a Dropdown List field where you want to put “Please select blah blah blah”, you cannot do this as you cannot associate text with Empty List value options using the “out of the box” (OOTB) feature to add an empty list option, nor can you have an empty Value field on an Item when using a Dynamic datasource.

Let me create a dummy form to illustrate the issue (no, I’m not calling you a dummy πŸ˜‰ ). Let’s have fun with donuts!

Suppose we have the following donut items in a folder somewhere which will be used as list options in a Dropdown List field in Forms:

We also have one which will serve as the top option telling the user to choose a donut:

Well, OOTB, this Item with an empty value field will not be an option in my field:

Sure, you might say “Hey Mike, you can associate empty options in these fields by setting the ‘Empty list item at top of list’ property setting in Forms.”

Yes, you can do that but you cannot associate text with that empty option:

Β‘Esta no bueno!

Well, after some digging, I discovered the service Sitecore.ExperienceForms.Mvc.DataSource.IListDataSourceProvider in Sitecore.ExperienceForms.Mvc.dll which seemed like a good place to look (I’ve mentioned before that virtually everything on Sitecore Experience Forms is in the Sitecore IoC so you should have a look at /sitecore/admin/showservicesconfig.aspx when looking to customize it).

When I looked at this interface’s implementation, Sitecore.ExperienceForms.Mvc.DataSource.ListDataSourceProvider, I noticed ListFieldItem — a POCO which represents options for List Fields on Forms — are only added when their Value properties aren’t null or empty, so I decided to customize this to allow for those which have empty Value properties but not empty Text properties. The following solution does just that.

I first created the following POCO class to contain any hardcoded string values I saw in the OOTB service (please don’t hardcode things as you won’t be invited to parties if you do so πŸ˜‰ ). An instance of this class will be hydrated from values housed in a Sitecore configuration file further down in this post:

namespace Sandbox.Foundation.Form.Models.Mvc.Datasource
{
	public class ListDataSourceProviderSettings
	{
		public string ListDelimiters { get; set; }
	}
}

I then created the following configurator to hydrate the POCO instance above from the Sitecore configuration file further down. I ultimately stick this instance into the Sitecore IoC as a singleton so I can inject it into any service classes that need it:

using System;
 
using Microsoft.Extensions.DependencyInjection;
 
using Sitecore.Abstractions;
using Sitecore.DependencyInjection;
 
namespace Sandbox.Foundation.Form
{
    public class ListDataSourceFieldsConfigurator : IServicesConfigurator
    {
        public void Configure(IServiceCollection serviceCollection)
        {
            serviceCollection.AddSingleton(CreateListDataSourceProviderSettings);
        }
 
        private ListDataSourceProviderSettings CreateListDataSourceProviderSettings(IServiceProvider provider) => CreateConfigObject<ListDataSourceProviderSettings>(provider, "moduleSettings/foundation/form/listDataSourceProviderSettings");
 
        private TConfigObject CreateConfigObject<TConfigObject>(IServiceProvider provider, string path) where TConfigObject : class => GetService<BaseFactory>(provider).CreateObject(path, true) as TConfigObject;
        
        private TService GetService<TService>(IServiceProvider provider) => provider.GetService<TService>();
    }
}

Next, I created the following class to replace the OOTB one:

using System.Collections.Generic;
using System.Linq;

using Sitecore.Data.Items;
using Sitecore.ExperienceForms.Mvc.DataSource;
using Sitecore.ExperienceForms.Mvc.Models;
using Sitecore.ExperienceForms.Mvc.Pipelines;

using Sandbox.Foundation.Form.Models.Mvc.Datasource;

namespace Sandbox.Foundation.Form.Services.Mvc.Datasource
{
	public class AllowEmptyValuesListDataSourceProvider : ListDataSourceProvider
	{
		private readonly ListDataSourceProviderSettings _settings;
		private readonly IFormBuilderContext _formBuilderContext;
		private readonly IListItemParser _listItemParser;

		public AllowEmptyValuesListDataSourceProvider(ListDataSourceProviderSettings settings, IFormBuilderContext formBuilderContext, IListItemParser listItemParser)
			: base(formBuilderContext, listItemParser)
		{
			_settings = settings;
			_listItemParser = listItemParser;
		}

		public override IEnumerable<ListFieldItem> GetListItems(string dataSource, string displayFieldName, string valueFieldName, string defaultSelection)
		{
			IEnumerable<Item> items = GetDataItems(dataSource);
			if(items == null || !items.Any())
			{
				return CreateEmptyListFieldItemCollection();
			}

			IList<ListFieldItem> options = CreateNewListFieldItemList();
			if(options == null)
			{
				return CreateEmptyListFieldItemCollection();
			}

			IEnumerable<string> itemsToSelect = GetItemsToSelect(defaultSelection);
			foreach (Item item in items)
			{
				ListFieldItem listFieldItem = ParseListFieldItem(item, displayFieldName, valueFieldName);
				if (listFieldItem == null || string.IsNullOrWhiteSpace(listFieldItem.Text))
				{
					continue;
				}

				SetSelected(itemsToSelect, listFieldItem);
				options.Add(listFieldItem);
			}

			return options;
		}

		protected virtual IEnumerable<ListFieldItem> CreateEmptyListFieldItemCollection() => CreateEmptyCollection<ListFieldItem>();

		protected virtual IList<ListFieldItem> CreateNewListFieldItemList() => CreateNewList<ListFieldItem>();

		protected virtual IList<TObject> CreateNewList<TObject>() => new List<TObject>();

		protected virtual IEnumerable<string> GetItemsToSelect(string itemsToSelect)
		{
			char[] delimiters = GetListDelimiters();
			if (string.IsNullOrWhiteSpace(itemsToSelect) || !HasAny(delimiters))
			{
				return CreateEmptyCollection<string>();
			}

			return itemsToSelect.Split(delimiters);
		}

		protected virtual char[] GetListDelimiters()
		{
			if (string.IsNullOrWhiteSpace(_settings.ListDelimiters))
			{
				return CreateEmptyCollection<char>().ToArray();
			}

			return _settings.ListDelimiters.ToCharArray();
		}

		protected virtual IEnumerable<TObject> CreateEmptyCollection<TObject>() => Enumerable.Empty<TObject>();

		protected virtual ListFieldItem ParseListFieldItem(Item item, string displayFieldName, string valueFieldName) => _listItemParser.Parse(item, displayFieldName, valueFieldName);

		protected virtual void SetSelected(IEnumerable<string> itemsToSelect, ListFieldItem listFieldItem)
		{
			if(!HasAny(itemsToSelect) || string.IsNullOrWhiteSpace(listFieldItem?.ItemId))
			{
				return;
			}

			listFieldItem.Selected = ShouldSelect(itemsToSelect, listFieldItem);
		}

		protected virtual bool ShouldSelect(IEnumerable<string> itemsToSelect, ListFieldItem listFieldItem) => ContainsValue(itemsToSelect, listFieldItem?.ItemId);

		protected virtual bool ContainsValue(IEnumerable<string> items, string value) => HasAny(items) && !string.IsNullOrWhiteSpace(value) && items.Contains(value);

		protected virtual bool HasAny<TObject>(IEnumerable<TObject> collection) => collection != null && collection.Any();
	}
}

I’m not going to go much into how this class works as I think you should read it over a few times to discover for yourself on how it works. Ultimately, it will add ListFieldItem instances with empty Value properties but not those with empty Text properties.

I then tied it all together using the following Sitecore configuration file:

<?xml version="1.0"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:env="http://www.sitecore.net/xmlconfig/env">
  <sitecore>
	  <moduleSettings>
		  <foundation>
			  <form>
				  <listDataSourceProviderSettings type="Sandbox.Foundation.Form.Models.Mvc.Datasource.ListDataSourceProviderSettings, Sandbox.Foundation.Form" singleInstance="true">
					  <ListDelimiters>|</ListDelimiters>
				  </listDataSourceProviderSettings>
			  </form>
		  </foundation>
	  </moduleSettings>
	  <services>
		  <configurator type="Sandbox.Foundation.Form.ListDataSourceFieldsConfigurator, Sandbox.Foundation.Form"/>
		  <register serviceType="Sitecore.ExperienceForms.Mvc.DataSource.IListDataSourceProvider, Sitecore.ExperienceForms.Mvc">
			  <patch:attribute name="implementationType">Sandbox.Foundation.Form.Services.Mvc.Datasource.AllowEmptyValuesListDataSourceProvider, Sandbox.Foundation.Form</patch:attribute>
		  </register>
	  </services>
  </sitecore>
</configuration>

One thing to note is I’m replacing the OOTB service with my custom one above under /services/register[@serviceType=’Sitecore.ExperienceForms.Mvc.DataSource.IListDataSourceProvider, Sitecore.ExperienceForms.Mvc’]

After doing a build and deploy of my solution locally, I reloaded the Forms Builder/designer page. As you can see my empty valued option with text now displays:

Yep, it was fixed.

Yes, you can have your donut and eat it too. πŸ˜‰

Until next time, keep on Sitecoring.

Oh, and Happy Holidays as well!