1

I'm working on a project which contains a TabControl. In each of the TabItems there is a DataGrid which is bound to an ObservableCollection in my view model.

I need to bind the Edit Button to the DataGrid which is focused at the moment (TabItem is focused). Is it possible to accomplish this fairly easy without having multiple buttons that are "hard" coded to one DataGrid/TabItem and using the MVVM pattern?

So basically this order: TabControl -> Selected TabItem -> DataGrid -> SelectedItem

Tabcontrol with Datagrids and Button

Example XAMLcode (basically real format without styling etc.):

<Button Content="Edit"
        Command="{Binding ExecuteEditMessmittelCommand}"
        CommandParameter="{Binding ElementName=Messmittel_DataGrid, Path=SelectedItem}">
</Button>

<TabControl>

   <TabItem Header="Messmittel">
             <DataGrid x:Name="Messmittel_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>
   </TabItem>

   <TabItem Header="Mechanik">
             <DataGrid x:Name="Mechanik_Datagrid"
                       ItemsSource="{Binding MechanikDisplayCollection}">
                //here is the datagrid content
             </DataGrid>
   </TabItem>

   <TabItem Header="Prüfhilfsmittel">
             <DataGrid x:Name="Pruefhilfsmittel_Datagrid"
                       ItemsSource="{Binding PruefhilfsmittelDisplayCollection}">
                //here is the datagrid content
             </DataGrid>
   </TabItem>

</TabControl>

View model (SetProperty just triggers INotifyPropertyChanged and sets the value):

public ObservableCollection<MessmittelModel> MessmittelDisplayCollection
{
        get { return DatabaseDisplayModel.MessmittelCollection; }
        set { SetProperty(ref DatabaseDisplayModel.MessmittelCollection, value);}
}

public ObservableCollection<MessmittelModel> MechanikDisplayCollection
{
        get { return DatabaseDisplayModel.MechanischeMessmittelCollection; }
        set { SetProperty(ref DatabaseDisplayModel.MechanischeMessmittelCollection, value); }
}
public ObservableCollection<MessmittelModel> PruefhilfsmittelDisplayCollection
{
        get { return DatabaseDisplayModel.PruefhilfsmittelCollection; }
        set { SetProperty(ref DatabaseDisplayModel.PruefhilfsmittelCollection, value); }
}

I don't think that my view model is important in this case, but if you need further information, just tell me and I will provide it.

thatguy
  • 13,242
  • 6
  • 19
  • 33
LYCRIs
  • 40
  • 8
  • Do you have view models for your tab items or just one view model for the whole view? What are the `ItemsSource`s of the `DataGrid`s bound to? They are not bound in the XAML that you have provided. – thatguy Aug 31 '20 at 10:44
  • The provided xaml is just an example which should show how it works. Each Datagrid is bound to an List inside the Viewmodel of the whole Window, so there is only 1 Viewmodel I will edit the itemsource so its better to understand – LYCRIs Aug 31 '20 at 10:51

3 Answers3

2

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 DataContexts 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.

marc_s
  • 675,133
  • 158
  • 1,253
  • 1,388
thatguy
  • 13,242
  • 6
  • 19
  • 33
  • I have added another variant using a multi-value converter that does not require you to change your view model and is MVVM compliant and reusable, too. – thatguy Aug 31 '20 at 13:59
  • Wow, thats alot of work you put into this, thank you very much for that. So I've used the convert option for now until I need it to be non static, but as I'm not planing on adding more Items to that TabControl, it's not necessary for me to make it dynamically: Still I will keep them in mind for future projects :) – LYCRIs Sep 01 '20 at 06:29
0

Multiple ways of doing this this is an easy fast one

"Switch" with cheeking which tab is Selected

Button_OnButtonClicked(//eventargs){
   DataGrid dgridToEdit = null;
   if(TabControl.SelectedTabIndex == 0){
      dgridToEdit = firstDGrid;
   }
   else if(TabControl.SelectedTabIndex == 1){
      dgridToEdit = secondDGrid;
   }
   else if(TabControl.SelectedTabIndex == 2){
      dgridToEdit = thirdDGrid;
   }


   //... then do your things with the dGrid
   }

