Child elements of scrollviewer preventing scrolling with mouse wheel? - wpf

I'm having a problem getting mouse wheel scrolling to work in the following XAML, which I have simplified for clarity:
<ScrollViewer
HorizontalScrollBarVisibility="Visible"
VerticalScrollBarVisibility="Visible"
CanContentScroll="False"
>
<Grid
MouseDown="Editor_MouseDown"
MouseUp="Editor_MouseUp"
MouseMove="Editor_MouseMove"
Focusable="False"
>
<Grid.Resources>
<DataTemplate
DataType="{x:Type local:DataFieldModel}"
>
<Grid
Margin="0,2,2,2"
>
<TextBox
Cursor="IBeam"
MouseDown="TextBox_MouseDown"
MouseUp="TextBox_MouseUp"
MouseMove="TextBox_MouseMove"
/>
</Grid>
</DataTemplate>
</Grid.Resources>
<ListBox
x:Name="DataFieldListBox"
ItemsSource="{Binding GetDataFields}"
SelectionMode="Extended"
Background="Transparent"
Focusable="False"
>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemContainerStyle>
<Style
TargetType="ListBoxItem"
>
<Setter
Property="Canvas.Left"
Value="{Binding dfX}"
/>
<Setter
Property="Canvas.Top"
Value="{Binding dfY}"
/>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
</Grid>
</ScrollViewer>
Visually, the result is an area of some known size where DataFields read from a collection can be represented with TextBoxes which have arbitrary position, size, et cetera. In cases where the ListBox's styled "area" is too large to display all at once, horizontal and vertical scrolling is possible, but only with the scroll bars.
For better ergonomics and sanity, mouse wheel scrolling should be possible, and normally ScrollViewer would handle it automatically, but the ListBox appears to be handing those events such that the parent ScrollViewer never sees them. So far I have only been able to get wheel scrolling working be setting IsHitTestVisible=False for either the ListBox or the parent Grid, but of course none of the child element's mouse events work after that.
What can I do to ensure the ScrollViewer sees mouse wheel events while preserving others for child elements?
Edit: I just learned that ListBox has a built-in ScrollViewer which is probably stealing wheel events from the parent ScrollViewer and that specifying a control template can disable it. I'll update this question if that resolves the problem.

You can also create a behavior and attach it to the parent control (in which the scroll events should bubble through).
// Used on sub-controls of an expander to bubble the mouse wheel scroll event up
public sealed class BubbleScrollEvent : Behavior<UIElement>
{
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.PreviewMouseWheel += AssociatedObject_PreviewMouseWheel;
}
protected override void OnDetaching()
{
AssociatedObject.PreviewMouseWheel -= AssociatedObject_PreviewMouseWheel;
base.OnDetaching();
}
void AssociatedObject_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
e.Handled = true;
var e2 = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta);
e2.RoutedEvent = UIElement.MouseWheelEvent;
AssociatedObject.RaiseEvent(e2);
}
}
<SomePanel>
<i:Interaction.Behaviors>
<viewsCommon:BubbleScrollEvent />
</i:Interaction.Behaviors>
</SomePanel>

Specifying a ControlTemplate for the Listbox which doesn't include a ScrollViewer solves the problem. See this answer and these two MSDN pages for more information:
ControlTemplate
ListBox Styles and Templates

Another way of implementing this, is by creating you own ScrollViewer like this:
public class MyScrollViewer : ScrollViewer
{
protected override void OnMouseWheel(MouseWheelEventArgs e)
{
var parentElement = Parent as UIElement;
if (parentElement != null)
{
if ((e.Delta > 0 && VerticalOffset == 0) ||
(e.Delta < 0 && VerticalOffset == ScrollableHeight))
{
e.Handled = true;
var routedArgs = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta);
routedArgs.RoutedEvent = UIElement.MouseWheelEvent;
parentElement.RaiseEvent(routedArgs);
}
}
base.OnMouseWheel(e);
}
}

