A Static MVVM Variant
I think you have to decompose your main view model. Your main view model contains lists that are bound by data grids inside of TabItems
. This view model gets bloated quickly and does not separate views and concerns well. Instead, you should have a main view model for the view containing the TabControl
and a separate view model for each unique view in the tab control.
In your example, one view model type is enough, as all three tabs contain the same controls and display just a list in a DataGrid
. This view model would expose the collection to be bound by the DataGrid
and a property for the current selection. You could assign the collection in many ways, set it from outside e.g. through the main view model, pass the collection through the constructor or passing in a service for that.
public class MessmittelViewModel : BindableBase
{
private MessmittelModel _selected;
private ObservableCollection<MessmittelViewModel> _messmittelModels;
// ...constructor, initialization of properties, other properties.
public MessmittelModel Selected
{
get => _selected;
set => SetProperty(ref _selected, value);
}
public ObservableCollection<MessmittelModel> MessmittelDisplayCollection
{
get => _messmittelModels;
set { SetProperty(ref _messmittelModels, value);
}
}
In your main view model, you could expose a view model property for each tab.
public class MainViewModel: BindableBase
{
private MessmittelViewModel _selectedViewModel;
private MechanischeMessmittel _mechanischeMessmittelViewModel;
// ...contructor, initialize properties, other code.
public MessmittelViewModel SelectedViewModel
{
get => _selectedViewModel;
set => SetProperty(ref _selectedViewModel, value);
}
public MechanischeMessmittelViewModel
{
get => _mechanischeMessmittelViewModel;
private set => SetProperty(ref _mechanischeMessmittelViewModel, value);
}
}
Then in your XAML bind the SelectedItem
of the TabControl
and the DataContext
s for the tabs.
<TabControl SelectedItem="{Binding SelectedViewModel}">
<!-- ...other content. -->
<TabItem Header="Mechanik"
DataContext={Binding MechanischeMessmittelViewModel}">
<DataGrid ItemsSource="{Binding MessmittelDisplayCollection}">
<!-- ...data grid content. -->
</DataGrid>
</TabItem>
</TabControl>
Now you can either bind the command parameter to the Selected
property...
<Button Content="Edit"
Command="{Binding ExecuteEditMessmittelCommand}"
CommandParameter="{Binding SelectedViewModel.Selected}"/>
...or access the selected view model in your MainViewModel
and get its Selected
list item.
var parameter = SelectedViewModel.Selected;
A Dynamic MVVM Variant
Exposing three static properties is not very extensible when you think about creating additional tabs that may contain different views, so I show you how to do this in a more dynamic fashion. Let's suppose you have created the MessmittelViewModel
as above and a FoobarMessmittelViewModel
. Create an ObservableCollection
of a base type, e.g. MessmittelViewModelBase
and a selected property as before.
public class MainViewModel: BindableBase
{
private MessmittelViewModelBase _selectedViewModel;
private ObservableCollection<MessmittelViewModelBase> _messmittelModels;
public MainViewModel()
{
// ...other code.
MessmittelViewModels = new ObservableCollection<MessmittelViewModelBase>();
MessmittelViewModels.Add(new MessmittelViewModel(DatabaseDisplayModel.MessmittelCollection));
// ...add view models for the other tabs.
}
// ...other code.
public MessmittelViewModelBase SelectedViewModel
{
get => _selectedViewModel;
set => SetProperty(ref _selectedViewModel, value);
}
public ObservableCollection<MessmittelViewModelBase> MessmittelViewModels
{
get => _messmittelModels;
set { SetProperty(ref _messmittelModels, value);
}
}
Bind the ItemsSource
to the MessmittelViewModels
collection and create a DataTemplate
that represents its view for each concrete view model type.
<TabControl ItemsSource="{Binding MessmittelViewModels}">
<TabControl.Resources>
<DataTemplate DataType="{x:Type MessmittelViewModel}">
<DataGrid ItemsSource="{Binding MessmittelDisplayCollection}">
<DataGrid.Columns>
<DataGridTextColumn Header="ID"
Binding="{Binding Path=Benutzer_ID}"
<DataGridTextColumn Header="Seriennummer"
Binding="{Binding Path=Seriennummer}"
<DataGridTextColumn Header="MessmittelArt"
Binding="{Binding Path=Vorname}"
</DataGrid.Columns>
</DataGrid>
</DataTemplate>
<!-- ...other data templates -->
</TabControl.Resources>
</TabControl>
That's it, the TabControl
will now create an appropriate view for each item in the view models collection based on its type. Since there are different types now, you would choose your paramter for the command (and maybe even if it is enabled for this tab) in code by checking the type.
if (SelectedViewModel is MessmittelViewModel messmittelViewModel)
{
var parameter = messmittelViewModel.Selected;
}
An MVVM Variant Using A Converter
For your current approach, you could also solve the issue in XAML only, using a multi-value converter. However, this is also a static variant. This converter will return a bound value based on the selected index in the tab control.
public class SelectedIndexBindingConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
return values[(int)values[0] + 1];
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new InvalidOperationException();
}
}
In XAML, you have to bind the CommandParameter
like this.
<Window.Resources>
<local:SelectedIndexBindingConverter x:Key="SelectedIndexBindingConverter"/>
</Window.Resources>
<Button Content="Edit"
Command="{Binding CommandTest}">
<Button.CommandParameter>
<MultiBinding Converter="{StaticResource SelectedIndexBindingConverter}">
<Binding Path="SelectedIndex" ElementName="MyTabControl"/>
<Binding Path="SelectedItem" ElementName="Messmittel_Datagrid"/>
<Binding Path="SelectedItem" ElementName="Mechanik_Datagrid"/>
<Binding Path="SelectedItem" ElementName="Pruefhilfsmittel_Datagrid"/>
</MultiBinding>
</Button.CommandParameter>
</Button>
<TabControl x:Name="MyTabControl">
<!-- ...tab items as before. -->
</TabControl>
You could also create a converter to walk the visual tree and get the selected item of the corresponding DataGrid
, without binding it, but that would assume a visual structure and this solution is more robust, as you specify the elements explicitly. In fact this converter is more flexible in that it allows you to bind the command parameter to any control with any property in a tab item.
Just as a note, you could achieve the same in XAML through triggers, too, but I think that would interfere too much with styling of controls and might be harder to reuse.