Better performance from datagrid grouping - c#

I have a DataGrid which I am using with an ICollectionView to group items in a large collection, 20k+ rows in some instances. I have used this approach before with varying success with showing all rows or virtualizing to create a more responsive page. In this instance I would like to virtualize as much as possible to keep the UI responsive. I have used the tips in this answer with little success to my issues.
Helpful DataGrid Link
My main issue is a couple second lag on the DataGrid when loading the data into the ICollectionView View/Source, which I would like to minimize with the proper virtualization. Here is some of my code:
<DataGrid Margin="0,2,0,0" IsReadOnly="True" ItemsSource="{Binding DataView,IsAsync =True}" EnableRowVirtualization ="True" MaxWidth="2560" MaxHeight="1600"
Grid.Row="2" SelectionMode="Extended" VirtualizingPanel.IsVirtualizingWhenGrouping="True" SelectionUnit="FullRow" SelectedItem="{Binding SelectedOutage}">
<DataGrid.GroupStyle>
<GroupStyle>
<GroupStyle.ContainerStyle>
<Style TargetType="{x:Type GroupItem}">
<Setter Property="Margin" Value="0,0,0,5"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type GroupItem}">
<Expander IsExpanded="True" Foreground="{StaticResource Foreground}" Background="{StaticResource AlternatingBackground}">
<Expander.Header>
<TextBlock Text="{Binding Name}" Margin="5,0,0,0" Width="300"/>
</Expander.Header>
<ItemsPresenter />
</Expander>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</GroupStyle.ContainerStyle>
</GroupStyle>
</DataGrid.GroupStyle>
<!--Follows are DataGrid.ContextMenu and DataGridTextColumns with fixed widths-->
</DataGrid>
And c# items:
public ICollectionView DataView { get; set; }
private readonly ObservableCollection<EquipmentMonitorRow> equipment = new ObservableCollection<EquipmentMonitorRow>();
DataView = CollectionViewSource.GetDefaultView(equipment);
DataView.GroupDescriptions.Add(new PropertyGroupDescription("GroupName"));
equipment.Clear();
//Lag is during this item adding.
equipment.AddRange(data);
So hopefully I'm missing some virtualization or maybe I can add the items differently or something.
Any help would be appreciated. Thanks.

Changing my ObservableCollection source to a List source solved the initial load lag.
private readonly List<EquipmentMonitorRow> equipment = new List<EquipmentMonitorRow>();
Also using a combination of VirtualizingPanel properties I achieved the best virtualization. Specifically if I omitted VirtualizingPanel.IsVirtualizingWhenGrouping="True" then the application lagged for almost a minute before showing anything in my DataGrid. The scrolling was a lot better without virtualization but the initial load was unacceptable in my case.
VirtualizingPanel.IsVirtualizingWhenGrouping="True" VirtualizingPanel.ScrollUnit ="Item" VirtualizingPanel.VirtualizationMode="Recycling"
Thanks for the help.

Datagrid by default support UI virtualization but as soon as you apply grouping on ICollectionView, UI virtualization will be turned off.
You can refer to MSDN sample which basically flattens the grouped list which supports virtualization.
UPDATE for comment:
This doesn't specifically mention a collectionview but seems there is
virtualization
From the link:
In a normal WPF ItemsControl that virtualizes, such as ListBox or
ListView, virtualization turns off when you turn grouping on.
And DataGrid derives from ItemsControl only, hence UI virtualization is turned off on grouping.

Related

WPF ComboBox: Align selected value and/or drop down items

I have an almost default WPF ComboBox:
What is the simplest way to align items, for instance, to the right in combobox's input and/or drop down?
I've looked around and found only solutions messing with the control templates which in my opinion is too long shot for such a simple thing. I can't believe there is no simpler solution we can do for aligning the items.
UPDATE:
I've slightly reformulated the question to cover a broader set of cases further readers might experience on this topic as it turned out thanks to dkozl's answer.
Also, it should prevent some people from trying to close this question as a duplicate.
If you want to right align both selected value and drop down items then setting HorizontalContentAlignment="Right" against ComboBox should be enough
<ComboBox ... HorizontalContentAlignment="Right">
if you want to right align only drop down items then you need to change HorizontalContentAlignment of the ComboBoxItem
<ComboBox>
<ComboBox.ItemContainerStyle>
<Style TargetType="{x:Type ComboBoxItem}">
<Setter Property="HorizontalContentAlignment" Value="Right"/>
</Style>
</ComboBox.ItemContainerStyle>
</ComboBox>
and to right align only selected value combination of both
<ComboBox ... HorizontalContentAlignment="Right">
<ComboBox.ItemContainerStyle>
<Style TargetType="{x:Type ComboBoxItem}">
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
</Style>
</ComboBox.ItemContainerStyle>
</ComboBox>
I've tried both things from dkozl's answer and by some reason setting HorizontalContentAlignment against my ComboBox was sufficient to align to the right items in both combobox's input and drop down.
I had the problem of aligning the selected item on the right, so that users can always see the end of the string.
Simplest solution I found was the following
<ComboBox ...>
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock HorizontalAlignment="Right" Text="{Binding textProp}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>