I know it's a little late but I have another solution that worked for me. I switched out my stackpanel/listbox for an itemscontrol/grid. Not sure why the scroll events work properly but they do in my case.
<ScrollViewer VerticalScrollBarVisibility="Auto" PreviewMouseWheel="ScrollViewer_PreviewMouseWheel">
<StackPanel Orientation="Vertical">
<ListBox ItemsSource="{Binding DrillingConfigs}" Margin="0,5,0,0">
<ListBox.ItemTemplate>
<DataTemplate>
became
<ScrollViewer VerticalScrollBarVisibility="Auto" PreviewMouseWheel="ScrollViewer_PreviewMouseWheel">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ItemsControl ItemsSource="{Binding DrillingConfigs}" Margin="0,5,0,0" Grid.Row="0">
<ItemsControl.ItemTemplate>
<DataTemplate>

isHitTestVisible=False in the child works great for me
Edit This isnt a good way to do it

Related

Creating a WPF Window that allows zooming and panning

I want to create a Window that will hold several controls. However, I would like the user to be able to pan around and zoom in and out to see larger versions of those controls.
I don't even know where to begin looking.
I was going to start at ScaleTransform that responds to the use of the scroll button on the mouse but I am not sure if that is the best idea.
Just need a push in the right direction.
thanks!
This might be a good candidate for a Viewbox.
See here: http://msdn.microsoft.com/en-us/library/system.windows.controls.viewbox(v=vs.110).aspx
Basically, you can Wrap the entire contents of the Window into a Viewbox like so:
<Window>
<Viewbox>
<!-- content here -->
</Viewbox>
</Window>
and then bind to the Viewbox control's width and height to simulate the zooming. For a quick test, you could just listen to scroll wheel events via code-behind, name the Viewbox control, and access the Viewbox directly at change the values there.
Edit: here's a scenario I just found to get you started. They are using an image, but it's the exact same concept that I described above.
http://www.c-sharpcorner.com/uploadfile/yougerthen/working-with-wpf-viewbox-control/
Edit2: Quick working example using mouse-scroll
Xaml:
<Window x:Class="WpfApplication2.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow"
MouseWheel="MainWindow_OnMouseWheel">
<Grid>
<Viewbox x:Name="ZoomViewbox" Stretch="Fill">
<StackPanel>
<Label Content="Label" HorizontalAlignment="Left" VerticalAlignment="Top"/>
<Button Content="Button" HorizontalAlignment="Left" VerticalAlignment="Top" />
</StackPanel>
</Viewbox>
</Grid>
</Window>
C#:
using System.Windows;
using System.Windows.Input;
namespace WpfApplication2
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
ZoomViewbox.Width = 100;
ZoomViewbox.Height = 100;
}
private void MainWindow_OnMouseWheel(object sender, MouseWheelEventArgs e)
{
UpdateViewBox((e.Delta > 0) ? 5 : -5);
}
private void UpdateViewBox(int newValue)
{
if ((ZoomViewbox.Width >= 0) && ZoomViewbox.Height >= 0)
{
ZoomViewbox.Width += newValue;
ZoomViewbox.Height += newValue;
}
}
}
}
You can get functionality out of a ScrollViewer and a ScaleTransform. Here's an example:
<Window x:Class="CSharpWpf.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- This ScrollViewer enables the panning -->
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
<!-- This StackPanel is the container for the zoomable/pannable content. -->
<!-- Any container control (StackPanel, DockPanel, Grid, etc) may be used here. -->
<StackPanel HorizontalAlignment="Left" VerticalAlignment="Top">
<!-- This ScaleTransform implements the zooming and is bound the Value of the ZoomSlider -->
<StackPanel.LayoutTransform>
<ScaleTransform ScaleX="{Binding ElementName=ZoomSlider, Path=Value}" ScaleY="{Binding ElementName=ZoomSlider, Path=Value}" />
</StackPanel.LayoutTransform>
<!-- Here is your custom content -->
<Button>Foo</Button>
<Button>Bar</Button>
</StackPanel>
</ScrollViewer>
<!-- This Slider controls the zoom level -->
<Slider x:Name="ZoomSlider" Orientation="Horizontal" Grid.Row="1" Minimum="0.0" Maximum="8.0" LargeChange="0.25" SmallChange="0.01" Value="1.0" />
</Grid>
</Window>
Simple solution :
private void Window_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
if (Keyboard.Modifiers != ModifierKeys.Control)
return;
if (e.Delta < 0 && _scale > 0.7)
{
_scale -= 0.1;
MainGrid.LayoutTransform = new ScaleTransform(_scale, _scale);
}
else if (e.Delta > 0 && _scale < 1.5)
{
_scale += 0.1;
MainGrid.LayoutTransform = new ScaleTransform(_scale, _scale);
}
}

