0

I have an application that uses a slightly modified ObservableConcurrentDictionary (exposed Count & added constructor for ConcurrentDictionary comparer) for data bound to custom FrameworkElements. The data stored in the dictionary has many properties that need to be displayed or affect rendering.

public class DictionaryData : ObservableConcurrentDictionary<string, ItemValueData> 
{ public DictionaryData() : base(StringComparer.InvariantCultureIgnoreCase) { } }

public class ItemValueData
{
    // properties
    public string Source { get; set; }
    public string Name   { get; set; }
    public int Quality   { get; set; }
    public double Value  { get; set; }
    // ... many other properties
    // omitted members / constructors / private variable etc.
}

The ObservableConcurrentDictionary data is instantiated as DD a DependencyProperty of the Window/Canvas/Page/Container...

public DictionaryData DD {
    get => (DictionaryData)GetValue(DDProperty); 
    set { SetValue(DDProperty, value); OnPropertyChanged("DDProperty"); }
}

public readonly DependencyProperty DDProperty =
    DependencyProperty.Register("DD", typeof(DictionaryData), typeof(MyWindowApp)
    , new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsRender));

In the working XAML, I'm currently using a different binding converter for each unique property in the ItemValueData class.

<ElementA Value="{av:Binding DD Converter={Converters:ItemConverterName}
                 , ConverterParameter='Item001Name'}" .../>
<ElementB Value="{av:Binding DD Converter=Converters:ItemConverterQuality}
                 , ConverterParameter='Item001Name'}" .../>
<ElementC Value="{av:Binding DD Converter=Converters:ItemConverterValue}
                 , ConverterParameter='Item001Name'}" .../>
<ElementD Value="{av:Binding DD Converter=Converters:ItemConverterSource}
                 ,ConverterParameter='Item001Name'}" .../>
<!-- several hundred FrameWorkElements -->
<ElementA Value="{av:Binding DD Converter={Converters:ItemConverterValue}
                 , ConverterParameter='Item400Name'}" .../>

Where each converter handles a single property of the ItemValueData class. (.Name maps to ItemConverterName and so on...)

What I want is a converter that will convert any of the properties by passing in the name of the property to convert as well as the key to the dictionary that looks up the data.

[ValueConversion(typeof(DictionaryData), typeof(object))]
public class ItemConverterGeneric : MarkupExtension, IValueConverter {
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {    
        try {
            if (value != null && parameter != null) {
                DictionaryData dict = (DictionaryData)value;
                // Would like make the property use a class here 
                // SomeClass x = parameter as SomeClass;
                // string key = x.Key;
                // string prop = x.Prop;
                string key = parameter as string;
                if (dict.ContainsKey(key)) { 
                   // switch(prop) { pick the right property }
                   return dict[key].Name; // pass as parameter?
                } 
            }
            return Binding.DoNothing;
        } catch (Exception ex) {
            Console.WriteLine(ex.Message);
            return Binding.DoNothing;
        }
    }

    public object ConvertBack(object value, Type targetTypes, object parameter, CultureInfo culture)
    { return DependencyProperty.UnsetValue; }

    public override object ProvideValue(IServiceProvider serviceProvider)
    { return this; }
}

I've seen answers that used an array to pass several parameters: multiple parameters not truly converted and multiple parameters order matters and a question asking for two parameters . None that I've seen accomplish having a converter able to use multiple named parameters where the parameter is a class (its an object anyway) and have that syntax in the XAML.

<ElementX Value="{av:Binding DD, Converter={Converters:ItemConverterGeneric}
        , ConverterParameterKey='ItemXName', ConvertProperty='Name', ConvertSource='DatabaseX'}" .../>
<ElementX Value="{av:Binding DD, Converter={Converters:ItemConverterGeneric}
        , ConverterParameterKey='ItemXName', ConvertProperty='Value', ConvertSource='DatabaseX'}" .../>

Justification of using an IValueConverter/MarkupExtension

The dictionary is built dynamically by processing the content of a dynamically loaded XAML that is the source for the container. var tempLoad = XamlReader.Load(Fs); Using a converter gets around issues with nonexistent keys, keys with special characters and having to rely on string parsing the content of Binding b.Path.Path versus Binding b.ConverterParameter since the latter is simply the key.

