0

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.

Fang
  • 2,199
  • 4
  • 23
  • 44
  • Can you show your viewmodel – Kasper Due Feb 27 '16 at 18:58
  • What about the `SelectedItem`? What is wrong with it? – Eugene Podskal Feb 27 '16 at 19:03
  • @Eugene Podksal Using SelectedItem was my first approach. However, that will always close the Tab that is Selected (i.e. the active Tab), which is not necessarily the tab that should be closed. – Fang Feb 27 '16 at 19:04
  • @Fang: Any reason you are not adding the `buttonRemoveDatabaseCommand` to your TabItem's viewmodel? Then you'll have the reference (the ViewModel itself). You can use a messanger/event aggregator to notify the viewmodel with the collection – Tseng Feb 27 '16 at 19:07
  • @Tseng: I hope I understand you correctly. I'm not exactly proficient in WPF. The buttonRemoveDatabaseCommand is part of my ViewModel. What I need is the CommandParameter to reference the Object that the Tab represents. – Fang Feb 27 '16 at 19:14
  • @Fang Hmm, I thought that the tab becomes Selected before the button click is handled. Seems I was wrong. – Eugene Podskal Feb 27 '16 at 19:17
  • 1
    Yes, but why not move it to the ViewModel of the tabitem rather than the viewmodel of the tabcontrol? Other than that, you pass the tabitems context as parameter via `CommandParameter={Binding}`. An empty `{Binding}` always refer to the current datacontext – Tseng Feb 27 '16 at 19:17
  • @Tseng: I've created a single ViewModel for this application. That ViewModel holds a reference to an object that holds an Observable Collection of Databases. Are you suggesting to create one ViewModel for the Tabcontrol and another ViewModel for TabItems? Would you be so kind to show a rough draft of how this is supposed to look like? – Fang Feb 27 '16 at 19:27
  • @Fang: Yes. The content of the TabItem is considered a view of it's own, so it only makes sense to have your own ViewModel for it. – Tseng Feb 27 '16 at 20:25
  • @Tseng: Thanks a lot. It will take some time to digest that answer of yours though. – Fang Feb 27 '16 at 20:30
  • MVVM can be rough, as to fully utilize it you need more than just XAML, `INotifyPropertyChanged` and `ICommand`, like dependency injection, event aggregators, navigation services and much more :P – Tseng Feb 27 '16 at 20:39

1 Answers1

1

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.

Tseng
  • 61,549
  • 15
  • 193
  • 205