Improve performance for huge ListBox in StackPanel? - wpf

I am using a StackPanel to layout several controls vertically (ie, Title, sub titles, listbox, separator, listbox, etc).
The StackPanel is a child of a ScrollViewer to ensure its content is always scrollable.
One of the controls in the StackPanel is a ListBox.
Its ItemsSource is data bound to a huge collection, and a complex DataTemplate is used to realise each item.
Unfortunately, I'm getting really poor performance (high cpu/memory) with it.
I tried
setting the ListBox's ItemsPanel to a VirtualizingStackPanel, and
overriding its ControlTemplate to only an ItemsPresenter (remove the ListBox's ScrollViewer).
But there were no difference in performances. I'm guessing the StackPanel gives its internal children infinite height during measure?
When I replaced the ScrollViewer and StackPanel with other panels/layouts (e.g, Grid, DockPanel) and the performance improves significantly, which leads me to believe the bottleneck, as well as solution, is in virtualization.
Is there any way for me to improve the cpu/memory performance of this view?
[Update 1]
Original Sample project: http://s000.tinyupload.com/index.php?file_id=29810707815310047536
[Update 2]
I tried restyling/templating TreeView/TreeViewItems to come up with the following example. It still takes a long time to start/same,high memory usage. But once loaded, scrolling feels a lot more responsive than the original sample.
Wonder if there's any other way to further improve the start up time/memory usage?
Restyled TreeView project: http://s000.tinyupload.com/index.php?file_id=00117351345725628185
[Update 2]
pushpraj's solution works like a charm
Original:
Startup: 35s,
Memory: 393MB
Scrolling: Slow
TreeView:
Startup: 18s,
Memory 377MB,
Scrolling: Fast
pushpraj's solution:
Startup: <1s,
Memory: 20MB,
Scrolling: Fast

you may perhaps limit the maximum size of the huge list box and enable Virtualization
eg
<ListBox MaxHeight="500"
VirtualizingPanel.IsVirtualizing="true"
VirtualizingPanel.VirtualizationMode="Recycling" />
this will enable the ListBox to load a few items only and will enable a scrollbar on listbox to scroll to rest of the items if needed.
at the same time setting VirtualizationMode to Recycling will help you to reuse the complex data templates thus eliminating the need of re creating them again for every item.
EDIT
here is a solution based on your sample, I have used CompositeCollection with Virtualization to achieve the desired.
xaml
<Grid xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:l="clr-namespace:PerfTest">
<Grid.Resources>
<DataTemplate DataType="{x:Type l:Permission}">
<StackPanel Orientation="Horizontal">
<CheckBox />
<TextBlock Text="{Binding Name}" />
<Button Content="+" />
<Button Content="-" />
<Button Content="..." />
</StackPanel>
</DataTemplate>
<CompositeCollection x:Key="data">
<!-- Content 1 -->
<TextBlock Text="Title"
FontSize="24"
FontWeight="Thin" />
<!-- Content 2 -->
<TextBlock Text="Subtitle"
FontSize="16"
FontWeight="Thin" />
<!-- Content 3 -->
<CollectionContainer Collection="{Binding DataContext, Source={x:Reference listbox}}" />
<!-- Content 4 -->
<TextBlock Text="User must scroll past the entire list box before seeing this"
FontSize="16"
FontWeight="Thin"
Padding="5"
TextWrapping="Wrap"
Background="#99000000"
Foreground="White" />
</CompositeCollection>
</Grid.Resources>
<ListBox x:Name="listbox"
VirtualizingPanel.IsVirtualizing="True"
VirtualizingPanel.VirtualizationMode="Recycling"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ItemsSource="{StaticResource data}" />
</Grid>
code
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
var items = new ObservableCollection<Permission>();
foreach (var i in Enumerable.Range(0, 10000).Select(i => new Permission() { Name = "Permission " + i }))
{ items.Add(i); }
DataContext = items;
}
}
public class Permission
{
public string Name { get; set; }
}
since we can not create data template for string so I changed the string collection to Permission collection. I hope in your real project it would be something similar.
give this a try and see if this is close to what you need.
note: you may safely ignore if there is any designer warning on Collection="{Binding DataContext, Source={x:Reference listbox}}"

Related

Modify content of resource

