The problem is that you block the setter with a dialog. At the time the dialogue is shown, the property must have some value, so the Binding system is knocked down and does not work as we would like. The solution is to make the logic of allowing the change of tab from the setter. At the same time, the setter will become universally applicable. Well, the UI-logic (message box) can be unloaded from the VM, which is also not bad.
So look what you need.
First, we remove long operations from the setter. Setter must be fast.
class OuterVM : INotifyPropertyChanged { // ... private VM _selectedTabVM; public VM SelectedTabVM { get { return _selectedTabVM; } set { if (_selectedTabVM == value) return; _selectedTabVM = value; NotifyPropertyChanged(nameof(SelectedTabVM)); } }
Now, we need to disable the normal click on TabItem 's, and redirect it to our code. For this, you need some EventTrigger that would cancel the event. (For example, like this .) But this technique will deliver EventArgs to a VM in which they don’t belong, so let's go through the attached behavior. (Yes, this is a serious weapon, I have not found another.)
First, let's connect via nuget System.Windows.Interactivity.WPF (References → right mouse button → Manage NuGet Packages ... → Search = System.Windows.Interactivity.WPF ). Write Behavior :
class RouteClickBehaviour : Behavior<TabItem> { protected override void OnAttached() { base.OnAttached(); AssociatedObject.PreviewMouseDown += OnTabItemMouseDown; } protected override void OnDetaching() { AssociatedObject.PreviewMouseDown -= OnTabItemMouseDown; base.OnDetaching(); } void OnTabItemMouseDown(object sender, MouseButtonEventArgs e) { e.Handled = true; // что делать тут? } }
Everything is simple: when connecting, subscribe to PreviewMouseDown at TabItem 's, when disconnecting, unsubscribe, when a click arrives, we cancel the standard processing via e.Handled . Why PreviewMouseDown ? Because this event comes to us before internal handlers, and we can cancel it without letting it go inside.
Now the question arises, what to do when the click is detected? Okay, you need to call a command from the VM, let the VM decide what to do next. Where to get the command and argument? The answer is obvious - attach through attached property. These attached property could be put in a separate class, but you can put it in RouteClickBehaviour .
We get an improved version:
class RouteClickBehaviour : Behavior<TabItem> { protected override void OnAttached() { base.OnAttached(); AssociatedObject.PreviewMouseDown += OnTabItemMouseDown; } protected override void OnDetaching() { AssociatedObject.PreviewMouseDown -= OnTabItemMouseDown; base.OnDetaching(); } void OnTabItemMouseDown(object sender, MouseButtonEventArgs e) { e.Handled = true; var command = GetClickCommand(AssociatedObject); var commandParameter = GetClickCommandParameter(AssociatedObject); if (command == null) return; Dispatcher.CurrentDispatcher.InvokeAsync(() => command.Execute(commandParameter)); } #region attached property ICommand ClickCommand public static ICommand GetClickCommand(DependencyObject obj) => (ICommand)obj.GetValue(ClickCommandProperty); public static void SetClickCommand(DependencyObject obj, ICommand value) => obj.SetValue(ClickCommandProperty, value); public static readonly DependencyProperty ClickCommandProperty = DependencyProperty.RegisterAttached( "ClickCommand", typeof(ICommand), typeof(RouteClickBehaviour)); #endregion #region attached property object ClickCommandParameter public static object GetClickCommandParameter(DependencyObject obj) => obj.GetValue(ClickCommandParameterProperty); public static void SetClickParameterCommand(DependencyObject obj, object value) => obj.SetValue(ClickCommandParameterProperty, value); public static readonly DependencyProperty ClickCommandParameterProperty = DependencyProperty.RegisterAttached( "ClickCommandParameter", typeof(object), typeof(RouteClickBehaviour)); #endregion }
The only subtlety is that we send the command asynchronously.
The next problem, but how to add an attached behavior through the style in TabItem ? If we created a TabItem manually, there would be no problems:
<TabItem Header="{Binding Header}" Width="100"> <i:Interaction.Behaviors> <local:RouteClickBehaviour/> </i:Interaction.Behaviors>
(and the command could be passed through parameters). But we have a style, but with delivery behavior through style everything is difficult .
Let's go as a standard workaround: through another attached property. RouteClickBehaviour 's add to RouteClickBehaviour this:
#region attached property bool Inject, calls OnInject on change public static bool GetInject(DependencyObject obj) => (bool)obj.GetValue(InjectProperty); public static void SetInject(DependencyObject obj, bool value) => obj.SetValue(InjectProperty, value); public static readonly DependencyProperty InjectProperty = DependencyProperty.RegisterAttached( "Inject", typeof(bool), typeof(RouteClickBehaviour), new PropertyMetadata(false, OnInject)); #endregion static void OnInject(DependencyObject d, DependencyPropertyChangedEventArgs e) { bool newValue = (bool)e.NewValue; var behaviours = Interaction.GetBehaviors(d); if (newValue) { behaviours.Add(new RouteClickBehaviour()); } else { foreach (var b in behaviours.OfType<RouteClickBehaviour>().ToList()) behaviours.Remove(b); } }
Now if you set Inject = true , the desired behavior will hang automatically.
OK, then - we need a team in the VM, which will decide whether to change the Tab or not. For teams, you can use an ordinary RelayCommand . modeChangeExecute command, of course, in the same place where modeChangeExecute lies:
class OuterVM : INotifyPropertyChanged { // ... public OuterVM() { ChangeRequested = new RelayCommand(o => { if (modeChangeExecute()) SelectedTabVM = (VM)o; }); } public ICommand ChangeRequested { get; } bool modeChangeExecute() { return MessageBox.Show("?", "?", MessageBoxButton.YesNo) == MessageBoxResult.Yes; }
OK, and the VM part is ready. Now we tie it all together via XAML.
<TabControl ItemsSource="{Binding TabItemsVM}" SelectedItem="{Binding SelectedTabVM}" TabStripPlacement="Top"> Name="TK"> <TabControl.ItemContainerStyle> <Style TargetType="TabItem"> <Setter Property="Header" Value="{Binding Header}"/> <Setter Property="Width" Value="100"/> <Setter Property="local:RouteClickBehaviour.Inject" Value="True"/> <Setter Property="local:RouteClickBehaviour.ClickCommand" Value="{Binding DataContext.ChangeRequested, ElementName=TK}"/> <Setter Property="local:RouteClickBehaviour.ClickCommandParameter" Value="{Binding}"/> </Style> </TabControl.ItemContainerStyle> </TabControl>
Set local:RouteClickBehaviour.Inject = True to connect the behavior. The command needs to be taken from the VM for TabControl 'a, TabControl it is the external VM that decides the issue of switching. As a parameter, we transfer the local VM that wants to become active.
We are checking. Should work.
modeChangeExecute()-modeChangeExecute()returns true or false? - Pavel Mayorovда или нет. In both cases, regardless of the result, two active tabs are obtained. - Gardes