Surely others have mapped a dictionary or table to numerous (several hundred) single FrameworkElement / Control / Custom Elements and encountered this problem...

Is there a way to make the MarkupExtension extend the XAML syntax and convert the properties?

2
  • 1
    Still not clear what you want. Maybe you should provide a real meaningful coherent example and try to explain it or your intentions. I only read converter and mapping, but you never explained what you are mapping and converting. What are you doing? "I'm currently using a different binding converter for each unique property in the ItemValueData class" does not make any sense, so please explain. You asked for alternative solutions, which are hard to suggest without knowing your intention.
    – BionicCode
    Commented Dec 27, 2020 at 18:53
  • A Dictionary is a collection for fast lookup. It's not the best choice as data source for data binding. But why don't you bind directly instead of using a converter? Having a converter implementation for each property is a clue that something is wrong. Implementing a custom markup extension is done very quickly, but still over complicates everything. Use a simple binding.
    – BionicCode
    Commented Dec 27, 2020 at 22:03

2 Answers 2

3
+100

You shouldn't make it artificially complicated. Simply use common data binding and make use of collection indexers:

Dictionary<string, ItemValueData> Map { get; } = new Dictionary<string, ItemValueData>
{
  { "Key to value1", new ItemValueData() },
  { "Key to value2", new ItemValueData() }
};

<TextBlock Text="{Binding Map[Key to value1].Name}" />    
<TextBlock Text="{Binding Map[Key to value2].Quality}" />

Binding Path Syntax


As you insist on MarkupExtension, I can offer you the custom DictBindingExtension.
It wraps/reinvents the default binding that already provides everything you need (see example above).
It is still not clear why this does not workout for you and I would have stopped here. But since I found the existing class of the custom BindingResolver I once wrote, I will provide you with a simple extension that builds on top of this class. All your arguments against using the common Binding markup extension (in your Justification of using an IValueConverter/MarkupExtension section) are not reasonable.

Also your approach of storing you entire UI related data in a Dictionary is very wrong. I have never encountered such a scenario that I or someone "have mapped a dictionary or table to numerous (several hundred) single FrameworkElement / Control / Custom Elements". How does such a solution scale?
Whatever your data input is, it needs another level of abstraction to get rid of those key-value pair structured data.
You would typically prefer to structure your data properly using view models and then let the framework populate the controls for you dynamically based on data templates.

Solution #1

Since MarkupExtension itself is not dynamic as it is called only once during initialization, the BindingResolver will hook into a Binding e.g. to a Dictionary and will allow to apply a filter on that value before updating the original target e.g. TextBlock.Text with the converted/filtered value. It's basically the encapsulation of a (optional) value converter that allows the custom MarkupExtension to accept a dynamic Binding.
Alternatively set the DictBindingExtension.Source property of the extension via StaticResource to get rid of the binding feature.

Usage

<TextBox Text="{local:DictBind {Binding DictionaryData}, Key=Key to value2, ValuePropertyName=Quality}" />

DictBindingExtension.cs

class DictBindExtension : MarkupExtension
{
  public object Source { get; }
  public object Key { get; set; }
  public string ValuePropertyName { get; set; }

  public DictBindExtension(object source)
  {
    this.Source = source;
    this.Key = null;
    this.ValuePropertyName = string.Empty;
  }

  #region Overrides of MarkupExtension

  /// <inheritdoc />
  public override object ProvideValue(IServiceProvider serviceProvider)
  {
    IDictionary sourceDictionary = null;
    switch (this.Source)
    {
      case IDictionary dictionary:
        sourceDictionary = dictionary;
        break;
      case BindingBase binding:
        var provideValueTargetService =
          serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;
        object targetObject = provideValueTargetService?.TargetObject;
        if (targetObject == null)
        {
          return this;
        }

        var bindingResolver = new BindingResolver(
          targetObject as FrameworkElement,
          provideValueTargetService.TargetProperty as DependencyProperty)
        {
          ResolvedSourceValueFilter = value => GetValueFromDictionary(value as IDictionary)
        };

        var filteredBinding = bindingResolver.ResolveBinding(binding as Binding) as BindingBase;
        return filteredBinding?.ProvideValue(serviceProvider);
      case MarkupExtension markup:
        sourceDictionary = markup.ProvideValue(serviceProvider) as IDictionary;
        break;
    }


    return GetValueFromDictionary(sourceDictionary);
  }

