TabItem header click

Sidebp picture Sidebp · Mar 21, 2011 · Viewed 7.6k times · Source

I have defined a control template/style for my tab items as follows:

<Style x:Key="TabItemStyle" TargetType="{x:Type TabItem}">
        <Setter Property="Header" Value="{Binding Content.DataContext.Header, RelativeSource={RelativeSource Self}}" />
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type TabItem}">                    
                    <Grid Width="Auto" Height="Auto" x:Name="TabItemRoot" Margin="10,0,10,0">                       
                        <Button Command="{Binding Content.DataContext.HeaderClickedCommand}">
                            <ContentPresenter Margin="13,5,13,5"
                                              x:Name="Content"
                                              ContentSource="Header"
                                              RecognizesAccessKey="True">
                            </ContentPresenter>
                        </Button>
                    </Grid>   
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

When I click on the tab header the command bound to the button is called OK, however, the click event appears to have eaten the SelectionChanged event and therefore the tab page doesn't change.

Is there a better way to implement this so I can call a VM method and get still get the tab page to change?

Update as per comment

The idea being that if the user clicks the header of the currently active tab it updates the active tabs content through changes in the VM. Thanks

Answer

vortexwolf picture vortexwolf · Mar 21, 2011

Here is my way to implement such functionality, but whether it is better or not - it's matter of taste.

The main xaml looks so:

    <TabControl ItemsSource="{Binding TabItems}">
        <TabControl.ItemTemplate>
            <DataTemplate>
                <Grid>
                    <TextBlock Text="{Binding Title}"/>
                    <i:Interaction.Triggers>
                        <i:EventTrigger EventName="MouseLeftButtonDown">
                            <local:ExecuteCommandAction Command="{Binding HeaderClickCommand}"/>
                        </i:EventTrigger>
                    </i:Interaction.Triggers>
                </Grid>   
            </DataTemplate>
        </TabControl.ItemTemplate>
    </TabControl>

There is no ControlTemplate, just DataTemplate with the attached property Interaction.Triggers, where the prefix i is defined in this string:

xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"

This library can be found in MS Blend SDK or with the library mvvm light (on codeplex).

Also, don't confuse EventTriggers that has the prefix i and generic EventTriggers, they are different at some point, but I don't know exactly what the difference is, except that the EventTrigger class from the custom library work with Silverlight too.

In my example the trigger is subscribed to the event MouseLeftButtonDown and calls the special action class every time when the event is raised. This action class is a custom class and it is defined in code:

/// <summary>
/// Behaviour helps to bind any RoutedEvent of UIElement to Command.
/// </summary>
[DefaultTrigger(typeof(UIElement), typeof(System.Windows.Interactivity.EventTrigger), "MouseLeftButtonDown")]
public class ExecuteCommandAction : TargetedTriggerAction<UIElement>
{
    /// <summary>
    /// Dependency property represents the Command of the behaviour.
    /// </summary>
    public static readonly DependencyProperty CommandParameterProperty = DependencyProperty.RegisterAttached("CommandParameter",
                        typeof(object), typeof(ExecuteCommandAction), new FrameworkPropertyMetadata(null));

    /// <summary>
    /// Dependency property represents the Command parameter of the behaviour.
    /// </summary>
    public static readonly DependencyProperty CommandProperty = DependencyProperty.RegisterAttached("Command",
                        typeof(ICommand), typeof(ExecuteCommandAction), new FrameworkPropertyMetadata(null));

    /// <summary>
    /// Gets or sets the Commmand.
    /// </summary>
    public ICommand Command
    {
        get
        {
            return (ICommand)this.GetValue(CommandProperty);
        }
        set
        {
            this.SetValue(CommandProperty, value);
        }
    }

    /// <summary>
    /// Gets or sets the CommandParameter.
    /// </summary>
    public object CommandParameter
    {
        get
        {
            return this.GetValue(CommandParameterProperty);
        }
        set
        {
            this.SetValue(CommandParameterProperty, value);
        }
    }

    /// <summary>
    /// Invoke method is called when the given routed event is fired.
    /// </summary>
    /// <param name="parameter">
    /// Parameter is the sender of the event.
    /// </param>
    protected override void Invoke(object parameter)
    {
        if (this.Command != null)
        {
            if (this.Command.CanExecute(this.CommandParameter))
            {
                this.Command.Execute(this.CommandParameter);
            }
        }
    }
}

That is all. Now the command doesn't prevent the tabcontrol from selection of items. Code-behind to test this xaml:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        var items = new ObservableCollection<TabItemViewModel>
        {
            new TabItemViewModel("Item 1"), new TabItemViewModel("Item 2"), new TabItemViewModel("Item 3")
        };
        this.DataContext = new MainViewModel(){TabItems = items};
    }


}

public class MainViewModel
{
    public ObservableCollection<TabItemViewModel> TabItems { get; set; }
}

public class TabItemViewModel
{
    public TabItemViewModel(string title)
    {
        this.Title = title;
        this.HeaderClickCommand = new RelayCommand(() => MessageBox.Show("Clicked "+this.Title));
    }
    public string Title { get; set; }
    public RelayCommand HeaderClickCommand { get; set; }
}

To call the command only when an item is selected, change this code:

<local:ExecuteCommandAction Command="{Binding HeaderClickCommand}"
    CommandParameter="{Binding IsSelected, RelativeSource={RelativeSource FindAncestor, AncestorType=TabItem}}"/>

And this (the second parameter is the CanExecute delegate, it checks IsSelected == true):

this.HeaderClickCommand = new RelayCommand<bool>(b => {/*???*/}, b => b == true);