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();
}