  private object GetValueFromDictionary(IDictionary sourceDictionary)
  {
    if (sourceDictionary == null)
    {
      throw new ArgumentNullException(nameof(sourceDictionary), "No source specified");
    }

    object value = sourceDictionary[this.Key];
    PropertyInfo propertyInfo = value?.GetType().GetProperty(this.ValuePropertyName);
    return propertyInfo == null ? null : propertyInfo.GetValue(value);
  }

  #endregion
}

BindingResolver.cs

class BindingResolver : FrameworkElement
{
  #region ResolvedValue attached property

  public static readonly DependencyProperty ResolvedValueProperty = DependencyProperty.RegisterAttached(
    "ResolvedValue", typeof(object), typeof(BindingResolver), new PropertyMetadata(default(object), BindingResolver.OnResolvedValueChanged));

  public static void SetResolvedValue(DependencyObject attachingElement, object value) => attachingElement.SetValue(BindingResolver.ResolvedValueProperty, value);

  public static object GetResolvedValue(DependencyObject attachingElement) => (object)attachingElement.GetValue(BindingResolver.ResolvedValueProperty);

  #endregion ResolvedValue attached property

  public DependencyProperty TargetProperty { get; set; }
  public WeakReference<DependencyObject> Target { get; set; }
  public WeakReference<Binding> OriginalBinding { get; set; }
  public Func<object, object> ResolvedSourceValueFilter { get; set; }
  public Func<object, object> ResolvedTargetValueFilter { get; set; }
  private bool IsUpDating { get; set; }
  private static ConditionalWeakTable<DependencyObject, BindingResolver> BindingTargetToBindingResolversMap { get; } = new ConditionalWeakTable<DependencyObject, BindingResolver>();

  public BindingResolver(DependencyObject target, DependencyProperty targetProperty)
  {
    if (target == null)
    {
      throw new ArgumentNullException(nameof(target));
    }
    if (targetProperty == null)
    {
      throw new ArgumentNullException(nameof(targetProperty));
    }
    this.Target = new WeakReference<DependencyObject>(target);
    this.TargetProperty = targetProperty;
  }

  private void AddBindingTargetToLookupTable(DependencyObject target) => BindingResolver.BindingTargetToBindingResolversMap.Add(target, this);

  public object ResolveBinding(Binding bindingExpression)
  {
    if (!this.Target.TryGetTarget(out DependencyObject bindingTarget))
    {
      throw new InvalidOperationException("Unable to resolve sourceBinding. Binding target is 'null', because the reference has already been garbage collected.");
    }

    AddBindingTargetToLookupTable(bindingTarget);

    Binding binding = bindingExpression;
    this.OriginalBinding = new WeakReference<Binding>(binding);

    // Listen to data source
    Binding sourceBinding = CloneBinding(binding);
    BindingOperations.SetBinding(
      bindingTarget,
      BindingResolver.ResolvedValueProperty,
      sourceBinding);

    // Delegate data source value to original target of the original Binding
    Binding targetBinding = CloneBinding(binding, this);
    targetBinding.Path = new PropertyPath(BindingResolver.ResolvedValueProperty);

    return targetBinding;
  }

  private Binding CloneBinding(Binding binding)
  {
    Binding clonedBinding;
    if (!string.IsNullOrWhiteSpace(binding.ElementName))
    {
      clonedBinding = CloneBinding(binding, binding.ElementName);
    }
    else if (binding.Source != null)
    {
      clonedBinding = CloneBinding(binding, binding.Source);
    }
    else if (binding.RelativeSource != null)
    {
      clonedBinding = CloneBinding(binding, binding.RelativeSource);
    }
    else
    {
      clonedBinding = CloneBindingWithoutSource(binding);
    }

    return clonedBinding;
  }

  private Binding CloneBinding(Binding binding, object bindingSource)
  {
    Binding clonedBinding = CloneBindingWithoutSource(binding);
    clonedBinding.Source = bindingSource;
    return clonedBinding;
  }

  private Binding CloneBinding(Binding binding, RelativeSource relativeSource)
  {
    Binding clonedBinding = CloneBindingWithoutSource(binding);
    clonedBinding.RelativeSource = relativeSource;
    return clonedBinding;
  }

