Virtualizing an ItemsControl? - wpf

I have an ItemsControl containing a list of data that I would like to virtualize, however VirtualizingStackPanel.IsVirtualizing="True" does not seem to work with an ItemsControl.
Is this really the case or is there another way of doing this that I am not aware of?
To test I have been using the following block of code:
<ItemsControl ItemsSource="{Binding Path=AccountViews.Tables[0]}"
VirtualizingStackPanel.IsVirtualizing="True">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Initialized="TextBlock_Initialized"
Margin="5,50,5,50" Text="{Binding Path=Name}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
If I change the ItemsControl to a ListBox, I can see that the Initialized event only runs a handful of times (the huge margins are just so I only have to go through a few records), however as an ItemsControl every item gets initialized.
I have tried setting the ItemsControlPanelTemplate to a VirtualizingStackPanel but that doesn't seem to help.

There's actually much more to it than just making the ItemsPanelTemplate use VirtualizingStackPanel. The default ControlTemplate for ItemsControl does not have a ScrollViewer, which is the key to virtualization. Adding to the the default control template for ItemsControl (using the control template for ListBox as a template) gives us the following:
<ItemsControl ItemsSource="{Binding AccountViews.Tables[0]}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Initialized="TextBlock_Initialized"
Text="{Binding Name}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel IsVirtualizing="True"
VirtualizationMode="Recycling" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.Template>
<ControlTemplate TargetType="ItemsControl">
<Border BorderThickness="{TemplateBinding BorderThickness}"
BorderBrush="{TemplateBinding BorderBrush}"
Background="{TemplateBinding Background}">
<ScrollViewer CanContentScroll="True"
Padding="{TemplateBinding Padding}"
Focusable="False">
<ItemsPresenter />
</ScrollViewer>
</Border>
</ControlTemplate>
</ItemsControl.Template>
</ItemsControl>
(BTW, a great tool for looking at default control templates is Show Me The Template)
Things to notice:
You have to set ScrollViewer.CanContentScroll="True", see here for why.
Also notice that I put VirtualizingStackPanel.VirtualizationMode="Recycling". This will reduce the numbers of times TextBlock_Initialized is called to however many TextBlocks are visible on the screen. You can read more on UI virtualization here
.
EDIT: Forgot to state the obvious: as an alternate solution, you can just replace ItemsControl with ListBox :)
Also, check out this Optimizing Performance on MSDN page and notice that ItemsControl isn't in the "Controls That Implement Performance Features" table, which is why we need to edit the control template.

Building on DavidN's answer, here is a style you can use on an ItemsControl to virtualise it:
<!--Virtualised ItemsControl-->
<Style x:Key="ItemsControlVirtualizedStyle" TargetType="ItemsControl">
<Setter Property="VirtualizingStackPanel.IsVirtualizing" Value="True"/>
<Setter Property="ScrollViewer.CanContentScroll" Value="True"/>
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ItemsControl">
<Border
BorderThickness="{TemplateBinding Border.BorderThickness}"
Padding="{TemplateBinding Control.Padding}"
BorderBrush="{TemplateBinding Border.BorderBrush}"
Background="{TemplateBinding Panel.Background}"
SnapsToDevicePixels="True"
>
<ScrollViewer Padding="{TemplateBinding Control.Padding}" Focusable="False">
<ItemsPresenter SnapsToDevicePixels="{TemplateBinding UIElement.SnapsToDevicePixels}" />
</ScrollViewer>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
I do not like the suggestion to use a ListBox as they allow the selection of rows where you do not necessarily want it.

It is just that the default ItemsPanel isn't a VirtualizingStackPanel. You need to change it:
<ItemsControl>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>

Related

How to make Items in ItemsControl scrollable? [duplicate]

I followed this small "tutorial" on how to add a scrollbar to an ItemsControl, and it works in Designer view, but not when I compile and execute the program (only the first few items show up, and no scrollbar to view more - even when VerticalScrollbarVisibility is set to "Visible" instead of "Auto").
Any idea on how to solve this?
This is the code I use to show my items (normally I work with Databinding, but to see the items in my Designer I added them manually):
<ItemsControl x:Name="itemCtrl" Style="{DynamicResource UsersControlStyle}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical" HorizontalAlignment="Center" VerticalAlignment="Top">
</StackPanel>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<uc:UcSpeler />
<uc:UcSpeler />
<uc:UcSpeler />
<uc:UcSpeler />
<uc:UcSpeler />
</ItemsControl>
And this is my Template:
<Style x:Key="UsersControlStyle" TargetType="{x:Type ItemsControl}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ItemsControl}">
<Border SnapsToDevicePixels="true" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Padding="{TemplateBinding Padding}">
<ScrollViewer VerticalScrollBarVisibility="Visible">
<ItemsPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
</ScrollViewer>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
To get a scrollbar for an ItemsControl, you can host it in a ScrollViewer like this:
<ScrollViewer VerticalScrollBarVisibility="Auto">
<ItemsControl>
<uc:UcSpeler />
<uc:UcSpeler />
<uc:UcSpeler />
<uc:UcSpeler />
<uc:UcSpeler />
</ItemsControl>
</ScrollViewer>
You have to modify the control template instead of ItemsPanelTemplate:
<ItemsControl >
<ItemsControl.Template>
<ControlTemplate>
<ScrollViewer x:Name="ScrollViewer" Padding="{TemplateBinding Padding}">
<ItemsPresenter />
</ScrollViewer>
</ControlTemplate>
</ItemsControl.Template>
</ItemsControl>
Maybe, your code does not working because StackPanel has own scrolling functionality. Try to use StackPanel.CanVerticallyScroll property.
Put your ScrollViewer in a DockPanel and set the DockPanel MaxHeight property
[...]
<DockPanel MaxHeight="700">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<ItemsControl ItemSource ="{Binding ...}">
[...]
</ItemsControl>
</ScrollViewer>
</DockPanel>
[...]