WPF DataGrid: IsVirtualizingWhenGrouping="True" not working

I have a DataGrid that has a CollectionViewSource bound to it's ItemSource Property:
<DataGrid Grid.Row="0" RowBackground="#10808080" AlternatingRowBackground="Transparent"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
ItemsSource="{Binding Source={StaticResource bookingsViewSource}}"
RowHeight="27"
VirtualizingPanel.IsVirtualizingWhenGrouping="True"
VirtualizingPanel.IsContainerVirtualizable="True"
VirtualizingPanel.ScrollUnit="Item"
AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding date, StringFormat=dd.MM.yyyy}" Header="date"/>
<DataGridTextColumn Binding="{Binding Path=customers.name}" Header="customer"/>
<DataGridTextColumn Binding="{Binding Path=customers.street}" Header="adress"/>
</DataGrid.Columns>
<DataGrid.GroupStyle>
<GroupStyle>
<GroupStyle.ContainerStyle>
<Style TargetType="{x:Type GroupItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Expander Header="{Binding Path=Name}" IsExpanded="True">
<ItemsPresenter />
</Expander>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</GroupStyle.ContainerStyle>
</GroupStyle>
</DataGrid.GroupStyle>
</DataGrid>
bookingsViewSource is defined as
<CollectionViewSource x:Key="bookingsViewSource"
d:DesignSource="{d:DesignInstance {x:Type Database:bookings}}">
<CollectionViewSource.GroupDescriptions>
<PropertyGroupDescription PropertyName="providerID"/>
</CollectionViewSource.GroupDescriptions>
</CollectionViewSource>
and get's filled in code behind section. Everything was doing fine fast and smooth without grouping. But when I added grouping <PropertyGroupDescription PropertyName="providerID"/> the DataGrid needs around one minute to load.
In .NET 4.5 there is a new Property called VirtualizingPanel.IsVirtualizingWhenGrouping and I already set this to true but loading time was not decreasing.
I can not figure out why. Any ideas?
From the book: MacDonald M. - Pro WPF 4.5 in C#
A number of factors can break UI virtualization, sometimes when you don’t expect it:
Putting your list control in a ScrollViewer: The ScrollViewer provides a window onto
its child content. The problem is that the child content is given unlimited “virtual”
space. In this virtual space, the ListBox renders itself at full size, with all of its child
items on display. As a side effect, each item gets its own memory-hogging
ListBoxItem object. This problem occurs any time you place a ListBox in a container
that doesn’t attempt to constrain its size; for example, the same problem crops up if
you pop it into a StackPanel instead of a Grid.
Changing the list’s control template and failing to use the ItemsPresenter: The
ItemsPresenter uses the ItemsPanelTemplate, which specifies the
VirtualizingStackPanel. If you break this relationship or if you change the
ItemsPanelTemplate yourself so it doesn’t use a VirtualizingStackPanel, you’ll lose
the virtualization feature.
Not using data binding: It should be obvious, but if you fill a list programmatically—
for example, by dynamically creating the ListBoxItem objects you need—no
virtualization will occur. Of course, you can consider using your own optimization
strategy, such as creating just those objects that you need and only creating them at
the time they’re needed. You’ll see this technique in action with a TreeView that uses
just-in-time node creation to fill a directory tree in Chapter 22.
If you have a large list, you need to avoid these practices to ensure good performance.
Also, in my case the issue was caused by MahApps.Metro styles.

ListView items from an ObservableCollection that need to prepare themselves for realization