I have a UserControl that contains a TabControl that has an ItemTemplate that in turn has a Button. I want the user be able to change the content of that button, e.g. change it from TextBlock to Image.
A solution I thought of was to set the button's content from the Resources of the UserControl and overwrite the Resource by setting it on the ResourceDictionary of the entailing Window. Of course that does not work as StaticResource always resolves to the "closest" instance it can find.
I then thought of modifying the resource in the constructor of my UserControl, depending on some property. But it seems, one cannot change a resource. Below is a close sample showing the idea with a simple ListBox in a Window in which I try to change "What" to "How".
How would you approach this?
<Window.Resources>
<TextBlock x:Key="key" Text="What: " x:Shared="false" />
</Window.Resources>
<Grid Margin="10">
<ListBox Name="lbTodoList" HorizontalContentAlignment="Stretch">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<StaticResource ResourceKey="key" />
<TextBlock Text="{Binding}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
TextBlock tb = FindResource("key") as TextBlock;
tb.Text = "How: ";
List<string> items = new List<string>();
items.Add("Item 1");
items.Add("Item 2");
items.Add("Item 3");
lbTodoList.ItemsSource = items;
}
}
Instead of trying to override a resource, you should just treat it like any other XAML data-binding templating issue.
Instead of:
<StaticResource ResourceKye="key" />
Do something like this:
<TextBlock Text="{Binding LabelText}" />
In your UserControl set the default value of LabelText to "What:" and then allow the user to override the value. The data binding will take care of the rest.
If you want to have more dynamic content, then use a ContentControl instead and have properties for Content, ContentTemplate, and even ContentTemplateSelector depending on what you need to do.
<ContentControl
Content="{Binding MyContent}"
ContentTemplate="{Binding MyContentTemplate}"
ContentTemplateSelector="{Binding MyContentTemplateSelector}" />
This opens up a lot of flexibility.

Need a toggle menu that closes when it loses focus