WPF ItemsControl scrollbar

Using ItemsControl to display a collection of items on a Canvas.
Probelm is that I can't see all the items on my screen (need to use Scrollbars), I've checked this post out and tried the same but it doesn't work for me, the Scrollbar is shown but disabled. My XAML:
<Grid>
<DockPanel>
<ScrollViewer>
<ItemsControl ItemsSource={Binding MyCollection}>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
....
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</DockPanel>
</Grid>
ItemsControl by default doesn't have ScrollViewer in it's Template unlike ListBox.
Get rid of the outer scrollViewer and set the Template of ItemsControl to contain ScrollViewer. Also, I don't see any usage of DockPanel when you already wrap ItemsControl inside Grid.
Change layout something like this:
<Grid>
<ItemsControl ItemsSource={Binding MyCollection}>
<ItemsControl.Template>
<ControlTemplate>
<Border
BorderThickness="{TemplateBinding Border.BorderThickness}"
Padding="{TemplateBinding Control.Padding}"
BorderBrush="{TemplateBinding Border.BorderBrush}"
Background="{TemplateBinding Panel.Background}"
SnapsToDevicePixels="True">
<ScrollViewer
Padding="{TemplateBinding Control.Padding}"
Focusable="False">
<ItemsPresenter SnapsToDevicePixels="{TemplateBinding
UIElement.SnapsToDevicePixels}"/>
</ScrollViewer>
</Border>
</ControlTemplate>
</ItemsControl.Template>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
....
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>

Is it possible to override the ItemsPresenter to use a Virtualizing StackPanel instead of a regular stack panel?

Background
I have a custom control that inherits from a TreeView and is modified to display in a data grid style. The problem I am seeing is with performance when expanding the tree. This is common from my research with Tree Views. Upon inspection with the WPF Performance tools I noticed that the ItemsPresenter class is using a regular Stack Panel instead of a Virtualizing Stack Panel.
Here is the section of code where the ScrollContentPresenter is used (showing in image).
<ScrollContentPresenter Name="PART_ScrollContentPresenter"
KeyboardNavigation.DirectionalNavigation="Local"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
CanContentScroll="{TemplateBinding CanContentScroll}"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
Here is the template being passed in.
<ControlTemplate TargetType="CommonControls:TreeListViewItem508">
<Grid >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Border x:Name="item">
<Border Name="InnerBorder">
<Grid Style="{StaticResource GridBackgroundStyle}">
<Rectangle Visibility="Collapsed" Fill="#75FFFFFF" Name="UpperHighlight" />
</Grid>
</Border>
</Border>
<ItemsPresenter Grid.Row="1" Name="ItemsHost" />
</Grid>
</ControlTemplate>
Question
Is it possible to force the items presenter to use a virtualizing stack panel?
Notes
I have already tried wrapping the ItemsPresenter in a ScrollViewer but that gives undesired results (scroll bars for each row).
I hard coded the option CanContentScroll = true as a test since this disables virtualization when its set to false.
This control is in production and used in multiple places so I don't have the option to replace / rewrite / or perform major modifications to the design at this point. I am just looking to override this one section if possible.
Any suggestions or options are much appreciated.
Resolved:
I modified the template's style by adding this to the style and it switched the stack panels to virtualizing.
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
You can use a VirtualizingStackPanel, however be aware that there is more to virtualizing a StackPanel than just using a VirtualizingStackPanel
Here is an example using the code found in the link posted above which lists the items that are needed:
<ItemsControl ...
VirtualizingStackPanel.IsVirtualizing="True" <!-- this is needed -->
ScrollViewer.CanContentScroll="True" > <!-- this is needed -->
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel /> <!-- this is needed -->
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.Template>
<ControlTemplate>
<Border ...>
<ScrollViewer> <!-- this is needed -->
<ItemsPresenter />
</ScrollViewer>
</Border>
</ControlTemplate>
</ItemsControl.Template>
</ItemsControl>
Try
<TreeView>
<TreeView.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel/>
</ItemsPanelTemplate>
</TreeView.ItemsPanel>
</TreeView>
or
<TreeView VirtualizingStackPanel.IsVirtualizing="True">
Obviosuly replace TreeView, with your treeview control name.
Hope that helps
Paul

