Friday, December 4, 2020

Two-way bindings in markup extensions

 In my earlier blog entry I showed how to implement one-way (source to target) bindings in a markup extension. This entry will finish the job by implementing two-way bindings. We start with the finished project from the earlier blog entry.

We are going to add an account number structure and bind the text of the text boxes to it using a new AccountNumberExtension markup extension. As we enter values into the text boxes, we will update the source property. We will add some text blocks that are bound to those properties so we can see them changing in real time.

Let's start by adding the account number class. It subclasses a base class that implements INotifyPropertyChanged. Add a class called BaseClass.

using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
 
namespace AccountSectionExtension
{
    public class BaseClass : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        public void SetProperty<T>(ref T storageT value, [CallerMemberNamestring name = "")
        {
            if (!Object.Equals(storagevalue))
            {
                storage = value;
                PropertyChanged?.Invoke(thisnew PropertyChangedEventArgs(name));
            }
        }
    }
}

Now add a class called AccountNumber. We never want this class's objects to be unpopulated or the section list to be empty so we populate it in the constructor. That means we must pass the account structure to the constructor. We expect the view to bind to the value properties so they must implement INotifyPropertyChanged.

using System;
using System.Collections.Generic;
using System.ComponentModel;
 
namespace AccountSectionExtension
{
    public class cAccountNumberSection : BaseClass
    {
        private string _value = "";
 
        public int ID;
        public String value
        {
            get { return _value; }
            set { SetProperty(ref _value, value); }
        }
    }
 
    public class cAccountNumber
    {
        public cAccountStructure accountStructure { getset; }
        public List<cAccountNumberSection> accountNumberSections { getset; }
 
        public cAccountNumber(cAccountStructure accountStructure)
        {
            this.accountStructure = accountStructure;
            this.accountNumberSections = new List<cAccountNumberSection>();
            foreach (cAccountSection s in accountStructure.Sections)
                this.accountNumberSections.Add(new cAccountNumberSection() { ID = s.ID, value = "" });
        }
    }
}

The AccountNumberExtension is similar to the AccountStructureExtension but includes some extra code for handling the Target-to-Source binding. It requires a reference to an AccountNumber object in the DataContext as well as the section index it is bound to. 

The AddPropertyChangedEventHandler method subscribes to the data context's PropertyChanged event and handles source-to-target binding. The AddTextChangedEventHandler subscribes to the DependencyObject's TextChanged event and handles target-to-source binding. 

This code assumes the DependencyObject is a TextBox and would need to be enhanced to handle other types of DependencyObjects.

using AccountSectionExtension;
using System;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Markup;
 
namespace AccountStructureExtension
{
    class AccountNumberExtension : System.Windows.Markup.MarkupExtension
    {
        public String AccountNumberPropertyPath { getset; }
        public int SectionIndex { getset; }
 
        public DependencyObject dependencyObject;
        public DependencyProperty dependencyProperty;
 
        public AccountNumberExtension(int SectionIndex)
        {
            this.AccountNumberPropertyPath = "AccountNumber";
            this.SectionIndex = SectionIndex;
        }
 
        ~AccountNumberExtension()
        {
            RemovePropertyChangedEventHandler();
            RemoveTextChangedEventHandler();
        }
 
        public override object ProvideValue(System.IServiceProvider serviceProvider)
        {
            IProvideValueTarget pvt = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;
            dependencyObject = pvt.TargetObject as DependencyObject;
            dependencyProperty = pvt.TargetProperty as DependencyProperty;
            AddPropertyChangedEventHandler();
            AddTextChangedEventHandler();
            return GetValue();
        }
 
        public void AddPropertyChangedEventHandler()
        {
            object dc = (dependencyObject as FrameworkElement).DataContext;
            EventInfo npc = dc.GetType().GetEvent("PropertyChanged");
            MethodInfo mi = this.GetType().GetMethod("NotifyPropertyChanged");
            Delegate d = Delegate.CreateDelegate(npc.EventHandlerType, thismi);
            npc.AddEventHandler(dcd);
        }
 
