WPF ListBox with a ListBox - UI Virtualization and Scrolling - wpf

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

Related

WPF: ListView with Images from a folder

I'm sort of new to WPF, but I have to do this and it's taking a lot of my time. I've searched for a solution but there are many alternative solutions and I honestly don't understand most of this. I have this XAML code:
<ListView Name="Thumbnails">
<ListView.ItemTemplate>
<DataTemplate>
<Image Source="{Binding}" Height="30" Width="30" Margin="5"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
As well as this codebehind:
private void Window_Loaded(object sender, RoutedEventArgs e)
{
DirectoryInfo folder = new DirectoryInfo(Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName) + #"\SlikeSportista\");
FileInfo[] images = folder.GetFiles("*.jpg");
foreach (FileInfo img in images)
{
Thumbnails.Items.Add(img);
}
}
I've also tried this line of code in the foreach loop:
Thumbnails.Items.Add(System.Drawing.Image.FromFile(img.FullName));
In both cases the items are added, but the images are not displayed correctly, or rather, at all. You can select them, and there are the same amount of elements as there are in the folder, but there is no display.
Another question (less important one) would be how to display the images in squares instead of rows. Basically I want to have about 4 or so images per row, but now I have only 1 element per row, stretched all the way (although I can't see what is being displayed).
In your first attempt, you're adding FileInfo objects to the ListView's items collections. These aren't automatically converted to ImageSource items, as required by the binding in your DataTemplate. Add the FileInfo's FullName instead:
foreach (FileInfo img in images)
{
Thumbnails.Items.Add(img.FullName);
}
In your second attempt the problem is that you add instances of System.Drawing.Image, which is not part of WPF, but WinForms, and will also not be converted automatically. You may use BitmapImage instead:
foreach (FileInfo img in images)
{
Thumbnails.Items.Add(new BitmapImage(new Uri(img.FullName)));
}
The difference between both solutions is that in the second one you manually create image objects, whereas the first one relies on automatic conversion from string to ImageSource, which is built into WPF as a TypeConverter.
A solution for your second question would be to replace the ListView's ItemsPanel, perhaps by a UniformGrid:
<ListView Name="Thumbnails">
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="4"/>
</ItemsPanelTemplate>
</ListView.ItemsPanel>
...
</ListView>
As a general rule, you should keep in mind that adding images in the code-behind file (.xaml.cs file) is bad practice. In WPF there is a very widely used and common design pattern called MVVM (Model-View-ViewModel) which you should familiarize with and use. In your case, you should have had a ViewModel class containing an IEnumerable<BitmapImage> property that contains the images you wish to display in your ListView.
For example, let's say your ViewModel class is called ImagesViewModel and your view is ImagesView:
ImagesViewModel will have a property called:
ObservableCollection<BitmapImage> Images
ImagesView will contain:
<ListView Name="Thumbnails" ItemsSource="{Binding Images}">
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="4"/>
</ItemsPanelTemplate>
</ListView.ItemsPanel>
...
Now, if you add / remove images to Images, they will be automatically added / removed from your list view (you have to implement the INotifyPropertyChanged interface in your view model and you're done).

KeyboardNavigation.ControlTabNavigation Application Wide?

I have an issue I've been trying to deal with - the following:
KeyboardNavigation.ControlTabNavigation="None"
doesn't seem to work anyplace in my application despite my best efforts...I'm not entirely sure why but regardless of what I do, the CTRL+TAB functionality always works, and in my case the behavior is detrimental to how I'd like my tab controls to operate. Ideally, rather than placing the above tag in every container in my application (which I can't get to work anyway), I'd like to disable ControlTabNavigation across the entire application. Is there a way to do this without having to go container by container, and is there any obvious "gotchas" that normally keep the above from working properly?
Thank you!
Aj
I find that the KeyboardNavigation does not work as I would expect as it pertains to Ctrl-Tab and a TabControl. I put together a simple prototype and KeyboardNavigation.ControlTabNavigation="None" just does not seem to have the expected impact on the switching of Tabs using Ctrl-Tab, once I left-click a tab and the keyboard focus is within the TabControl.
However, using InputBindings with a Command can override the unwanted Ctrl-Tab default behavior. From there, I found that KeyboardNavigation.TabNavigation="Cycle", as well as the other the other options for TabNavigation seem to behave reasonably. Using The FocusManager and other techniques described in the resource links below should allow one to obtain desired keyboard navigation, albeit using somewhat counter-intuitive techniques.
The InputBindings do have to be set for each control that has the unwanted default behavior, although a more sophisticated solution might walk the visual tree to set the InputBindings for all controls of a certain type, for example. I found having the Command simply do nothing neutralizes the key sequence adequately. In my example, I display a dialog box for testing.
Note, below Command binding requires you target WPF 4.0; please see resources at end of post for resource on how to target WPF 3.5 or earlier
In XAML:
<TabControl
x:Name="tabControl1"
IsSynchronizedWithCurrentItem="True"
SelectedItem="{Binding SelectedTabItem}"
ItemsSource="{Binding TabItemViewModels}"
KeyboardNavigation.ControlTabNavigation="None"
KeyboardNavigation.TabNavigation="Continue">
<TabControl.InputBindings>
<KeyBinding Modifiers="Control"
Key="Tab"
Command="{Binding ShowDialogCommand}" />
</TabControl.InputBindings>
</TabControl>
Note, in above XAML, KeyboardNavigation.ControlTabNavigation="None" is of no effect and can be eliminated.
In backing DataContext, typically a ViewModel:
Declare your binding property:
public RelayCommand ShowDialogCommand
{
get;
private set;
}
Initialize the property; for example, can be in the constructor of the ViewModel (Note, RelayCommand is from MVVM-Light library.):
ShowDialogCommand = new RelayCommand(() =>
{
MessageBox.Show("Show dialog box command executed", "Show Dialog Box Command", MessageBoxButton.OK, MessageBoxImage.Information);
});
Resources:
Helpful StackOverflow post on KeyBindings
More detail on KeyBinding to a Command; describes special CommandReference technique needed if targeting WPF framewrok 3.5 or earlier
Microsoft's Focus Overview
I hadn't looked at this issue in a while, but since Bill asked it sparked a renewed interest. Rather than going control by control, I used an empty command as Bill suggested, but applied it to a TabControl template...as Bill pointed out, somewhat of a counter-intuitive solution, but it works (I also accounted for Ctrl+Shift+Tab which is just the opposite direction of Ctrl+Tab):
MyClass:
public static readonly RoutedCommand CancelTabChangeCommand = new RoutedCommand() { };
XAML:
<Style TargetType="{x:Type TabControl}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TabControl}">
<Grid ClipToBounds="true" SnapsToDevicePixels="true" KeyboardNavigation.TabNavigation="Local">
<Grid.InputBindings>
<KeyBinding Command="{x:Static local:myClass.CancelTabChangeCommand}" Key="Tab" Modifiers="Control" />
<KeyBinding Command="{x:Static star:Startup.CancelTabChangeCommand}" Key="Tab" Modifiers="Shift+Control"/>
</Grid.InputBindings>
I left off the rest of my class and XAML as it wasn't pertinent to the example, but I'm happy to provide more if anyone needs it. On a related note, I also found that create a control template for the TabItem and setting the IsTabStop property to false also keeps my users from tabbing across and changing tabs in that fashion as well...in case anyone was having this issue as I was.
Hope it helps!

How do you navigate a complex Visual Tree in order to re-bind an existing element?

In the above image, child is a ContentPresenter. Its Content is a ViewModel. However, its ContentTemplate is null.
In my XAML, I have a TabControl with the following structure:
<local:SuperTabControlEx DataContext="{Binding WorkSpaceListViewModel}"
x:Name="superTabControl1" CloseButtonVisibility="Visible" TabStyle="OneNote2007" ClipToBounds="False" ContentInnerBorderBrush="Red" FontSize="24" >
<local:SuperTabControlEx.ItemsSource>
<Binding Path="WorkSpaceViewModels" />
</local:SuperTabControlEx.ItemsSource>
<TabControl.Template>
<ControlTemplate
TargetType="TabControl">
<DockPanel>
<TabPanel
DockPanel.Dock="Top"
IsItemsHost="True" />
<Grid
DockPanel.Dock="Bottom"
x:Name="PART_ItemsHolder" />
</DockPanel>
<!-- no content presenter -->
</ControlTemplate>
</TabControl.Template>
<TabControl.Resources>
<DataTemplate DataType="{x:Type vm:WorkSpaceViewModel}">
....
WorkSpaceViewModels is an ObservableCollection of WorkSpaceViewModel. This code uses the code and technique from Keeping the WPF Tab Control from destroying its children.
The correct DataTemplate - shown above in the TabControl.Resource - appears to be rendering my ViewModel for two Tabs.
However, my basic question is, how is my view getting hooked up to my WorkSpaceViewModel, yet, the ContentTemplate on the ContentPresenter is null? My requirement is to access a visual component from the ViewModel because a setting for the view is becoming unbound from its property in the ViewModel upon certain user actions, and I need to rebind it.
The DataTemplate is "implicitly" defined. The ContentPresenter will first use it's ContentTemplate/Selector, if any is defined. If not, then it will search for a DataTemplate resource without an explicit x:Key and whose DataType matches the type of it's Content.
This is discussed here and here.
The View Model shouldn't really know about it's associated View. It sounds like there is something wrong with your Bindings, as in general you should not have to "rebind" them. Either way, an attached behavior would be a good way to accomplish that.
I think the full answer to this question entails DrWPF's full series ItemsControl: A to Z. However, I believe the gist lies in where the visual elements get stored when a DataTemplate is "inflated" to display the data item it has been linked to by the framework.
In the section Introduction to Control Templates of "ItemsControl: 'L' is for Lookless", DrWPF explains that "We’ve already learned that a DataTemplate is used to declare the visual representation of a data item that appears within an application’s logical tree. In ‘P’ is for Panel, we learned that an ItemsPanelTemplate is used to declare the items host used within an ItemsControl."
For my issue, I still have not successfully navigated the visual tree in order to get a reference to my splitter item. This is my best attempt so far:
// w1 is a Window
SuperTabControlEx stc = w1.FindName("superTabControl1") as SuperTabControlEx;
//SuperTabItem sti = (SuperTabItem)(stc.ItemContainerGenerator.ContainerFromItem(stc.Items.CurrentItem));
ContentPresenter myContentPresenter = FindVisualChild<ContentPresenter>(stc);
//ContentPresenter myContentPresenter = FindVisualChild<ContentPresenter>(sti);
DataTemplate myDataTemplate = myContentPresenter.ContentTemplate;
The above code is an attempt to implement the techniques shown on the msdn web site. However, when I apply it to my code, everything looks good, except myDataTemplate comes back null. As you can see, I attempted the same technique on SuperTabControlEx and SuperTabItem, derived from TabControl and TabItem, respectively. As described in my original post, and evident in the XAML snippet, the SuperTabControlEx also implements code from Keeping the WPF Tab Control from destroying its children.
At this point, perhaps more than anything else, I think this is an exercise in navigating the Visual Tree. I am going to modify the title of the question to reflect my new conceptions of the issue.

WPF Animation on listbox items

Any idea what approach I would follow to get the items of a databound listbox to "fly" into the their position in the listbox similarly to the effect you see when a deck of cards is dealt in those Windows Card Games? (I'm using WPF)
You would need to extend the Panel class and use it as the ItemsPanelTemplate of your ListBox.ItemsPanel property. It's really easy... there's just two methods to override; one to measure the items in the ListBox and one to arrange them. Here is a microsoft article on the subject.
Here is perhaps a more useful article [unfortunately, no longer available] that shows how to animate the items. For your effect, you would simply set the from value on your position animations to be the same location just off the viewable area for each of your items. For example, using a from position of 0, finalSize.Height would mean that each item would slide to its position from the bottom left corner of the ListBox.
You could use your new animated Panel as follows:
<ListBox>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<YourXmlNamespace:YourAnimatedPanel AnyCustomProperty="value" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>

WPF : Multiple views, one DataContext

I'm working on a WPF application which must handle multiple screens (two at this this time).
One view can be opened on several screens and user actions must be reflected consistently on all screens.
To achieve this, for a given type of view, a single DataContext is instantiated. Then, when a view is displayed on a screen, the unique DataContext is attached to it. So, one DataContext, several views (same type of view/xaml).
So far so good. It works quite well in most cases.
I do have a problem with a specific view which relies on ItemsControl. These ItemsControl are used to display UIElements dynamically build in the ViewModel/DataContext (C# code). These UIElements are mostly Path objects. Example :
<ItemsControl ItemsSource="{Binding WindVectors}">
<ItemsControl.Template>
<ControlTemplate TargetType="{x:Type ItemsControl}">
<Canvas IsItemsHost="True" />
</ControlTemplate>
</ItemsControl.Template>
</ItemsControl>
Here, WindVectors is a ObservableCollection<UIElement>.
When the view is opened the first time, everything is fine. The problem is that when the view is opened one another screen, all ItemsControl are removed from the first screen and displayed one the second screen. Other WPF components (TextBlock for instance) on this view react normally and are displayed on both screens.
Any help would be greatly appreciated.
Thanks.
Fabrice
This is the expected behavior (ie been that way since winforms)- this is because the ObservableCollection is a reference. This wont happen with value types, only reference types.
The short answer is 'dont do that'. You could try looking into defining a collection view in the xaml or code a custom data provider and bind to one of those instead.

Resources