Monday, April 26, 2021

Binding a multi-band XamDataGrid to a data set

I prefer to bind my grids to collections because it gives me strongly named properties and Intellisense lets me know when I have fat-fingered a property name. But when editing data we need to track row state so we can surgically persist the data back to the database. It's hard to improve on the DataRow.RowState so I'm not even going to try. But how do we handle child rows? 

I like the Infragistics solution - it's elegant. Define a data relationship and reference that in the FieldLayout. Like this...

<igDP:XamDataGrid DataSource="{Binding DS.Tables[Vendors]}">
    <igDP:XamDataGrid.FieldLayouts>
        <igDP:FieldLayout Description="Vendors" Key="Vendors">
            <igDP:FieldLayout.Fields>
                <igDP:TextField Label="Vendor Number" Name="VendorNumber" Width="80"/>
                <igDP:TextField Label="Detail" Name="VendorDetailNumber" Width="40"/>
                <igDP:TextField Label="Vendor Name" Name="Name" Width="200"/>
                <igDP:Field Label="" Name="Vendor_Contacts"/>
            </igDP:FieldLayout.Fields>
        </igDP:FieldLayout>
        <igDP:FieldLayout Description="Contacts" ParentFieldLayoutKey="Vendors" ParentFieldName="Vendor_Contacts">
            <igDP:FieldLayout.Fields>
                <igDP:TextField Label="Name" Name="Name" Width="150"/>
                <igDP:TextField Label="Title" Name="Title" Width="100"/>
                <igDP:TextField Label="Phone" Name="Phone1"/>
            </igDP:FieldLayout.Fields>
        </igDP:FieldLayout>
    </igDP:XamDataGrid.FieldLayouts>
</igDP:XamDataGrid>
 

See how the DataRelation is named in the Parent Field definition and referenced in the child's FieldLayout. This is very similar to how we bind child bands when binding to collections. the result is exactly what you would expect.



Friday, February 5, 2021

Multibinding in a XamDataGrid

 A colleague of mine has a need to display a field in an Infragistics XamDataGrid that concatenates two other fields. He is binding to an ObservableCollection and wants the know the 'best' way to do this. I came up with three solutions.

1. Add a readonly property to the collection.
2. Write a multi-value converter and persuade the XamDataGrid to use it
3. Use a template field and a stackpanel

As usual, everything that isn't easy with an Infragistics control is very difficult.

Let's start by looking at solution 1. All I did was add a read-only property to the collection so that it looks like this.

    public class cResult
    {
        public  string Warehouse { get; set; }
        public string StockNumber { get; set; }
        public string WarehouseStock
        {
            get { return string.Format("{0}-{1}", Warehouse, StockNumber); }
        }
    }

And here is the XAML.

<igDP:TextField Label="Warehouse/Stock #" Name="WarehouseStock" Width="auto" />

But it seems much more fun to me to write a StringFormat multivalue converter and use that. Well, it would be if it wasn't such a pain to persuade the XamDataGrid to use it.

Here is the converter...

    public class StringFormatConverter : IMultiValueConverter
    {
        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
        {
            return string.Format(parameter.ToString(), values);
        }
        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }

And there is the XAML, quite a mouthful, isn't it?

<igDP:TextField Label="Warehouse/Stock #" BindingType="Unbound" EditAsType="sys:String" Width="auto">
    <igDP:TextField.Settings>
        <igDP:FieldSettings>
            <igDP:FieldSettings.EditorStyle>
                <Style TargetType="igEditors:XamTextEditor">
                    <Setter Property="Value">
                        <Setter.Value>
                            <MultiBinding Converter="{StaticResource StringFormatConverter}" ConverterParameter='{}{0}-{1}'>
                                <Binding Path="DataItem.Warehouse"/>
                                <Binding Path="DataItem.StockNumber"/>
                            </MultiBinding>
                        </Setter.Value>
                    </Setter>
                </Style>
            </igDP:FieldSettings.EditorStyle>
        </igDP:FieldSettings>
    </igDP:TextField.Settings>
</igDP:TextField>