I have a ListView which is supposed to display a rather large number of items comprised of a "Name", a "Thumbnail" and a "AnimationPosition" property. A background task in each item's type is in charge of switching the thumbnails in order to animate them.
Now it goes without saying that this is a rather heavy operation and should be limited to as few items as possible e.g. to visible/realizing items of a Virtualized ListView. Now I already have set the DataContext of my ListView to the ObeservableCollection instance and have bound it to the properties of its type. Here's a peek into my XAML code for that.
<TabControl Grid.Row="0" Grid.Column="2">
<TabControl.Resources>
<Style x:Key="MediaItemStyle" TargetType="{x:Type ListViewItem}">
<Setter Property="Margin" Value="5,5,5,5"/>
<Setter Property="Padding" Value="0,0,0,0"/>
<Setter Property="HorizontalAlignment" Value="Left"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListViewItem}">
<Grid HorizontalAlignment="Left" VerticalAlignment="Top" Height="Auto" >
<Border x:Name="border" BorderBrush="{x:Null}" BorderThickness="1" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" CornerRadius="2.5"/>
<StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<ContentPresenter/>
</StackPanel>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="custom:MediaContainerListView">
<Setter Property="ItemsSource" Value="{Binding}"/>
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled"/>
<Setter Property="ItemContainerStyle" Value="{StaticResource MediaItemStyle}"/>
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<WrapPanel/>
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
<Setter Property="ItemTemplate">
<Setter.Value>
<DataTemplate>
<DockPanel Width="256">
<Image DockPanel.Dock="Top" Height="144" StretchDirection="Both"
Stretch="Fill" Source="{Binding Thumbnail.Source,Mode=OneWay}"/>
<ProgressBar DockPanel.Dock="Top" Height="2"
Minimum="0" Maximum="{Binding Thumbnail.AnimationPosition.Length}"
Value="{Binding Thumbnail.AnimationPosition.Position}"
Visibility="{Binding Thumbnail.AnimationPosition.Visibility}"/>
<TextBlock DockPanel.Dock="Bottom" Height="40"
TextWrapping="Wrap" TextTrimming="CharacterEllipsis"
TextAlignment="Center" Text="{Binding Name}"/>
</DockPanel>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</TabControl.Resources>
<TabItem Header="">
<custom:MediaContainerListView x:Name="MediaContainerView"></custom:MediaContainerListView>
</TabItem>
</TabControl>
Basically, I have two methods that start/stop the animation for each individual item.
public async void StartAnimation()
{
if( Count > 1 )
{
Task thumbnailAnimationTask = AnimationTask( AnimationCancellationToken.Token );
await thumbnailAnimationTask;
}
}
public void StopAnimation()
{
AnimationCancellationToken.Cancel();
}
I have two issues here.
The ListView seems to realize all the items rather than only those visible or within the realization range. I suspect my XAML somehow kills the Virtualization and have tried many solutions with no success. Mind you I need my ListView to be scalable to the MainWindow's dimensions and not be of fixed hight and width.
I need to call StartAnimation when an item is about to be realized and StopAnimation when it has left the view.
Even though my ListView is not correctly Virtualized, if my understanding of how ObservableCollections work is correct, it's only the UI representation of the items that are managed by the Virtualization and not the items themselves i.e. calling the StartAnimation/StopAnimation from the Constructor/Destructor of the items don't help much as they are called for every single item at the time of creation anyway.
Is there a neat way to somehow inform each item that they are about to be realized or leave the ListView view?
Update:
The issue of virtualizing not working correctly was related to the WrapPanel and once I switched to VirtualizingStackPanel it started to work correctly. Unfortunately it's not exactly the same as a WrapPanel and since .NET framework does not offer a VirtualizingWrapPanel, I chose to use the one from here. It's not perfect but it does the job.
You said it was 1/2
Look for a call to GetHashcode
I think it calls it GetHashcode just to find it
By accident I discovered it is called when the item is virtualized
Have an animation that terminates (does not loop)
I finally solved the problem myself. I knew it wouldn't have to be so complicated and it really isn't. Since I already had created my own ListView inherited class called MediaContainerListView, I could override a few of its virtual methods. Two of them proved to be just what I wanted.
protected override void PrepareContainerForItemOverride( DependencyObject element, object item )
which is called just before the item is about to appear, and
protected override void ClearContainerForItemOverride( DependencyObject element, Object item )
which is called just before the item is about to disappear. So I call the StartAnimation in the first one and the StopAnimation in the second one and it works flawlessly!

UserControl with Horizontal ListView in vertical ListView