Silverlight 4: how to show ToolTip on keyboard focus (revised)

My original question:
Is there an easy way for a ToolTip to be shown when an item gets keyboard focus, not just mouse over? We have a list of items with tooltips that users will probably tab through, and the desired behavior is for a tooltip to be shown then too.
Added example XAML. A HyperlinkButton with the Tooltip set is what needs the keyboard focus as well.
<DataTemplate x:Key="OfferingItemDT">
<HyperlinkButton Command="{Binding Path=NavigateToLinkCommand}" ToolTipService.ToolTip="{Binding Tooltip}">
<Grid x:Name="gOfferingButtonRoot" Width="275" MaxHeight="78" Margin="5,3">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="40"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Image x:Name="imgServiceOfferingIcon"
Grid.RowSpan="2"
VerticalAlignment="Top"
Source="{Binding Path=Image, Converter={StaticResource ByteArrayToImageConverter}}"
Stretch="UniformToFill"
Margin="2,10,0,0"
MaxHeight="32" MaxWidth="32"
/>
<TextBlock x:Name="txbOfferingTitle"
Grid.Column="1"
Grid.Row="0"
Text="{Binding Title}"
TextWrapping="Wrap"
Style="{StaticResource OfferingTileTitleText}"/>
<TextBlock x:Name="txbOfferingDesc"
Grid.Column="1"
Grid.Row="1"
Style="{StaticResource OfferingTileBodyText}"
Text="{Binding BriefDescription}" />
</Grid>
</HyperlinkButton>
</DataTemplate>
Updated:
Based on info in WPF: Show and persist ToolTip for a Textbox based on the cursor as well as Anthony's comments, I tried this code in the GotFocus eventhandler:
private void showTooltip(object sender, RoutedEventArgs e)
{
HyperlinkButton hb = new HyperlinkButton();
ToolTip ttip = new ToolTip();
hb = sender as HyperlinkButton;
ttip = ToolTipService.GetToolTip(hb) as ToolTip;
ttip.IsOpen = true;
}
This seems like it would work, but ttip is always null. Help?
"Easy" is subjective term. Yes its easy. On the same UI element on which you attach the ToolTip you can hook the GotFocus and LostFocus event handler the will use ToolTipService.GetToolTip to acquire the tooltip and the set IsOpen to true and false respectively.
The missing part is to define the tooltip in XAML so that we can access the Tooltip element.
<HyperlinkButton MouseLeftButtonUp="showTooltip">
<ToolTipService.ToolTip>
<ToolTip>
<TextBlock Text="My tooltip text"/>
</ToolTip>
</ToolTipService.ToolTip>
<!-- ... -->
</HyperlinkButton>
Code behind
private void showTooltip(object sender, RoutedEventArgs e)
{
FrameworkElement frameworkElement = (FrameworkElement)sender;
ToolTip tooltip = ToolTipService.GetToolTip(frameworkElement) as ToolTip;
if (tooltip != null)
{
tooltip.IsOpen = true;
frameworkElement.MouseLeave += new MouseEventHandler(frameworkElement_MouseLeave);
}
}
static void frameworkElement_MouseLeave(object sender, MouseEventArgs e)
{
FrameworkElement frameworkElement = (FrameworkElement)sender;
frameworkElement.MouseLeave -= new MouseEventHandler(frameworkElement_MouseLeave);
ToolTip tooltip = ToolTipService.GetToolTip(frameworkElement) as ToolTip;
if (tooltip != null)
{
tooltip.IsOpen = false;
}
}

WPF Scroll & focus changing problem