Option 3 (template field) might be the most flexible if you don't need complex formatting . It certainly doesn't require as much XAML. Note the use of AlternativeBinding - that was the tricky part to figure out. Also note the use of TextBlock which has a smaller memory and CPU footprint than Label.

<igDP:TemplateField Label="Warehouse/Stock #" BindingType="UseAlternateBinding" AlternateBinding="{Binding}" Width="auto">
    <igDP:TemplateField.DisplayTemplate>
        <DataTemplate>
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="{Binding Warehouse}"/>
                <TextBlock Text="-"/>
                <TextBlock Text="{Binding StockNumber}"/>
            </StackPanel>
        </DataTemplate>
    </igDP:TemplateField.DisplayTemplate>
</igDP:TemplateField>

Here is a full example showing the two techniques side-by-side.

<Window x:Class="XamDataGridMultiBind.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:XamDataGridMultiBind"
        xmlns:sys="clr-namespace:System;assembly=mscorlib"
        xmlns:igDP="http://infragistics.com/DataPresenter"
        xmlns:ig="http://schemas.infragistics.com/xaml"
        xmlns:igEditors="http://infragistics.com/Editors"
        mc:Ignorable="d"
        DataContext="{Binding RelativeSource={RelativeSource Self}}"
        Title="MainWindow" Height="450" Width="800">
    <Window.Resources>
        <local:StringFormatConverter x:Key="StringFormatConverter" />
    </Window.Resources>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <igDP:XamDataGrid Theme="LunaSilver" DataSource="{Binding Results}" Grid.Column="0" GroupByAreaLocation="None">
            <igDP:XamDataGrid.FieldLayoutSettings>
                <igDP:FieldLayoutSettings SelectionTypeRecord="Single" AutoGenerateFields="False" AllowFieldMoving="No"/>
            </igDP:XamDataGrid.FieldLayoutSettings>
            <igDP:XamDataGrid.FieldSettings>
                <igDP:FieldSettings AllowSummaries="False" SummaryUIType="SingleSelect" LabelTextAlignment="Center" AllowEdit="False"/>
            </igDP:XamDataGrid.FieldSettings>
            <igDP:XamDataGrid.FieldLayouts>
                <igDP:FieldLayout Description="Results">
                    <igDP:FieldLayout.Fields>
                        <igDP:TextField Label="Warehouse" Name="Warehouse" Width="auto"/>
                        <igDP:TextField Label="Stock #" Name="StockNumber" Width="auto"/>
                        <igDP:TextField Label="Warehouse/Stock #" BindingType="Unbound" EditAsType="sys:String" Width="auto">
                            <igDP:TextField.Settings>
                                <igDP:FieldSettings>
                                    <igDP:FieldSettings.EditorStyle>
                                        <Style TargetType="igEditors:XamTextEditor">
                                            <Setter Property="Value">
                                                <Setter.Value>
                                                    <MultiBinding Converter="{StaticResource StringFormatConverter}" ConverterParameter='{}{0}-{1}'>
                                                        <Binding Path="DataItem.Warehouse"/>
                                                        <Binding Path="DataItem.StockNumber"/>
                                                    </MultiBinding>
                                                </Setter.Value>
                                            </Setter>
                                        </Style>
                                    </igDP:FieldSettings.EditorStyle>
                                </igDP:FieldSettings>
                            </igDP:TextField.Settings>
                        </igDP:TextField>
                    </igDP:FieldLayout.Fields>
                </igDP:FieldLayout>
            </igDP:XamDataGrid.FieldLayouts>
        </igDP:XamDataGrid>

        <igDP:XamDataGrid Theme="LunaSilver" DataSource="{Binding Results}" Grid.Column="1" GroupByAreaLocation="None">
            <igDP:XamDataGrid.FieldLayoutSettings>
                <igDP:FieldLayoutSettings SelectionTypeRecord="Single" AutoGenerateFields="False" AllowFieldMoving="No"/>
            </igDP:XamDataGrid.FieldLayoutSettings>
            <igDP:XamDataGrid.FieldSettings>
                <igDP:FieldSettings AllowSummaries="False" SummaryUIType="SingleSelect" LabelTextAlignment="Center" AllowEdit="False"/>
            </igDP:XamDataGrid.FieldSettings>
            <igDP:XamDataGrid.FieldLayouts>
                <igDP:FieldLayout Description="Results">
                    <igDP:FieldLayout.Fields>
                        <igDP:TextField Label="Warehouse" Name="Warehouse" Width="auto"/>
                        <igDP:TextField Label="Stock #" Name="StockNumber" Width="auto"/>
                        <igDP:TextField Label="Warehouse/Stock #" Name="WarehouseStock" Width="auto" />
                    </igDP:FieldLayout.Fields>
                </igDP:FieldLayout>
            </igDP:XamDataGrid.FieldLayouts>
        </igDP:XamDataGrid>

        <igDP:XamDataGrid Theme="LunaSilver" DataSource="{Binding Results}" Grid.Column="2" GroupByAreaLocation="None">
            <igDP:XamDataGrid.FieldLayoutSettings>
                <igDP:FieldLayoutSettings SelectionTypeRecord="Single" AutoGenerateFields="False" AllowFieldMoving="No"/>
            </igDP:XamDataGrid.FieldLayoutSettings>
            <igDP:XamDataGrid.FieldSettings>
                <igDP:FieldSettings AllowSummaries="False" SummaryUIType="SingleSelect" LabelTextAlignment="Center" AllowEdit="False"/>
            </igDP:XamDataGrid.FieldSettings>
            <igDP:XamDataGrid.FieldLayouts>
                <igDP:FieldLayout Description="Results">
                    <igDP:FieldLayout.Fields>
                        <igDP:TextField Label="Warehouse" Name="Warehouse" Width="auto"/>
                        <igDP:TextField Label="Stock #" Name="StockNumber" Width="auto"/>
                        <igDP:TemplateField Label="Warehouse/Stock #" BindingType="UseAlternateBinding" AlternateBinding="{Binding}" Width="auto">
                            <igDP:TemplateField.DisplayTemplate>
                                <DataTemplate>
                                    <StackPanel Orientation="Horizontal">
                                        <TextBlock Text="{Binding Warehouse}"/>
                                        <TextBlock Text="-"/>
                                        <TextBlock Text="{Binding StockNumber}"/>
                                    </StackPanel>
                                </DataTemplate>
                            </igDP:TemplateField.DisplayTemplate>
                        </igDP:TemplateField>
                    </igDP:FieldLayout.Fields>
                </igDP:FieldLayout>
            </igDP:XamDataGrid.FieldLayouts>
        </igDP:XamDataGrid>
    </Grid>
