0

I have a TabControl in my app. I'd like to have as many TabItems as many entries are in my dictionary.

Here's my dictionary:

public Dictionary<string , ObservableCollection<PerformanceCounter>> Counters
{
    get { return _Counters; }
}
Dictionary<string, ObservableCollection<PerformanceCounter>> _Counters = new Dictionary<string , ObservableCollection<PerformanceCounter>>();

Every entry has a string key and ObservableCollection of PerformanceCounter objects. Important thing is the fact that every PerformanceCounter object has properties: CounterName and InstanceName - I'll need these two to display them.

Now, to my XAML:

<TabItem Header="Memory">
    <Grid Name="RAMGrid">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="1*"/>
            <RowDefinition  Height="Auto"/>
        </Grid.RowDefinitions>
        <ListBox Name="RAMListBox" Grid.Column="0" Grid.ColumnSpan="2" Grid.Row="0" ItemsSource="{Binding Memory, Mode=OneWay}" SelectionMode="Multiple" BorderThickness="1" BorderBrush="#FF8B8B8B" SelectionChanged="RAMListBox_SelectionChanged">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <TextBlock>
                        <Run Text="{Binding CounterName, Mode=OneWay}" />
                        <Run Text="{Binding InstanceName, Mode=OneWay}" />
                    </TextBlock>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>                                         
        <Button Name="RAMSelectAllButton" Margin="0,10,0,0" Grid.Column="0" Grid.Row="1" Click="RAMSelectAllButton_Click" >
            <TextBlock Text="SELECT ALL"/>
        </Button>
        <Button Name="RAMUnSelectAllButton" Margin="0,10,0,0" Grid.Column="1" Grid.Row="1" Click="RAMUnSelectAllButton_Click" >
            <TextBlock Text="UNSELECT ALL"/>
        </Button>
    </Grid>
</TabItem>

That's what I did and, as you might already know, it does not work. The above code is only for one entry of my dictionary, where the key is "Memory".

In my code I set DataContext:

this.DataContext = appData.Counters;

appData.Counters is that dictionary I presented at the beginning.

Here's what I'd like to achieve: No matter how many entries there are in my dictionary, my TabControl would display TabItem for each of them. Each TabItem has a ListBox and 2 buttons. I'll need too be able to access those (in order to clear the list and to have click event for each button).

I really don't know how to do it, I hope you can help me out.

Imran Sh
  • 1,623
  • 4
  • 27
  • 50
mnj
  • 2,539
  • 3
  • 29
  • 58
  • Is the number of items in your dictionary going to change at runtime? – Arie Sep 08 '16 at 06:32
  • No, it's not gonna change – mnj Sep 08 '16 at 06:34
  • then check this out: https://stackoverflow.com/questions/2981046/wpf-binding-to-items-within-a-dictionary-by-key – Arie Sep 08 '16 at 06:34
  • I would be very grateful if someone just told me how to bind to my dictionary correctly (even without datatemplate - for now i can manually add as many TabItems as I need). That's most important for me. – mnj Sep 08 '16 at 06:35
  • @Arie I tried, that. here's what I did: I bound my ListBox to Counters. Then in my textBlock I did this: This example is for the key "Processor" in my dictionary. It didn't work - listbox is empty – mnj Sep 08 '16 at 06:41
  • I managed to display the data correctly. i bound ListBox to Path=Counters[processor]. Then my TextBlock: – mnj Sep 08 '16 at 07:09
  • Now, there is a problem: turns out number of items in dictionary is going to be changed during runtime. Items will be added to dictionary once the button is clicked. Currently my solution works only, if the items are added to the dictionary right after i start the program. I'd like to be able to display the items after clicking the button. – mnj Sep 08 '16 at 07:11

3 Answers3

1

Binding TabControl to items in Dictionary:

<Window.Resources>

    <DataTemplate x:Key="templateForTheContent" >
        <StackPanel>
            <ListBox Grid.Column="0" Grid.ColumnSpan="2" Grid.Row="0" 
                     ItemsSource="{Binding Value, Mode=OneWay}"
                     SelectionMode="Multiple"
                         BorderThickness="1" BorderBrush="#FF8B8B8B">
                <ListBox.ItemTemplate>
                    <DataTemplate>
                        <TextBlock>
                                    <Run Text="{Binding CounterName, Mode=OneWay}" />
                                    <Run Text="{Binding InstanceName, Mode=OneWay}" />
                        </TextBlock>
                    </DataTemplate>
                </ListBox.ItemTemplate>
            </ListBox>
        </StackPanel>
    </DataTemplate>

    <DataTemplate x:Key="templateForTheHeader" >
        <TextBlock Text="{Binding Key}"/>
    </DataTemplate>

</Window.Resources>
<Grid>
    <TabControl TabStripPlacement="Left"  VerticalAlignment="Stretch" HorizontalContentAlignment="Stretch"
        ItemsSource="{Binding Counters}"
        ContentTemplate="{StaticResource templateForTheContent}"
        ItemTemplate="{StaticResource templateForTheHeader}">
    </TabControl>
</Grid>

Now, Dictionary is not observable so if items will be added/removed during runtime, you may consider using something like ObservableDictionary instead