I am having a problem with scrolling for my WPF application.
Here is the deal. My UI is the following:
The role of my application is to act as a central hub for many applications and launch them. An admin can launch a dump recorded by another user.
Therefore, I have a ListView, showing the application list, which is scrollable if needed.
I defined a GroupStyle in order to show expanders and emulate a Windows Explorer view.
Everything works fine, I just have a problem: when scrolling with the mouse wheel, the component in clear blue ("Launch mode") seems to be catching the focus and stop scrolling.
This means especially that if my mouse is anywhere out of this control, the scrolling is okay. But whenever the mouse enters this control, I can't scroll anymore.
I tried to modify the property Focusable and set it to False everywhere I could but nothing changed. I'd guess that it is finally not a focus problem.
Anybody has an idea on how to avoid the scrolling to be caught by the element?
Here is some (simplified, removed some useless properties so as to make it as clear as possible) XAML for the expander's content:
<StackPanel Orientation="Vertical" VerticalAlignment="Top" >
<ToggleButton>
<!-- ToggleButton Content... -->
</ToggleButton>
<!-- This is the custom component in which you can see "Launch mode" -->
<my:UcReleaseChooser >
<!-- Properties there. I tried to set Focusable to False, no impact... -->
</my:UcReleaseChooser>
</StackPanel>
And the code for UcReleaseChooser:
<StackPanel HorizontalAlignment="Stretch"
Focusable="False" ScrollViewer.CanContentScroll="False">
<ListBox ItemsSource="{Binding ListChosenReleases}" BorderBrush="LightGray" Background="AliceBlue"
HorizontalAlignment="Stretch" Focusable="False" ScrollViewer.CanContentScroll="False">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical"
Focusable="False" ScrollViewer.CanContentScroll="False"/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<DockPanel LastChildFill="True" HorizontalAlignment="Stretch"
Focusable="False" ScrollViewer.CanContentScroll="False">
<TextBlock DockPanel.Dock="Top"
HorizontalAlignment="Left" Text="{Binding Key}"
FontStyle="Italic"/>
<ListBox DockPanel.Dock="Bottom"
HorizontalAlignment="Right" ItemsSource="{Binding Value}"
BorderBrush="{x:Null}" Background="AliceBlue"
Focusable="False" ScrollViewer.CanContentScroll="False">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Focusable="False"/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<-- Blah blah about style -->
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<RadioButton Content="{Binding Key}" Margin="3"
IsChecked="{Binding Path=IsSelected, Mode=TwoWay,
RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type ListBoxItem}}}"
Focusable="False" ScrollViewer.CanContentScroll="False"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DockPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>
As you can see, the UcReleaseChooser contains a list of RadioButton lists. I tried to set Focusable & CanContentScroll to False everywhere it seemed appropriate, but the control keeps preventing the main UI to scroll...
I guess I should change another property... Any idea?
Thanks!
The problem is the ListBox, or more specifically, the ScrollViewer within the ListBox's template. This is getting your scroll events and consuming them before the outer ScrollViewer in the ListView even sees them.
I would advise replacing the ListBox with an ItemsControl if possible. However, that implies there will be no SelectedItem property. If you need that, I would suggest setting ScrollViewer.HorizontalScrollBarVisibility (or VerticalScrollBarVisibility) to Disabled. Failing that, I can only suggest re-templating ListBox to not contain a ScrollViewer at all.
I had an issue with a listbox stealing focus inside a scrollviewer (I have multiple listboxes inside the scrollviewer). So I created an attached property which denies the listbox the ability to scroll. So therefore the scrollviewer which is housing the listbox can scroll
Your control is a listbox, so this should work as is, but there is no reason the Extension should be limited to a Listbox; it just is to match my exact purpose.
public static class ListboxExtensions
{
public static DependencyProperty IgnoreScrollProperty = DependencyProperty.RegisterAttached("IgnoreScroll", typeof(bool), typeof(ListboxExtensions), new UIPropertyMetadata(false, IgnoreScrollChanged));
public static bool GetIgnoreScroll(DependencyObject dependencyObject)
{
return (bool)dependencyObject.GetValue(IgnoreScrollProperty);
}
public static void SetIgnoreScroll(DependencyObject dependencyObject, bool value)
{
dependencyObject.SetValue(IgnoreScrollProperty, value);
}
private static void IgnoreScrollChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var newValue = (bool)e.NewValue;
var oldValue = (bool)e.OldValue;
var frameworkElement = d as FrameworkElement;
if (frameworkElement == null) return;
if (!newValue || oldValue || frameworkElement.IsFocused) return;
var lb = frameworkElement as ListBox;
if (lb == null) return;
lb.PreviewMouseWheel += LbOnPreviewMouseWheel;
}
private static void LbOnPreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
if (!(sender is ListBox) || e.Handled) return;
e.Handled = true;
var eventArg = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta)
{
RoutedEvent = UIElement.MouseWheelEvent,
Source = sender
};
var parent = ((Control)sender).Parent as UIElement;
if (parent != null) parent.RaiseEvent(eventArg);
}
}
And then in your XAML, you just put this on the listbox:
<ListBox extensions:ListboxExtensions.IgnoreScroll="True">
Of course, remembering to include the namespace to your extensions in the top of the XAML:
xmlns:extensions="clr-namespace:UI.Extensions"