</Window>


--------------------------------------------------------------------------------------

using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Data;
namespace XamDataGridMultiBind
{
    public class cResult
    {
        public  string Warehouse { get; set; }
        public string StockNumber { get; set; }
        public string WarehouseStock
        {
            get { return string.Format("{0}-{1}", Warehouse, StockNumber); }
        }
    }
    public partial class MainWindow : Window, INotifyPropertyChanged
    {
        private ObservableCollection<cResult> _Results = new ObservableCollection<cResult>()  { 
            new cResult() { Warehouse = "Warehouse 1", StockNumber = "Stock Number 1" }, 
            new cResult() { Warehouse = "Warehouse 2", StockNumber = "Stock Number 2" } 
        };
        public ObservableCollection<cResult> Results
        {
            get { return _Results; }
            set { SetProperty(ref _Results, value); }
        }
        public MainWindow()
        {
            InitializeComponent();
        }
        public event PropertyChangedEventHandler PropertyChanged;
        public void SetProperty<T>(ref T storage, T value, [CallerMemberName] string name = "")
        {
            if (!Object.Equals(storage, value))
            {
                storage = value;
                if (PropertyChanged != null)
                    PropertyChanged(this, new PropertyChangedEventArgs(name));
            }
        }
    }
    public class StringFormatConverter : IMultiValueConverter
    {
        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
        {
            return string.Format(parameter.ToString(), values);
        }
        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
}



Friday, January 15, 2021

DependencyProperty as List of unknown type

I wanted to improve an earlier ListBox control that exposed the SelectedItems as a dependency property. The previous attempt had assumed the SelectedItems would be bound to a List<String> but Microsoft's ItemSource property is an IEnumerable so I thought I should be able to make my DependencyProperty an IList, but it's more complicated than I expected.

You have to cast the collection to a List<T> where T is the List member class the DependencyProperty is bound to. To do this you have to capture T and you have to cast List<object> to List<T> in the DependencyProperty setter.

In addition I want to tweak the ListBox ItemTemplate to insert a check box in each row, making it easier for users to toggle the row selected states. I decided to use the FrameworkElementFactory even though it is not recommended. It still does everything I need and the alternative of creating XAML in code and parsing it is a total bodge.

Start with a new User Control project in Visual Studio 2019, C#, .Net Core. Call it MultiSelectListBox.

The code will start by defining the default style, the SelectedItemsList DependencyProperty, its getter and setter, and somewhere to store the element type the DependencyProperty is bound to.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media;
 
namespace MultiSelectListbox
{
    public class MultiSelectListbox : ListBox
    {
        private Type boundType;
 