Arie
  • 5,251
  • 2
  • 33
  • 54
  • is there an alternative to using ObservableDictionary? I tried using it, but I get exception that leads to the code of ObservableDictionary class. Maybe there is a possibility to manually trigger change notification each time i add something to the dictionary? – mnj Sep 08 '16 at 07:37
  • Sure, you simply need to implement INotifyCollectionChanged-interface. This can be done inheriting from say a List. Nice side-effect: you'll end up having all the nice LINQ-extensions that the basic ObservableCollection does not have. – Sancho Panza Sep 08 '16 at 07:47
  • @Loreno are you positive you have to use a dictionary? Somtimes is simpler to just use the ObservableCollection instead, or something else that implements INotifyCollectionChanged – Arie Sep 08 '16 at 09:09
  • I changed my code a little bit, so that now i don't use Dictionary. Now I have ObservableColection> – mnj Sep 09 '16 at 05:48
  • I'll try to apply the above solution to my current situation, but I hope someone could help me with that. Binding is really hard for me – mnj Sep 09 '16 at 05:49
  • I tried to use your solution (with my current ObservableColection>) and it doesn't fully work. i changed binding in templateForTheHeader to CategoryName (it's a property inside every PerformanceCounter object) and it displays correctly. If I have 2 entries in my Counters ObservableCollection it displays two tabs with correct names. But the content is empty. I bound my ListBox to Counters and TextBlock to CounterName and instanceName. – mnj Sep 09 '16 at 06:10
  • Sorry for so many comments, but I DID IT!!! I'll put my solution as another answer. I'll mark your answer as a solution, because your code really helped me. If you have time, could you look at my answer? – mnj Sep 09 '16 at 06:20
  • Add the property IsSelected to your PerformanceCounter class, then bind it to IsSelected property of your listbox as is shown here http://stackoverflow.com/a/803256/891715 – Arie Sep 09 '16 at 07:15
  • @Arie, I actually managed to get the selected items from each listbox (I used event SelectionChanged and from that i cast sender object to the ListBox and get all the selections). However, I have another problem: whenever I switch tabs in my TabControl - selections in the previos tab's ListBox are reset - so when i get back to the previous Tab the ListBox doesn't have any selections. My current XAML is below in my answer. And another thing; PerformanceCounter is not my class - it's a part of .NET, so i can't add anything to it (it's sealed) – mnj Sep 09 '16 at 08:07
  • That's why I suggested binding to IsSelected property. The selection won't be reset then as long as IsSelected=true. If the PerformanceCounter class is sealed, you may use a wrapper class around it (remember to implement INotifyPropertyChanged): class MyPerformanceCounter{public PerformanceCounter Value{get;set;} public bool IsSelected{get;set;}} – Arie Sep 09 '16 at 10:04
0

Create a ViewModel-class containing:

  1. your Dictionary
  2. two ICommand-Implementations for your Buttons

then

  1. set the ViewModel-class as DataContext of the TabControl
  2. set Counters as the ItemSource of the TabControl
  3. reuse your XAML-Code defined within the TabItem and use it as the Tabcontrol.ContentTemplate
  4. Bind .Command of your Buttons to the ICommands in your ViewModel using RelativeSource

see for samples:

  1. ContentTemplate: https://wpf.2000things.com/tag/tabcontrol/
  2. ICommand https://stackoverflow.com/a/1468830/4919708
  3. RelativeSource: https://stackoverflow.com/a/84317/4919708
Community
  • 1
  • 1
Sancho Panza
  • 670
  • 4
  • 11
  • I don't use MVVM - I don't even really know what it is, but I see suggestions like this everywhere, so eventually I'll have to read something about it. – mnj Sep 09 '16 at 05:50
0

As i said in one of the comments above I changed my Dictionary to this:

//list of all counters
        public ObservableCollection<ObservableCollection<PerformanceCounter>> Counters
        {
            get { return _Counters; }
        }
        ObservableCollection<ObservableCollection<PerformanceCounter>> _Counters = new ObservableCollection<ObservableCollection<PerformanceCounter>>();    

i used @Arie's solution to write this XAML:

            <DataTemplate x:Key="templateForTheContent" >
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="*"/>
                    <ColumnDefinition Width="*"/>
                </Grid.ColumnDefinitions>
                <Grid.RowDefinitions>
                    <RowDefinition Height="1*"/>
                    <RowDefinition  Height="Auto"/>
                </Grid.RowDefinitions>
                <ListBox Grid.Column="0" Grid.ColumnSpan="2" Grid.Row="0" 
                         ItemsSource="{Binding}"
                         SelectionMode="Multiple"
                             BorderThickness="1" BorderBrush="#FF8B8B8B">
                    <ListBox.ItemTemplate>
                        <DataTemplate>
                            <TextBlock>
                                        <Run Text="{Binding CounterName, Mode=OneWay}" />
                                        <Run Text="{Binding InstanceName, Mode=OneWay}" />
                            </TextBlock>
                        </DataTemplate>
                    </ListBox.ItemTemplate>
                </ListBox>
                <Button Name="RAMSelectAllButton" Margin="0,10,0,0" Grid.Column="0" Grid.Row="1"  >
                    <TextBlock Text="SELECT ALL"/>
                </Button>
                <Button Name="RAMUnSelectAllButton" Margin="0,10,0,0" Grid.Column="1" Grid.Row="1" >
                    <TextBlock Text="UNSELECT ALL"/>
                </Button>
            </Grid>
        </DataTemplate>

            <DataTemplate x:Key="templateForTheHeader" >
                <TextBlock Text="{Binding CategoryName}"/>
            </DataTemplate>

        </Window.Resources>

It displays correctly as many Tabs as I add entries to my Class ObservableCollection in the code behind.

Now i have a new problem: I don't know how to access each listBox from each Tab. i need to be able to read the list of selected objects.

mnj
  • 2,539
  • 3
  • 29
  • 58