Sitecore 10 Forms and special characters in list controls

When you create forms in Sitecore Forms, it provides you with functionality to managing multilingual forms setup. Creating a new form each control in the form will be created as a separate item for each specific control. But in the case of list fields where each list item is created as a static list item (and list items not rendered from existing items), for the item name Sitecore uses the field value if the field name/label is empty. All good, all good – until you want to use a not valid character in the field value/label like ë, ä etc. Then Sitecore overrides the value to be item name compliant. And therefor, the values in the list control will be changed to item name compliant values. Sitecore simply removes/deletes the incompliant characters. But is that really what you want? Don’t you want to be able to use language specific characters to be displayed or stored from your list control?


The issue is that Sitecore uses the ItemUtil.ProposeValidItemName for creating the display name for the item. Making Sitecore Forms compliant with these characters, then instead of using the ProposeValidItemName when creating the items, we will simply use the listFieldItem.Text or listFieldItem.Value.



As an example we replace the I"temUtil.ProposeValidItemName(listFieldItem.Text) : ItemUtil.ProposeValidItemName(listFieldItem.Value)" with "listFieldItem.Text : listFieldItem.Value"

(from Sitecore.ExperienceForms.Mvc.DataSource.DataSourceSettingsManager.UpdateStaticItems()):  

using System;

using System.Collections.Generic;

using System.Globalization;

using Sitecore;

using Sitecore.Data;

using Sitecore.Data.Items;

using Sitecore.Diagnostics;

using Sitecore.ExperienceForms.Extensions;

using Sitecore.ExperienceForms.FieldSettings;

using Sitecore.ExperienceForms.Mvc.Constants;

using Sitecore.ExperienceForms.Mvc.DataSource;

using Sitecore.ExperienceForms.Mvc.Models;

 

namespace Norican.Infrastructure.Web.Sc.Forms

{

    public class DataSourceSettingsManagerWithDisplayName : DataSourceSettingsManager

    {

        protected override void UpdateSettings(

            ListFieldItemCollection settings,

            Item fieldSettingsItem,

            FieldSettingsContext settingsContext)

        {

            Assert.ArgumentNotNull((object)settings, nameof(settings));

            Assert.ArgumentNotNull((object)fieldSettingsItem, nameof(fieldSettingsItem));

            Assert.ArgumentNotNull((object)settingsContext, nameof(settingsContext));

            if (settingsContext.FieldItem == null)

                return;

            if (MainUtil.GetBool(settingsContext.FieldItem.Fields["Is Dynamic"]?.Value, false))

            {

                foreach (Item child in fieldSettingsItem.Children)

                    child.Recycle();

            }

            else

            {

                string displayFieldName = settingsContext.FieldItem.Fields["Display Field Name"]?.Value;

                string valueFieldName = settingsContext.FieldItem.Fields["Value Field Name"]?.Value;

                this.UpdateStaticItemsInternal((List<ListFieldItem>)settings, fieldSettingsItem, displayFieldName, valueFieldName);

            }

        }

 

        protected void UpdateStaticItemsInternal(

      List<ListFieldItem> items,

      Item fieldSettingsItem,

      string displayFieldName,

      string valueFieldName)

        {

            Assert.ArgumentNotNull((object)items, nameof(items));

            Assert.ArgumentNotNull((object)fieldSettingsItem, nameof(fieldSettingsItem));

            for (int index = 0; index < items.Count; ++index)

            {

                ListFieldItem listFieldItem = items[index];

                if (string.IsNullOrEmpty(listFieldItem.Value))

                {

                    listFieldItem.ItemId = "";

                }

                else

                {

                    string str = ItemUtil.ProposeValidItemName(listFieldItem.Value);

                    Item obj = ID.IsID(listFieldItem.ItemId) ? fieldSettingsItem.Database.GetItem(listFieldItem.ItemId, fieldSettingsItem.Language) : (Item)null;

                    if (obj == null)

                        obj = this.AddItem(str, fieldSettingsItem, new TemplateID(TemplateIds.ListFieldTemplateId));

                    else if (!obj.Axes.IsDescendantOf(fieldSettingsItem))

                        obj = obj.CopyTo(fieldSettingsItem, str);

                    listFieldItem.ItemId = obj?.ID.ToString() ?? string.Empty;

                    if (obj != null)

                    {

                        if (string.IsNullOrEmpty(valueFieldName))

                            valueFieldName = obj.Template.IsBasedOnTemplate(TemplateIds.ListFieldTemplateId) ? "Value" : "__ItemName";

                        obj.Editing.BeginEdit();

                        obj.Name = str;

                        obj.Fields[valueFieldName]?.SetValue(listFieldItem.Value, false);

 

                        /* Use item text or item value as displayName.

                           Original Sitecore code uses ItemUtil.ProposeValidItemName:

                           ... ItemUtil.ProposeValidItemName(listFieldItem.Text) : ItemUtil.ProposeValidItemName(listFieldItem.Value); */

                        (string.IsNullOrEmpty(displayFieldName) || obj.Fields[displayFieldName] == null ? obj.Fields[FieldIDs.DisplayName] : obj.Fields[displayFieldName])?.

                            SetValue(!string.IsNullOrEmpty(listFieldItem.Text) ? listFieldItem.Text : listFieldItem.Value, false);

                        

                        obj.Fields[FieldIDs.Sortorder]?.SetValue((index * 100).ToString((IFormatProvider)CultureInfo.InvariantCulture), false);

                        obj.Editing.EndEdit();

                    }

                }

            }

            foreach (Item child1 in fieldSettingsItem.Children)

            {

                Item child = child1;

                if (!items.Exists((Predicate<ListFieldItem>)(li => child.ID.ToString().Equals(li.ItemId, StringComparison.OrdinalIgnoreCase))))

                    child.Delete();

            }

        }

    }

}



