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!

Thursday, November 19, 2020

DataGrid captures mousewheel and stops page from scrolling

One of the frustrating things about WPF is when you have a scrollable data grid inside a scrolling page. If you mouse wheel the page up or down it stops working as soon as the data grid scrolls under the cursor. The data grid's scroll viewer is receiving the mouse wheel events and, even when the data grid can't scroll, it does not pass them up to the page so the page scrolls no more.

Fortunately there is a fairly simple solution. Let's start by demonstrating the problem. Start a new C#, .Net Core Visual Studio project called IntelligentScrolling.

We will start by writing a base WPF class because we're good little programmers not like we used to be. Add a class called BaseWPFWindow and populate it thusly. It contains the boilerplate code for handling PropertyChanged.

using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
 
namespace IntelligentScrolling
{
    public class BaseWPFWindow:WindowINotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        protected void SetProperty<T>(ref T storageT value, [CallerMemberNamestring name = "")
        {
            if (!Object.Equals(storagevalue))
            {
                storage = value;
                if (PropertyChanged != null)
                    PropertyChanged(thisnew PropertyChangedEventArgs(name));
            }
        }
    }
}

The XAML looks like this.

<local:BaseWPFWindow x:Class="IntelligentScrolling.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:IntelligentScrolling"
        mc:Ignorable="d"
        DataContext="{Binding RelativeSource={RelativeSource Self}}"
        Title="Intelligent Scrolling" Height="450" Width="800" MaxHeight="450">
    <ScrollViewer>
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="300"/>
                <RowDefinition Height="*"/>
            </Grid.RowDefinitions>
            <TextBlock Grid.Row="0" Text="Header Area" FontSize="100" HorizontalAlignment="Center" VerticalAlignment="Center"/>
            <DataGrid Grid.Row="1" ItemsSource="{Binding Items}" Height="300" IsReadOnly="True"/>
        </Grid>
    </ScrollViewer>
</local:BaseWPFWindow>

and the code behind looks like this...

using System.Collections.ObjectModel;
 
namespace IntelligentScrolling
{
    public partial class MainWindow :  BaseWPFWindow
    {
        public class cItem
        {
            public string Code { getset; }
            public string Description { getset; }
        }
 
        private Collection<cItem> _Items;
        public Collection<cItem> Items
        {
            get { return _Items; }
            set { SetProperty(ref _Items, value); }
        }
 
        public MainWindow()
        {
            Items = PopulateItems();
            InitializeComponent();
        }
 
        public Collection<cItemPopulateItems()
        {
            Collection<cItemItems = new Collection<cItem>();
            for (int i = 0; i < 30; i++)
            {
                Items.Add(new cItem() { Code = "Code" + i, Description = "Description" + i });
            }
            return Items;
        }
    }
}

If you run this you will see a static label at the top with a data grid below. The whole page is in a scroll viewer. If you place the cursor as shown below and start scrolling with the mouse wheel you will see that as soon as the data grid moves under the cursor the page can no longer be scrolled. We want to change this behavior so that if the data grid cannot be scrolled in the desired direction the page scrolls instead.


We are going to capture the data grid's PreviewMouseWheel event. If the data grid cannot scroll any further in the desired direction we will scroll the containing scroll viewer instead.

Add the following code to the base class. All the interesting code is in the IntelligentScroll method. The other two methods should already be somewhere in your WPF library. All this code is written generically so it only has to exist in your base class (you DO have a base class, right?).

