Expander with Virtualization inside WPF Datagrid - wpf

I have a performance issue in a prototype I am working on. The requirement is to build a datagrid with multiple synchronized frozen panes, supporting grouping and sorting etc... For more details about the grid I am building, see this previous question.
Now, I have a question related to Grouping and in particular Expanders. I have a GroupStyle defined by the following Xaml, and taken from this blog post.
<!--Default GroupStyle-->
<GroupStyle x:Key="gs_Default">
<GroupStyle.Panel>
<ItemsPanelTemplate>
<DataGridRowsPresenter/>
</ItemsPanelTemplate>
</GroupStyle.Panel>
<GroupStyle.HeaderTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Path=Name}" FontWeight="Bold" Padding="3"/>
</StackPanel>
</DataTemplate>
</GroupStyle.HeaderTemplate>
<GroupStyle.ContainerStyle>
<Style TargetType="{x:Type GroupItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type GroupItem}">
<Expander x:Name="exp"
BorderBrush="#FFA4B97F"
BorderThickness="0,0,0,1"
IsExpanded="{Binding Path=Items[0].IsExpanded}">
<Expander.Header>
<DockPanel TextBlock.FontWeight="Bold">
<TextBlock Text="{Binding Path=Name}" Margin="5,0,5,0" />
<TextBlock Text="{Binding Path=ItemCount}"/>
</DockPanel>
</Expander.Header>
<ItemsPresenter/>
</Expander>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</GroupStyle.ContainerStyle>
</GroupStyle>
The Expander is not virtualized and we are experiencing a performance issue when there are several hundred rows in a group.
Has anyone encoutered this before and have a fix? I am ideally looking for a Virtualizing Expander, and have seen workarounds such as this (which doesn't solve the problem).

If you're using .NET 4 then get the ItemsPresenters visibility to track/be dependent on the IsExpanded state.
WPF DataGrid Virtualization with Grouping
In .NET 4.5 there's a new attached property VirtualizingPanel.IsVirtualizingWhenGrouping.
Another alternative is to push the grouping into the ViewModel rather than making the DataGrid/CollectionView be responsible for that. See here:
http://blog.smoura.com/wpf-toolkit-datagrid-part-iv-templatecolumns-and-row-grouping/

In order to solve this, we did the following.
We used DataTemplates to present a different row view for different row viewmodels. We had a GroupRowViewModel and ItemRowViewModel. Also a parent ViewModel which had a sorted list of Group/Item viewmodels.
When the grid was instantiated, the parent ViewModel would sort all child viewmodels into the following:
Group
Item
Item
Item
Group
Item
Item
Item
When a GroupRow is clicked you want to execute some code where the parent (which contains a sorted list of group+item rows) will remove or include the items. E.g. say the second gropu was clicked, your list of rowviewmodels you bind to now becomes
Group
Item
Item
Item
Group (Collapsed)
That's it. So no magic at all, you manually remove or include the rows you want depending on what was clicked. It worked with virtualization and hundreds of thousands of rows at an acceptible speed.
Sorry I can't post any code (due to NDA) but I hope that helps you. Also - I would suggest looking at Telerik Grid as this is awesomely fast for large datasets

Related

How to retrieve VisualChild from ControlTemplate

I have a WPF application in .NET 3.5 SP1 which is using TabControl.
In that we have TabItems, which in turn have their Styles to determine currently displayed items.
Let's say we have a TabItem named Books, now Books will have three stages of display:
1. Loading results,
2. Displaying results,
3. Displaying no results - i.e. nothing found.
<TabControl>
<TabItem Header="Books"/>
<TabItem Header="DVD's"/>
...
</TavControl>
Now I have 5 TabItems which let's say represent "DVD's", "Blu-Rays", "CD's", "Books" and "Comics".
<TabItem Header="Books">
<Control>
<Control.Resources>
<Style TargetType="Control">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Control">
<ListView ItemsSource="{Binding Books}"/>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<!-- Assign different Visuals depending on the current state of the app, i.e. Loading, No results, results found
<DataTrigger .../>
</Style.Triggers>
</Style>
</Control.Resources>
</Control>
</TabItem>
Underneath the TabItem I have a TextBlock to display number of currently found results:
<TextBlock>
<TextBlock.Text>
<MultiBinding StringFormat="Displaying {0} records for your Shop ({1})" Converter="{StaticResource tstMVC}">
<Binding ElementName="Tc" Path="SelectedValue"/>
<Binding Path="ShopId" FallbackValue="Liverpool"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
Converter is there for me to check what values are passed in the MultiBinding.
ISSUE:
When user selects a tab item I would like to display current number of the items displayed but I can't locate the ListView in the Control, as that's the Current Content of the TabItem.
I have tried TabControl.SelectedItem, SelectedValue and still can't find Current ItemsSource.Count.
Thanks in advance
UPDATE:
I have tried both of the solutions big thanks to #Sheridan and #pushpraj!
Unfortunately I haven't used either of them, instead I used ListView inside of the TabItem and then accessed it with this code:
<TabControl Name="Tc">
<TabItem><ListView ItemsSource="{Binding Books}"/></TabItem>
...
</TabControl>
<TextBlock Text="{Binding ElementName=Tc, Path=SelectedItem.Content.Items.Count}"/>
This way Content of my TextBlock changes every time user selects different Tab.
P.S. Nevertheless I wouldn't have done it without evaluation of both answers.
if you have separate collection classes for all you entities you may use the following approach
define data templates for your collection classes
eg
<DataTemplate DataType="{x:Type l:BookCollection}">
<ListView ItemsSource="{Binding}" />
</DataTemplate>
xaml
<TabControl x:Name="Tc">
<TabItem Header="Books"
Content="{Binding Books}" />
<TabItem Header="DVD's"
Content="{Binding DVDs}" />
</TabControl>
or if you do not have separate collections then use DataTemplate as follows
<TabControl x:Name="Tc">
<TabControl.ItemTemplate>
<DataTemplate>
<ListView ItemsSource="{Binding}" />
</DataTemplate>
</TabControl.ItemTemplate>
<TabItem Header="Books"
Content="{Binding Books}" />
<TabItem Header="DVD's"
Content="{Binding DVDs}" />
</TabControl>
then the binding to get the selected tab's items count will be
<TextBlock Text="{Binding SelectedItem.Content.Count, ElementName=Tc}" />
In WPF, we generally work with data elements rather than UI elements. By that, I mean that it is customary to data bind data elements from our code behind or view models to the UI elements in the views, UserControls and Windows.
Therefore, if you have a data collection property named Books in your code behind or view model, then you can simply refer to that collection to find out how many items are in it, rather than trying to find it via the UI controls:
<ListView ItemsSource="{Binding Books}" />
You could even expose the count of items as a separate property and data bind to it directly:
public ObservableCollection<Book> Books
{
get { return books; }
set
{
books = value;
NotifyPropertyChanged("Books");
NotifyPropertyChanged("BookCount");
}
}
public int BookCount
{
get { return Books.Count; }
}
UPDATE >>>
In response to your latest comment, you can find out how to access UI elements from within a ControlTemplate from the How to: Find ControlTemplate-Generated Elements page on MSDN. In short though, you need to access the element that has the ControlTemplate applied (the relevant TabItem in your case) and then you can use the FrameworkTemplate.FindName Method to find the internally declared elements. Take this example from the linked page:
// Finding the grid that is generated by the ControlTemplate of the Button
Grid gridInTemplate = (Grid)myButton1.Template.FindName("grid", myButton1);
// Do something to the ControlTemplate-generated grid
MessageBox.Show("The actual width of the grid in the ControlTemplate: "
+ gridInTemplate.GetValue(Grid.ActualWidthProperty).ToString());

Using checkboxes in a grouped vb.net datagrid

I have three kind of separate checkbox questions, all related to their use in the same datagrid.
I've loaded a datatable, dtAll, into a datagrid, dgdList, and bound each column to the imported data. The datagrid also has an initial column with checkboxes. How can I determine which rows of data have been checked. The intended functionality is for a user to mark two duplicates and merge together. I need to be able to gather row data from the datatable or datagrid whenever the checkbox column is checked
When I group my datagrid into tables, below, it somehow turns off my ability to check more than one checkbox. Everytime i select a new row, it clears all previous checkboxes. I would like to be able to check boxes in multiple rows. (Further, I have to click twice, once to select the row, once to edit the value of the checkbox to true - it would be great if I could do it in a single click).
Dim myView As System.ComponentModel.ICollectionView
myView = CollectionViewSource.GetDefaultView(dtAll)
myView.GroupDescriptions.Add(New PropertyGroupDescription("GROUP ID"))
dgdList.ItemsSource = dtAll.DefaultView
I've added a checkbox to the group header. When clicked, I'd like it to fill in each checkbox in all the rows inside of that group. Is there any way I can do this as well? If it helps, below is the Xaml code I'm using to split up groups of records
<DataGrid.GroupStyle>
<GroupStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Path=Name}" />
</StackPanel>
</DataTemplate>
</GroupStyle.HeaderTemplate>
<GroupStyle.ContainerStyle>
<Style TargetType="{x:Type GroupItem}" x:Name="Style1">
<Setter Property="Template" x:Name="Setter1">
<Setter.Value>
<ControlTemplate TargetType="{x:Type GroupItem}" x:Name="ControlTemplate1">
<Expander IsExpanded="True" Name="Expander1">
<Expander.Header>
<StackPanel Orientation="Horizontal">
<CheckBox Name="CheckBox9" />
<TextBlock Text=" Exact Name Match Group #"/>
<TextBlock Text="{Binding Path=Name}" />
</StackPanel>
</Expander.Header>
<ItemsPresenter />
</Expander>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</GroupStyle.ContainerStyle>
</GroupStyle>
</DataGrid.GroupStyle>