I want to archive a vertical ListView that contains a UserControl in which i have some infos on the left and a second ListView that will scroll horizontal.
The problem is that my second listview will not start to scroll. It seems to pick unlimited space.
First of all some more infos about my setup:
My window is arranged via a grid basically just a row on top with text and the first listview in the second row.
The first listview uses a ItemTamplate like the following:
<ListView.ItemTemplate>
<DataTemplate>
<controls:MyControl />
</DataTemplate>
</ListView.ItemTemplate>
Beside that it has a ItemContainerStyle to display a horizontal line between the items:
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem"
BasedOn="{StaticResource {x:Type ListViewItem}}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListViewItem">
<StackPanel>
<Rectangle x:Name="Separator"
Height="2"
SnapsToDevicePixels="True"
Fill="{DynamicResource AccentColorBrush}" />
<ContentPresenter />
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ListView.ItemContainerStyle>
(trigger that disables the unnecessary first line omitted)
MyControl is a UserControl which is also just a grid first column some text and second column is my second listview.
This one is special because it is horizontal. Since their are various approaches for this from arround the internet i will show the one i ended using with.
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel Orientation="Horizontal"
IsItemsHost="True"
IsVirtualizing="True"/>
</ItemsPanelTemplate>
</ListView.ItemsPanel>
Item template is again a UserControl (at the moment just a TextBlock) and again the separator lines but this time vertically.
The problem is now that the second ListView is not scrollable. Even if i explicit set them to be visible they are disabled (also after resizing window).
My approach solving this was binding MaxWidth of Stackpanel in ListViewItem Template to Actual with of my MetroWindow.
MaxWidth="{Binding Path=ActualWidth, RelativeSource={RelativeSource AncestorType={x:Type metroControls:MetroWindow}}}"
But this and some other tries binding different sizes of different items does not work.
Can somebody give a hint to resolve this?

C#/XAML/WPF binding working partially, only displays first item in List

I have what should be a really simple binding, but the problem I'm seeing is that instead of displaying the three companies (company_list is a List, where Company contains a company_id to bind to), I see the window pop up with only the first company_id in company_list. I have other bindings which seem to work fine, and in some other cases I see that I've used ItemSource instead of DataContext, but when I use that I get "Items collection must be empty before using ItemsSource". I've searched extensively for a simple answer to this in stackoverflow, msdn and elsewhere, and have seen mostly really complex solutions that I haven't been able to understand/apply.
When my window appears, it has:
CompanyA
where it should have:
CompanyA
CompanyB
CompanyC
which is the content of the company_list (yes, verified in debugger). Suggestions appreciated! Code and XAML follow.
ReadMasterCompanyList(); // populates a_state.company_list with 3 companies
// display company list dialog
CompanySelect cs_window = new CompanySelect();
cs_window.CompanyListView.DataContext = a_state.company_list;
// fails: cs_window.CompanyListView.ItemsSource = a_state.company_list;
cs_window.Show();
And the XAML from CompanySelect:
<Grid>
<ListView IsSynchronizedWithCurrentItem="True"
x:Name="CompanyListView"
SelectionMode="Single" SelectionChanged="CompanyListView_SelectionChanged">
<ListView.ItemContainerStyle>
<Style TargetType="{x:Type ListViewItem}">
<Setter Property="Height" Value="30"/>
</Style>
</ListView.ItemContainerStyle>
<ListViewItem Content="{Binding Path=company_id}"></ListViewItem>
</ListView>
</Grid>
I would set the ItemsSource of the ListView, rather than the DataContext, either in codebehind:
cs_window.CompanyListView.ItemsSource = a_state.company_list;
or with binding:
<ListView ItemsSource="{Binding company_list}">
And then set the ItemTemplate of the ListView instead.
...
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding company_id}" />
</DataTemplate>
</ListView.ItemTemplate>
...
I would also look into using the MVVM design pattern for testability and separation of concerns, and look at using PascalCase for your property names.
Also, unless you specifically wanted a ListView, I would use a ListBox.
First, set the DataContext only after cs_window.Show().
Second, the ListViewItem you have as a child in your ListView's XAML is why you're only seeing one.
Third, might work better (and would be more MVVM-ish) if you define ItemsSource in the XAML, like this:
<ListView ItemsSource="{Binding Path=company_list}" ...>
That's after making a_state the DataContext of the ListView's container or some other ancestor element.
The problem is, that you define one ListViewItem in your XAML code. You shouldn't do this.
Try something like this:
<Grid>
<ListView IsSynchronizedWithCurrentItem="True"
x:Name="CompanyListView"
SelectionMode="Single" SelectionChanged="CompanyListView_SelectionChanged">
<ListView.ItemContainerStyle>
<Style TargetType="{x:Type ListViewItem}">
<Setter Property="Height" Value="30"/>
</Style>
</ListView.ItemContainerStyle>
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock Content={Binding Path=company_id}/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>

Categories