wpfbindingmarkup-extensions

WPF: Why is my markup extension binding not working?


Working Example with "Binding":

I have a UserControl which I use like this in my MainWindow:

<userControls:NoMarkupControl/>

The ViewModel of my MainWindow contains this property:

private string _exampleText = "example";
public string ExampleText
{
   get { return _exampleText; }
   set
   {
      _exampleText = value;
      OnPropertyChanged();
   }
}

inside the UserControl I bind my ViewModel to this property:

<TextBlock Text="{Binding ExampleText}"/>

as a result "example" gets displayed when I start the app. Everything works.

Not working example with Custom Markup Extension:

Now I have a MarkupExtension:

public class ExampleTextExtension : MarkupExtension
{
    private static readonly List<DependencyProperty> StorageProperties = new List<DependencyProperty>();

    private readonly object _parameter;

    private DependencyProperty _dependencyProperty;

    public ExampleTextExtension(object parameter)
    {
        _parameter = parameter;
    }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        var target = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;
        DependencyObject targetObject;
        if (target?.TargetObject is DependencyObject dependencyObject &&
            target.TargetProperty is DependencyProperty)
        {
            targetObject = dependencyObject;
        }
        else
        {
            return this;
        }

        _dependencyProperty = SetUnusedStorageProperty(targetObject, _parameter);

        return GetLocalizedText((string)targetObject.GetValue(_dependencyProperty));
    }

    private static string GetLocalizedText(string text)
    {
        return text == null ? null : $"markup: {text}";
    }

    private static DependencyProperty SetUnusedStorageProperty(DependencyObject obj, object value)
    {
        var property = StorageProperties.FirstOrDefault(p => obj.ReadLocalValue(p) == DependencyProperty.UnsetValue);

        if (property == null)
        {
            property = DependencyProperty.RegisterAttached("Storage" + StorageProperties.Count, typeof(object), typeof(ExampleTextExtension), new PropertyMetadata());
            StorageProperties.Add(property);
        }

        if (value is MarkupExtension markupExtension)
        {
            var resolvedValue = markupExtension.ProvideValue(new ServiceProvider(obj, property));
            obj.SetValue(property, resolvedValue);
        }
        else
        {
            obj.SetValue(property, value);
        }

        return property;
    }

    private class ServiceProvider : IServiceProvider, IProvideValueTarget
    {
        public object TargetObject { get; }
        public object TargetProperty { get; }

        public ServiceProvider(object targetObject, object targetProperty)
        {
            TargetObject = targetObject;
            TargetProperty = targetProperty;
        }

        public object GetService(Type serviceType)
        {
            return serviceType.IsInstanceOfType(this) ? this : null;
        }
    }
}

Again I have a UserControl which I use like this in my MainWindow:

<userControls:MarkupControl/>

The ViewModel of my MainWindow stays the same like above.

inside the UserControl I bind to my TextBlock Text property like this:

<TextBlock Text="{markupExtensions:ExampleText {Binding ExampleText}}"/>

as a result my UserControl displays nothing. I would have expected to display "markup: example"

The binding somehow does not work in this case.

Does anybody know how to fix this?

Additional information:

it works when used like this (dependency property MarkupText is created in user control):

<userControls:MarkupControl MarkupText={markupExtensions:ExampleText {Binding ExampleText}}/>

<TextBlock Text="{Binding Text, ElementName=MarkupControl}"/>


Solution

  • Firstly, you need to refactor your extension to simplify the implementation. You don't need a static context here. Getting rid of the class context will make the tracking of the created attached properties obsolete. You can drop the related collection safely. In your case, it's more efficient to store values in an instance context. Attached properties are also a convenient solution to store values per instance especially in a static context.

    Secondly, you got a timing issue. The first time the extension is called, the Binding is not initialized properly: it doesn't provide the final value of the Binding.Source. Additionally, your current implementation does not support property changes.
    To fix this, you would have to track the Binding.Target updates when a value is sent from the Binding.Source (for a default BindingMode.OneWay). You can achieve this by listening to the Binding.TargetUpdated event (as stated in my previous comment) or register a property changed handler with the attached property (recommended).
    To support two way binding, you would also have to track the target property (the property your MarkupExtension is assigned to).

    A fixed and improved version could look as follows:

    public class ExampleTextExtension : MarkupExtension
    {
      private static DependencyProperty ResolvedBindingSourceValueProperty = DependencyProperty.RegisterAttached(
        "ResolvedBindingSourceValue",
        typeof(object),
        typeof(ExampleTextExtension),
        new PropertyMetadata(default(object), OnResolvedBindingSourceValueChanged));  
    
      // Use attached property to store the target object
      // for reference from a static context without dealing with class level members that are shared between instances.
      private static DependencyProperty TargetPropertyProperty = DependencyProperty.RegisterAttached(
        "TargetProperty",
        typeof(DependencyProperty),
        typeof(ExampleTextExtension),
        new PropertyMetadata(default));
    
      private Binding Binding { get; }
    
      // Accept BindingBase to support MultiBinding etc.
      public ExampleTextExtension(Binding binding)
      {
        this.Binding = binding;
      }
    
      public override object ProvideValue(IServiceProvider serviceProvider)
      {
        var provideValueTargetService = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;
        if (provideValueTargetService?.TargetObject is not DependencyObject targetObject
          || provideValueTargetService?.TargetProperty is not DependencyProperty targetProperty)
        {
          return this;
        }
    
        targetObject.SetValue(ExampleTextExtension.TargetPropertyProperty, targetProperty);
        AttachBinding(targetObject);
        return string.Empty;
      }
    
      private static string GetLocalizedText(string text) 
        => String.IsNullOrWhiteSpace(text) 
          ? string.Empty 
          : $"markup: {text}";
    
      // By now, only supports OneWay binding
      private void AttachBinding(DependencyObject targetObject)
      {
        switch (this.Binding.Mode)
        {
          case BindingMode.OneWay:
          case BindingMode.Default:
            HandleOneWayBinding(targetObject); break;
          default: throw new NotSupportedException();
        }
      }
    
      private void HandleOneWayBinding(DependencyObject targetObject)
      {
        BindingOperations.SetBinding(targetObject, ExampleTextExtension.ResolvedBindingSourceValueProperty, this.Binding);
      }
    
      // Property changed handler to update the target of this extension
      // with the localized value
      private static void OnResolvedBindingSourceValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
      {
        string localizedText = GetLocalizedText(e.NewValue as string);
        var targetProperty = d.GetValue(ExampleTextExtension.TargetPropertyProperty) as DependencyProperty;
        d.SetValue(targetProperty, localizedText);
      }
    }
    

    Remarks

    There are better solutions to introduce localization without compromising the general syntax or legacy code. For example, introducing this MarkupExtension to existing code will break this code as all relevant data bindings (C# and XAML) have to be modified.

    The most common approach is to use satellite assemblies and localized resources. Instead of converting text values during data binding you should localize the value source directly (so that the Binding transfers already localized values).
    In other words, make sure that the data source is localized. Let the binding source expose the text by fetching it from a localized repository.