简体   繁体   English

如何在WPF TabControl中取消选项卡更改

[英]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. 我在SO上发现了有关此问题的多个问题,但是我仍然不太能找到可行的解决方案。 Here is what I came up with after reading the answers. 这是我在阅读答案后想到的。

Xaml: 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. 如果您多次单击“ Tab2”,则每次在消息框中选择“否”时,它将在某些时候停止工作:事件停止触发。 Event will trigger again if you click on "Tab3", but if you choose "yes" it opens second tab and not third. 如果您单击“ Tab3”,事件将再次触发,但如果选择“是”,它将打开第二个选项卡,而不是第三个选项卡。 I am having trouble figuring out wtf is going on. 我无法确定wtf正在进行。 :) :)

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. 我也愿意使用任何具有适当的SelectionChanging事件的开源选项卡控件。 I could not find any though. 我什么也找不到。

I am using .Net 4.0. 我正在使用.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. 奇怪的。

This solution http://coderelief.net/2011/11/07/fixing-issynchronizedwithcurrentitem-and-icollectionview-cancel-bug-with-an-attached-property/ 此解决方案http://coderelief.net/2011/11/07/fixing-issynchronizedwithcurrentitem-and-icollectionview-cancel-bug-with-an-attached-property/

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
}

For some reason adding TabControl.Focus() fixes things: 由于某种原因,添加TabControl.Focus()可以解决问题:

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);
            TabControl.Focus();
        }));
    }
}

I still have no clue what on Earth is going on here. 我仍然不知道这里到底发生了什么。 So I will gladly accept the answer, which sheds some light on this issue. 因此,我很乐意接受答案,这为这个问题提供了一些启示。

He who must be obeyed requested that the application ask the user if they wish to leave the page so here is the slightly changed code: 必须服从的用户要求应用程序询问用户是否要离开页面,因此此处是稍有更改的代码:

    private Object _selectedTab;

    public Object SelectedTab
    {
        get
        {
            return _selectedTab;
        }
        set
        {
            if (
                  !(_selectedTab is ADR_Scanner.ViewModel.ConfigurationViewModel) || 
                  !_configurationViewModel.HasChanged ||
                  (System.Windows.Forms.MessageBox.Show("Are you sure you want to leave this page without saving the configuration changes", ADR_Scanner.App.Current.MainWindow.Title, System.Windows.Forms.MessageBoxButtons.YesNo, System.Windows.Forms.MessageBoxIcon.Error) == System.Windows.Forms.DialogResult.Yes)
                )
            {
                _selectedTab = value;
            }
            OnPropertyChanged("SelectedTab");
        }
    }

I think this small change does pretty much what you wanted. 我认为这个小变化几乎可以满足您的需求。

private void MainTabControl_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        if (ReasonBecauseLeaveTabItemIsForbidden)
        {
            if (MainTabControl.SelectedIndex == IndexOfTabItem)
            {
                MessageBox.Show(SomeMessageWhyLeaveTabItemIsForbidden);
            }
            MainTabControl.SelectedIndex = IndexOfTabItem;
        }
    }

IndexOfTabItem - index of TabItem that disabled for leaving. IndexOfTabItem-禁止离开的TabItem的索引。

inside the tabControl_SelectionChanged event handler: 在tabControl_SelectionChanged事件处理程序中:

if (e.OriginalSource == tabControl) //if this event fired from your tabControl
            {
                e.Handled = true;

                if (!forbiddenPage.IsSelected)  //User leaving the tab
                {
                    if (forbiddenTest())
                    {
                        forbiddenPage.IsSelected = true;
                        MessageBox.Show("you must not leave this page");
                    }
             }

Note that setting forbiddenPage.IsSelected = true causes a loop and you reenter this event handler. 请注意,设置forbiddenPage.IsSelected = true会导致循环,然后您重新输入此事件处理程序。 This time, however, we exit because the page selected IS the forbidden page. 但是,这一次我们退出,因为选择的页面是禁止页面。

There is a much easier solution. 有一个更简单的解决方案。 Add a binding to the selected item in the XAML: 将绑定添加到XAML中的所选项目:

    <TabControl SelectedItem="{Binding SelectedTab}" ... 

Then in the view model: 然后在视图模型中:

    private Object _selectedTab;

    public Object SelectedTab
    {
        get
        {
            return _selectedTab;
        }
        set
        {
            if (_selectedTab is ADR_Scanner.ViewModel.ConfigurationViewModel && _configurationViewModel.HasChanged)
            {
                System.Windows.Forms.MessageBox.Show("Please save the configuration changes", ADR_Scanner.App.ResourceAssembly.GetName().Name, System.Windows.Forms.MessageBoxButtons.OK, System.Windows.Forms.MessageBoxIcon.Error);
            }
            else
            {
                _selectedTab = value;
            }
            OnPropertyChanged("SelectedTab");
        }
    }

Obviously you replace ADR_Scanner.ViewModel.ConfigurationViewModel with your own view model class. 显然,您用自己的视图模型类替换了ADR_Scanner.ViewModel.ConfigurationViewModel。 Lastly make sure you initialise _selectedTab in your constructor otherwise the TabControl will have no initial selection. 最后,请确保在构造函数中初始化_selectedTab,否则TabControl将没有初始选择。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM