TabControl becoming very laggy - wpf

We're using a Tabcontrol to display a number of items with rather expensive content, and the issue we're having is that as you iterate over the tabs (selecting them one by one), the responsiveness of the application becomes slower and slower.
This behavior is unexpected as from what I understand, as the selected tab changes, the previously selected tabs content is unloaded first, so that you're only paying the price for one tabs content at a time.
I've managed to simulate the behaviour with the code below. To reproduce :
Run the application
Launch the selected tabs contextmenu (the tabs header contextmenu), it will be responsive
From left to right, go through each tab, selecting one by one
By the time you reach tab ~10, the responsiveness of its contextmenu is now very laggy, as you click a checkbox, its animation takes a few seconds to run through
<Window x:Class="WpfApplication4.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow"
WindowStartupLocation="CenterScreen">
<Window.Resources>
<Style TargetType="{x:Type TabItem}"
BasedOn="{StaticResource {x:Type TabItem}}">
<Setter Property="ContextMenu">
<Setter.Value>
<ContextMenu>
<CheckBox Content="CheckBox" />
<CheckBox Content="CheckBox" />
<CheckBox Content="CheckBox" />
<CheckBox Content="CheckBox" />
<CheckBox Content="CheckBox" />
</ContextMenu>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<TabControl Name="tabControl" />
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
for (int i = 0; i < 25; i++)
{
CreateTab();
}
}
void CreateTab()
{
var itemsControl = new ItemsControl();
for (int i = 0; i < 1000; ++i)
{
itemsControl.Items.Add(new TextBox());
}
tabControl.Items.Add(new TabItem()
{
Header = string.Format("Tab{0}", tabControl.Items.Count),
Content = itemsControl
});
}
}

I am not sure about your complex scenario what you have but for posted sample, issue is not in tabControl but instead in ItemsControl.
ItemsControl by default does not support UI virtualization, you have to make it UI virtualized i.e. whenever TabItem gets loaded, all UI containers to host items will be created i.e. 1000 items will be created.
You can verify that by replacing ItemsControl with ListBox and you can see considerable increase in performance because ListBox by default support UI virtualization and only containers for visible items will be created (may be 100 at a time).
Replace
var itemsControl = new ItemsControl();
with
var itemsControl = new ListBox();
and you will see difference in performance.
In case you want some performance with ItemsControl, you have to make it UI virtualized. Refer to the answer here to make it UI virtualized.
UPDATE
For comment:
The problem is that the application becomes slower and slower as you
select different tabs. This is unexpected. Due to each item being
unloaded before loading a new item and due to each item having the
same content, I'd expect the responsiveness to remain constant.
Yeah you are right that Unloaded event gets called for content of last selected tab item but it only disconnect ItemsControl from Visual Tree. However its containers remains intact and remains in memory. So, with every switch new containers are getting created in memory. That I guess is fair reason for sluggishness of your application.
That you can verify by hooking onto StatusChanged event:
itemsControl.ItemContainerGenerator.StatusChanged += (s, e) => { };
You will see that it gets called twice on every switch to new tabItem but doesn't gets called on switch to already visited tabItem.

Related

How to have dynamically loaded instances of unbound named XAML elements behave independently from each other?

I have a WPF application where I dynamically load document view instances into a TabControl. The view has a ToolBar with some ToggleButtons which I use to control the visibility of certain elements in that view like so (only relevant elements shown):
<UserControl x:Class="MyProject.View.Views.DocumentView" ...>
...
<ToolBar>
<ToggleButton x:Name="togglePropInspector" ... />
...
</ToolBar>
...
<Border Visibility={Binding ElementName=togglePropInspector, Path=IsChecked, Converter={StaticResource BoolToVisibilityConverter}}">
...
</Border>
</UserControl>
I found this kinda neat as everything is handled inside the view and didn't require adding code to the view model (or code behind). However, the problem is that checking the toggle button on one tab now checks it in all instances of the view, not just the current tab. This basically applies to all elements whose state is not bound to the view model in any way. Is there a way around this without having to add code to the view model?
For completeness' sake here's the relevant part of how I'm loading the views:
<TabControl ItemsSource="{Binding Documents}">
<TabControl.Resources>
<DataTemplate DataType="{x:Type viewModels:DocumentViewModel}">
<local:DocumentView />
</DataTemplate>
</TabControl.Resources>
</TabControl>
TabControl has a single content host that is used for all TabItem instances. When the data models assigned to the TabItem.Content property are of the same data type, then the TabControl will reuse the same DataTemplate, which means same element instances, only updated with the changed data from data bindings.
To change the state of the reused controls, you must either access the control explicitly, or force the TabControl to reapply the ContentTemplate by temporarily changing the data type of the Content:
<TabControl SelectionChanged="TabControl_SelectionChanged" />
private void TabControl_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
var tabControl = sender as TabControl;
var tabItemContainer = tabControl.ItemContainerGenerator.ContainerFromItem(tabControl.SelectedItem) as TabItem;
object currentContent = tabItemContainer.Content;
tabItemContainer.Content = null;
// Defer and leave the context to allow the TabControl to handle the new data type (null).
// The content switch shouldn't be noticable in the GUI.
Dispatcher.InvokeAsync(() => tabItemContainer.Content = currentContent);
}
You can also use a dedicated data type for each tab. This way the TabControl is automatically forced to switch the DataTemplate.
The cleanest solution would be to bind the ToggleButton to the data model.

TabControl's SelectedItem gets overwritten by NewItemPlaceholder when adding tab