And adding it to the config file: 

<sitecore>

  <services>

      <register patch:instead="register[@implementationType='Sitecore.ExperienceForms.Mvc.DataSource.DataSourceSettingsManager, Sitecore.ExperienceForms.Mvc']" serviceType="Sitecore.ExperienceForms.FieldSettings.IFieldSettingsManager`1[[Sitecore.ExperienceForms.Mvc.Models.ListFieldItemCollection, Sitecore.ExperienceForms.Mvc]], Sitecore.ExperienceForms" implementationType="Norican.Infrastructure.Web.Sc.Forms.DataSourceSettingsManagerWithDisplayName, Norican.Infrastructure" lifetime="Transient" />

    </services>

</sitecore>













Sitecore 10 Forms support of fallback language in Forms Builder

For one of the implementations I worked with, the customer was heavily using fallback languages. This was also the case regarding the forms created.




But after enabling the fallback languages at the Sitecore Forms templates (__Standard Values), the Form builder application start acting differently regarding the forms created in the main language but not translated. For each fallback language, Sitecore Forms stated that the language version was created: 



When we tried to select the new language and saved the version, the form item was marked as protected – and Sitecore Form application provided a notification saying:

“You cannot edit the [Name of the form] item because it is protected. To save your
changes, click Save as and save it as a new form.”

Not quite what we expected. 



It seemed like the Sitecore Form implementation didn’t handled the fallback language correctly. 

Step 1

The first step was to modify the implementation for how Sitecore resolves whether the language has been created for a specific language version. We just added a statement for “!item.IsFallback” in the Sitecore.ExperienceForms.Client.Pipelines.GetLanguages.GetLanguageData.ToLanguageListItem(). To check whether the item is marked with fallback language for the selected language version:

Check whether language version has been created:

using System;

using Sitecore.Data.Items;

using Sitecore.Diagnostics;

using Sitecore.ExperienceForms.Client.Models.Builder;

using Sitecore.ExperienceForms.Client.Pipelines.GetLanguages;

using Sitecore.Globalization;

 

namespace Norican.Infrastructure.Web.Sc.Forms.Pipelines

{

    public class GetLanguageDataWithItemFallback : GetLanguageData

    {

        protected override LanguageListItem ToLanguageListItem(Item currentItem, Language language)

        {

            Assert.ArgumentNotNull(language, "language");

            Item item = currentItem?.Database.GetItem(currentItem.ID, language);

            //enhanced standard Sitecore implementation by checking if form item is not language fallback

            bool flag = item != null && !item.IsFallback && item.Versions.Count > 0;

            LanguageListItem languageListItem = new LanguageListItem

            {

                Name = language.Name,

                DisplayName = language.CultureInfo.DisplayName,

                Text = FormattableString.Invariant(

                    $"{language.CultureInfo.DisplayName} : {language.CultureInfo.NativeName}"),

                Description = (flag ? Translate.Text("Created.") : Translate.Text("Not created.")),

                HasVersions = flag

            };

            return languageListItem;

        }

    }

}


Step 2

Next step was to handle the notification for creating a new version of the form item. Once again we simply added the check whether the form is marked with fallback language in the Sitecore.ExperienceForms.Client.Pipelines.GetImportantNotes.CheckFormIsEditable with !args.Item.IsFallback.

Check when for form is editable: 

using System;

using System.Globalization;

using Sitecore.Diagnostics;

using Sitecore.ExperienceForms.Client.Pipelines.GetImportantNotes;

using Sitecore.Globalization;

 

namespace Norican.Infrastructure.Web.Sc.Forms.Pipelines

{

    public class CheckFormIsEditableWithItemFallback : CheckFormIsEditable

    {

        public override void Process(ImportantNotesEventArgs args)

