如何在 WPF TabControl 中取消选项卡切换

5

我在SO上找到了多个关于这个问题的问题,但是我仍然无法得到一个可靠的解决方案。在阅读答案后,这是我想出的解决方案。

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>

代码后台:

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;
}

基本上,我想在用户导航到不同的选项卡时显示确认消息,并且如果他点击“否” - 中止转换。但是,这段代码不起作用。如果您在“Tab2”上多次单击,每次在消息框中选择“否”,则在某个时候它会停止工作:事件停止触发。如果您单击“Tab3”,事件将再次触发,但是如果您选择“是”,它将打开第二个选项卡,而不是第三个。我有困难弄清楚正在发生什么。:)
有人看到我的解决方案中的错误吗?还是有更简单的方法来显示确认消息,当用户切换选项卡时?我也愿意使用任何具有适当SelectionChanging事件的开源选项卡控件。但我找不到任何一个。
我正在使用.Net 4.0。
编辑: 如果我注释掉消息框:
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;
}

一切都正常运作。奇怪。


@thumbmunkeys,这并没有考虑到用户可以使用键盘导航选项卡的事实。而且我不想完全禁用选项卡。如果用户在消息框中选择“是”,他仍应该能够正常更改选项卡。 - Nikita B
8个回答

2

这个来自web.archive的解决方案

似乎在编程中运作得非常好

<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
}

1
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - Nikita B
1
点头,但至少它是设置并忘记的 :) - Matt Searles

2
由于某种原因,添加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();
        }));
    }
}

我还是一点也不知道这里到底在发生什么。所以我很愿意接受这个答案,它为这个问题带来了一些启示。


使用这种方法与您的其余代码一起运行对我很有效,谢谢! - gusmally supports Monica

1
在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");
                    }
             }

请注意,设置 forbiddenPage.IsSelected = true 会导致循环并重新进入此事件处理程序。但是,这次我们会退出,因为所选页面是禁止页面。

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

IndexOfTabItem - 禁止离开的 TabItem 的索引。


0

在ViewModel中有一个解决方案,其中一个属性绑定到TabControll.SelectedItem。在setter中更改值,但使用Dispatcher.BeginInvoke将其更改回来并引发已更改的事件,然后View接受更改...

private int selectedTabIndex;
public int SelectedTabIndex {
    get => selectedTabIndex;
    set {
        var oldValue = selectedTabIndex;
        selectedTabIndex = value;
        if(!TabCanBeChanged(value)) {
            System.Windows.Application.Current.Dispatcher.BeginInvoke(
                System.Windows.Threading.DispatcherPriority.Normal,
                new Action(() => {
                    selectedTabIndex = oldValue;
                    RaisePropertyChanged(nameof(SelectedTabIndex));
                })
            );
        }
    }
}

0
这对我来说很有效:
xaml:
<TabControl PreviewMouseLeftButtonDown="TabControl_PreviewMouseLeftButtonDown">

代码背后:
private void TabControl_PreviewMouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
    TabControl tabControl = sender as TabControl;
    MyVM vm = tabControl.DataContext as MyVM;

    // Cancel Tab Changed if Save failed
    if (e.Source is TabItem && !vm.Save())
    {
        e.Handled = true;
    }
}

-1

必须服从的人要求该应用程序询问用户是否希望离开页面,因此这是稍作更改后的代码:

    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");
        }
    }

我认为这个小改动基本上实现了你想要的。


这是下面答案的重复。我已经验证,如果不使用计时器将其切换回来,那么无法从VM中防止选项卡更改。 - Neil B

-2

有一个更简单的解决方案。在XAML中为所选项添加绑定:

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

然后在视图模型中:

    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");
        }
    }

显然,您需要将ADR_Scanner.ViewModel.ConfigurationViewModel替换为您自己的视图模型类。最后,请确保在构造函数中初始化_selectedTab,否则TabControl将没有初始选择。


不,事实并非如此简单。你的代码防止了_selectedTab被更改,但这几乎是它所能做到的全部。它不会以任何方式影响用户界面,因此无法解决这个问题。 - Nikita B
不,它防止用户在不适当的时候更改选项卡。这与取消选项卡更改相同。它解决了我遇到的问题。 - Leif Goodwin
3
我在点踩之前尝试过它。它在 .Net 4.0 和 .Net 4.5.2 中都不起作用,这就是为什么在 SO 上有那么多奇怪和复杂的解决方法:你不能通过简单的绑定来解决这个问题。 - Nikita B
1
我怎么能在不看你的源代码的情况下解释它在你的应用程序中为什么有效呢?只有你自己能够进行解释。话虽如此,如果你的应用程序是开源的,你可以在这里链接它(或将链接发送给我),我会去看一下。或者你可以创建一个小项目来演示你的方法,并将其链接到这里。我自己尝试过了,但像我说的那样,它并没有起作用(至少我做的时候没有),我也不知道为什么它应该起作用。 - Nikita B
1
或者我们可以就同意不同意而结束。 :) 如果你的方法很好,你很快就会得到赞同。 - Nikita B
显示剩余5条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接