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.






No comments:

Post a Comment