        {

            Assert.ArgumentNotNull((object)args, nameof(args));

            if (args.Item == null)

            {

                args.AddMessage(Translate.Text("Item does not exist."));

                args.AbortPipeline();

            }

            else

            {

                string str = "";

                if (args.Item.Parent.Access.CanCreate())

                    str = Translate.Text("To save your changes, click Save as and save it as a new form.");

                if (!args.Item.Access.CanWrite())

                    args.Result = (Translate.Text("You cannot edit the '{0}' item because you do not have write access.", (object)args.Item.DisplayName) + " " + str).ToString((IFormatProvider)CultureInfo.CurrentCulture);

                //enhanced standard Sitecore implementation by checking if form item is not language fallback

                else if (!args.Item.IsFallback && args.Item.Appearance.ReadOnly)

                {

                    args.Result = (Translate.Text("You cannot edit the '{0}' item because it is protected.", (object)args.Item.DisplayName) + " " + str).ToString((IFormatProvider)CultureInfo.CurrentCulture);

                }

                else

                {

                    if (Sitecore.Context.User.IsAdministrator || !args.Item.Locking.IsLocked() || args.Item.Locking.HasLock())

                        return;

                    args.Result = (Translate.Text("You cannot edit this item because '{0}' has locked it.", (object)args.Item.Locking.GetOwnerWithoutDomain()) + " " + str).ToString((IFormatProvider)CultureInfo.CurrentCulture);

                }

            }

        }

    }

}


Step 3

The last step was for saving the form item. Once again by adding a check for whether the form was marked with use of fallback language !obj2.IsFallback in the Sitecore.ExperienceForms.Client.Pipelines.SaveForm.CheckAccessRights:

Check when saving the forms:

using System;

using System.Globalization;

using System.Linq;

using Sitecore;

using Sitecore.Data;

using Sitecore.Data.Items;

using Sitecore.Diagnostics;

using Sitecore.ExperienceForms;

using Sitecore.ExperienceForms.Client.Models.Builder;

using Sitecore.ExperienceForms.Client.Pipelines.SaveForm;

using Sitecore.ExperienceForms.Mvc.Constants;

using Sitecore.Globalization;

 

namespace Norican.Infrastructure.Web.Sc.Forms.Pipelines

{

    public class CheckAccessRightsWithItemFallback : CheckAccessRights

    {

        public override void Process(SaveFormEventArgs args)

        {

            Assert.ArgumentNotNull((object)args, nameof(args));

            if (args.ViewModelWrappers == null)

            {

                args.AddMessage(Translate.Text("There is no data in the view model wrappers list."));

            }

            else

            {

                ViewModelWrapper viewModelWrapper = args.ViewModelWrappers.FirstOrDefault<ViewModelWrapper>((Func<ViewModelWrapper, bool>)(m => ID.Parse(m.Model.TemplateId) == TemplateIds.FormTemplateId));

                if (viewModelWrapper == null)

                {

                    args.AddMessage(Translate.Text("The form model was not found in the view model wrappers collection."));

                }

                else

                {

                    Item obj1 = args.FormBuilderContext.Database.GetItem(viewModelWrapper.ParentId, args.FormBuilderContext.Language);

                    if ((args.FormBuilderContext.FormBuilderMode == FormBuilderMode.New || args.FormBuilderContext.FormBuilderMode == FormBuilderMode.Copy) && !obj1.Access.CanCreate())

                    {

                        args.AddMessage(Translate.Text("You do not have permission to create items here."));

                        args.AbortPipeline();

                    }

                    else

                    {

                        string str = "";

                        if (obj1.Access.CanCreate())

                            str = " " + Translate.Text("To save your changes, click Save as and save it as a new form.");

                        if (args.FormBuilderContext.FormBuilderMode != FormBuilderMode.Edit)

                            return;

                        ID id = ID.Parse(viewModelWrapper.Model.ItemId);

                        if (!args.FormBuilderContext.Database.Items.Exists(id))

                            return;

                        Item obj2 = args.FormBuilderContext.Database.GetItem(id, args.FormBuilderContext.Language);

                        if (!obj2.Access.CanWrite())

                        {

                            args.AddMessage((Translate.Text("You cannot edit the '{0}' item because you do not have write access.", (object)obj2.DisplayName) + str).ToString((IFormatProvider)CultureInfo.CurrentCulture));

                            args.AbortPipeline();

                        }

                        //enhanced standard Sitecore implementation by checking if form item is not language fallback

                        else if (!obj2.IsFallback && obj2.Appearance.ReadOnly)

                        {

                            args.AddMessage((Translate.Text("You cannot edit the '{0}' item because it is protected.", (object)obj2.DisplayName) + str).ToString((IFormatProvider)CultureInfo.CurrentCulture));

                            args.AbortPipeline();

                        }

                        else

                        {

                            if (Context.User.IsAdministrator || !obj2.Locking.IsLocked() || obj2.Locking.HasLock())

                                return;

                            args.AddMessage((Translate.Text("You cannot edit this item because '{0}' has locked it.", (object)obj2.Locking.GetOwnerWithoutDomain()) + str).ToString((IFormatProvider)CultureInfo.CurrentCulture));

                            args.AbortPipeline();

                        }

                    }

                }

            }

        }

    }

}



After that, testing the language versions of the forms behaved as expected:




Kudos to my coll' Giedrius Gobe for the fix!