wpf icon indicating copy to clipboard operation
wpf copied to clipboard

WPF TabControl selected tab out-of-sync after validation

Open vsfeedback opened this issue 5 months ago • 4 comments

This issue has been moved from a ticket on Developer Community.


[severity:It's more difficult to complete my work] When using a WPF TabControl, in the event that the bound model reverts/does-not-accept the selection of a new tab, the selected header of the TabControl becomes out-of-sync. i.e. the model matches the TabControl.SelectedItem, but the TabControl.SelectedIndex is out-of-sync (left at the last clicked-on tab header).

This minimum reproduceable sample demonstrates the issue we are seeing. For context, below is a screenshot from the attached sample application (that targets net8.0-windows). image.png It binds the TabControl.SelectedItem property to the MainWindowViewModel.SelectedTab property using a TwoWay binding. When the 'Deny tab change' checkbox is checked (default), MainWindowViewModel.SelectedTab does not update its backing field in order to represent the model validating the selection and choosing to prevent it.

Steps to reproduce the TabControl's SelectedItem and SelectedIndex properties becoming out-of-sync:

  1. starting the applicaiton, the first tab (tab 'A') is selected.
  2. With the 'Deny tab change' checkbox checked, click on tab 'B'.
  3. The MainWindowViewModel.SelectedTab does not change its value from 'A', but still raises a property changed event.
  4. The SelectedItem and SelectedIndex properties of the TabControl become mis-matched, 'A' and 1 respectively. I.e. it shows the 'Content of A' in the content area of the control (which is expected), but the visibly selected tab header is 'B' (unexpected)

A similar out-of-sync issue is seen if TwoWay binding the view model to the SelectedIndex instead.


Original Comments

Feedback Bot on 14/8/2025, 09:16 AM:

We have directed your feedback to the appropriate engineering team for further evaluation. The team will review the feedback and notify you about the next steps.

vsfeedback avatar Aug 20 '25 05:08 vsfeedback

+1 to fix this

quicoli avatar Oct 15 '25 11:10 quicoli

@quicoli I can not download the sample files. Could you upload the mini repro project code?

lindexi avatar Oct 16 '25 02:10 lindexi

Here is the sample project: TabControlBug.zip.

If downloading of the zip file is still an issue, the important code is:

MainWindowViewModel.cs

public sealed class MainWindowViewModel : ViewModel
{
    private bool _denyTabChange;
    private int _selectedIndex;
    private TabViewModel _selectedTab;

    public MainWindowViewModel()
    {
        PropertyChanged += MainWindowViewModel_PropertyChanged;

        Tabs = new List<TabViewModel>
        {
            new("A", "Content of A"),
            new("B", "Content of B"),
            new("C", "Content of C"),
            new("D", "Content of D")
        };

        SelectedTab = Tabs.First();
        DenyTabChange = true;
    }

    public bool DenyTabChange
    {
        get => _denyTabChange;
        set
        {
            _denyTabChange = value;
            InvokePropertyChanged();
        }
    }

    public int SelectedIndex
    {
        get => _selectedIndex;
        set
        {
            if (!DenyTabChange)
                _selectedIndex = value;
            InvokePropertyChanged();

            Debug.WriteLine($"MainWindowViewModel.SelectedIndex set to {_selectedIndex}");
        }
    }

    public TabViewModel SelectedTab
    {
        get => _selectedTab;
        set
        {
            // Our code-base does this
            if (!DenyTabChange)
                _selectedTab = value;
            InvokePropertyChanged();

            // This also fails in the same manner
            //var previousTab = _selectedTab;
            //_selectedTab = value;
            //InvokePropertyChanged();

            //if (DenyChange)
            //{
            //    _selectedTab = previousTab;
            //    InvokePropertyChanged();
            //}

            Debug.WriteLine($"MainWindowViewModel.SelectedTab set to {_selectedTab.Header.Name}");
        }
    }

    public IEnumerable<TabViewModel> Tabs { get; }

    private void MainWindowViewModel_PropertyChanged(object? sender, PropertyChangedEventArgs e)
    {
        Debug.WriteLine($"MainWindowViewModel.PropertyChanged event fired for property {e.PropertyName}");
    }
}

And the tab control in the view using the above view model as data context:

<TabControl x:Name="TabControl"
                    Grid.Row="1"
                    ItemsSource="{Binding Tabs}"
                    SelectedItem="{Binding SelectedTab, Mode=TwoWay}"
                    ItemTemplate="{StaticResource HeaderDataTemplate}"
                    ContentTemplate="{StaticResource ContentDataTemplate}"
                    SelectionChanged="TabControl_OnSelectionChanged"/>

gordon-frost-hwu avatar Nov 05 '25 10:11 gordon-frost-hwu

@gordon-frost-hwu I think the main issues is the MainWindowViewModel.SelectedTab do not change the _selectedTab field, but raise the PropertyChanged event.

The WPF framework will break the property chaning process.

 	TabControlBug.dll!TabControlBug.MainWindowViewModel.SelectedTab.set(TabControlBug.TabViewModel value = {TabControlBug.TabViewModel}) line 59	C#
 	System.Private.CoreLib.dll!System.Reflection.MethodBaseInvoker.InvokeWithOneArg(object obj, System.Reflection.BindingFlags invokeAttr = Default, System.Reflection.Binder binder, object[] parameters = {object[0x00000001]}, System.Globalization.CultureInfo culture)	
 	System.ComponentModel.TypeConverter.dll!System.ComponentModel.ReflectPropertyDescriptor.SetValue(object component = {TabControlBug.MainWindowViewModel}, object value = {TabControlBug.TabViewModel})	
 	PresentationFramework.dll!MS.Internal.Data.PropertyPathWorker.SetValue(object item, object value) line 373	C#
 	PresentationFramework.dll!System.Windows.Data.BindingExpression.UpdateSource(object value = {TabControlBug.TabViewModel}) line 1378	C#
 	PresentationFramework.dll!System.Windows.Data.BindingExpressionBase.UpdateValue() line 947	C#
 	PresentationFramework.dll!System.Windows.Data.BindingExpressionBase.ProcessDirty() line 1202	C#
 	PresentationFramework.dll!System.Windows.Data.BindingExpressionBase.Dirty() line 1160	C#
 	PresentationFramework.dll!System.Windows.Data.BindingExpressionBase.SetValue(System.Windows.DependencyObject d, System.Windows.DependencyProperty dp, object value) line 769	C#
 	WindowsBase.dll!System.Windows.DependencyObject.SetValueCommon(System.Windows.DependencyProperty dp = {System.Windows.DependencyProperty}, object value = {TabControlBug.TabViewModel}, System.Windows.PropertyMetadata metadata = {System.Windows.FrameworkPropertyMetadata}, bool coerceWithDeferredReference = false, bool coerceWithCurrentValue = true, System.Windows.OperationType operationType, bool isInternal)	
 	WindowsBase.dll!System.Windows.DependencyObject.SetCurrentValueInternal(System.Windows.DependencyProperty dp, object value)	
 	PresentationFramework.dll!System.Windows.Controls.Primitives.Selector.UpdatePublicSelectionProperties() line 1866	C#
 	PresentationFramework.dll!System.Windows.Controls.Primitives.Selector.SelectionChanger.End() line 71	C#
 	PresentationFramework.dll!System.Windows.Controls.Primitives.Selector.SetSelectedHelper(object item, System.Windows.FrameworkElement UI, bool selected) line 1655	C#
 	PresentationFramework.dll!System.Windows.Controls.Primitives.Selector.OnSelected(object sender, System.Windows.RoutedEventArgs e = {System.Windows.RoutedEventArgs}) line 1970	C#
 	PresentationCore.dll!System.Windows.EventRoute.InvokeHandlersImpl(object source = {System.Windows.Controls.TabItem}, System.Windows.RoutedEventArgs args = {System.Windows.RoutedEventArgs}, bool reRaised = false)	
 	PresentationCore.dll!System.Windows.UIElement.RaiseEventImpl(System.Windows.DependencyObject sender = {System.Windows.Controls.TabItem}, System.Windows.RoutedEventArgs args = {System.Windows.RoutedEventArgs})	
>	PresentationFramework.dll!System.Windows.Controls.TabItem.OnIsSelectedChanged(System.Windows.DependencyObject d, System.Windows.DependencyPropertyChangedEventArgs e) line 75	C#
 	PresentationFramework.dll!System.Windows.FrameworkElement.OnPropertyChanged(System.Windows.DependencyPropertyChangedEventArgs e)	C#
 	WindowsBase.dll!System.Windows.DependencyObject.NotifyPropertyChange(System.Windows.DependencyPropertyChangedEventArgs args)	
 	WindowsBase.dll!System.Windows.DependencyObject.UpdateEffectiveValue(System.Windows.EntryIndex entryIndex, System.Windows.DependencyProperty dp = {System.Windows.DependencyProperty}, System.Windows.PropertyMetadata metadata, System.Windows.EffectiveValueEntry oldEntry, ref System.Windows.EffectiveValueEntry newEntry = {System.Windows.EffectiveValueEntry}, bool coerceWithDeferredReference, bool coerceWithCurrentValue, System.Windows.OperationType operationType)	
 	WindowsBase.dll!System.Windows.DependencyObject.SetValueCommon(System.Windows.DependencyProperty dp, object value, System.Windows.PropertyMetadata metadata, bool coerceWithDeferredReference, bool coerceWithCurrentValue, System.Windows.OperationType operationType, bool isInternal)	
 	WindowsBase.dll!System.Windows.DependencyObject.SetCurrentValueInternal(System.Windows.DependencyProperty dp, object value)	
 	PresentationFramework.dll!System.Windows.Controls.TabItem.OnPreviewGotKeyboardFocus(System.Windows.Input.KeyboardFocusChangedEventArgs e = {System.Windows.Input.KeyboardFocusChangedEventArgs}) line 214	C#
 	PresentationCore.dll!System.Windows.RoutedEventArgs.InvokeHandler(System.Delegate handler, object target)	
 	PresentationCore.dll!System.Windows.EventRoute.InvokeHandlersImpl(object source = {System.Windows.Controls.TabItem}, System.Windows.RoutedEventArgs args = {System.Windows.Input.KeyboardFocusChangedEventArgs}, bool reRaised)	
 	PresentationCore.dll!System.Windows.UIElement.RaiseEventImpl(System.Windows.DependencyObject sender = {System.Windows.Controls.TabItem}, System.Windows.RoutedEventArgs args = {System.Windows.Input.KeyboardFocusChangedEventArgs})	
 	PresentationCore.dll!System.Windows.UIElement.RaiseTrustedEvent(System.Windows.RoutedEventArgs args = {System.Windows.Input.KeyboardFocusChangedEventArgs})	
 	PresentationCore.dll!System.Windows.Input.InputManager.ProcessStagingArea()	
 	PresentationCore.dll!System.Windows.Input.KeyboardDevice.TryChangeFocus(System.Windows.DependencyObject newFocus = {System.Windows.Controls.TabItem}, System.Windows.Input.IKeyboardInputProvider keyboardInputProvider = {System.Windows.Interop.HwndKeyboardInputProvider}, bool askOld, bool askNew, bool forceToNullIfFailed)	
 	PresentationCore.dll!System.Windows.Input.KeyboardDevice.Focus(System.Windows.DependencyObject focus, bool askOld, bool askNew, bool forceToNullIfFailed)	
 	PresentationFramework.dll!System.Windows.Controls.TabItem.SetFocus() line 317	C#
 	PresentationFramework.dll!System.Windows.Controls.TabItem.OnMouseLeftButtonDown(System.Windows.Input.MouseButtonEventArgs e = {System.Windows.Input.MouseButtonEventArgs}) line 166	C#

lindexi avatar Nov 06 '25 02:11 lindexi

@lindexi Thanks for the response, and sorry it's taken so long for me to get back to you.

I've not double checked it recently, but I believe that if the code under '// This also fails in the same manner' is uncommented in the SelectedTab setter, it will exhibit the same behaviour and that code does update the _selectedTab field before raising the PropertyChanged event. Do you know an alternative way of preventing a tab change if neither of these two methods are supposed to work?

I'm not quite sure how the stack trace you posted indicates that WPF broke the property changing process, could you explain a bit more what you mean please?

gordon-frost-hwu avatar Nov 24 '25 16:11 gordon-frost-hwu

@gordon-frost-hwu As the stacktrace shows:

  1. PresentationFramework.dll!System.Windows.Controls.TabItem.OnIsSelectedChanged
  2. TabControlBug.dll!TabControlBug.MainWindowViewModel.SelectedTab.set(TabControlBug.TabViewModel value = {TabControlBug.TabViewModel})

It means that the MainWindowViewModel.SelectedTab be changed by TabItem.OnIsSelectedChanged. But the MainWindowViewModel.SelectedTab do not update the field and raise the event, that will cause the exception status.

lindexi avatar Nov 25 '25 00:11 lindexi