1

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}"/>

Sebastian
  • 87
  • 1
  • 9
  • Why a markup extension? Rather than a dynamicresource or just a property in a viewmodel? – Andy Nov 17 '22 at 16:50
  • You must set the passed in Binding to a dependency property in order to activate it. It's the binding engine that actually does all the work of wiring the target property to a source property. The binding engine is part of the dependency property infrastructure. That's why the Binding target **must** be a dependency property. You need to create an intermediate dependency property to resolve the Binding. Handle the Binding events SourceUpdated and TargetUpdated to capture the updated value. Then process/manipulate it and send it the target of your custom markup extension. – BionicCode Nov 17 '22 at 22:29
  • To attach the Binding your intermediate property must be defined by a DependencyObject. This means you need to create a dedicated class to resolve the binding. – BionicCode Nov 17 '22 at 22:56
  • @Andy I created this markup extension just to show what is not working, my real markup extension handles some sort of language change. I could do that in the VM as well but I think a markup extension makes it cleaner and (if working) easier to use – Sebastian Nov 18 '22 at 07:54
  • @BionicCode I am not sure if I understand you. I thought I am already using a dependency property: `property = DependencyProperty.RegisterAttached("Storage" + StorageProperties.Count, typeof(object), typeof(ExampleTextExtension), new PropertyMetadata());` and here I am linking the dp to a dependency object: `var resolvedValue = markupExtension.ProvideValue(new ServiceProvider(obj, property)); obj.SetValue(property, resolvedValue);` can you post an example or try to specify what you mean please? The binding is basically working just not in the case posted in my question – Sebastian Nov 18 '22 at 08:01
  • By "some sort of language change" do you literally mean see everything in french or german or english - localisation? Dynamicresource and merging a resource dictionary per language is a good way to do that. – Andy Nov 18 '22 at 08:44
  • @Andy yes to your question but no to your suggestion. all texts are stored in a database and are accessed over a service... – Sebastian Nov 18 '22 at 12:31
  • Why does that mean you can't merge them as resources? You can build a resource dictionary in code or set values already merged or merge a flat file or merge a string et al. This markupextension doesn't look like a good idea to me. I think the problem is you have the dp in one class and are attaching it to another. – Andy Nov 18 '22 at 18:55
  • Another option is a lookless control exposing a dp string[] or observable dictionary of strings. You could use a regular binding on either and load any way you like. – Andy Nov 18 '22 at 19:23
  • @Andy i am not sure how i would get the texts from this database (third party owned, i only have access through an sdk) and then use it as a resource. can you explain this approach a little further? – Sebastian Nov 24 '22 at 16:37
  • If you can read them at all then you can read and save as a conventional uncompiled resource dictionary. Which can then be merged whenever you like. What am i explaining though? How you're getting these strings seems like it might be relevent in one way or another. In any case. You have a weird design here. – Andy Nov 24 '22 at 20:42
  • @Andy ah ok now i get what you mean... haven't thought about that, thank you – Sebastian Nov 28 '22 at 07:50

1 Answers1

0

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.

BionicCode
  • 1
  • 4
  • 28
  • 44
  • thank you. the _OnResolvedBindingSourceValueChanged_ part was what was missing and what i did not get. so this was the answer i was looking for... – Sebastian Nov 24 '22 at 16:17
  • i kinda already had the rest (simplification) of your implementation but added the tracking of via the collection after i read this article: https://www.singulink.com/codeindex/post/building-the-ultimate-wpf-event-method-binding-extension where the author stated: _to store their values on their target element and we aren't needlessly registering more attached properties or leaking memory as UI elements are created and destroyed over and over and registering more and more properties_ , can you explain why this is not a problem in my case? – Sebastian Nov 24 '22 at 16:19
  • in the implementation i had before (kinda like this: https://stackoverflow.com/a/37667896) _bindingbase_ was used instead of _binding_ and the return in _providevalue_ was also different. can you explain the difference? – Sebastian Nov 24 '22 at 16:27
  • i will have a look into what you suggested. is it possible to change the language during runtime with these satellite assemblies? this is mandatory in my app. – Sebastian Nov 24 '22 at 16:29
  • You don't need the collection part. The author used it to allow unlimited arguments to be passed to the markup extension. He creates a dedicated property for each argument. I guess to create a single attached property and store the arguments in a list would be a far better solution. Storing that many dependency properties in a static collection effectively creates unnecessary "memory leaks", because the garbage collector can't collect static references or references that are referenced by static references. I assume he was aware and therefore tried to reuse as much properties as possible. – BionicCode Nov 24 '22 at 18:15
  • Instead he should have stored a list of values in a single attached property. Since your extension only accepts a single value per instance, you definitely don't need the collection part. A single attached property to resolve a Binding does the trick. == – BionicCode Nov 24 '22 at 18:16
  • BindingBase is the base class of Binding, MultiBinding and PriorityyBinding. To allow compatibility of the extension with all type of bindings, you would write code that targets the common base class (BindingBase). *"and the return in providevalue was also different. can you explain the difference?"* - what do you mean by "the return was also different" Different compared to what? === – BionicCode Nov 24 '22 at 18:16
  • [Developing Localizable Applications](https://learn.microsoft.com/en-us/dotnet/desktop/wpf/advanced/globalization-for-wpf?view=netframeworkdesktop-4.8#developing-localizable-applications), [Localize a WPF Application](https://learn.microsoft.com/en-us/dotnet/desktop/wpf/advanced/wpf-globalization-and-localization-overview?view=netframeworkdesktop-4.8#localize-a-wpf-application) and many more resources on the internet. – BionicCode Nov 24 '22 at 18:16
  • thank you very much for your explanations... what i meant with "return was different" was the return in the `ProvideValue` function. in the solution i linked `return param1InnerBinding.ProvideValue(serviceProvider); // return binding to Param1.SomeInnerProperty` was returned. since you just return `string.empty` i am not getting what the return of `ProvideValue` is used for because in your implementation it seems unnecessary... – Sebastian Nov 28 '22 at 08:05