other ways of doing it would be, onTabChanged event then Referencing the DataGrid on the new Tab or a method which gives you the current focused DataGrid (like my example) and so on. I hope i could help you.. good luck !

  • Thank you for you answer. The problem is, that this is in code behind and in order to use it in MVVM I need to get the object of the selected item and therefore selected datagrid as the CommandParameter I guess this wont work with your solution does it? – LYCRIs Aug 31 '20 at 10:27
  • I dont actually see the Problem, but you could make a method returning a reference to the DataGrid, if u have no way of communicating between your codeworks, than the wont be code to help obviously. But you spoke as your button does reference to a DataGrid and you just need to change it. – Nico Walsemann Aug 31 '20 at 10:37
  • The problem in my opinion is, that even though I technically know which datagrid is in focus, I need to set the commandparameter to that. I mean I could set a property inside the Viewmodel as the object of the selected item of the focused tabitem because the object of the viewmodel can be accessed by the code behind, but I guess that would be against the MVVM Pattern isnt it? – LYCRIs Aug 31 '20 at 10:59
  • Depends, on how you see it, you could see it as an interpretation of making the back -end easier to develop, if its "just" the target for a button, there is much more you have to do for it to be mvvm. So its on you how you wanna do it. These "patterns" are also just a guideline for what is efficent and easy to handle for larger teams (which seperate back and front end) our Team also goes after the MVVM pattern but as we all do front and back and, some little tweaks which give us quality of life are allowed. – Nico Walsemann Aug 31 '20 at 11:33
  • Yeah true, I guess I need a mixture of both anyways as it will probably not work how I want it to be. But thank you anyways for your help and time. My company does both aswell, but code behind is not welcomed that much :/ – LYCRIs Aug 31 '20 at 11:38
  • [link](https://stackoverflow.com/questions/667781/what-is-the-difference-between-mvc-and-mvvm) I know your struggle but that post even explains that there always will be a need for Controlers, sometimes more than asking if something is okay is not needed to get a way how its should be done/ having it be allowed – Nico Walsemann Aug 31 '20 at 11:42
0

One possible MVVM-friendly solution to this problem would be using the IsSelected property of TabItems:

<!-- language: lang-xml -->
<Button Content="Edit"
        Command="{Binding ExecuteEditCommand}">
</Button>

<TabControl>

   <TabItem Header="Messmittel"
            IsSelected={Binding IsMessmittelSelected}>
             <DataGrid x:Name="Messmittel_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>
   </TabItem>

   <TabItem Header="Mechanik"
            IsSelected={Binding IsMechanikSelected}>
             <DataGrid x:Name="Mechanik_Datagrid"
                       ItemsSource="{Binding MechanikDisplayCollection}">
                //here is the datagrid content
             </DataGrid>
   </TabItem>

   <TabItem Header="Prüfhilfsmittel"
            IsSelected={Binding IsPrüfhilfsmittelSelected}>
             <DataGrid x:Name="Pruefhilfsmittel_Datagrid"
                       ItemsSource="{Binding PruefhilfsmittelDisplayCollection}">
                //here is the datagrid content
             </DataGrid>
   </TabItem>

</TabControl>
// DataContext above control is bound to:
// This method would be used by ExecuteEditCommand
private void OnExecuteEdit()
{
    if (IsMessmittelSelected)
    {
        // IsMessmittelSelected logic
    }
    else if (IsMechanikSelected)
    {
        // IsMechanikSelected logic
    }
    else if (IsPrüfhilfsmittelSelected)
    {
        // IsPrüfhilfsmittelSelected logic
    }
}
lv1
  • 56
  • 6
  • 1
    Thank you for your reply. Therefore I need to use the selectedItem property of the datagrids aswell which is not what I really wanted in my code, but thats alright. Even though its not as "pretty" as using CommandParameter, I guess this is one of the best options I will get so I will use it for now :) So thank your for helping I will not mark it as the answer for now because maybe someone who is really good at this stuff knows an answer, but I will do it later if there is no other reply :) – LYCRIs Aug 31 '20 at 11:31
  • You are welcome! Yep, I agree it's not too pretty - let's see if someone comes up with a more elegant answer. – lv1 Aug 31 '20 at 12:25