  private Binding CloneBinding(Binding binding, string elementName)
  {
    Binding clonedBinding = CloneBindingWithoutSource(binding);
    clonedBinding.ElementName = elementName;
    return clonedBinding;
  }

  private MultiBinding CloneBinding(MultiBinding binding)
  {
    IEnumerable<BindingBase> bindings = binding.Bindings;
    MultiBinding clonedBinding = CloneBindingWithoutSource(binding);
    bindings.ToList().ForEach(clonedBinding.Bindings.Add);
    return clonedBinding;
  }

  private PriorityBinding CloneBinding(PriorityBinding binding)
  {
    IEnumerable<BindingBase> bindings = binding.Bindings;
    PriorityBinding clonedBinding = CloneBindingWithoutSource(binding);
    bindings.ToList().ForEach(clonedBinding.Bindings.Add);
    return clonedBinding;
  }

  private TBinding CloneBindingWithoutSource<TBinding>(TBinding sourceBinding) where TBinding : BindingBase, new()
  {
    var clonedBinding = new TBinding();
    switch (sourceBinding)
    {
      case Binding binding:
        {
          var newBinding = clonedBinding as Binding;
          newBinding.AsyncState = binding.AsyncState;
          newBinding.BindingGroupName = binding.BindingGroupName;
          newBinding.BindsDirectlyToSource = binding.BindsDirectlyToSource;
          newBinding.Converter = binding.Converter;
          newBinding.ConverterCulture = binding.ConverterCulture;
          newBinding.ConverterParameter = binding.ConverterParameter;
          newBinding.FallbackValue = binding.FallbackValue;
          newBinding.IsAsync = binding.IsAsync;
          newBinding.Mode = binding.Mode;
          newBinding.NotifyOnSourceUpdated = binding.NotifyOnSourceUpdated;
          newBinding.NotifyOnTargetUpdated = binding.NotifyOnTargetUpdated;
          newBinding.NotifyOnValidationError = binding.NotifyOnValidationError;
          newBinding.Path = binding.Path;
          newBinding.StringFormat = binding.StringFormat;
          newBinding.TargetNullValue = binding.TargetNullValue;
          newBinding.UpdateSourceExceptionFilter = binding.UpdateSourceExceptionFilter;
          newBinding.UpdateSourceTrigger = binding.UpdateSourceTrigger;
          newBinding.ValidatesOnDataErrors = binding.ValidatesOnDataErrors;
          newBinding.ValidatesOnExceptions = binding.ValidatesOnExceptions;
          newBinding.XPath = binding.XPath;
          newBinding.Delay = binding.Delay;
          newBinding.ValidatesOnNotifyDataErrors = binding.ValidatesOnNotifyDataErrors;
          binding.ValidationRules.ToList().ForEach(newBinding.ValidationRules.Add);
          break;
        }
      case PriorityBinding priorityBinding:
        {
          var newBinding = clonedBinding as PriorityBinding;
          newBinding.BindingGroupName = priorityBinding.BindingGroupName;
          newBinding.FallbackValue = priorityBinding.FallbackValue;
          newBinding.StringFormat = priorityBinding.StringFormat;
          newBinding.TargetNullValue = priorityBinding.TargetNullValue;
          newBinding.Delay = priorityBinding.Delay;
          break;
        }
      case MultiBinding multiBinding:
        {
          var newBinding = clonedBinding as MultiBinding;
          newBinding.BindingGroupName = multiBinding.BindingGroupName;
          newBinding.Converter = multiBinding.Converter;
          newBinding.ConverterCulture = multiBinding.ConverterCulture;
          newBinding.ConverterParameter = multiBinding.ConverterParameter;
          newBinding.FallbackValue = multiBinding.FallbackValue;
          newBinding.Mode = multiBinding.Mode;
          newBinding.NotifyOnSourceUpdated = multiBinding.NotifyOnSourceUpdated;
          newBinding.NotifyOnTargetUpdated = multiBinding.NotifyOnTargetUpdated;
          newBinding.NotifyOnValidationError = multiBinding.NotifyOnValidationError;
          newBinding.StringFormat = multiBinding.StringFormat;
          newBinding.TargetNullValue = multiBinding.TargetNullValue;
          newBinding.UpdateSourceExceptionFilter = multiBinding.UpdateSourceExceptionFilter;
          newBinding.UpdateSourceTrigger = multiBinding.UpdateSourceTrigger;
          newBinding.ValidatesOnDataErrors = multiBinding.ValidatesOnDataErrors;
          newBinding.ValidatesOnExceptions = multiBinding.ValidatesOnExceptions;
          newBinding.Delay = multiBinding.Delay;
          newBinding.ValidatesOnNotifyDataErrors = multiBinding.ValidatesOnNotifyDataErrors;
          multiBinding.ValidationRules.ToList().ForEach(newBinding.ValidationRules.Add);
          break;
        }
      default: return null;
    }

    return clonedBinding;
  }