VirtualizingStackPanel doesn't clear the text of TextBoxes in the ItemTemplate

I have ItemsControl with VirtualizingStackPanel as items panel like this:
<ItemsControl Style="{StaticResource ItemsControl}" Name="itemsControl"
Margin="0,100,0,0" HorizontalAlignment="Stretch" Height="80">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBox />
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
Style is following:
<Style x:Key="ItemsControl" TargetType="ItemsControl">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ItemsControl">
<ScrollViewer VerticalScrollBarVisibility="Hidden"
HorizontalScrollBarVisibility="Visible">
<ItemsPresenter />
</ScrollViewer>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
I set a collection with 100.000 elements as ItemsSource and get really good performance. Everything is fine except of one thing. When I input text in one of the text boxes and then start to scroll I see that that text appears everywhere throughout the list!
I understand what the VirtualizingStackPanel does. It's continuously loading elements that become visible as we scroll. I understand some aspects of it's virtualizing technique but I have no idea how to understand this strange behavior. I failed to find good doc's on WPF/Silverlight virtualization, so, please, explain me what is going on
VirtualizingStackPanel does not actually continiously load elements. Instead, it re-uses the existing elements (controls) and simply replaces the DataContext behind them.
So if you have an VirtualizingStackPanel with 100,000 items, and only 10 are visible at a time, it usually renders about 14 items (extra items for a scroll buffer). When you scroll, the DataContext behind those 14 controls gets changed, but the actual controls themselves will never get replaced.
If you do something like enter Text in TextBox #1, and that TextBox.Text is not bound to anything, then the Text will always show up because the control is getting re-used. If you bind the TextBox.Text to a value, then the DataContext will change when you scroll which will replace the displayed Text.
Not sure how to turn off recycling directly in a VirtualizingStackPanel but this is the syntax in a ListBox. I would have posted as a comment but I wanted formatted code.
<ListBox VirtualizingStackPanel.VirtualizationMode="Standard" />