I'm working on a WPF TabControl whose last item is always a button to add a new tab, similar to Firefox:
The TabControl's ItemSource is bound to an ObservableCollection, and adding an item to the collection via this "+" button works very well. The only problem I'm having is that, after having clicked the "+" tab, I cannot for the life of me set the newly created (or any other existing tab) to focus, and so when a tab is added, the UI looks like this:
To explain a bit how I'm achieving this "special" tab behavior, the TabControl is templated and its NewButtonHeaderTemplate has a control (Image in my case) which calls the AddListener Command in the view-model (only relevant code is shown):
<Window x:Class="AIS2.PortListener.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ais="http://www.leica-geosystems.com/xaml"
xmlns:l="clr-namespace:AIS2.PortListener"
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
xmlns:cmd="clr-namespace:GalaSoft.MvvmLight.Command;assembly=GalaSoft.MvvmLight.Extras.WPF4"
DataContext="{Binding Source={StaticResource Locator}>
<Window.Resources>
<ResourceDictionary>
<DataTemplate x:Key="newTabButtonHeaderTemplate">
<Grid>
<Image Source="..\Images\add.png" Height="16" Width="16">
</Image>
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseLeftButtonDown">
<cmd:EventToCommand
Command="{Binding Source={StaticResource Locator},
Path=PortListenerVM.AddListenerCommand}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</Grid>
</DataTemplate>
<DataTemplate x:Key="newTabButtonContentTemplate"/>
<DataTemplate x:Key="itemHeaderTemplate">
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
<DataTemplate x:Key="itemContentTemplate">
<l:ListenerControl></l:ListenerControl>
</DataTemplate>
<l:ItemHeaderTemplateSelector x:Key="headerTemplateSelector"
NewButtonHeaderTemplate="{StaticResource newTabButtonHeaderTemplate}"
ItemHeaderTemplate="{StaticResource itemHeaderTemplate}"/>
<l:ItemContentTemplateSelector x:Key="contentTemplateSelector"
NewButtonContentTemplate="{StaticResource newTabButtonContentTemplate}"
ItemContentTemplate="{StaticResource itemContentTemplate}"/>
</ResourceDictionary>
</Window.Resources>
<TabControl Name="MainTab" Grid.Row="2" ItemsSource="{Binding Listeners}"
ItemTemplateSelector="{StaticResource headerTemplateSelector}"
ContentTemplateSelector="{StaticResource contentTemplateSelector}"
SelectedItem="{Binding SelectedListener}">
</TabControl>
The AddListener command simply adds an item to the ObservableCollection which has for effect to update the TabControl's ItemSource and add a new tab:
private ObservableCollection<Listener> _Listeners;
public ObservableCollection<Listener> Listeners
{
get { return _Listeners; }
}
private object _SelectedListener;
public object SelectedListener
{
get { return _SelectedListener; }
set
{
_SelectedListener = value;
OnPropertyChanged("SelectedListener");
}
}
public PortListenerViewModel()
{
// Place the "+" tab at the end of the tab control
var itemsView = (IEditableCollectionView)CollectionViewSource.GetDefaultView(_Listeners);
itemsView.NewItemPlaceholderPosition = NewItemPlaceholderPosition.AtEnd;
}
private RelayCommand _AddListenerCommand;
public RelayCommand AddListenerCommand
{
get
{
if (_AddListenerCommand == null)
_AddListenerCommand = new RelayCommand(param => this.AddListener());
return _AddListenerCommand;
}
}
public void AddListener()
{
var newListener = new TCPListener(0, "New listener");
this.Listeners.Add(newListener);
// The following two lines update the property, but the focus does not change
//this.SelectedListener = newListener;
//this.SelectedListener = this.Listeners[0];
}
But setting the SelectedListener property does not work, even though the TabControl's SelectedItem is bound to it. It must have something to do with the order in which things get updated in WPF, because if I set a breakpoint in the SelectedListener's set I can see the following happening:
this.Listeners.Add(newListener);
this.SelectedListener = newListener;
SelectedListener set gets called with correct Listener object
SelectedListener set gets called with NewItemPlaceholder object (of type MS.Internal.NamedObject according to the debugger)
Is there a way that I can work around this issue? Do I have the wrong approach?
I think you are triggering two events when you click the new tab: MouseLeftButtonDown and TabControl.SelectionChanged
I think they're both getting queued, then processing one at a time.
So your item is getting added, set as selected, and then before the re-draw occurs the SelectionChanged event occurs to change the selection to the [+] tab.
Perhaps try using the Dispatcher to set the SelectedItem so it occurs after the TabControl changes it's selection. Or make it so if the user tries to switch to the NewTab, it cancels the SelectionChanged event so the selected tab doesn't actually change (of course, the SelectedTab will be your NewItem since the MouseDown event will have occurred)
When I did something like this in the past, I actually overwrote the TabControl Template to create the AddTab button as a Button, not as a TabItem. I want to suggest doing that instead of using the NewItemPlaceholder in the first place, but I've never tried working with the NewItemPlaceholder so don't really know if it's better or worse than overwriting the Template.
Take a look at this post regarding sentinel objects: WPF Sentinel objects and how to check for an internal type
There are several ways to work around issues with them, that post offers one of them.

Wpf Xbap TreeView VirtualizingStackPanel bug?

