Search code examples
c#wpftabcontrol

How to cancel tab change in WPF TabControl


I have found multiple questions about this problem on SO, however I still can't quite get a realiable solution. Here is what I came up with after reading the answers.

Xaml:

<Window x:Class="WpfApplication1.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="300" Width="300" x:Name="this">
    <TabControl IsSynchronizedWithCurrentItem="True" ItemsSource="{Binding Tabs, ElementName=this}" x:Name="TabControl"/>
</Window>

Code behind:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        var tabs = new ObservableCollection<string> {"Tab1", "Tab2", "Tab3"};
        Tabs = CollectionViewSource.GetDefaultView(tabs);
        Tabs.CurrentChanging += OnCurrentChanging;
        Tabs.CurrentChanged += OnCurrentChanged;
        Tabs.MoveCurrentToFirst();
        CurrentTab = tabs.First();
    }

    private void OnCurrentChanging(object sender, CurrentChangingEventArgs e)
    {
        //only show message box when tab is changed by user input
        if (!_cancelTabChange)
        {
            if (MessageBox.Show("Change tab?", "Message", MessageBoxButton.YesNo) == MessageBoxResult.No)
            {
                _cancelTabChange = true;
                return;
            }
        }
        _cancelTabChange = false;
    }

    private void OnCurrentChanged(object sender, EventArgs e)
    {
        if (!_cancelTabChange)
        {
            //Update current tab property, if user did not cancel transition
            CurrentTab = (string)Tabs.CurrentItem;
        }
        else
        {
            //navigate back to current tab otherwise
            Dispatcher.BeginInvoke(new Action(() => Tabs.MoveCurrentTo(CurrentTab)));
        }
    }

    public string CurrentTab { get; set; }

    public static readonly DependencyProperty TabsProperty = DependencyProperty.Register("Tabs", typeof(ICollectionView), typeof(MainWindow), new FrameworkPropertyMetadata(default(ICollectionView)));
    public ICollectionView Tabs
    {
        get { return (ICollectionView)GetValue(TabsProperty); }
        set { SetValue(TabsProperty, value); }
    }

    private bool _cancelTabChange;
}

Basically I want to display a confirmation message, when user navigates to different tab, and if he clicks "no" - abort the transition. This code does not work though. If you click multiple times on "Tab2", each time choosing "no" in message box, at some point it stops working: events stop triggering. Event will trigger again if you click on "Tab3", but if you choose "yes" it opens second tab and not third. I am having trouble figuring out wtf is going on. :)

Does anyone see a bug in my solution? Or is there an easier way to display a confirmation message, when user switches tabs? I am also willing to use any opensource tab control, which does have a proper SelectionChanging event. I could not find any though.

I am using .Net 4.0.

Edit: If I comment the message box out:

private void OnCurrentChanging(object sender, CurrentChangingEventArgs e)
{
    //only show message box when tab is changed by user input
    if (!_cancelTabChange)
    {
        //if (MessageBox.Show("Change tab?", "Message", MessageBoxButton.YesNo) == MessageBoxResult.No)
        //{
            Debug.WriteLine("Canceled");
            _cancelTabChange = true;
            return;
        //}
    }
    _cancelTabChange = false;
}

Everything works fine. Weird.


Solution

  • This solution from web.archive

    seems to work quite well with

    <TabControl ... yournamespace:SelectorAttachedProperties.IsSynchronizedWithCurrentItemFixEnabled="True" .../>
    
    private void OnCurrentChanging(object sender, CurrentChangingEventArgs e)
    {                   
        if (MessageBox.Show("Change tab?", "Message", MessageBoxButton.YesNo) == MessageBoxResult.No)
        {
            e.Cancel = true;                    
        }                     
    }
    
    
    
    public static class SelectorAttachedProperties
    {
        private static Type _ownerType = typeof(SelectorAttachedProperties);
     
        #region IsSynchronizedWithCurrentItemFixEnabled
     
        public static readonly DependencyProperty IsSynchronizedWithCurrentItemFixEnabledProperty =
            DependencyProperty.RegisterAttached("IsSynchronizedWithCurrentItemFixEnabled", typeof(bool), _ownerType,
            new PropertyMetadata(false, OnIsSynchronizedWithCurrentItemFixEnabledChanged));
     
        public static bool GetIsSynchronizedWithCurrentItemFixEnabled(DependencyObject obj)
        {
            return (bool)obj.GetValue(IsSynchronizedWithCurrentItemFixEnabledProperty);
        }
     
        public static void SetIsSynchronizedWithCurrentItemFixEnabled(DependencyObject obj, bool value)
        {
            obj.SetValue(IsSynchronizedWithCurrentItemFixEnabledProperty, value);
        }
     
        private static void OnIsSynchronizedWithCurrentItemFixEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            Selector selector = d as Selector;
            if (selector == null || !(e.OldValue is bool && e.NewValue is bool) || e.OldValue == e.NewValue)
                return;
     
            bool enforceCurrentItemSync = (bool)e.NewValue;
            ICollectionView collectionView = null;
     
            EventHandler itemsSourceChangedHandler = null;
            itemsSourceChangedHandler = delegate
            {
                collectionView = selector.ItemsSource as ICollectionView;
                if (collectionView == null)
                    collectionView = CollectionViewSource.GetDefaultView(selector);
            };
     
            SelectionChangedEventHandler selectionChangedHanlder = null;
            selectionChangedHanlder = delegate
            {
                if (collectionView == null)
                    return;
     
                if (selector.IsSynchronizedWithCurrentItem == true && selector.SelectedItem != collectionView.CurrentItem)
                {
                    selector.IsSynchronizedWithCurrentItem = false;
                    selector.SelectedItem = collectionView.CurrentItem;
                    selector.IsSynchronizedWithCurrentItem = true;
                }
            };
     
            if (enforceCurrentItemSync)
            {
                TypeDescriptor.GetProperties(selector)["ItemsSource"].AddValueChanged(selector, itemsSourceChangedHandler);
                selector.SelectionChanged += selectionChangedHanlder;
            }
            else
            {
                TypeDescriptor.GetProperties(selector)["ItemsSource"].RemoveValueChanged(selector, itemsSourceChangedHandler);
                selector.SelectionChanged -= selectionChangedHanlder;
            }
        }
     
        #endregion IsSynchronizedWithCurrentItemFixEnabled
    }