        public void AddTextChangedEventHandler()
        {
            EventInfo tc = dependencyObject.GetType().GetEvent("TextChanged");
            MethodInfo mi = this.GetType().GetMethod("NotifyTextChanged");
            if (tc != null)
            {
                Delegate d = Delegate.CreateDelegate(tc.EventHandlerType, thismi);
                tc.AddEventHandler(dependencyObject, d);
            }
        }
 
        public void RemovePropertyChangedEventHandler()
        {
            object dc = (dependencyObject as FrameworkElement).DataContext;
            EventInfo npc = dc.GetType().GetEvent("PropertyChanged");
            MethodInfo mi = this.GetType().GetMethod("NotifyPropertyChanged");
            Delegate d = Delegate.CreateDelegate(npc.EventHandlerType, thismi);
            npc.RemoveEventHandler(dcd);
        }
 
        public void RemoveTextChangedEventHandler()
        {
            EventInfo tc = dependencyObject.GetType().GetEvent("TextChanged");
            MethodInfo mi = this.GetType().GetMethod("NotifyTextChanged");
            if (tc != null)
            {
                Delegate d = Delegate.CreateDelegate(tc.EventHandlerType, thismi);
                tc.RemoveEventHandler(dependencyObject, d);
            }
        }
 
        public object GetValue()
        {
            object dc = (dependencyObject as FrameworkElement).DataContext;
            if (dc == nullreturn DependencyProperty.UnsetValue;
            cAccountNumber accountNumber = dc.GetType().GetProperty(AccountNumberPropertyPath).GetValue(dcnullas cAccountNumber;
            if (accountNumber == nullreturn DependencyProperty.UnsetValue;
            cAccountStructure accountStructure = accountNumber.accountStructure;
            if (accountStructure == nullreturn DependencyProperty.UnsetValue;
            if (SectionIndex < 1) return DependencyProperty.UnsetValue;
 
            switch (dependencyProperty.Name.ToLower())
            {
                case "text":
                    return (SectionIndex > accountStructure.SectionCount) ? DependencyProperty.UnsetValue : accountNumber.accountNumberSections[SectionIndex - 1].value;
                defaultreturn DependencyProperty.UnsetValue;
            }
        }
 
        public void NotifyPropertyChanged(Object senderPropertyChangedEventArgs e)
        {
            if (e.PropertyName == AccountNumberPropertyPath)
            {
                Action updateAction = () => dependencyObject.SetValue(dependencyProperty, GetValue());
                if (dependencyObject.CheckAccess())
                    updateAction();
                else
                    dependencyObject.Dispatcher.Invoke(updateAction);
            }
        }
 
        public void NotifyTextChanged(object senderTextChangedEventArgs e)
        {
            TextBox tb = (dependencyObject as TextBox);
            object dc = tb.DataContext;
            (dc.GetType().GetProperty(AccountNumberPropertyPath).GetValue(dcas cAccountNumber).accountNumberSections.First(s => s.ID == SectionIndex).value = tb.Text;
        }
    }
}

We need to enhance the XAML to add the required references to AccountNumber. It now looks like this.

<Window x:Class="AccountStructureExtension.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:AccountStructureExtension"
        mc:Ignorable="d"
        DataContext="{Binding RelativeSource={RelativeSource Self}}"
        Title="Account Structure" Height="450" Width="800">
    <StackPanel Orientation="Vertical">
        <ComboBox ItemsSource="{Binding AccountStructures}" SelectedItem="{Binding AccountStructure}" DisplayMemberPath="AccountStructureType"/>
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="auto"/>
                <RowDefinition Height="auto"/>
                <RowDefinition Height="auto"/>
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="auto"/>
                <ColumnDefinition Width="auto"/>
                <ColumnDefinition Width="auto"/>
                <ColumnDefinition Width="auto"/>
                <ColumnDefinition Width="auto"/>
                <ColumnDefinition Width="auto"/>
            </Grid.ColumnDefinitions>
 
            <TextBlock Grid.Row="0" Grid.Column="0" Text="{local:AccountStructure 1}" Visibility="{local:AccountStructure 1}"/>
            <TextBox Grid.Row="1" Grid.Column="0" MaxLength="{local:AccountStructure 1}" Width="{local:AccountStructure 1}" Visibility="{local:AccountStructure 1}" 
                     Text="{local:AccountNumber 1}"/>
 
            <TextBlock Grid.Row="0" Grid.Column="1" Text="{local:AccountStructure 2}" Visibility="{local:AccountStructure 2}"/>
            <TextBox Grid.Row="1" Grid.Column="1" MaxLength="{local:AccountStructure 2}" Width="{local:AccountStructure 2}" Visibility="{local:AccountStructure 2}" 
                     Text="{local:AccountNumber 2}"/>
 
            <TextBlock Grid.Row="0" Grid.Column="2" Text="{local:AccountStructure 3}" Visibility="{local:AccountStructure 3}"/>
            <TextBox Grid.Row="1" Grid.Column="2" MaxLength="{local:AccountStructure 3}" Width="{local:AccountStructure 3}" Visibility="{local:AccountStructure 3}" 
                     Text="{local:AccountNumber 3}"/>
 
            <TextBlock Grid.Row="0" Grid.Column="3" Text="{local:AccountStructure 4}" Visibility="{local:AccountStructure 4}"/>
            <TextBox Grid.Row="1" Grid.Column="3" MaxLength="{local:AccountStructure 4}" Width="{local:AccountStructure 4}" Visibility="{local:AccountStructure 4}" 
                     Text="{local:AccountNumber 4}"/>
 
            <TextBlock Grid.Row="0" Grid.Column="4" Text="{local:AccountStructure 5}" Visibility="{local:AccountStructure 5}"/>
            <TextBox Grid.Row="1" Grid.Column="4" MaxLength="{local:AccountStructure 5}" Width="{local:AccountStructure 5}" Visibility="{local:AccountStructure 5}" 
                     Text="{local:AccountNumber 5}"/>
 
            <TextBlock Grid.Row="0" Grid.Column="5" Text="{local:AccountStructure 6}" Visibility="{local:AccountStructure 6}"/>
            <TextBox Grid.Row="1" Grid.Column="5" MaxLength="{local:AccountStructure 6}" Width="{local:AccountStructure 6}" Visibility="{local:AccountStructure 6}" 
                     Text="{local:AccountNumber 6}"/>
 
            <TextBlock Grid.Row="2" Grid.Column="0" Text="{Binding AccountNumber.accountNumberSections[0].value}" HorizontalAlignment="Stretch" Background="AliceBlue"/>
            <TextBlock Grid.Row="2" Grid.Column="1" Text="{Binding AccountNumber.accountNumberSections[1].value}" HorizontalAlignment="Stretch" Background="AliceBlue"/>
            <TextBlock Grid.Row="2" Grid.Column="2" Text="{Binding AccountNumber.accountNumberSections[2].value}" HorizontalAlignment="Stretch" Background="AliceBlue"/>
            <TextBlock Grid.Row="2" Grid.Column="3" Text="{Binding AccountNumber.accountNumberSections[3].value}" HorizontalAlignment="Stretch" Background="AliceBlue"/>
            <TextBlock Grid.Row="2" Grid.Column="4" Text="{Binding AccountNumber.accountNumberSections[4].value}" HorizontalAlignment="Stretch" Background="AliceBlue"/>
            <TextBlock Grid.Row="2" Grid.Column="5" Text="{Binding AccountNumber.accountNumberSections[5].value}" HorizontalAlignment="Stretch" Background="AliceBlue"/>
        </Grid>
    </StackPanel>
</Window>

Finally, in the code behind, we need to add an AccountNumber property and make sure it is initialized whenever the account structure is changed. Modify MainWindow.xaml.cs as shown below.

private cAccountStructure _AccountStructure;
public cAccountStructure AccountStructure
{
    get { return _AccountStructure; }
    set { SetProperty(ref _AccountStructure, value);
        AccountNumber = new cAccountNumber(AccountStructure);
    }
}
 
private cAccountNumber _AccountNumber;
public cAccountNumber AccountNumber
{
    get { return _AccountNumber; }
    set { SetProperty(ref _AccountNumber, value); }
}

Run the program and type into the text boxes. You will see that the account number object is being updated. This demonstrates target-to-source binding.


Now select the International account structure. You see the labels and text boxes change and the account number is initialized. This demonstrates source-to-target binding.


If you have ever wondered why the default UpdateSourceTrigger is LostFocus, I can only assume that Microsoft decided the cost of executing their version of NotifyTextChanged, which potentially has to evaluate triggers and execute converters, was high enough to warrant not executing it on every key stroke.






Monday, November 30, 2020

Make Markup Extensions update like Bindings

 If you've ever written a Markup Extension you know that ProvideValue is only called once. So how do you update target values when the source changes later? The {Binding} markup extension can do it and so can we. The trick is to subscribe to the page's PropertyChanged event, detect if the change is relevant, and update the dependency property's value.

In this walkthrough we will write a Markup Extension that will provide a UI for an "account structure" which is a series of text boxes and labels that change depending on whether we are dealing with a "domestic" or "international" account. We could have achieved the same result with simple bindings (with a LOT more XAML) or with some custom controls (which is the correct solution).

Here are a couple of screen prints to show you how it is supposed to work.



The intent is that by merely changing the selected account structure type, we can get labels and text boxes to change properties including visibility.

We will start by implementing a simple Markup Extension that does not respond to changes to the source values.

Start a new C#, Core 3.1, WPF Visual Studio project called AccountStructureExtension. The XAML for the MainWindow contains a combo box and a grid of labels and text boxes.

<Window x:Class="AccountStructureExtension.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:AccountStructureExtension"
        mc:Ignorable="d"
        DataContext="{Binding RelativeSource={RelativeSource Self}}"
        Title="Account Structure" Height="450" Width="800">
    <StackPanel Orientation="Vertical">
        <ComboBox ItemsSource="{Binding AccountStructures}" SelectedItem="{Binding AccountStructure}" DisplayMemberPath="AccountStructureType"/>
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="auto"/>
                <RowDefinition Height="auto"/>
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="auto"/>
                <ColumnDefinition Width="auto"/>
                <ColumnDefinition Width="auto"/>
                <ColumnDefinition Width="auto"/>
                <ColumnDefinition Width="auto"/>
                <ColumnDefinition Width="auto"/>
            </Grid.ColumnDefinitions>
 
            <TextBlock Grid.Row="0" Grid.Column="0" Text="{local:AccountSection 1}" Visibility="{local:AccountSection 1}"/>
            <TextBox Grid.Row="1" Grid.Column="0" MaxLength="{local:AccountSection 1}" Width="{local:AccountSection 1}" Visibility="{local:AccountSection 1}"/>
 
            <TextBlock Grid.Row="0" Grid.Column="1" Text="{local:AccountSection 2}" Visibility="{local:AccountSection 2}"/>
            <TextBox Grid.Row="1" Grid.Column="1" MaxLength="{local:AccountSection 2}" Width="{local:AccountSection 2}" Visibility="{local:AccountSection 2}"/>
 
            <TextBlock Grid.Row="0" Grid.Column="2" Text="{local:AccountSection 3}" Visibility="{local:AccountSection 3}"/>
            <TextBox Grid.Row="1" Grid.Column="2" MaxLength="{local:AccountSection 3}" Width="{local:AccountSection 3}" Visibility="{local:AccountSection 3}"/>
 
            <TextBlock Grid.Row="0" Grid.Column="3" Text="{local:AccountSection 4}" Visibility="{local:AccountSection 4}"/>
            <TextBox Grid.Row="1" Grid.Column="3" MaxLength="{local:AccountSection 4}" Width="{local:AccountSection 4}" Visibility="{local:AccountSection 4}"/>
 
            <TextBlock Grid.Row="0" Grid.Column="4" Text="{local:AccountSection 5}" Visibility="{local:AccountSection 5}"/>
            <TextBox Grid.Row="1" Grid.Column="4" MaxLength="{local:AccountSection 5}" Width="{local:AccountSection 5}" Visibility="{local:AccountSection 5}"/>
 
            <TextBlock Grid.Row="0" Grid.Column="5" Text="{local:AccountSection 6}" Visibility="{local:AccountSection 6}"/>
            <TextBox Grid.Row="1" Grid.Column="5" MaxLength="{local:AccountSection 6}" Width="{local:AccountSection 6}" Visibility="{local:AccountSection 6}"/>
        </Grid>
    </StackPanel>
</Window>

The code behind defines some properties and populates them. It also implements INotifyPropertyChanged.

using AccountSectionExtension;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows;
 
namespace AccountStructureExtension
{
    public partial class MainWindow : WindowINotifyPropertyChanged
    {
        public List<cAccountStructure> AccountStructures { get; } = new List<cAccountStructure>()
                {
                    new cAccountStructure()
                    {
                        AccountStructureType = "Domestic",
                        Sections = new List<cAccountSection>()
                        {
                            new cAccountSection() { Title = "Div", MaxLength = 4 },
                            new cAccountSection() { Title = "FY", MaxLength = 4 },
                            new cAccountSection() { Title = "Dept", MaxLength = 8 },
                            new cAccountSection() { Title = "Dist", MaxLength = 2 }
                        }
                    }
                    ,
                    new cAccountStructure()
                    {
                        AccountStructureType = "International",
                        Sections = new List<cAccountSection>()
                        {
                            new cAccountSection() { Title = "Country", MaxLength = 8 },
                            new cAccountSection() { Title = "FY", MaxLength = 4 },
                            new cAccountSection() { Title = "Region", MaxLength = 6 },
                            new cAccountSection() { Title = "Unit", MaxLength = 3 },
                            new cAccountSection() { Title = "Channel", MaxLength = 6 },
                            new cAccountSection() { Title = "Sub", MaxLength = 2 }
                        }
                    }
                };
 
        private cAccountStructure _AccountStructure;
        public cAccountStructure AccountStructure
        {
            get { return _AccountStructure; }
            set { SetProperty(ref _AccountStructure, value); }
        }
 
        public MainWindow()
        {
            AccountStructure = AccountStructures[0];
            InitializeComponent();
        }
 
        public event PropertyChangedEventHandler PropertyChanged;
        public void SetProperty<T>(ref T storageT value, [CallerMemberNamestring name = "")
        {
            if (!Object.Equals(storagevalue))
            {
                storage = value;
                PropertyChanged?.Invoke(thisnew PropertyChangedEventArgs(name));
            }
        }
    }
}

Add a new class called AccountStructure.

using System;
using System.Collections.Generic;
using System.Text;
 
namespace AccountSectionExtension
{
    public class cAccountSection
    {
        public string Title;
        public int MaxLength;
    }
 
    public class cAccountStructure
    {
        public String AccountStructureType { getset; }
        public int SectionCount { get { return Sections.Count; } }
        public List<cAccountSection> Sections = new List<cAccountSection>();
    }
}

Now we get to the meat of the project. Add a class called AccountStructureExtension. Initially it will implement a simple markup extension that populates the controls but does not respond to changes in the source values.

using AccountSectionExtension;
using System;
using System.ComponentModel;
using System.Reflection;
using System.Windows;
using System.Windows.Markup;
 
namespace AccountStructureExtension
{
    class AccountSectionExtension : System.Windows.Markup.MarkupExtension
    {
        public String AccountStructurePropertyPath { getset; }
        public int SectionIndex { getset; }
 
        public DependencyObject dependencyObject;
        public DependencyProperty dependencyProperty;
 
        public AccountSectionExtension()
        {
            this.AccountStructurePropertyPath = "AccountStructure";
            this.SectionIndex = 1;
        }
 
        public AccountSectionExtension(int SectionIndex)
        {
            this.AccountStructurePropertyPath = "AccountStructure";
            this.SectionIndex = SectionIndex;
        }
 
        public AccountSectionExtension(String AccountStructurePropertyPathint SectionIndex)
        {
            this.AccountStructurePropertyPath = AccountStructurePropertyPath;
            this.SectionIndex = SectionIndex;
        }
 
        public override object ProvideValue(System.IServiceProvider serviceProvider)
        {
            IProvideValueTarget pvt = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;
            dependencyObject = pvt.TargetObject as DependencyObject;
            dependencyProperty  = pvt.TargetProperty as DependencyProperty;
 
            return GetValue();
        }
 
        public object GetValue()
        {
            object dc = (dependencyObject as FrameworkElement).DataContext;
            cAccountStructure accountStructure = dc.GetType().GetProperty(AccountStructurePropertyPath).GetValue(dcnullas cAccountStructure;
            if (accountStructure == nullreturn DependencyProperty.UnsetValue;
            if (SectionIndex < 1) return DependencyProperty.UnsetValue;
 
            switch (dependencyProperty.Name.ToLower())
            {
                case "maxlength"return (SectionIndex > accountStructure.SectionCount) ? DependencyProperty.UnsetValue : accountStructure.Sections[SectionIndex - 1].MaxLength;
                case "text"return (SectionIndex > accountStructure.SectionCount) ? DependencyProperty.UnsetValue : accountStructure.Sections[SectionIndex - 1].Title;
                case "visibility"return (SectionIndex > accountStructure.SectionCount) ? Visibility.Collapsed : Visibility.Visible;
                case "width"return (SectionIndex > accountStructure.SectionCount) ? DependencyProperty.UnsetValue : (double)(4 + accountStructure.Sections[SectionIndex - 1].MaxLength * 8);
                defaultreturn DependencyProperty.UnsetValue;
            }
        } 
    }
}

If you run the project now, you will see the labels and text boxes work correctly at first, but if you select the International account structure, nothing changes.


Well that's not right!
We need to subscribe to the PropertyChanged event of the Data Context (and unsubscribe when we are destroyed). Then we need to check the name of the property that was changed and, if it is the Account Structure we are bound to, force an update. This all happens in AccountSectionExtension.

Add the following code after the constructors.
~AccountSectionExtension()
{
    RemoveEventHandler();
}
Modify ProvideValue to call AddEventHandler.
public override object ProvideValue(System.IServiceProvider serviceProvider)
{
    IProvideValueTarget pvt = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;
    dependencyObject = pvt.TargetObject as DependencyObject;
    dependencyProperty  = pvt.TargetProperty as DependencyProperty;
    AddEventHandler();
 
    return GetValue();
}

Add the two new methods we just referenced.

public void AddEventHandler()
{
    object dc = (dependencyObject as FrameworkElement).DataContext;
    EventInfo npc = dc.GetType().GetEvent("PropertyChanged");
    MethodInfo mi = this.GetType().GetMethod("NotifyAccountStructureChanged");
    Delegate d = Delegate.CreateDelegate(npc.EventHandlerType, thismi);
    npc.AddEventHandler(dcd);
}
 
public void RemoveEventHandler()
{
    object dc = (dependencyObject as FrameworkElement).DataContext;
    EventInfo npc = dc.GetType().GetEvent("PropertyChanged");
    MethodInfo mi = this.GetType().GetMethod("NotifyAccountStructureChanged");
    Delegate d = Delegate.CreateDelegate(npc.EventHandlerType, thismi);
    npc.RemoveEventHandler(dcd);
}

And lastly, add the method that forces the update.

public void NotifyAccountStructureChanged(Object senderPropertyChangedEventArgs e)
{
    if (e.PropertyName == AccountStructurePropertyPath)
    {
        Action updateAction = () => dependencyObject.SetValue(dependencyProperty, GetValue());
        if (dependencyObject.CheckAccess())
            updateAction();
        else
            dependencyObject.Dispatcher.Invoke(updateAction);
    }
}
Yay, it works!