Removing tabs from a TabControl: Error 4 cannot find source for binding

51 views Asked by At

I have a TabControl with a special "+ tab" that is defined as such:

<TabControl x:Name="ProjectsTabControl">
   <local:ProjectTabItem x:Name="AddProjectTabTabItem" Header="+" MouseUp="AddProjectButton_Click" PreviewMouseDown="AddProjectTabTabItem_PreviewMouseDown"/>
</TabControl>

"MouseDown" simply marks the mouse event args as handled (in order to prevent +the tab from being selected). "Button_Click" adds a new tab with the following style:

<Window.Resources>
    <Style x:Key="CloseableTabItemStyle" TargetType="local:ProjectTabItem">
        <Setter Property="HeaderTemplate">
            <Setter.Value>
                <DataTemplate>
                    <DockPanel>
                        <TextBlock Text="{Binding}" Margin="0,0,5,0"/>
                        <Button Content="X" Click="CloseButton_Click" Tag="{Binding RelativeSource={RelativeSource AncestorType={x:Type local:ProjectTabItem}}}"/>
                    </DockPanel>
                </DataTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</Window.Resources>

These are my "actual" tabs that I use. Each such tab has a "close button" in its header. As you can see, the close button's tag is set to a ProjectTabItem ancestor.

My CloseButton logic is as follows:

private async void CloseButton_Click(object sender, RoutedEventArgs e)
{
    if (sender is Button closeButton && closeButton.Tag is ProjectTabItem tabItem)
    {
        ProjectsTabControl.Items.Remove(tabItem);
        if (ProjectsTabControl.SelectedIndex == ProjectsTabControl.Items.Count - 1)
        {
            ProjectsTabControl.SelectedIndex--;
        }
    }
}

The idea here is to prevent the +tab from being selected (however, the issue that I will soon describe happens even if I let the +tab be selected).

When I close the last tab (i.e., the last tab before the +tab), I see the following error

System.Windows.Data Error: 4 : Cannot find source for binding with reference 'RelativeSource FindAncestor, AncestorType='System.Windows.Controls.TabControl', AncestorLevel='1''. BindingExpression:Path=TabStripPlacement; DataItem=null; target element is 'ProjectTabItem' (Name=''); target property is 'NoTarget' (type 'Object')

This only happens when the tab is selected when closing it.

It seems like if I preemptively open a new tab before closing the "last" one, the issue does not happen. (However, this does not help me as I would like to allow having no tabs at all, except the +tab.)

What is causing this? How do I fix it?

Edit: I've found a, I guess, workaround here. Setting tabItem.Template = null; before removing the tab actually fixes the issue. So let me change my question to: why does this happen? and is this "solution" actually a solution or does it simply supresses some underlying important issue in my code?

1

There are 1 answers

0
RockingRoli On

You will never have fun with WPF as long as you use code-behind. You will always step in the 'workaround - trap'. If you follow the MVVM-Pattern, things will get much easier.

I just created in my app a TabControl wich is bound to an Observable Collection

In this Observable Collection are TabItemModels:

public class TabItemModel : ViewModelBase
{
    private string _header;
    private ObservableObject _content;

    public string Header
    {
        get { return _header; }
        set
        {
            _header = value;
            RaisePropertyChanged(nameof(Header));
        }
    }
    public ObservableObject Content
    {
        get { return _content; }
        set
        {
            _content = value;
            RaisePropertyChanged(nameof(Content));
        }
    }

    private readonly IMessenger _messenger;
    public RelayCommand CloseTabCommand { get; set; }

    public TabItemModel(IMessenger messenger)
    {
        _messenger = messenger;
        CloseTabCommand = new RelayCommand(CloseThisTab);
    }

    private void CloseThisTab()
    {
        _messenger.Send(this);
    }
}

Here the XAML - Part of this TabControl:

        <TabControl Grid.Column="1" Grid.ColumnSpan="2" 
                Grid.Row="1" Grid.RowSpan="2"
                ItemsSource="{Binding TabItems.Models}"
                SelectedItem="{Binding ActiveTab, Mode=TwoWay}">
        <TabControl.ItemTemplate>
            <DataTemplate>
                <Border BorderBrush="Black" BorderThickness="2" CornerRadius="5" Margin="10,0,0,0">
                    <Grid Width="auto">
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="*"/>
                            <ColumnDefinition Width="9*"/>
                        </Grid.ColumnDefinitions>
                        <TextBlock Text="{Binding Header}"
                                      FontSize="12" FontWeight="DemiBold"
                                      HorizontalAlignment="Left"
                                      VerticalAlignment="Center"
                                      Margin="12,0"
                                      Width="auto"/>

                        <Button Grid.Column="1"
                                   Foreground="White" Command="{Binding CloseTabCommand}"
                                   HorizontalAlignment="Right"
                                   Padding="5"
                                   VerticalContentAlignment="Center"
                                   Margin="5">
                            <Button.Content>
                                <Image Source="/icon-delete.png" Height="12" Width="12"/>
                            </Button.Content>

                        </Button>
                    </Grid>
                </Border>
            </DataTemplate>
        </TabControl.ItemTemplate>

Declarations in MainViewModel:

    [ObservableProperty]
    TabItemViewModel tabItems;

    [ObservableProperty]
    TabItemModel activeTab;

The button in XAML triggers the Relay-Command in the TabItemModel wich sends a message to the View-Model to close the tab. I just remove this particular Model from the Collection:

        public void Handle(TabItemModel message)
    {
        if (TabItems.Models.Count > 0)
        {
            TabItems.Models.Remove(message);
        }
    }

Works fine for me.