  private static void OnResolvedValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
  {
    if (d is BindingResolver bindingResolver)
    {
      if (bindingResolver.IsUpDating)
      {
        return;
      }

      bindingResolver.IsUpDating = true;
      bindingResolver.UpdateSource();
      bindingResolver.IsUpDating = false;
    }
    else
    {
      if (BindingResolver.BindingTargetToBindingResolversMap.TryGetValue(d, out bindingResolver))
      {
        if (bindingResolver.IsUpDating)
        {
          return;
        }

        bindingResolver.IsUpDating = true;
        bindingResolver.UpdateTarget();
        bindingResolver.IsUpDating = false;
      }
    }
  }

  private static bool TryClearBindings(DependencyObject bindingTarget, BindingResolver bindingResolver)
  {
    if (bindingTarget == null)
    {
      return false;
    }

    Binding binding = BindingOperations.GetBinding(bindingTarget, bindingResolver.TargetProperty);
    if (binding != null && binding.Mode == BindingMode.OneTime)
    {
      BindingOperations.ClearBinding(bindingTarget, BindingResolver.ResolvedValueProperty);
      BindingOperations.ClearBinding(bindingTarget, bindingResolver.TargetProperty);
    }

    return true;
  }

  private void UpdateTarget()
  {
    if (!this.Target.TryGetTarget(out DependencyObject target))
    {
      return;
    }

    object resolvedValue = BindingResolver.GetResolvedValue(target);
    object value = this.ResolvedSourceValueFilter.Invoke(resolvedValue);

    BindingResolver.SetResolvedValue(this,value);
  }

  private void UpdateSource()
  {
    if (!this.Target.TryGetTarget(out DependencyObject target))
    {
      return;
    }

    object resolvedValue = BindingResolver.GetResolvedValue(this);
    object value = this.ResolvedTargetValueFilter.Invoke(resolvedValue);

    BindingResolver.SetResolvedValue(target, value);
  }
}

Solution #2

Add related properties to your IValueConverter implementation:

Usage

<TextBox>
  <TextBox.Text>
    <Binding Path="DictionaryData">
      <Binding.Converter>
        <ItemConverterGeneric Key="Key to value2" PropertyName="Quality" />
      </Binding.Converter>
    </Binding>
  </TextBox.Text>
</TextBox>

ItemConverterGeneric.cs

public class ItemConverterGeneric : IValueConverter 
{
    public object Key { get; set; }
    public object ValuePropertyName { get; set; }

    public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 
    {    
            if (!(value is Dictionary<string, object> dict) || Key == null || ValuePropertyName == null) 
            {
                return Binding.DoNothing;
            }
            string key = Key as string;
            if (dict.TryGetValue(key, out object dataItem)) 
            { 
                // Use reflection to pick the right property of dataItem                  
            } 
    }

    public object ConvertBack(object value, Type targetTypes, object parameter, CultureInfo culture) => throw new NotSupportedException();
}
16
  • Then why don't you do it, what is the reason?
    – BionicCode
    Commented Dec 27, 2020 at 23:45
  • I don't see any of your justifications being justified. "The dictionary is built dynamically ... " The bindings and converters you are setting up are not dynamic. Nothing in your example is dynamic. Even when using a custom markup extension, the parameters of this extension are static, provided during design time. "Using a converter gets around issues with nonexistent keys" All keys are hardcoded, so you must know them in advance when implementing the UI or bindings will fail. Converter will change nothing in this scenario: the key is still not present.
    – BionicCode
    Commented Dec 28, 2020 at 8:50
  • "keys with special characters" Why are there "special characters" used as key? When using a converter you have to specify the key as parameter in XAML. Where is the difference in specifying the key as index of a collection binding? "having to rely on string parsing ..."_ I don't see any string parsing nor can't I come up with a scenario of string parsing in this context. I understood the string is the key.
    – BionicCode
    Commented Dec 28, 2020 at 8:51
  • I have removed the default constructor from DictBindingExtension and the set method from Source property to ensure proper initialization (prevent wrong usage).
    – BionicCode
    Commented Dec 28, 2020 at 11:41
  • I misunderstood how a MarkupExtension / IValueConverter based class introduces its properties into the XAML within Converter and not the ConverterParameter. Additionally it doesn't appear that I need the parameter in my case (although I'd really like to see an example with both.)
    – codebender
    Commented Dec 28, 2020 at 19:38
