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?

BionicCode
  • 1
  • 4
  • 28
  • 44
codebender
  • 469
  • 1
  • 6
  • 23
  • 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 Dec 27 '20 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 Dec 27 '20 at 22:03

2 Answers2

3

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();
}
BionicCode
  • 1
  • 4
  • 28
  • 44
  • Then why don't you do it, what is the reason? – BionicCode Dec 27 '20 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 Dec 28 '20 at 08: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 Dec 28 '20 at 08:51
  • I have removed the default constructor from DictBindingExtension and the set method from Source property to ensure proper initialization (prevent wrong usage). – BionicCode Dec 28 '20 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 Dec 28 '20 at 19:38
  • No problem. Can you be more specific, please? I don't really understand what you have misunderstood. Using the markup extension doesn't require any additional converter. When you use a converter, you can add public properties that you can set instead of using the `Binding.ConverterParameter` property. The only "downside" is that you have to create local converter instances via Property Element syntax (see example usage) of the converter instead of using a static resource. – BionicCode Dec 28 '20 at 20:50
  • In the Convert the parameter is an object, so I thought there would be a way to pass a class to it and somehow expose its properties. – codebender Dec 28 '20 at 20:51
  • The value of the parameter `value` depends on your binding source. When you bind to a Dictionary, the value is this Dictionary. Dictionary is also a class. C# is object oriented, so basically everything is a class. ConverterParameter is also of type object, which means you can assign any object to this property. Not sure what you want here. The object you want is inside the Dictionary. You retrieve it via the key. The result is the object that _"somehow exposes its properties"_. You seem to have a wrong understanding of whats going on or I misunderstand you. Please feel free to ask to help me. – BionicCode Dec 28 '20 at 20:56
  • You should think about to "flatten" your dictionary by extracting each key-value pair to a property on a view model class. You can name the property using the key. This would significantly simply data handling and UI design. Usually you choose a collection as data structure if you want to group items for grouped or list presentation e.g. by using a ListBox. In this case you would extract the Dictionary.ValueCollection to a binding source collection and create a DataTemplate for the data items. You seem to have a homogeneous data collection. So not sure what you want/need. – BionicCode Dec 28 '20 at 21:08
  • You have abstracted your question so much, that it's impossible to tell what your original goal is and where you failed. Sometimes it is better to break a real code down to provide a simple real-world example instead of abstracting everything with Foo and Bar. There is always a lot of information that gets lost during abstraction and "compression". To me it seems obvious that the converter is not the real problem. It's only the symptom of the true flaw in your design. – BionicCode Dec 28 '20 at 21:08
  • Your current design has brought you here: you have to use a `IValueConveter` _for each property of you data class!_ - unacceptable. You have to deal with hard coded dictionary keys in your view. You don't seem to notice how the static key-value mapping eliminates scalability and dynamic data presentation. You have _"several hundreds"_ UI elements that you have to map manually to an entry of a dictionary. You are heading towards a dead-end. – BionicCode Dec 28 '20 at 21:24
  • Are you saying that WPF cannot support hundreds of UI elements where each may have one or more data bindings? Because regardless of the model, that's what I need. – codebender Dec 28 '20 at 21:49
  • No I said a lot, but not that WPF doesn't support hundreds of UI elements. I don't know your exact scenario. but since you have a homogeneous collection of data models I would assume you should let the framework generate the several hundreds of elements for you by just defining a single template. But it's also possible that using a collection of homogeneous data models is already the wrong choice. I don't know it, you don't provide any details. – BionicCode Dec 28 '20 at 22:01
  • From experience I'm very sure that your approach is wrong. I have never came across such an application where several hundreds of UI elements had to be wired manually to a Dictionary. It feels wrong, it looks wrong and it only introduces serious disadvantages when analyzing such an implementation. That's what I was trying to say. Of course WPF can handle hundreds of elements, but as the smart developers that we are, we would try hard to avoid doing this dry, repetitive and unmaintainable work manually -> DataTemplate. – BionicCode Dec 28 '20 at 22:01
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/226532/discussion-between-codebender-and-bioniccode). – codebender Dec 28 '20 at 22:03
  • It's funny that this is the only conclusion you draw after reading my comments :) – BionicCode Dec 28 '20 at 22:03
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'}}" .../>
halfer
  • 19,824
  • 17
  • 99
  • 186
codebender
  • 469
  • 1
  • 6
  • 23
  • 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 Jan 02 '21 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 Jan 02 '21 at 16:44