WPF UI persistence in TabControl

I am having issues with something that seems like it should be very simple but in fact has proven quite difficult.
Lets say you have a TabControl bound to an itemsource of ViewModels and the items displayed using a DataTemplate. Now lets say the DataTemplate consists of a Grid with two columns and a Grid splitter to resize the columns.
The problem is if you resize the columns on one tab, and switch to another tab, the columns are also resized. This is because the TabControl shares the DataTemplate among all tabs. This lack of UI persistence is applied to all elements of the template which can make for a frustrating experience when various UI components are adjusted. Another example is the scroll position in a DataGrid (on a tab). A DataGrid with few items will be scrolled out of view (only one row visible) if a DataGrid with more rows was scrolled to the bottom on another tab. On top of this, if the TabControl has various items defined in multiple DataTemplates the view is reset when you switch between items of differenet types. I can understand that this approach saves resources but the resultant functionality seems quite contradictory to expected UI behavior.
And so i'm wondering if there is a solution/workaround to this as i'm sure it's something that others have encountered before. I've noticed a few similar questions on other forums but there was no real solution. One about using the AdornerDecorator but that doesn't seem to work when used with a DataTemplate. I'm not keen on binding all the UI properties (like column width, scroll position) to my ViewModels and in fact I tried it for the simple GridSplitter example and I didn't manage to make it work. The width of the ColumnDefinitions were not necessarily affected by a grid splitter. Regardless, it would be nice if there were a general solution to this. Any thoughts?
If I ditch the TabControl and use an ItemsControl will I encounter a similar issue? Would it be possible to modify the TabControl Style so it doesn't share the ContentPresenter between tabs?
I've been messing with this on and off for a quite a while now. Finally, instead of trying to fix/modify the TabControl I simply recreated it's functionality. It's actually worked out really well. I made a Tab'like'Control out of a Listbox (Tab headers) and an ItemsControl. The key thing was to set the ItemsPanelTemplate of the ItemsControl to a Grid. A bit of Styling, and a DataTrigger to manage the Visibility of the Items and voila. It works perfect, each "Tab" is a unique object and preserves all it's UI states like scroll position, selections, column widths, etc. Any downsides or problems that might occur with this type of solution?
<DockPanel>
<ListBox
DockPanel.Dock="Top"
ItemsSource="{Binding Tabs}"
SelectedItem="{Binding SelectedTab}"
ItemContainerStyle="{StaticResource ImitateTabControlStyle}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel
Orientation="Horizontal">
</StackPanel>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel
Margin="2,2,2,0"
Orientation="Horizontal" >
<TextBlock
Margin="4,0" FontWeight="Bold"
Padding="2"
VerticalAlignment="Center" HorizontalAlignment="Left"
Text="{Binding Name}" >
</TextBlock>
<Button
Margin="4,0"
Command="{Binding CloseCommand}">
<Image Source="/TERM;component/Images/Symbol-Delete.png" MaxHeight="20"/>
</Button>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<ItemsControl
ItemsSource="{Binding Tabs}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Grid />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<ContentControl
Content="{Binding}">
<ContentControl.Style>
<Style>
<Style.Triggers>
<DataTrigger
Binding="{Binding IsSelected}" Value="False">
<Setter Property="ContentControl.Visibility" Value="Hidden" />
</DataTrigger>
</Style.Triggers>
</Style>
</ContentControl.Style>
</ContentControl>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DockPanel>