        static MultiSelectListbox()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(MultiSelectListbox), new FrameworkPropertyMetadata(typeof(ListBox)));
        }
 
        public static readonly DependencyProperty SelectedItemsListProperty = DependencyProperty.Register("SelectedItemsList", typeof(IList), typeof(MultiSelectListbox));
 
        public static IList GetSelectedItemsList(DependencyObject obj)
        {
            return obj.GetValue(SelectedItemsListProperty) as IList;
        }
 
        public static void SetSelectedItemsList(DependencyObject obj, IList value)
        {
            Type boundType = (obj as MultiSelectListbox).boundType;
            Type genericType = typeof(List<>);
            Type returnType = genericType.MakeGenericType(boundType);
 
            IList castValue = System.Activator.CreateInstance(returnType) as IList;
            foreach (object o in value)
                castValue.Add(Convert.ChangeType(o, boundType));
 
            obj.SetValue(SelectedItemsListProperty, castValue);
        }
 
    }
}

The setter contains the first interesting bit of code. We have to convert value from IList to List<boundType>. We will populate boundType from the binding source during the Loaded event. By doing this, we ensure the source can be updated. If we don't do this, the source will always be null.

Next we write the constructor. It simply assigns some event handlers.

public MultiSelectListbox() 
{
    this.Loaded += MultiSelectListbox_Loaded;
    this.SelectionChanged += MultiSelectListbox_SelectionChanged;
}
 

Whever the list boxes SelectedItems collection changes we need to make the same changes to our DependencyProperty. We cannot simply assign the new SelectedItems collection to our DependencyProperty because that would break the binding. For each item that was added we add it to our DependencyProperty and check the associated check box. For each item that was removed we remove it from our DependencyProperty and uncheck the associated check box. Once we have our new Selected collection we call SetSelectedItemsList to convert it and SetValue.

private static void MultiSelectListbox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    MultiSelectListbox lb = sender as MultiSelectListbox;
    IList SelectedItemsList = GetSelectedItemsList(lb);
    if (lb.SelectedItems == null) return;
    if (SelectedItemsList == null) SelectedItemsList = new List<object>();
 
    foreach (object addedItem in e.AddedItems)
    {
        SelectedItemsList.Add(addedItem);
        ListBoxItem lbi = lb.ItemContainerGenerator.ContainerFromItem(addedItem) as ListBoxItem;
        FindChildrenOfType<CheckBox>(lbi as DependencyObject).First().IsChecked = true;
    }
 
    foreach (object removedItem in e.RemovedItems)
    {
        SelectedItemsList.Remove(removedItem);
        ListBoxItem lbi = lb.ItemContainerGenerator.ContainerFromItem(removedItem) as ListBoxItem;
        FindChildrenOfType<CheckBox>(lbi as DependencyObject).First().IsChecked = false;
    }
 
    SetSelectedItemsList(lb, SelectedItemsList); 

}

