1 year ago
#94451
tethered.sun
WPF binding source mismatch for a MahApps.Metro.Controls.NumericUpDown inside a TabControl
I often use MahApps.Metro.Controls.NumericUpDown
to manipulate numerical values from the user interface. Recently I have observed a binding behaviour so strange and surreal that completely undermined my faith in the WPF binding infrastructure.
The phenomenon occurred with a collection of view models bound to the ItemsSource
of a TabControl
. Within each of these view models, there is a property of type int
that I bind to the Value
property of a MahApps.Metro.Controls.NumericUpDown
. I noticed that when I manipulate a NumericUpDown
through its TextBox
and leave the focus there just before I select a new tab in the TabControl
, the value I have just set gets written into the NumericUpDown
inside the new tab being selected. That is clearly undesirable as that tab contains a value I did not intend to overwrite just by selecting the containing tab.
This only occurs if I use the TextBox
part to set the value, and not when I use the +
and -
buttons. Furthermore, taking the focus from the NumericUpDown
prevents the bug from occurring. Hooking into the LostFocus
event of the NumericUpDown
, I could observe that the binding source of the respective binding had already changed to that within the view model associated with the next tab (instead of that associated with the NumericUpDown
actually firing the LostFocus
event), and that explains the surreal value bleed. (Additionally, it gets fired twice, as you will be able to see in the MWE.)
I could find a workaround by registering for the PreviewKeyDown
event of the tab label and setting the focus to another element programmatically before the tab switch actually occurs. Still, as I said, this undermined my confidence in the WPF binding architecture. Is it a general WPF binding bug or is it specific to MahApps.Metro.Controls.NumericUpDown
? What can I do to ensure such bugs do not occur in my code (apart from the amateur workaround I could come up with)?
The XAML part of the MWE:
<Window x:Class="NumericUpDownMWE.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:mahApps="http://metro.mahapps.com/winfx/xaml/controls"
xmlns:local="clr-namespace:NumericUpDownMWE"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<TabControl
ItemsSource="{Binding Items}">
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" VerticalAlignment="Top">
<TextBlock Text="Value" />
<mahApps:NumericUpDown
LostFocus="HandleLostFocus"
Value="{Binding Value}" />
</StackPanel>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
</Grid>
</Window>
The respective code-behind:
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows;
using System.Windows.Data;
namespace NumericUpDownMWE
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new ExampleViewModel();
}
private void HandleLostFocus(object sender, RoutedEventArgs e)
{
FrameworkElement? frameworkElement = sender as FrameworkElement;
BindingExpression? bindingExpression = frameworkElement?.GetBindingExpression(MahApps.Metro.Controls.NumericUpDown.ValueProperty);
string? sourceName = (bindingExpression?.ResolvedSource as ExampleItem)?.Name;
}
}
public class ExampleItem : INotifyPropertyChanged
{
private int value;
public event PropertyChangedEventHandler? PropertyChanged;
public string Name { get; }
public int Value {
get { return value; }
set {
if (value != this.value) {
this.value = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Value)));
}
}
}
public ExampleItem(string name, int value)
{
this.Name = name;
this.value = value;
}
}
public class ExampleViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
public ObservableCollection<ExampleItem> Items { get; }
public ExampleViewModel()
{
this.Items = new ObservableCollection<ExampleItem>() {
new ExampleItem("Item A", 1),
new ExampleItem("Item B", 2),
new ExampleItem("Item C", 3)
};
}
}
}
c#
wpf
data-binding
mahapps.metro
lost-focus
0 Answers
Your Answer