Using ItemsControl on a multi-leveled TreeView

My co-worker threatened to put me on TheDailyWTF today because of my property I wrote to be used to build a 3-tiered treeview with ItemsControl.
I bear you the footprint:
ObservableCollection<KeyValuePair<string, ObservableCollection<KeyValuePair<string, ObservableCollection<MyType>>>>>;
My goal was to create an ItemsControl that would use the Key as the header, and Value as the ItemsSource for 3 levels:
<Style x:Key="filterTreeStyle" TargetType="ItemsControl">
<Setter Property="ItemTemplate">
<Setter.Value>
<DataTemplate>
<controls:TreeViewItem IsExpanded="True">
<controls:TreeViewItem.Header>
<controlsToolkit:TreeViewItemCheckBox Content="{Binding Key}"/>
</controls:TreeViewItem.Header>
<ItemsControl ItemsSource="{Binding Value}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<controls:TreeViewItem>
<controls:TreeViewItem.Header>
<controlsToolkit:TreeViewItemCheckBox Content="{Binding Key}"/>
</controls:TreeViewItem.Header>
<controlsToolkit:TreeViewItemCheckBox IsChecked="{Binding Enabled}" Content="{Binding FilterTypeText}"/>
</controls:TreeViewItem>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</controls:TreeViewItem>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
Can anyone save me from the clutches of TheDailyWTF? What is a cleaner way to do this. Bonus if we can figure out a way to make the number of levels dynamic.
Uh, maybe I'm being dumb here, but since you want a TreeView... why not use a TreeView? You'll also need to use a HierarchicalDataTemplate instead of a vanilla DataTemplate: the content of the HDT becomes the Header, and the ItemsSource is used to create the child nodes. That will also take care of making the number of levels dynamic.
TreeView is built into WPF and is available in Silverlight as part of the SDK.

Resources