private void IntelligentScroll(object senderMouseWheelEventArgs e)
{
    if (!(sender is DataGrid)) throw new NotImplementedException("IntelligentScroll is only implemented for DataGrids");
 
    // Is the datagrid at the limit for the direction of scroll?
    ScrollViewer svDataGrid = GetFirstChildOfType<ScrollViewer>(sender as DependencyObject);
    bool IsAtLimit = false;
 
    if (svDataGrid == nullreturn;
    if (e.Delta > 0 && svDataGrid.VerticalOffset == 0) IsAtLimit = true;
    if (e.Delta < 0 && svDataGrid.VerticalOffset == svDataGrid.ScrollableHeight) IsAtLimit = true;
 
    if (IsAtLimit)
    {
        ScrollViewer svParent = GetParentOfType<ScrollViewer>(svDataGridas ScrollViewer;
        if (svParent != nullsvParent.ScrollToVerticalOffset(svParent.VerticalOffset - e.Delta);
    }
}
 
public T GetFirstChildOfType<T>(System.Windows.DependencyObject propwhere T : System.Windows.DependencyObject
{
    for (int i = 0; i <= System.Windows.Media.VisualTreeHelper.GetChildrenCount(prop) - 1; i++)
    {
        System.Windows.DependencyObject child = System.Windows.Media.VisualTreeHelper.GetChild((prop), ias System.Windows.DependencyObject;
        if (child == null)
            continue;
 
        T castedProp = child as T;
        if (castedProp != null)
            return castedProp;
 
        castedProp = GetFirstChildOfType<T>(child);
 
        if (castedProp != null)
            return castedProp;
    }
    return null;
}
 
private T GetParentOfType<T>(DependencyObject controlwhere T:System.Windows.DependencyObject
{
    DependencyObject ParentControl = control;
 
     do
        ParentControl = VisualTreeHelper.GetParent(ParentControl);
    while (ParentControl != null && !(ParentControl is T));
 
    return ParentControl as T;
}

It is interesting to note that the scroll delta is positive when scrolling up and negative when scrolling down.

All we do now is add a PreviewMouseWheel event handler for the data grid.

<DataGrid Grid.Row="1" ItemsSource="{Binding Items}" Height="300" IsReadOnly="True" PreviewMouseWheel="IntelligentScroll"/>

If you run the application now you will see that you can mouse wheel the page until the data grid moves under the cursor. If you continue mouse wheeling the data grid will start to scroll until it cannot scroll anymore and then the page will start scrolling again. Scroll the other way too. This is far more convenient for the users.



Friday, November 6, 2020

Binding problem caused by SetProperty

 A popular implementation of the PropertyChanged event looks like this

public event PropertyChangedEventHandler PropertyChanged;
public void SetProperty<T>(ref T storageT value, [CallerMemberNamestring name = "")
{
    if (!Object.Equals(storagevalue))
    {
        storage = value;
        if (PropertyChanged != null)
            PropertyChanged(thisnew PropertyChangedEventArgs(name));
    }
}
 
If the value has changed, store it and notify the framework. You see it in many places on the Internet and I have copied and used it many times. But it causes a problem if you bind a Combobox.SelectedValue to an integer. Allow me to elucidate.

Start a new Visual Studio C#, WPF application. I used .Net Core. Call it "BindingToDataTable" (slightly wrong because it's binding to an integer that is the problem).

The XAML defines three combo boxes that are bound to the same data table but in different ways and a button that repopulates the data table.

<Window x:Class="BindingToDataTable.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:BindingToDataTable"
        mc:Ignorable="d"
        DataContext="{Binding RelativeSource={RelativeSource Self}}"
        Title="MainWindow" Height="450" Width="800">
    <Window.Resources>
        <RoutedCommand x:Key="RepopulateCommand"/>
    </Window.Resources>
    <Window.CommandBindings>
        <CommandBinding Command="{StaticResource RepopulateCommand}" Executed="RepopulateCommand_Executed"/>
    </Window.CommandBindings>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="20"/>
            <RowDefinition Height="20"/>
            <RowDefinition Height="20"/>
            <RowDefinition Height="20"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="200"/>
            <ColumnDefinition Width="200"/>
        </Grid.ColumnDefinitions>
        <ComboBox Grid.Row="0" Grid.Column="0" ItemsSource="{Binding Names}" SelectedValuePath="ID" SelectedValue="{Binding SelectedNameID}" DisplayMemberPath="Name"/>
        <TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding SelectedNameID}"/>
        <ComboBox Grid.Row="1" Grid.Column="0" ItemsSource="{Binding Names}" SelectedValuePath="ID" SelectedValue="{Binding SelectedNameIDString}" DisplayMemberPath="Name"/>
        <TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding SelectedNameIDString}"/>
        <ComboBox Grid.Row="2" Grid.Column="0" ItemsSource="{Binding Names}" SelectedItem="{Binding SelectedName}" DisplayMemberPath="Name"/>
        <TextBlock Grid.Row="2" Grid.Column="1" Text="{Binding SelectedName[ID]}"/>
        <Button Grid.Row="3" Grid.Column="0" Content="Repopulate" Command="{StaticResource RepopulateCommand}" Height="20" Width="200"/>
    </Grid>
</Window>

The code behind contains code to populate the data table and some properties.

using System;
using System.ComponentModel;
using System.Data;
using System.Runtime.CompilerServices;
using System.Windows;
namespace BindingToDataTable
{
    public partial class MainWindow : WindowINotifyPropertyChanged
    {
        private DataTable _Names = null;
        public DataTable Names
        {
            get { return _Names; }
            set { SetProperty(ref _Names, value); }
        }
        private int _SelectedNameID;
        public int SelectedNameID
        {
            get { return _SelectedNameID; }
            set { SetProperty(ref _SelectedNameID, value); }
        }
        private String _SelectedNameIDString;
        public String SelectedNameIDString
        {
            get { return _SelectedNameIDString; }
            set { SetProperty(ref _SelectedNameIDString, value); }
        }
        private DataRowView _SelectedName;
        public DataRowView SelectedName
        {
            get { return _SelectedName; }
            set { SetProperty(ref _SelectedName, value); }
        }
        public event PropertyChangedEventHandler PropertyChanged;
        public void SetProperty<T>(ref T storageT value, [CallerMemberNamestring name = "")
        {
            if (!Object.Equals(storagevalue))
            {
                storage = value;
                if (PropertyChanged != null)
                    PropertyChanged(thisnew PropertyChangedEventArgs(name));
            }
        }
        public MainWindow()
        {
            InitializeComponent();
            InitializeNames();
        }
        private void InitializeNames()
        {
            Names = CreateNamesTable();
            SelectedNameID = (int)Names.Rows[0]["ID"];
            SelectedNameIDString = Names.Rows[0]["ID"].ToString();
            SelectedName = Names.DefaultView[0];
        }
        private DataTable CreateNamesTable()
        {
            DataTable n = new DataTable();
            n.Columns.Add("ID"Type.GetType("System.Int32"));
            n.Columns.Add("Name"Type.GetType("System.String"));
            n.Rows.Add(0, "Anne");
            n.Rows.Add(1, "Bob");
            n.Rows.Add(2, "Stephan");
            return n;
        }
        private void RepopulateCommand_Executed(object sender, System.Windows.Input.ExecutedRoutedEventArgs e)
        {
            InitializeNames();
        }
    }
}

When you first run the program everything looks fine. Don't play with the combo boxes yet.


Now click the [Repopulate] button. Oh, dear!

You can see the first combo box did not set it's selected item correctly and has a binding error as indicated by the red border. The other two combo boxes repopulated correctly. Let's try an experiment. Start the program again, select a different name in the first combo box, and click [Repopulate] again. It worked correctly. That's a clue.



The first combo box binds SelectedValue to an integer.
The second combo box binds SelectedValue to a string
The third combo box binds SelectedItem to a DataRowView.

Here's what is happening - when you repopulate the Item Source, the SelectedValue is changed in curious ways. Strings and objects become null but integers are unchanged possibly because they can't hold a null value. When we populate SelectedNameID we set it to zero, SelectedNameString is set to "0", and SelectedName is set to a DataRowView. In each case, SetProperty is called. But the value of SelectedNameID didn't change so PropertyChanged is not called. That's the problem.

There are several ways to fix this problem.
  • Always call PropertyChanged from SetProperty. Could be side-effects.
    public event PropertyChangedEventHandler PropertyChanged;
    public void SetProperty<T>(ref T storageT value, [CallerMemberNamestring name = "")
    {
        storage = value;
        if (PropertyChanged != null)
            PropertyChanged(thisnew PropertyChangedEventArgs(name));
    }
    
  • Bind SelectedValue to a string (combo box 2). But if it's an integer, it should be stored as an integer.
  • Bind SelectedItem instead of SelectedValue (combo box 3 - my preferred solution)
  • Bind to a nullable integer (my second favorite solution)
    private int? _SelectedNameID;
    public int? SelectedNameID
    {
        get { return _SelectedNameID; }
        set { SetProperty(ref _SelectedNameID, value); }
    }
    
  • Set the bound integer to a non-zero value (-1) before repopulating the data table in InitializeNames. Guaranteed WTF during code review.
    private void InitializeNames()
    {
        SelectedNameID = -1;
        Names = CreateNamesTable();
        SelectedNameID = (int)Names.Rows[0]["ID"];
        SelectedNameIDString = Names.Rows[0]["ID"].ToString();
        SelectedName = Names.DefaultView[0];
    }