I have a TreeView with VirtualizingStackPanel on. The TreeView has a "Level 1" TreeViewItem, and I bind this TreeViewItem to a 10k items list. Each children is also another TreeViewItem.
Virtualization works well, and the performance is great, but there is a big issue. Let's say I am at the top of the page and press Ctrl-End to get to the bottom, browser goes blank. It will reappear if I scroll the mouse a bit or resize the browser.
Another big issue is: when i scroll very fast to the middle or the bottom of the tree. let's say I stop at item number 5000. Then I can't expand the child treeviewitem, the browser just
shows nothing until I scroll or resize a bit.
Any help is very much appreciated. Below is the sample xaml & code:
<Page x:Class="WpfBrowserApplication3.Page1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:s="clr-namespace:WpfBrowserApplication3"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300"
Title="Page1" DataContext="{StaticResource _mainViewModel}">
<Page.Resources>
<s:SingleToCollectionConverter x:Key="_collectionConverter"></s:SingleToCollectionConverter>
<DataTemplate x:Key="_level2Template">
<TreeViewItem Header="{Binding Order}">
<StackPanel Orientation="Horizontal">
<TextBlock Text="Order: "></TextBlock>
<TextBox Text="{Binding Order}"></TextBox>
</StackPanel>
</TreeViewItem>
</DataTemplate>
</Page.Resources>
<TreeView VirtualizingStackPanel.IsVirtualizing="True" VirtualizingStackPanel.VirtualizationMode="Standard">
<TreeViewItem Header="Level 1" ItemsSource="{Binding Level2List}" ItemTemplate="{StaticResource _level2Template}"></TreeViewItem>
</TreeView>
</Page>
public class MainViewModel
{
public MainViewModel()
{
Level2List = new List<Level2>();
for (int i = 0; i < 10000; i++)
{
Level2List.Add(new Level2(i));
}
}
public List<Level2> Level2List { get; set; }
}
public class Level2
{
public Level2(int order)
{
Order = order;
}
public int Order { get; set; }
}
I use Visual Studio 2010 with .Net 4. Plus I did notice if I set the Height & Width for the TreeViewItem under _level2Template, the issue is gone. But setting the Height is not an option in my case, cause the Height varies in the real Application.
Updated: It seems quite obvious to me that this issue happened because the height of the child treeviewitem may vary. Perhaps that's the reason why VirtualizingStackPanel is not turned on by default in TreeView, but it is turned on by default in DataGrid & ListBox. Needless to say the Height of a datagrid or listbox item is usually unchanged.
Updated: I downloaded a free trial of telerik RadTreeView and tested the virtualization. This problem does not appear at all in telerik radtreeview. May test the telerik one a little more then probably go with it then.
Found this: TreeView Virtualization
Same issue, and no solution for TreeView. Only way around is to use the ListBox instead of TreeView as suggested in http://www.beacosta.com/blog/?p=45
Pretty sure this is TreeView bug. I have tried Telerik RadTreeView and it also has its own bug when turn on Virtulization. I will change to use ListBox instead.

TabControl disposes of controls on inactive tabs

I'm using the MVVM pattern for my app. The MainWindow comprises a TabControl with the DataContext mapped to the ViewModel:
<Window.Resources>
<ResourceDictionary>
<DataTemplate x:Key="templateMainTabControl">
<ContentPresenter Content="{Binding Path=DisplayName}" />
</DataTemplate>
<local:ViewModel x:Key="VM" />
<local:WorkspaceSelector x:Key="WorkspaceSelector" />
<local:TabOneView x:Key="TabOneView" />
<local:TabTableView x:Key="TabTableView" />
<DataTemplate x:Key="TabOne">
<local:TabOneView />
</DataTemplate>
<DataTemplate x:Key="TabTable">
<local:TabTableView />
</DataTemplate>
</ResourceDictionary>
</Window.Resources>
<TabControl Grid.Row="0"
DataContext="{StaticResource VM}"
ItemsSource="{Binding Workspaces}"
SelectedItem="{Binding SelectedWorkspace}"
ItemTemplate="{StaticResource templateMainTabControl}"
ContentTemplateSelector="{StaticResource WorkspaceSelector}" />
The WorkspaceSelector looks like:
public class WorkspaceSelector : DataTemplateSelector
{
public override DataTemplate SelectTemplate( object item, DependencyObject container )
{
Window win = Application.Current.MainWindow;
Workspace w = ( Workspace ) item;
string key = w.DisplayName.Replace( " ", "" );
if ( key != "TabOne" )
{
key = "TabTable";
}
return win.FindResource( key ) as DataTemplate;
}
}
so that TabOne returns the DataTemplate. TabOne and the other two tabs return the DataTemplate TabTable.
If I run the application and click on each of the tabs twice (1, 2, 3, 1, 2, 3) I don't get what I expect, which is
TabOne's view is created
TabTwo's view is created
TabOne's View is created
TabTwo's view is created
that is, if the TemplateSelector returns a different value, the existing tab's controls are thrown away and the new tab's control's are created, and if the TemplateSelector returns the same value, nothing happens.
This is exactly what I don't want! I'd like the TabControl to keep all the controls on the tabs, and I would like to be able to do something about creating different controls in code for the case where I go from TabTwo to TabThree. I can live without the latter. But how do I tell the TabControl not to throw away each tab's controls when it's not selected?
This is a function of the TabControl and is the default behavior.
Basically, to save memory, the TabControl unloads the visual tree that is in its content area and replaces it with a newly crufted up one for the new tab. To prove this to yourself, you can listen to the Unload event on each control you template in and notice that it fires every time you switch tabs.
There are likely 2 reasons you would want to override this behavior:
You believe that there would be a significant performance penalty.
You are losing the state of the controls because any visual state that is being lost is not backed by a ViewModel.
As for #1, you shouldn't be concerned. CPU time is generally cheaper than RAM and the default behavior leans on the cheaper side of the resource equation. If you still feel like you REALLY don't want this behavior, you can see an example of overriding it here:
https://github.com/cefsharp/CefSharp/blob/master/CefSharp.Wpf.Example/Controls/NonReloadingTabControl.cs
However, I would consider this a "smell" for potentially a future performance issue you should spend the time figuring out now, rather than delaying figuring it out.
For #2, you have two options:
Make sure every property you want preserved (like IsSelected, etc) is backed by a ViewModel that preserves that state.
Create a persistent UserControl for each tab that you bind to, rather than to ViewModels (Workspaces in your case). There is an example of that in the "Writer" sample for WAF: http://waf.codeplex.com/