The Loaded event handler is mainly concerned with replacing the DisplayMemberPath with a new ItemTemplate which is built in code. It uses the FrameworkElementFactory which works well enough for our purposes. Note how we start by using reflection to get the binding's generic argument type and store it in boundType.

private static void MultiSelectListbox_Loaded(object sender, RoutedEventArgs e)
{
    MultiSelectListbox lb = sender as MultiSelectListbox;
    BindingExpression be = lb.GetBindingExpression(SelectedItemsListProperty);
    FrameworkElement rs = be.ResolvedSource as FrameworkElement;
    PropertyInfo lpi = rs.DataContext.GetType().GetProperty(be.ResolvedSourcePropertyName);
    lb.boundType = lpi.PropertyType.GetGenericArguments().FirstOrDefault();
 
    DataTemplate dt = new DataTemplate(typeof(MultiSelectListbox));
    FrameworkElementFactory fef = new FrameworkElementFactory(typeof(StackPanel)) { Name = "ItemTemplate" };
    fef.SetValue(StackPanel.OrientationProperty, Orientation.Horizontal);
 
    FrameworkElementFactory cb = new FrameworkElementFactory(typeof(CheckBox));
    cb.AddHandler(CheckBox.CheckedEvent, new RoutedEventHandler(CheckedChanged));
    cb.AddHandler(CheckBox.UncheckedEvent, new RoutedEventHandler(CheckedChanged));
    fef.AppendChild(cb);
 
    FrameworkElementFactory tb = new FrameworkElementFactory(typeof(TextBlock));
    tb.SetBinding(TextBlock.TextProperty, new Binding(lb.DisplayMemberPath));
    fef.AppendChild(tb);
 
    dt.VisualTree = fef;
 
    lb.DisplayMemberPath = null;
    lb.ItemTemplate = dt;
 
    if (be.ParentBinding.Mode != BindingMode.TwoWay)
        throw new NotSupportedException("SelectedItemsList Binding Mode must be TwoWay");
}

When a check box is checked or unchecked we need to update the IsSelected property of its parent row which will call MultiSelectListbox_SelectionChanged and update the DependencyProperty.

private static void CheckedChanged(object sender, RoutedEventArgs e)
{
    CheckBox cb = sender as CheckBox;
    ListBoxItem lbi = GetParentOfType<ListBoxItem>(cb);
    lbi.IsSelected = cb.IsChecked.Value; 

}

Between them MultiSelectListbox_SelectionChanged and CheckedChanged keep the check boxes and list box items synchronized.

We also need a couple of VisualTree helper methods that you probably already have somewhere.

private static T GetParentOfType<T>(DependencyObject control) where T : System.Windows.DependencyObject
{
    DependencyObject ParentControl = control;
 
    do
        ParentControl = VisualTreeHelper.GetParent(ParentControl);
    while (ParentControl != null && !(ParentControl is T));
 
    return ParentControl as T;
}
 
private static List<T> FindChildrenOfType<T>(DependencyObject depObj) where T : DependencyObject
{
    List<T> Children = new List<T>();
    if (depObj != null)
    {
        for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
        {
            DependencyObject child = VisualTreeHelper.GetChild(depObj, i);
            if (child != null && child is T)
                Children.Add(child as T);
            Children.AddRange(FindChildrenOfType<T>(child));
        }
    }
    return Children;
}

Let's see how we would consume our new ListBox. Add a WPF project called MultiSelectListBoxTest and make it the startup project.

The XAML is very simple.

<local:BaseWindow x:Class="MultiSelectListboxTest.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:custom="clr-namespace:MultiSelectListbox;assembly=MultiSelectListbox"
        xmlns:local="clr-namespace:MultiSelectListboxTest"
        mc:Ignorable="d"
        DataContext="{Binding RelativeSource={RelativeSource Self}}"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="auto"/>
        </Grid.RowDefinitions>
        <custom:MultiSelectListbox Grid.Row="0" ItemsSource="{Binding Selectables}" SelectedItemsList="{Binding Selecteds, Mode=TwoWay}" DisplayMemberPath="Name" SelectionMode="Extended"/>
        <TextBlock Grid.Row="1" Text="{Binding SelectedsString}"/>
    </Grid>