How to enable scrolling when listbox is disabled in Silverlight?

I want to disable listbox (not allow user to selected) but scrollable in Silverlight, how can I do that?
How about not using a ListBox. Despite it being the defacto means for displaying a list of items the true purpose of the ListBox is to select something from a list.
A scrollable list can be delivered by styleing the ItemsControl like this:-
<Grid.Resources>
<Style x:Key="ScrollableItemsControl" TargetType="ItemsControl">
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ItemsControl">
<Border CornerRadius="2" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}">
<ScrollViewer x:Name="ScrollViewer" Padding="{TemplateBinding Padding}" Background="{TemplateBinding Background}" BorderBrush="Transparent" BorderThickness="0" TabNavigation="{TemplateBinding TabNavigation}">
<ItemsPresenter />
</ScrollViewer>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Grid.Resources>
Now you apply this style to an ItemsControl:-
<ItemsControl Style="{StaticResource ScrollableItemsControl}" ItemsSource="{Binding SomeData}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding SomeProperty}" Margin="5" />
<TextBlock Text="{Binding SomeOtherProperty}" Margin="5" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
This is a scrollable list but with none of the selection semanitics of the ListBox. Note there are fewer intermediatory elements in the visual tree doing this way than fiddling around with the ListBox control.
Edit
In light of your comment there is an easy way to toggle the selection behaviour of a ListBox without disabling it. You disable the ListBoxItems it contains instead.
<Grid.Resources>
<Style x:Key="DisabledItem" TargetType="ListBoxItem">
<Setter Property="IsEnabled" Value="False" />
</Style>
</Grid.Resources>
<ListBox x:Name="lst" ItemsSource="{StaticResource TestData}" ItemContainerStyle="{StaticResource DisabledItem}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}" Margin="5" />
<TextBlock Text="{Binding Age}" Margin="5" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
In this case the ListBox starts of with disabled items. Scrolling however is still possible. But the items appear dimmed and there is no hover or selected highlighting going on.
The code below is able to toggle the "disabled" state on and off.
private void Button_Click(object sender, RoutedEventArgs e)
{
if (lst.ItemContainerStyle == null)
lst.ItemContainerStyle = (Style)LayoutRoot.Resources["DisabledItem"];
else
lst.ItemContainerStyle = null;
}

Empty WPF ListBox as Drop Target

I have a ListBox that is a drop target of items from other sources.
Everything is working fine except in a particularly situation. When the ListBox has no Items I can only drop in the border of ListBox (I have a trigger so the Border is visible when dragging).
To give a bigger drop area I set the MinHeight of the ListBox to 25. When dragging, the Border reflects the MinHeight of the ListBox but the area is not considered a target. What is probably happening is that the target is considered to be the background because there is no Item in the ListBox.
Here is the code for the ListBox:
<ListBox Name="itmCtrlSetupSteps" Grid.Row="1" MinHeight="25"
BorderThickness="2" BorderBrush="{Binding DropBrush}" Background="Transparent"
ItemsSource="{Binding SetupSteps}" SelectionMode="Single" ItemContainerStyle="{StaticResource StepItemStyle}"
HorizontalContentAlignment="Stretch" Focusable="True"
SelectionChanged="manageStep_SelectionChanged"
AllowDrop="True" DragOver="itmCtrls_DragOver" Drop="itmCtrls_Drop" KeyUp="List_KeyUp"
>
<ListBox.Template>
<ControlTemplate TargetType="ListBox">
<Border BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}">
<ItemsPresenter/>
</Border>
</ControlTemplate>
</ListBox.Template>
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type my:TestStepListingStepViewModel}">
<my:TestStepListingStepView HorizontalAlignment="Stretch" GotFocus="setupSteps_GotFocus" MouseDoubleClick="Step_MouseDoubleClick"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
If I set the ItemPanel to:
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<Grid ClipToBounds="True"/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
I can drop items in the empty ListBox but then the items are presented on top of each other, instead of as a list.
Any thoughts on this?
The problem is that your ListBox isn't showing up when it is hit tested. You need to set the Background Brush on the Border in the control template so that it reflects your setting of Transparent on the ListBox.
<ControlTemplate TargetType="ListBox">
<Border Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" >
<ItemsPresenter/>
</Border>
</ControlTemplate>
In case someone is having this issue with any other control, just surround it with a border, set the background to a colour, and add the drag/drop events on the border along with AllowDrop set to be true.

Resources