WPF ListBox with a ListBox - UI Virtualization and Scrolling

My prototype displays "documents" that contain "pages" that are
represented by thumbnail images. Each document can have
any number of pages. For example, there might be
1000 documents with 5 pages each, or 5 documents with 1000 pages
each, or somewhere inbetween. Documents do not contain other documents.
In my xaml markup I have a ListBox, whose ItemsTemplate
references an innerItemsTemplate that also has a ListBox. I want the
2 levels of selected items so that I can perform various operations
on documents or pages (delete, merge, move to new location, etc).
The innerItemsTemplate ListBox uses a WrapPanel as the ItemsPanelTemplate.
For the scenario where I have a large number of documents with a few
pages each (say, 10000 documents with 5 pages each), the scrolling
works great thanks to the UI Virtualization by the VirtualizingStackPanel.
However, I have problems if I have a large number of pages. A document
with 1000 pages will only display about 50 at a time (whatever fits on the screen), and when I scroll down, the outer ListBox moves to the next document, skipping the 950
pages or so that were not visible. Along with that, there is no
VirtualzingWrapPanel so the app memory really increases.
I'm wondering if I am going about this the right way, especially
since it is sort of difficult to explain! I would like to be able to display
10000 documents with 1000 pages each (only showing whatever fits on the screen),
using UI Virtualization, and also smooth scrolling.
How can I make sure the scrolling moves through all of the pages in document
before it displays the next document, and still keep UI virtualization?
The scrollbar seems to only move to the next document.
Does it seem logical to represent "documents" and "pages" -
with my current method of using a ListBox within a ListBox?
I would very much appreciate any ideas you have.
Thank You.
It is possible to achieve smooth scrolling VirtualizingStackPanels in WPF 4.0 without sacrificing virtualization if you're prepared to use reflection to access private functionality of the VirtualizingStackPanel. All you have to do is set the private IsPixelBased property of the VirtualizingStackPanel to true.
Note that in .Net 4.5 there's no need for this hack as you can set VirtualizingPanel.ScrollUnit="Pixel".
To make it really easy, here's some code:
public static class PixelBasedScrollingBehavior
{
public static bool GetIsEnabled(DependencyObject obj)
{
return (bool)obj.GetValue(IsEnabledProperty);
}
public static void SetIsEnabled(DependencyObject obj, bool value)
{
obj.SetValue(IsEnabledProperty, value);
}
public static readonly DependencyProperty IsEnabledProperty =
DependencyProperty.RegisterAttached("IsEnabled", typeof(bool), typeof(PixelBasedScrollingBehavior), new UIPropertyMetadata(false, HandleIsEnabledChanged));
private static void HandleIsEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var vsp = d as VirtualizingStackPanel;
if (vsp == null)
{
return;
}
var property = typeof(VirtualizingStackPanel).GetProperty("IsPixelBased",
BindingFlags.NonPublic | BindingFlags.Instance);
if (property == null)
{
throw new InvalidOperationException("Pixel-based scrolling behaviour hack no longer works!");
}
if ((bool)e.NewValue == true)
{
property.SetValue(vsp, true, new object[0]);
}
else
{
property.SetValue(vsp, false, new object[0]);
}
}
}
To use this on a ListBox, for example, you would do:
<ListBox>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel PixelBasedScrollingBehavior.IsEnabled="True">
</VirtualizingStackPanel>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
The answer here is surprising:
If you use ItemsControl or ListBox you will get the behavior you are experiencing, where the control scrolls "by item" so you jump over a whole document at once, BUT
If you use TreeView instead, the control will scroll smoothly so you can scroll through your document and into the next one, but it will still be able to virtualize.
I think the reason the WPF team chose this behavior is that TreeViewcommonly has items that are larger than the visible area, whereas typically ListBoxes don't.
In any case, it is trivial in WPF to make a TreeView look and act like a ListBox or ItemsControl by simply modifying the ItemContainerStyle. This is very straightforward. You can roll your own or just copy over the appropriate template from the system theme file.
So you will have something like this:
<TreeView ItemsSource="{Binding documents}">
<TreeView.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</TreeView.ItemsPanel>
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TreeViewItem}">
<ContentPresenter /> <!-- put your desired container style here with a ContentPresenter inside -->
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</TreeView.ItemContainerStyle>
<TreeView.ItemTemplate>
<DataTemplate TargetType="{x:Type my:Document}">
<Border BorderThickness="2"> <!-- your document frame will be more complicated than this -->
<ItemsControl ItemsSource="{Binding pages}">
...
</ItemsControl>
</Border>
</DataTemplate>
</TreeView.ItemTemplate>
</TreeView>
Getting pixel-based scrolling and ListBox-style multiselect to work together
If you use this technique to get pixel-based scrolling, your outer ItemsControl which shows the documents cannot be a ListBox (because ListBox is not a subclass of TreeView or TreeViewItem). Thus you lose all of ListBox's multiselect support. As far as I can tell, there is no way to use these two features together without including some of your own code for one feature or the other.
If you need both sets of functionality in the same control, you have basically several options:
Implement multi-selection yourself in a subclass of TreeViewItem. Use TreeViewItem instead of TreeView for the outer control, since it allows multiple children to be selected. In the template inside ItemsContainerStyle: Add a CheckBox around the ContentPresenter, template bind the CheckBox to IsSelected, and style the CheckBox with control template to get the look you want. Then add your own mouse event handlers to handle Ctrl-Click and Shift-Click for multiselect.
Implement pixel-scrolled virtualization yourself in a subclass of VirtualizingPanel. This is relatively simple, since most of VirtualizingStackPanel's complexity is related to non-pixel scrolling and container recycling. Dan Crevier's Blog has some useful infromation for understanding VirtualizingPanel.
.NET 4.5 now has the VirtualizingPanel.ScrollUnit="ScrollUnit" property. I just converted one of my TreeViews to a ListBox and the performance was noticeably better.
More information here: http://msdn.microsoft.com/en-us/library/system.windows.controls.virtualizingpanel.scrollunit(v=vs.110).aspx
This worked for me. Seems a couple of simple attributes will do it (.NET 4.5)
<ListBox
ItemsSource="{Binding MyItems}"
VirtualizingStackPanel.IsVirtualizing="True"
VirtualizingStackPanel.ScrollUnit="Pixel"/>
Please allow me to preface this answer with a question: Does the user have to see each and every thumbnail within every item in the list at all times?
If the answer to that question is 'no', then perhaps it would be feasible to limit the number of visible pages within the inner item template (given that you have indicated the scrolling works well with, say, 5 pages) and use a separate 'selected item' template that is larger and displays all pages for that document? Billy Hollis explains how to 'pop' a selected item out in a listbox on dnrtv episode 115

Resources