This is going to be difficult to explain but bear with me.
I'm creating an application in WPF using the MVVM pattern. I'm new to it which is why I'm asking this question.
I have the application set up as 3 pages or views within a window. One of these is static and is always there, the other two are a couple of small settings pages that open in the top corner over the top of everything using zindex. At the moment the menu that opens these pages uses a listbox with togglebuttons as it's template (the checked state is bound to the listbox) so that you can click to open the menu, then click the button again to close it.
In an ideal world I'd like it so that if the menu page were to lose focus (listen for a click on the static view?) the settings views close too. Also I wondered if anyone had a simpler solution for a menu that works in a similar way because at the moment it is a pretty messy solution. Here are some code samples:
<ListBox Grid.Row="0" Grid.Column="0" ItemsSource="{Binding PageViewModels}" SelectedItem="{Binding CurrentPageViewModel}">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<TextBlock Margin="10,0" Text="{Binding Name}" VerticalAlignment="Center" HorizontalAlignment="Center" Foreground="#FCCC"/>
<ToggleButton
VerticalAlignment="Stretch"
Content=""
IsChecked="{Binding IsSelected, Mode=TwoWay, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ListBoxItem}}}"
/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<!-- Settings views -->
<ContentControl Panel.ZIndex="2" Grid.Row="1" Grid.Column="0" Content="{Binding CurrentPageViewModel}"/>
<!-- Main page view -->
<ContentControl Grid.Row="1" Grid.RowSpan="2" Grid.ColumnSpan="2" Width="1000" Height="700" Content="{Binding StaticPageViewModel}"/>
I'm using the concepts in this blog post to manage my views and viewmodels, however I changed the way the menu is shown so I could remove the need for a change page command/ICommand.
TL;DR : I'm looking for suggestions and criticism with what I could do to improve the way I've currently created my menu bar.
I would create an attached property if you really wanna do the MVVM style to close the View when it loses focus.
public static readonly DependencyProperty CloseViewOnLostFocusProperty =
DependencyProperty.RegisterAttached("CloseViewOnLostFocus", typeof (object), typeof (MainWindow), new FrameworkPropertyMetadata(default(object), RegisterLostFocus));
private static void RegisterLostFocus(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
{
//This is the control that is attached to it e.g. ContentControl
var sender = dependencyObject as FrameworkElement;
//Your ViewModel
var viewModel = dependencyPropertyChangedEventArgs.NewValue as ViewModel;
if (sender != null)
{
sender.LostFocus += (o, args) =>
{
//Close whatever you are doing right now to close the View.
viewModel.Close();
};
}
}
And on your view you can attach whatever ViewModel you want to close when it loses focus e.g. your SettingsView got LostFocus it'll close that view. In here I created an attached property on my MainWindow class.
<!-- Settings views -->
<ContentControl
MainWindow.CloseViewOnLostFocus="{Binding RelativeSource={RelativeSource Self},Path=Content}"
x:Name="SettingsView" Panel.ZIndex="2" Grid.Row="1" Grid.Column="0" Content="{Binding CurrentPageViewModel}"/>
<!-- Main page view -->
<ContentControl x:Name="MainPageView" Grid.Row="1" Grid.RowSpan="2" Grid.ColumnSpan="2" Width="1000" Height="700" Content="{Binding StaticPageViewModel}"/>
You can have a ContextMenu attached to the button, which is opened when the button is clicked. This way, the close-when-unfocused behaviour is entirely automatic.
You can then just restyle the menu to look however you want.

How can I bind Image control to ListBox dynamically?

I want to implement a ListBox which each item include a Image control and some other controls, then use it to binding some data. Due to this ListBox need to contains more than 2000 items, this means I have to do some optimization for it.
First, I noticed that most of the Image controls has one same picture(Default avatar), so I create a singleton ImageSource-object for the data. But although the Image control's source is the same object, you know I also need to create 2000 Image control in the ListBox with DataTemplate below:
<ListBox ItemsSource="{Binding Items}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<Image Source="{Binding Avatar}" />
<TextBlock Text="{Binding Name}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Is there have a way to reduce Image control object's number in my program? Thank you!

Silverlight scaling listbox items within a wrappanel

I have created a listbox with a tiled data template. What I am trying to figure out now is how to properly apply a scale effect to each listbox item when a mouse over or selected event occurs and have it render properly within the wrap panel. I currently have the animations added to the visual states of the ListBoxItemTemplate.
A couple of thoughts:
When the animation is called the tiles within the wrap panel do not reposition to allow for the scaled item to be viewed properly. I would like to have the items within the wrap panel re-position to allow for the item scaled to be visible.
Also I notice that the items when scaled are going beyond the boundary of the wrap panel is there a way to also keep the item when scaled constrained to the viewable area of the wrap panel?
Code used in in search view
<Grid x:Name="LayoutRoot">
<ListBox x:Name="ResultListBox"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Background="{x:Null}"
BorderThickness="0"
HorizontalContentAlignment="Stretch"
ItemContainerStyle="{StaticResource TileListBoxItemStyle}"
ItemsPanel="{StaticResource ResultsItemsControlPanelTemplate}"
ItemsSource="{Binding SearchResults[0].Results}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListBox.ItemTemplate>
<DataTemplate>
<formatter:TypeTemplateSelector Content="{Binding}" HorizontalContentAlignment="Stretch" Margin="2.5">
<!-- Person Template -->
<formatter:TypeTemplateSelector.PersonTemplate>
<DataTemplate>
<qr:ucTilePerson />
</DataTemplate>
</formatter:TypeTemplateSelector.PersonTemplate>
<!-- Incident Template -->
<formatter:TypeTemplateSelector.IncidentTemplate>
<DataTemplate>
<qr:ucTileIncident />
</DataTemplate>
</formatter:TypeTemplateSelector.IncidentTemplate>
</formatter:TypeTemplateSelector>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
ResultsItemsControlPanelTemplate is defined in app.xaml as
<ItemsPanelTemplate x:Key="ResultsItemsControlPanelTemplate">
<toolkit:WrapPanel x:Name="wrapTile"/>
</ItemsPanelTemplate>
I would greatly appreciate any suggestions on where to look
Thanks in advance
Image of current result
Render transformers are applied after a layout has occured, its purely graphical task that the Silverlight layout engine knows very little about. What you need is control that when scaled actually increases the size it desires and causes Silverlight to update its layout.
The control you need is the LayoutTransformer control from the Silverlight Toolkit.
Place the content of each of your tiles inside a LayoutTransformer and assign a ScaleTransform to its LayoutTransform property. Now you can get your animations to manipulate the transform and as the tile grows the other tiles will flow.
Working through this I have my user controls stored within within a Listbox datatemplate which is also a userControl referenced within the project. I think I have a partial solution started so now I need to just continue to tweak what is happening. Since I am using a datatemplate I had to set a binding reference on the user control to the Layouttrasnformation
<ListBox x:Name="ResultListBox"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Background="{x:Null}"
BorderThickness="0"
HorizontalContentAlignment="Stretch"
ItemsPanel="{StaticResource ResultsItemsControlPanelTemplate}"
ItemContainerStyle="{StaticResource TileListBoxItemStyle}"
ItemsSource="{Binding SearchResults[0].Results}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListBox.ItemTemplate>
<DataTemplate>
<formatter:TypeTemplateSelector Content="{Binding}" HorizontalContentAlignment="Stretch" Margin="2.5">
<!-- Person Template -->
<formatter:TypeTemplateSelector.PersonTemplate>
<DataTemplate >
<layoutToolkit:LayoutTransformer x:Name="PersonTransformer">
<layoutToolkit:LayoutTransformer.LayoutTransform>
<ScaleTransform x:Name="personScale"/>
</layoutToolkit:LayoutTransformer.LayoutTransform>
<qr:ucTilePerson MouseEnter="ucTilePerson_MouseEnter" Tag="{Binding ElementName=PersonTransformer}" />
</layoutToolkit:LayoutTransformer>
</DataTemplate>
</formatter:TypeTemplateSelector.PersonTemplate>
//rest edited for brevity
Then within the code behind of the usercontrol which holds the listbox I used the following:
private void ucTilePerson_MouseEnter(object sender, MouseEventArgs e)
{
var ps = ((UserControl)sender).Tag as LayoutTransformer;
if (ps != null)
{
var transform = ps.LayoutTransform as ScaleTransform;
transform.ScaleX = (transform.ScaleX + 1.2);
transform.ScaleY = (transform.ScaleY + 1.2);
Dispatcher.BeginInvoke(() => { ps.ApplyLayoutTransform(); });
}
I still have to tweak this some as it does not seem to be as fluid as I like ( and I have to also setup mouseLeave events).
Not sure if this is the most appropriate approach and I would appreciate any feedback on alternatives.
Thanks again to Anthony for pointing me in the right direction

How to modify silverlight combobox data display

I have a combobox defined as follows:
<ComboBox x:Name="cboDept" Grid.Row="0" Margin="8,8,8,8" HorizontalAlignment="Left" VerticalAlignment="Top" Width="120"
ItemsSource="{Binding Source={StaticResource cvsCategories}}">
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Vertical" Width="Auto" Height="Auto">
<sdk:Label Content="{Binding CategoryID}" Height="20" />
<sdk:Label Content="{Binding CategoryName}" Height="20" />
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
It works fine. However, once I select an item in the list, I want a different template to be applied to the combobox selected item being shown to the user (the item shown after the disappearance of popup). In the above case, I want only CategoryName to be displayed in the ComboBox once I select the respective item.
Can anyone let me know on how to achieve this?
thanks
What you need to do is create a ResourceDictionary containing a few defined templates yourself. In the below, ComboBoxTemplateOne and ComboBoxTeplateTwo are user controls that are set out to display the combobox in the manor you want.
<UserControl.Resources>
<ResourceDictionary>
<DataTemplate x:Key="TemplateOne">
<local:ComboBoxTemplateOne />
</DataTemplate>
<DataTemplate x:Key="TemplateTwo">
<local:ComboBoxTemplateTwo />
</DataTemplate>
</ResourceDictionary>
</UserControl.Resources>
You will then need to create your own class that inherits from ContentControl "DataTemplateSelector", overriding OnContentChanged
Protected Overrides Sub OnContentChanged(ByVal oldContent As Object, ByVal newContent As Object)
MyBase.OnContentChanged(oldContent, newContent)
Me.ContentTemplate = SelectTemplate(newContent, Me)
End Sub
You will then need to create another class that inherits from the above DataTemplateSelector which overrides SelectTemplate ("TemplateSelectorClass"), which will return the DataTemplate defined above ("TemplateOne" or "TemplateTwo").
Also in this derived class, you will need to define a property for each of the templates you have
Public Property ComboboxTemplateOne As DataTemplate
Then head back to your XAML and n the blow XAML
<local:TemplateSelectorClass ComboboxTemplateOne="{StaticResource TemplateOne}" Content="{Binding Path=ActiveWorkspace}>
This should work, as it is effectively doing the same work as setting the "DataTemplate" property in WPF (which doesn't exist in SilverLight)
I realise there are a fair few steps here and its quite fiddly, but hopefully this will get you there. Any questions just shout.

Resources