1
votes

I'm attempting to implement this question: Closing TabItem by clicking middle button

However, the e.Source property of CloseCommandExecuted() returns a TabControl object. This cannot be used to determine which TabItem was clicked. e.OriginalSource returns the Grid that's defined just inside of the datatemplate. Finally, following the parents upward from original source never leads to a TabItem object.

How can I get the TabItem the user clicked on?

Edit: In my case I'm binding objects via the ItemsSource.

//Xaml for the Window
<Window.Resources>
    <DataTemplate x:Key="closableTabTemplate">
        <Border x:Name="testBorder">
            <Grid>
                <Grid.InputBindings>
                    <MouseBinding Command="ApplicationCommands.Close" Gesture="MiddleClick" />
                </Grid.InputBindings>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="*" />
                </Grid.ColumnDefinitions>
                <Grid>
                    <TextBlock Text="{Binding Headertext}"></TextBlock>
                </Grid>
            </Grid>
        </Border>
    </DataTemplate>
</Window.Resources>
<Window.CommandBindings>
    <CommandBinding Command="ApplicationCommands.Close" Executed="CloseCommandExecuted" CanExecute="CloseCommandCanExecute" />
</Window.CommandBindings>
<Grid>
    <TabControl x:Name="MainTabControl" ItemTemplate="{StaticResource closableTabTemplate}" Margin="10">
        <TabControl.ItemContainerStyle>
            <Style TargetType="TabItem">
                <!--<Setter Property="Header" Value="{Binding ModelName}"/>-->
                <Setter Property="Content" Value="{Binding Content}"/>
            </Style>
        </TabControl.ItemContainerStyle>
    </TabControl>
</Grid>

The object I'm binding. It's a sample specific for this question

public class TabContent
{
    public string Headertext { get; set; }
    public FrameworkElement Content = null;
}

The main window code

public partial class MainWindow
{
    public ObservableCollection<TabContent> MyCollection = new ObservableCollection<TabContent>();

    public MainWindow()
    {
        MyCollection.Add(new TabContent { Headertext = "item1" });
        MyCollection.Add(new TabContent { Headertext = "item2" });
        MyCollection.Add(new TabContent { Headertext = "item3" });

        InitializeComponent();

        MainTabControl.ItemsSource = MyCollection;
        MainTabControl.SelectedIndex = 0;
    }

    private void CloseCommandExecuted(object sender, ExecutedRoutedEventArgs e)
    {
        //Need some way to access the tab item or item bound to tab item

        //if (tabitem != null)
        //{
        //    //MainTabControl.Items.Remove(tabitem);
        //}
    }

    private void CloseCommandCanExecute(object sender, CanExecuteRoutedEventArgs e)
    {
        e.CanExecute = true;
    }
}
3

3 Answers

2
votes

Based on the code you added to your question, you could do something like this:

private void CloseCommandExecuted(object sender, ExecutedRoutedEventArgs e)
{           
    DependencyObject dep = (DependencyObject)e.OriginalSource;
    // Traverse the visual tree looking for TabItem
    while ((dep != null) && !(dep is TabItem))
        dep = VisualTreeHelper.GetParent(dep);

    if (dep == null)
    {
        // Didn't find TabItem
        return;
    }

    TabItem tabitem = dep as TabItem;
    if (tabitem != null)
    {
        TabContent content = tabitem.Header as TabContent;
        if(content !=null)
            MyCollection.Remove(content);               
    }
}

That gives you both the TabItem and the TabContent object that it is bound to.

0
votes

I just put this sample together, in the xaml, an empty TabControl

<TabControl x:Name="MyTabControl">            
</TabControl>

Assuming you are loading your TabItem objects at runtime, I added this to the constructor of the Window

public MainWindow()
{
    InitializeComponent();

    // Add some sample tabs to the tab control
    for (int i = 0; i < 5; i++)
    {
        TabItem ti = new TabItem() { Header = String.Format("Tab {0}", i + 1) };
        ti.PreviewMouseDown += ti_PreviewMouseDown;
        MyTabControl.Items.Add(ti);
    }
}

Or if you want to have your tabs defined in the xaml

<TabControl x:Name="MyTabControl">
    <TabItem Header="Tab 1" PreviewMouseDown="ti_PreviewMouseDown"></TabItem>
    <TabItem Header="Tab 2" PreviewMouseDown="ti_PreviewMouseDown"></TabItem>
    <TabItem Header="Tab 3" PreviewMouseDown="ti_PreviewMouseDown"></TabItem>
    <TabItem Header="Tab 4" PreviewMouseDown="ti_PreviewMouseDown"></TabItem>
    <TabItem Header="Tab 5" PreviewMouseDown="ti_PreviewMouseDown"></TabItem>
</TabControl>

Then in the event handler

 void ti_PreviewMouseDown(object sender, MouseButtonEventArgs e)
 {
      TabItem clickedTabItem = sender as TabItem;
      if (clickedTabItem != null)
      {               
           if (e.ChangedButton == MouseButton.Middle && e.ButtonState == MouseButtonState.Pressed)
           {
                // Do whatever you want to do with clickedTabItem here, I'm removing it from the TabControl
                MyTabControl.Items.Remove(clickedTabItem);
           }
      }
 }
0
votes

I think the MVVM way of doing this would be to add a button to each tab that, when clicked, would fire a command to close that tab or whatever. This is how Chrome works when closing tabs, for example. It's not the "middle-click" solution that you asked for, but could be modified to work that way with some extra work.

Anyway, you would add a command property to your TabContent class:

public class TabContent
{
    ...
    public RelayCommand CloseTabCommand { get; private set; }

    public TabContent()
    {
        CloseTabCommand = new RelayCommand(OnTabClosing);
    }

    public event EventHandler TabClosing;

    void OnTabClosing()
    {
        var handler = TabClosing;
        if (handler != null)
        {
            handler(this, EventArgs.Empty);
        }
    }
}

Here I'm using a RelayCommand which is just a class that implements ICommand: I'm sure you've seen or used similar types. When the command is executed it fires an event. When you create the TabItem objects you need to register to handle this event:

public MainWindow()
{
    MyCollection.Add(new TabContent { Headertext = "item1" });
    MyCollection.Add(new TabContent { Headertext = "item2" });
    MyCollection.Add(new TabContent { Headertext = "item3" });

    ...

    foreach (TabContent tab in MyCollection)
    {
        tab.TabClosing += OnTabClosing;
    }
}

Then, in the event handler, the sender parameter will reference the TabContent object that fired the event:

void OnTabClosing(object sender, EventArgs e)
{
    MyCollection.Remove(sender as TabContent);
}

Here I'm responding to that event by removing that tab from the collection.

On the XAML side, you just need to modify the data template to add a button control that will invoke the custom command via binding:

<DataTemplate x:Key="closableTabTemplate">
    <Button Command="{Binding CloseTabCommand}">
        <TextBlock Text="{Binding Headertext}"></TextBlock>
    </Button>
</DataTemplate>

Obviously you could change the template of the button to remove the borders and make it look less like a button, if that's what you wanted.

The only problem with this from your perspective is that the button will only execute the command in response to a single left-click. To get it to work with a middle-click instead you would need to replace the button element with something else (say, a Border) and use an attached behaviour to handle the mouse down event and act accordingly.