问题
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.
回答1:
This solution 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
}
回答2:
For some reason adding TabControl.Focus() fixes things:
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.
回答3:
inside the tabControl_SelectionChanged event handler:
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. This time, however, we exit because the page selected IS the forbidden page.
回答4:
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.
回答5:
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.
回答6:
There is a much easier solution. Add a binding to the selected item in the 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. Lastly make sure you initialise _selectedTab in your constructor otherwise the TabControl will have no initial selection.
来源:https://stackoverflow.com/questions/30706758/how-to-cancel-tab-change-in-wpf-tabcontrol