Performance problem with multiple ItemsControl inside a ScrollViewer

I need a single scrollable surface that contains two bound lists. At first, I used a ScrollViewer with two ListBox inside, each having their scrolling disabled, so I could still have item selection. Seeing the poor loading time performance, I changed my ListBoxes to ItemsControl, but the performance is still terrible. In total, my two lists have only 110 items.
<ScrollViewer Grid.Row="1">
<StackPanel>
<Button Style="{StaticResource EmptyNonSelectButtonStyle}" BorderThickness="0" HorizontalContentAlignment="Left" Click="AnyCityButton_Click">
<TextBlock Text="{Binding Resources.CurrentLocationItem, Source={StaticResource LocalizedResources}}" FontFamily="{StaticResource PhoneFontFamilyNormal}" FontSize="{StaticResource PhoneFontSizeLarge}" />
</Button>
<TextBlock Text="{Binding Resources.TopTenCitiesHeader, Source={StaticResource LocalizedResources}}" Style="{StaticResource PhoneTextSubtleStyle}" Margin="12,12,12,8" />
<ItemsControl ItemsSource="{Binding TopTenCities}" ItemTemplate="{StaticResource CityDataTemplate}" HorizontalContentAlignment="Stretch" />
<TextBlock Text="{Binding Resources.TopHundredCitiesHeader, Source={StaticResource LocalizedResources}}" Style="{StaticResource PhoneTextSubtleStyle}" Margin="12,12,12,8" />
<ItemsControl ItemsSource="{Binding TopHundredCities}" ItemTemplate="{StaticResource CityDataTemplate}" HorizontalContentAlignment="Stretch" />
</StackPanel>
</ScrollViewer>
What can I do to improve performance? I've tried setting the ItemsSource after the page loading, but it still ugly (empty lists for a few seconds), doesn't make more sense.
Thank you.
This answer has turned into a monster but slog through it and I think you'll find an answer.
We need in some way to use the VirtualizingStackPanel as ListBox. We need to collect all the items to display (the button, the two textblocks and two sets of city data) into a single enumerable of some type. The the real trick and would be to determine one of three templates to use to render the items.
Bottom line is we need to create a new type of ItemsControl. Now we can gain a little advantage by simply accepting we want to create a very specific ItemsControl that supports only this task. First here is a "starter for 10" (a UK media reference).
A really dumb example of creating a specific items control:-
public class SwitchingItemsControl : ItemsControl
{
public DataTemplate AlternativeItemTemplate { get; set; }
protected override void PrepareContainerForItemOverride(DependencyObject element, object item)
{
ContentPresenter cp = (ContentPresenter)element;
if (AlternativeItemTemplate != null && (((int)item) & 1) == 1)
cp.ContentTemplate = AlternativeItemTemplate;
else
cp.ContentTemplate = ItemTemplate;
cp.Content = item;
}
}
This control assumes its items are a set of integers. It has an AlternativeItemTemplate which if supplied it toggles between on an odd/even basis (note that is a facet of the item).
Now lets put that use with a VirtualizingStackPanel:-
<UserControl x:Class="CustomVirtualizingPanelInSL.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:SilverlightApplication1"
Width="400" Height="300">
<Grid x:Name="LayoutRoot" Background="White">
<local:SwitchingItemsControl x:Name="itemsControl" >
<local:SwitchingItemsControl.Template>
<ControlTemplate TargetType="local:SwitchingItemsControl">
<ScrollViewer VerticalScrollBarVisibility="Visible">
<ItemsPresenter />
</ScrollViewer>
</ControlTemplate>
</local:SwitchingItemsControl.Template>
<local:SwitchingItemsControl.ItemTemplate>
<DataTemplate>
<Border CornerRadius="2" BorderBrush="Blue" BorderThickness="1" Margin="2">
<TextBlock VerticalAlignment="Center" HorizontalAlignment="Center" Text="{Binding}" />
</Border>
</DataTemplate>
</local:SwitchingItemsControl.ItemTemplate>
<local:SwitchingItemsControl.AlternativeItemTemplate>
<DataTemplate>
<Border CornerRadius="2" BorderBrush="Red" BorderThickness="1" Margin="2">
<TextBlock VerticalAlignment="Center" HorizontalAlignment="Center" Text="{Binding}" />
</Border>
</DataTemplate>
</local:SwitchingItemsControl.AlternativeItemTemplate>
<local:SwitchingItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</local:SwitchingItemsControl.ItemsPanel>
</local:SwitchingItemsControl>
</Grid>
</UserControl>
Note the ItemsPanel is using the VirtualizingStackPanel and that gets presented in a ScrollViewer.
Now we can give it lot of content:-
namespace SilverlightApplication1
{
public partial class MainPage : UserControl
{
public MainPage()
{
InitializeComponent();
itemsControl.ItemsSource = Enumerable.Range(0, 10000);
}
}
}
If you switch to a standard StackPanel this takes ages to load, whereas it appears instant with virtualizing.
Armed with this info you should be able to create a special ItemsControl which has the properties:-
ButtonTemplate (DataTemplate)
HeaderTemplate (DataTemplate)
TopTenHeaderText (String)
TopHundredHeaderText (String)
TopTenSource (IEnumerable<City>)
TipHunderedSource (IEnumerable<City>)
Now you can create a single enumerable with some Linq extension methods:-
itemsControl.ItemsSource = Enumerable.Repeat((object)null, 1)
.Concat(Enumerable.Repeat((object)TopTenHeadeText))
.Concat(TopTenSource.Cast<object>())
.Concat(Enumerable.Repeat((object)TopHundredText))
.Concat(TopHundredSource.Cast<object>())
Now you just need to override PrepareContainerForItemOverride and choose between ButtonTemplate (for the first null item), the HeaderTemplate for item of type string or the ItemTemplate for an item of type City.
Thank you #AnthonyWJones, your answer was (almost) exactly what I was looking for. I've decided to provide my own answer here so that other readers know how I've adapted his answer to my needs.
First, as suggested, I'm deriving from ItemsControl, and providing a second "Template" property, called HeaderTemplate:
#region HeaderTemplate PROPERTY
public static readonly DependencyProperty HeaderTemplateProperty = DependencyProperty.Register(
"HeaderTemplate",
typeof( DataTemplate ),
typeof( ItemsControlWithHeaders ),
new PropertyMetadata( null, new PropertyChangedCallback( OnHeaderTemplateChanged ) ) );
public DataTemplate HeaderTemplate
{
get { return ( DataTemplate )this.GetValue( HeaderTemplateProperty ); }
set { this.SetValue( HeaderTemplateProperty, value ); }
}
private static void OnHeaderTemplateChanged( DependencyObject obj, DependencyPropertyChangedEventArgs args )
{
ItemsControlWithHeaders control = obj as ItemsControlWithHeaders;
control.InvalidateArrange();
}
#endregion
Second, I'm overriding PrepareContainerForItemOverride to provide my own template selection logic. What I'm doing is simply redirecting any "string" item to the HeaderTemplate, and other items to the usual ItemTemplate:
protected override void PrepareContainerForItemOverride( DependencyObject element, object item )
{
base.PrepareContainerForItemOverride( element, item );
ContentPresenter presenter = element as ContentPresenter;
if( presenter != null )
{
if( item is string )
{
presenter.ContentTemplate = this.HeaderTemplate;
}
else
{
presenter.ContentTemplate = this.ItemTemplate;
}
}
}
This control can now be used like this:
<local:ItemsControlWithHeaders Grid.Row="1" ItemsSource="{Binding GroupedCities}" ScrollViewer.VerticalScrollBarVisibility="Auto">
<local:ItemsControlWithHeaders.Template>
<ControlTemplate TargetType="local:ItemsControlWithHeaders">
<ScrollViewer>
<ItemsPresenter />
</ScrollViewer>
</ControlTemplate>
</local:ItemsControlWithHeaders.Template>
<local:ItemsControlWithHeaders.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</local:ItemsControlWithHeaders.ItemsPanel>
<local:ItemsControlWithHeaders.HeaderTemplate>
<DataTemplate>
<TextBlock Text="{Binding}" Style="{StaticResource PhoneTextSubtleStyle}" Foreground="{StaticResource PhoneAccentBrush}" Margin="12,12,12,8" />
</DataTemplate>
</local:ItemsControlWithHeaders.HeaderTemplate>
<local:ItemsControlWithHeaders.ItemTemplate>
<DataTemplate>
<Button Style="{StaticResource EmptyNonSelectButtonStyle}" BorderThickness="0" HorizontalContentAlignment="Left" Click="AnyCityButton_Click">
<TextBlock Text="{Binding Name, Mode=OneWay}" FontFamily="{StaticResource PhoneFontFamilyNormal}" FontSize="{StaticResource PhoneFontSizeLarge}" />
</Button>
</DataTemplate>
</local:ItemsControlWithHeaders.ItemTemplate>
</local:ItemsControlWithHeaders>
To build the data source you must pass to this special hybrid control, LINQ is fine, but I've chosen a much more explicit solution, implemented in my view-model:
public IEnumerable<object> GroupedCities
{
get
{
yield return new CurrentLocationCityViewModel();
yield return Localized.TopTenCitiesHeader; // string resource
foreach( CityViewModel city in this.TopTenCities )
{
yield return city;
}
yield return Localized.TopHundredCitiesHeader; // string resource
foreach( CityViewModel city in this.TopHundredCities )
{
yield return city;
}
}
}
I now have a generic ItemsControlWithHeaders I can reuse in more than just this scenario. Performance is great. The only problem remaining for a purist like me is that the base ItemsControl complains in DEBUG, since an "object" type does not have a "Name" property. It generates a System.Windows.Data Error: BindingExpression path error: 'Name' property not found message in the debug output, which can be ignored.
Could you use one list/itemscontrol, but different datatemplates to get the same effect?
Or you could use a pivot control instead, putting the top 10 sitties in one pivot, top 100 in another pivot..