1

While the answer(s) provided have some interesting possibilities, they rely upon reflection and runtime compiler services.

What I eventually came up with below doesn't.

I misunderstood how a MarkupExtension / IValueConverter based class introduces its properties into the XAML within Converter and not the ConverterParameter. Additionally it doesn't appear that I need the parameter in my case (although I'd really like to see an example that uses Converter and ConverterParameter).

The MarkupExtension/IValueConverter should have specified the named properties inside:

[ValueConversion(typeof(DictionaryData), typeof(object))]
public class ItemConverterGeneric : MarkupExtension, IValueConverter {
    public string Path {get; set;}
    public string Property {get; set;}
    public string Source {get; set;}
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { 
        try {
            if !(value == null || string.IsNullorEmpty(Path) || string.IsNullorEmpty(Property) || string.IsNullorEmpty(Source)) {
                DictionaryData dict = value as DictionaryData;
                // These are hard coded, but you could use refelction to make it look nicer 
                // and automatically handle any new properties added to the class.
                if (dict.ContainsKey(Path)) { 
                   switch(Property.ToUpper()) { 
                       case "SOURCE":   return dict[Path].Source;
                       case "NAME":     return dict[Path].Name;
                       case "QUALITY":  return dict[Path].Quality;
                       case "VALUE:     return dict[Path].Value;
                       //... etc and no default: needed as the outer return will handle it.
                   }
                } 
            }
            return Binding.DoNothing;
        } catch (Exception ex) {
            // Console.WriteLine(ex.Message); // or log the error 
            return Binding.DoNothing;
        }
    }

    public object ConvertBack(object value, Type targetTypes, object parameter, CultureInfo culture)
    { return DependencyProperty.UnsetValue; }

    public override object ProvideValue(IServiceProvider serviceProvider)
    { return this; }
}       

Althought my original XAML syntax was close, each property after the first one should have been specified inside the Converter separated by commas:

<ElementX Value="{av:Binding DD, Converter={Converters:ItemConverterGeneric
   Path='ItemXName001', Property='Value', Source='DataSourceX'}}" .../>
<!--  hundreds of DataSourceX bound items -->
<ElementX Value="{av:Binding DD, Converter={Converters:ItemConverterGeneric
   Path='ItemXName401', Property='Value', Source='DataSourceX'}}" .../>
<ElementY Value="{av:Binding DD, Converter={Converters:ItemConverterGeneric
   Path='ItemYName001', Property='Value', Source='DataSourceY'}}" .../>
<!--  hundreds of DataSourceY bound items -->
<ElementY Value="{av:Binding DD, Converter={Converters:ItemConverterGeneric
   Path='ItemYName401', Property='Value', Source='DataSourceY'}}" .../>
2
  • I have to pint out that your provided solution does not require reflection indeed, but has now become a nightmare in terms of extensibility. The reason why I chose reflection is that it improves extensibility/maintainability significantly. Using a switch like you did, requires to update the code whenever new properties are added or renamed. This switch will be grow and grow and grow and finally gets out of control. switch always scales very bad, that's why you typically avoid such statements (of course same applies to chained if as well).
    – BionicCode
    Commented Jan 2, 2021 at 16:44
  • You don't switch on objects that are likely to change or when objects are likely to be added (types, attributes etc.). I would always prefer reflection over an exploding switch. The use of reflection is not excessive in this case.
    – BionicCode
    Commented Jan 2, 2021 at 16:44

Not the answer you're looking for? Browse other questions tagged or ask your own question.