EPiServer provides a build-in SelectionFactory for enums and SelectOne attribute for selecting single value. But for flags enum we want to have a checkbox list and be able to select more than one value. In this article I’d like to show how to use SelectMany attribute for editing flags enum property.
Available days example
On the StartPage I will show list of available days. The list will be configurable and editor can select more than one day. I preapred new enum type, DayOfWeekFlags with the following definition:
1 2 3 4 5 6 7 8 9 10 11 |
[Flags] public enum DayOfWeekFlags { Monday = 1, Tuesday = 2, Wednesday = 4, Thursday = 8, Friday = 16, Saturday = 32, Sunday = 64 } |
Then I implemented a selection factory. It inherits from standard EnumSelectionFactory and use DaysOfWeekFlag as enum type. SelectionFactory will be used by SelectMany attribute to define the property.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class DaysOfWeekSelectionFactory : EnumSelectionFactory { public DaysOfWeekSelectionFactory(LocalizationService localizationService): base(localizationService) { } public DaysOfWeekSelectionFactory(): this(LocalizationService.Current) { } protected override Type EnumType => typeof(DayOfWeekFlags); protected override string GetStringForEnumValue(int value) { return DayOfWeekFlagsConverter.Translate((DayOfWeekFlags)value); } } |
CheckboxList widget works on coma separated strings, so property definition has PropertyString as a backing type and custom getter and setter responsible for converting string to DayOfWeekFlags.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public class StartPage : SitePageData { [Display(Name = "Available days", Order = 100, GroupName = SystemTabNames.Content)] [BackingType(typeof(PropertyString))] [SelectMany(SelectionFactoryType = typeof(DaysOfWeekSelectionFactory))] public virtual DayOfWeekFlags AvailableDays { get { var value = this.GetValue(nameof(AvailableDays)) as string; return DayOfWeekFlagsConverter.ConvertFromString(value); } set { this.SetValue(nameof(AvailableDays), DayOfWeekFlagsConverter.ConvertToString(value)); } } // ... // other properties // ... } |
The string conversion logic was moved to a separate class with static methods.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
public class DayOfWeekFlagsConverter { public static DayOfWeekFlags ConvertFromString(string value) { if (string.IsNullOrWhiteSpace(value)) { return 0; } int number; var result = value .Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries) .Select(v => int.TryParse(v, NumberStyles.Integer, CultureInfo.InvariantCulture, out number) ? number : (int?)null) .Where(i => i != null) .Cast<DayOfWeekFlags>() .Aggregate((i, j) => j | i); return result; } public static string ConvertToString(DayOfWeekFlags value) { var result = string.Join(",", Enum.GetValues(typeof(DayOfWeekFlags)).Cast<DayOfWeekFlags>().Where(d => value.HasFlag(d)).Cast<int>()); return result; } public static string Translate(DayOfWeekFlags dayOfWeekFlag) { var dayOfWeek = (DayOfWeek)Enum.Parse( typeof(DayOfWeek), dayOfWeekFlag.ToString()); return new CultureInfo(ContentLanguage.PreferredCulture.Name).DateTimeFormat.GetDayName(dayOfWeek); } } |
Now we can edit property in Forms Editing mode and use AvailableDays property through code.
On-Page edit renderer
The functionality can be improved by possibility of editing AvailableDays property in On-Page edit mode. It can be done by implementing property renderer. I added a simple view under ~\Views\Shared\DisplayTemplates\DaysOfWeek.cshtml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
@model DayOfWeekFlags @if(Model != 0) { <ul> @foreach(DayOfWeekFlags dayOfWeek in Enum.GetValues(typeof(DayOfWeekFlags))) { if (Model.HasFlag(dayOfWeek)) { <li>@DayOfWeekFlagsConverter.Translate(dayOfWeek)</li> } } </ul> } |
To use the renderer I had to add UIHint attribute with “DaysOfWeek” hint on the property.
The On-Page edit client editor should use CheckBoxListEditor (same as Forms editing). To configure the overlay I created new SelectManyOverlayAttribute attribute which implements IMetadataAware interface. In the OnMetadataCreated method I set “uiType” in overlay settings.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] public class SelectManyOverlayAttribute : Attribute, IMetadataAware { public void OnMetadataCreated(ModelMetadata metadata) { var extendedMetadata = metadata as ExtendedMetadata; if (extendedMetadata == null) { return; } extendedMetadata.CustomEditorSettings["uiType"] = "epi-cms/contentediting/editors/CheckBoxListEditor"; extendedMetadata.CustomEditorSettings["uiWrapperType"] = UiWrapperType.Floating; } } |
The CheckboxList works on coma separated values, so I had to inject ObjectSerializer and deserialize data in a proper way.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
[ModuleDependency(typeof(EPiServer.Web.InitializationModule))] public class SerializerInitializer : IConfigurableModule { public void ConfigureContainer(ServiceConfigurationContext context) { context.Services.Intercept<IObjectSerializer>( (locator, serializer) => new CustomJsonObjectSerializer(serializer)); } public void Initialize(InitializationEngine context) { } public void Uninitialize(InitializationEngine context) { } } public class CustomJsonObjectSerializer : IObjectSerializer { private readonly IObjectSerializer _serializer; public CustomJsonObjectSerializer(IObjectSerializer serializer) { _serializer = serializer; } public object Deserialize(TextReader reader, Type objectType) { if (objectType != typeof(DayOfWeekFlags)) { return _serializer.Deserialize(reader, objectType); } var value = reader.ReadToEnd(); if (string.IsNullOrWhiteSpace(value)) { return 0; } try { return _serializer.Deserialize(new StringReader(value), objectType); } catch (ArgumentException) { value = (string)_serializer.Deserialize<string>(new StringReader(value)); return DayOfWeekFlagsConverter.ConvertFromString(value); } catch (FormatException) { value = (string)_serializer.Deserialize<string>(new StringReader(value)); return DayOfWeekFlagsConverter.ConvertFromString(value); } } public void Serialize(TextWriter textWriter, object value) { _serializer.Serialize(textWriter, value); } public T Deserialize<T>(TextReader reader) { return _serializer.Deserialize<T>(reader); } public IEnumerable<string> HandledContentTypes => _serializer.HandledContentTypes; } |
Now the property definition with UIHint and SelectOverlaytAttribute looks like:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
[Display(Name = "Available days", Order = 100, GroupName = SystemTabNames.Content)] [BackingType(typeof(PropertyString))] [UIHint("DaysOfWeek")] [SelectMany(SelectionFactoryType = typeof(DaysOfWeekSelectionFactory))] [SelectManyOverlay] public virtual DayOfWeekFlags AvailableDays { get { var value = this.GetValue(nameof(AvailableDays)) as string; return DayOfWeekFlagsConverter.ConvertFromString(value); } set { this.SetValue(nameof(AvailableDays), DayOfWeekFlagsConverter.ConvertToString(value)); } } |
After running appliction the property can be edited directly from the page.
And here is a demo of using the property: