Search code examples
c#wpfxamlmvvmrelaycommand

TabControl with Closable TabItem Header


I'm trying to create TabItem Headers with Buttons that enable the User to close tabs. The visual representation and the Databinding of the object is just fine.

I've experimented with the DataContext, but so far I haven't found a workable solution.

My XAML:

<TabControl     
                    Grid.Column="3" 
                    Grid.Row="2"
                    x:Name="TabControlTargets" 
                    ItemsSource="{Binding Path=ViewModelTarget.IpcConfig.DatabasesList, UpdateSourceTrigger=PropertyChanged}"
                    SelectedItem="{Binding Path=ViewModelTarget.SelectedTab, UpdateSourceTrigger=PropertyChanged}">
                        <TabControl.ItemTemplate>
                            <DataTemplate>
                                <StackPanel Orientation="Horizontal" HorizontalAlignment="Left">
                                    <TextBlock FontFamily="Calibri" FontSize="15" FontWeight="Bold" Foreground="{Binding FontColor}" Text="{Binding Name, UpdateSourceTrigger=PropertyChanged}" HorizontalAlignment="Center" Margin="0,0,20,0"/>
                                    <Button HorizontalAlignment="Left" DataContext="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=DataContext}" Command="{Binding Path = ViewModelTarget.buttonRemoveDatabaseCommand}" 
                                            CommandParameter="**?**"
                                        >
                                        <Button.Content>
                                            <Image Height="15" Width="15" Source="pack://application:,,,/Images/cancel.png" />
                                        </Button.Content>
                                    </Button>
                                </StackPanel>
                            </DataTemplate>

I have trouble figuring out how to set the CommandParameter of my button so that it refers to the correct object.

Here is my RelayCommand:

    public ICommand buttonRemoveDatabaseCommand
    {
        get
        {
            if (_buttonRemoveDatabaseCommand == null)
            {
                _buttonRemoveDatabaseCommand = new RelayCommand(
                    param => RemoveDatabase(param)
                    );
            }
            return _buttonRemoveDatabaseCommand;
        }
    }

And here is my RemoveDatabase function:

public void RemoveDatabase(object dB)
    {
        this.IpcConfig.RemoveDataBase((PCDatabase)dB);
    }

I would strongly prefer a solution that sticks to my "no code behind" approach.


Solution

  • As pointed in the comments, you can use CommandParameter="{Binding}" to pass the TabItem context to the command.

    A better approach is though to move the command to the ViewModel of your TabItem.

    Here an example implementation using Prism and Prism's EventAggregator. You can of course implement this with every other MVVM Framework or even implement it yourself, but that's up to you.

    This would be your TabControl ViewModel, which contains a list of all databases or whatever it's meant to represent.

    public class DatabasesViewModel : BindableBase
    {
        private readonly IEventAggregator eventAggregator;
    
        public ObservableCollection<DatabaseViewModel> Databases { get; private set; }
        public CompositeCommand CloseAllCommand { get; }
    
        public DatabasesViewModel(IEventAggregator eventAggregator)
        {
            if (eventAggregator == null)
                throw new ArgumentNullException(nameof(eventAggregator));
    
            this.eventAggregator = eventAggregator;
    
            // Composite Command to close all tabs at once
            CloseAllCommand = new CompositeCommand();
            Databases = new ObservableCollection<DatabaseViewModel>();
    
            // Add a sample object to the collection
            AddDatabase(new PcDatabase());
    
            // Register to the CloseDatabaseEvent, which will be fired from the child ViewModels on close
            this.eventAggregator
                .GetEvent<CloseDatabaseEvent>()
                .Subscribe(OnDatabaseClose);
        }
    
        private void AddDatabase(PcDatabase db)
        {
            // In reallity use the factory pattern to resolve the depencency of the ViewModel and assing the
            // database to it
            var viewModel = new DatabaseViewModel(eventAggregator)
            {
                Database = db
            };
    
            // Register to the close command of all TabItem ViewModels, so we can close then all with a single command
            CloseAllCommand.RegisterCommand(viewModel.CloseCommand);
    
            Databases.Add(viewModel);
        }
    
        // Called when the event is received
        private void OnDatabaseClose(DatabaseViewModel databaseViewModel)
        {
            Databases.Remove(databaseViewModel);
        }
    }
    

    Each tab would get one DatabaseViewModel as it's context. This is where the close command is defined.

    public class DatabaseViewModel : BindableBase
    {
        private readonly IEventAggregator eventAggregator;
    
        public DatabaseViewModel(IEventAggregator eventAggregator)
        {
            if (eventAggregator == null)
                throw new ArgumentNullException(nameof(eventAggregator));
    
            this.eventAggregator = eventAggregator;
            CloseCommand = new DelegateCommand(Close);
        }
    
        public PcDatabase Database { get; set; }
    
        public ICommand CloseCommand { get; }
        private void Close()
        {
            // Send a refence to ourself
            eventAggregator
                .GetEvent<CloseDatabaseEvent>()
                .Publish(this);
        }
    }
    

    When you click the close Button on the TabItem, then CloseCommand would be called and send an event, that would notify all subscribers, that this tab should be closed. In the above example, the DatabasesViewModel listens to this event and will receive it, then can remove it from the ObservableCollection<DatabaseViewModel> collection.

    To make the advantages of this way more obvious, I added an CloseAllCommand, which is a CompositeCommand that registers to each DatabaseViewModels CloseCommand as it's added to the Databases observable collection, which will call all registered commands, when called.

    The CloseDatabaseEvent is a pretty simple and just a marker, that determines the type of payload it receives, which is DatabaseViewModel in this case.

    public class CloseDatabaseEvent : PubSubEvent<DatabaseViewModel> { }
    

    In real-world applications you want to avoid using the ViewModel (here DatabaseViewModel) as payload, as this cause tight coupling, that event aggregator pattern is meant to avoid.

    In this case it's can be considered acceptable, as the DatabasesViewModel needs to know about the DatabaseViewModels, but if possible it's better to use an ID (Guid, int, string).

    The advantage of this is, that you can also close your Tabs by other means (i.e. menu, ribbon or context menus), where you may not have a reference to the DatabasesViewModel data context.