Wrong selection in ListBox with VirtualizationMode="Recycling" and SeclectionMode="Extended"?

I have a really strage behaviour. I have a ListBox in the View with a DataTemplate for its items including ViewModels. I bind the IsSelected to my ViewModel and use SelectionMode="Extended". Everything works fine.
BUT if I add VirtualiuationMode="Recycling" the I get the wrong items.
To reproduce: select items with Ctrl, then scroll down and select just one item. The normal behaviour deselects all items and just select the last one without holded Ctrl.
But if I check my ViewModel all the old items are selected!?!
<Grid>
<StackPanel>
<ListBox ItemsSource="{Binding People}" MaxHeight="100"
SelectionMode="Extended"
VirtualizationMode="Recycling">
<!--VirtualizingStackPanel.IsVirtualizing="True">-->
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="IsSelected" Value="{Binding Path=IsSelected, Mode=TwoWay}" />
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<views:PeopleView />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Button Click="Button_Click">
OK
</Button>
</StackPanel>
</Grid>
The item template
<UserControl x:Class="WpfApplication1.View.PeopleView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Height="Auto" Width="Auto">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="A"/>
<ColumnDefinition Width="Auto" SharedSizeGroup="B"/>
</Grid.ColumnDefinitions>
<TextBox Text="{Binding Path=Name}"
Name="tbx_Name"
Grid.Column="0"/>
<CheckBox IsChecked="{Binding Path=IstAktiv}"
Name="cbx_IstAktiv"
Grid.Column="1"/>
</Grid>
Any idea?
I got a workaround but why do I have to change it "manually" in the change event and not by databinding?
private void Lbx_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
ListBox lbx = (ListBox)sender;
foreach (PersonViewModel item in lbx.Items)
{
item.IsSelected = lbx.SelectedItems.Contains(item);
}
}
Another option related to KCT's earlier answer is to use the AddedItems and RemovedItems from the SelectionChangedEventArgs and target the changes, such as:
private void Lbx_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
foreach (var item in e.AddedItems)
{
((PersonViewModel)item).IsSelected = true;
}
foreach (var item in e.RemovedItems)
{
((PersonViewModel)item).IsSelected = false;
}
}
This may give better performance with larger collections (I've got about 15,000 entries in a Virtualizing Tile Panel in a ListBox).

Resources