There is a TabControl with 3 tabs TabItem . Each tab has its own VM . The whole thing is stored in the ObservableCollection<AbstractVM> TabItemsVM .

  <TabControl ItemsSource="{Binding TabItemsVM}" SelectedItem="{Binding SelectedTabVM}" TabStripPlacement="Top"> <TabControl.ItemContainerStyle> <Style TargetType="TabItem"> <Setter Property="Header" Value="{Binding Header}"/> <Setter Property="Width" Value="100"/> </Style> </TabControl.ItemContainerStyle> </TabControl> 

works

If you select another tab from the code by assigning SelectedTabVM TabItemsVM[0] . Code below:

 _selectedTabVM = TabItemsVM[0]; //присваиваю именно приватному полю, //т.к. в публичном, в сеттере хранится некая логика проверки. //в то время как я хочу изменить вкладку без проверки //Вызываю PropertyChanged("SelectedTabVM"); 

The tab changes until everything is fine:

implementation of SelectedTabVM :

 private Abstract VM _selectedTabVM; public AbstractVM SelectedTabVM { get { return _selectedTabVM; } set { if (modeChangeExecute()) { _selectedTabVM = value; NotifyPropertyChanged("SelectedTabVM"); } } } 

works

But when you switch the tab through the interface, you get this picture:

does not work

those. two tabs feature IsSelected set to True . What can be wrong? Why is the tab on which I moved from the code to the IsSelected property IsSelected not removed

UPD Found not quite a similar problem on the English stackoverflow: https://stackoverflow.com/questions/7929646/how-to-programmaticaly-select-a-tabitem-in-wpf-tabcontrol

here it’s like the TabControl bug in WPF

  • Show the implementation of the SelectedTabVM property - Pavel Mayorov
  • @PavelMayorov updated - Gardes
  • When a situation with two active tabs modeChangeExecute() - modeChangeExecute() returns true or false? - Pavel Mayorov
  • there comes a dialog box, да или нет . In both cases, regardless of the result, two active tabs are obtained. - Gardes
  • @ S.Kost: This is the problem. Remove the dialog box from the setter, you get recursion: the value of the property has not changed yet, and the next iteration of the event loop has already been performed. What do you think the value of the selected item should be from the point of view of WPF for the duration of your dialog box? - VladD

2 answers 2

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.

  • as always more than detailed, thanks! I will understand and check. - Gardes
  • @ S.Kost: Please! - VladD
  • Everything works, the only thing I replaced Dispatcher.CurrentDispatcher with Application.Current.Dispather , because crawled error that доступ через ссылку на экземпляр невозможен, вместо этого уточните имя его типа . - Gardes
  • @ S.Kost: Strange, do you have the correct version of System.Windows.Interactivity.WPF , authored by Microsoft? I compiled this way. (Try then just Dispatcher.InvokeAsync(...) .) - VladD
  • Yes, version 2.0.20525. I tried, it works. - Gardes

And why do tabs property IsSelected band? Limit yourself to SelectedItem , that will be enough. And with the properties IsSelected here will understand without you. And if you need in VM to know the value of this property, then tie it through Mode=OneWay

UPD:

Here's what I got. Immediately I say, I didn’t really bother with the architecture, I just threw it up so quickly :)

 <Window x:Class="WpfApplication1.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:WpfApplication1" Title="MainWindow" Height="350" Width="525" DataContext="{Binding RelativeSource={RelativeSource Mode=Self}}"> <Grid> <TabControl ItemsSource="{Binding Tabs}" SelectedItem="{Binding SelectedTab}"> <TabControl.ItemContainerStyle> <Style TargetType="TabItem"> <Setter Property="Header" Value="{Binding Header}"/> <Setter Property="Width" Value="100"/> <Setter Property="Content" Value="{Binding Content}"/> </Style> </TabControl.ItemContainerStyle> </TabControl> </Grid> </Window> 

But bg

 public partial class MainWindow : Window { public ObservableCollection<Tab> Tabs { get; set; } public Tab SelectedTab { get; set; } public MainWindow() { Tabs = new ObservableCollection<Tab> { new Tab { Header="Tab1", Content="Tab1 content" }, new Tab { Header="Tab2", Content="Tab2 content" }, new Tab { Header="Tab3", Content="Tab3 content" } }; SelectedTab = Tabs[0]; InitializeComponent(); } } public class Tab { public string Header { get; set; } public string Content { get; set; } } 

And everything works fine for me, the tabs stand out one by one. The problem you probably have somewhere else that you have not shown, thinking that it is not related to the problem. Look for where you have assigned to tabs IsSelected = true . Or completely abstract from logic, leave only this foundation, installing plugs on the rest and slowly connect logic.

  • IsEnabled has nothing to do with it) there is no problem with it - Gardes
  • @ S.Kost Taki by the way without a clue, is it? In the state that on the screen - the second tab IsEnabled not true by chance? - Monk
  • @Monk, corrected. - Gardes
  • @ S.Kost, updated the answer - iRumba
  • I repeat. IsEnabled nothing to do with. I wrote in question that the IsSelected property IsSelected set to true and is not removed. - Gardes