</local:BaseWindow>

The code behind is mainly concerned with creating a Selectables and Selecteds property with type List<cSelectableItem>. When Selecteds is changed we concatenate the names and display them so you can see how the SelectedItemsList DependencyProperty works.

using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
 
namespace MultiSelectListboxTest
{
    public partial class MainWindow : BaseWindow
    {
        public class cSelectableItem : BaseClass
        {
            public string Name { get; set; }
        }
 
        private List<cSelectableItem> _Selectables;
        public List<cSelectableItem> Selectables
        {
            get { return _Selectables; }
            set { SetProperty(ref _Selectables, value); }
        }
 
        private List<cSelectableItem> _Selecteds;
        public List<cSelectableItem> Selecteds
        {
            get { return _Selecteds; }
            set
            {
                SetProperty(ref _Selecteds, value);
                SelectedsString = string.Join(",", Selecteds.Select(s => s.Name));
            }
        }
 
        private string _SelectedsString;
        public string SelectedsString
        {
            get { return _SelectedsString; }
            set { SetProperty(ref _SelectedsString, value); }
        }
 
        public MainWindow()
        {
            Selectables = new List<cSelectableItem>() { new cSelectableItem() { Name = "Alpha" }, new cSelectableItem() { Name = "Beta" }, new cSelectableItem() { Name = "Gamma" } };
            InitializeComponent();
        }
    }
}




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 storage, T value, [CallerMemberName] string name = "")
        {
            if (!Object.Equals(storage, value))
            {
                storage = value;
                PropertyChanged?.Invoke(this, new 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 { get; set; }
        public List<cAccountNumberSection> accountNumberSections { get; set; }
 
        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 { get; set; }
        public int SectionIndex { get; set; }
 
        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, this, mi);
            npc.AddEventHandler(dc, d);
        }
 
        public void AddTextChangedEventHandler()
        {
            EventInfo tc = dependencyObject.GetType().GetEvent("TextChanged");
            MethodInfo mi = this.GetType().GetMethod("NotifyTextChanged");
            if (tc != null)
            {
                Delegate d = Delegate.CreateDelegate(tc.EventHandlerType, this, mi);
                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, this, mi);
            npc.RemoveEventHandler(dc, d);
        }
 
        public void RemoveTextChangedEventHandler()
        {
            EventInfo tc = dependencyObject.GetType().GetEvent("TextChanged");
            MethodInfo mi = this.GetType().GetMethod("NotifyTextChanged");
            if (tc != null)
            {
                Delegate d = Delegate.CreateDelegate(tc.EventHandlerType, this, mi);
                tc.RemoveEventHandler(dependencyObject, d);
            }
        }
 
        public object GetValue()
        {
            object dc = (dependencyObject as FrameworkElement).DataContext;
            if (dc == null) return DependencyProperty.UnsetValue;
            cAccountNumber accountNumber = dc.GetType().GetProperty(AccountNumberPropertyPath).GetValue(dc, null) as cAccountNumber;
            if (accountNumber == null) return DependencyProperty.UnsetValue;
            cAccountStructure accountStructure = accountNumber.accountStructure;
            if (accountStructure == null) return 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;
                default: return DependencyProperty.UnsetValue;
            }
        }
 
        public void NotifyPropertyChanged(Object sender, PropertyChangedEventArgs e)
        {
            if (e.PropertyName == AccountNumberPropertyPath)
            {
                Action updateAction = () => dependencyObject.SetValue(dependencyProperty, GetValue());
                if (dependencyObject.CheckAccess())
                    updateAction();
                else
                    dependencyObject.Dispatcher.Invoke(updateAction);
            }
        }
 
        public void NotifyTextChanged(object sender, TextChangedEventArgs e)
        {
            TextBox tb = (dependencyObject as TextBox);
            object dc = tb.DataContext;
            (dc.GetType().GetProperty(AccountNumberPropertyPath).GetValue(dc) as 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.