I'm trying to create a similar experience as in the ScrollViewerSample from the Windows 8 SDK samples to be able to snap to the items inside a ScrollViewer when scrolling left and right. The implementation from the sample (which works) is like this:
<ScrollViewer x:Name="scrollViewer" Width="480" Height="270"
HorizontalAlignment="Left" VerticalAlignment="Top"
VerticalScrollBarVisibility="Disabled" HorizontalScrollBarVisibility="Auto"
ZoomMode="Disabled" HorizontalSnapPointsType="Mandatory">
<StackPanel Orientation="Horizontal">
<Image Width="480" Height="270" AutomationProperties.Name="Image of a cliff" Source="images/cliff.jpg" Stretch="None" HorizontalAlignment="Left" VerticalAlignment="Top"/>
<Image Width="480" Height="270" AutomationProperties.Name="Image of Grapes" Source="images/grapes.jpg" Stretch="None" HorizontalAlignment="Left" VerticalAlignment="Top"/>
<Image Width="480" Height="270" AutomationProperties.Name="Image of Mount Rainier" Source="images/Rainier.jpg" Stretch="None" HorizontalAlignment="Left" VerticalAlignment="Top"/>
<Image Width="480" Height="270" AutomationProperties.Name="Image of a sunset" Source="images/sunset.jpg" Stretch="None" HorizontalAlignment="Left" VerticalAlignment="Top"/>
<Image Width="480" Height="270" AutomationProperties.Name="Image of a valley" Source="images/valley.jpg" Stretch="None" HorizontalAlignment="Left" VerticalAlignment="Top"/>
</StackPanel>
</ScrollViewer>
The only difference with my desired implementation is that I don't want a StackPanel with items inside, but something I can bind to. I am trying to accomplish this with an ItemsControl, but for some reason the Snap behavior does not kick in:
<ScrollViewer x:Name="scrollViewer" Width="480" Height="270"
HorizontalAlignment="Left" VerticalAlignment="Top"
VerticalScrollBarVisibility="Disabled" HorizontalScrollBarVisibility="Auto"
ZoomMode="Disabled" HorizontalSnapPointsType="Mandatory">
<ItemsControl>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<Image Width="480" Height="270" AutomationProperties.Name="Image of a cliff" Source="images/cliff.jpg" Stretch="None" HorizontalAlignment="Left" VerticalAlignment="Top"/>
<Image Width="480" Height="270" AutomationProperties.Name="Image of Grapes" Source="images/grapes.jpg" Stretch="None" HorizontalAlignment="Left" VerticalAlignment="Top"/>
<Image Width="480" Height="270" AutomationProperties.Name="Image of Mount Rainier" Source="images/Rainier.jpg" Stretch="None" HorizontalAlignment="Left" VerticalAlignment="Top"/>
<Image Width="480" Height="270" AutomationProperties.Name="Image of a sunset" Source="images/sunset.jpg" Stretch="None" HorizontalAlignment="Left" VerticalAlignment="Top"/>
<Image Width="480" Height="270" AutomationProperties.Name="Image of a valley" Source="images/valley.jpg" Stretch="None" HorizontalAlignment="Left" VerticalAlignment="Top"/>
</ItemsControl>
</ScrollViewer>
Suggestions would be greatly appreciated!
Thanks to Denis, I ended up using the following Style on the ItemsControl and removed the ScrollViewer and inline ItemsPanelTemplate altogether:
<Style x:Key="ItemsControlStyle" TargetType="ItemsControl">
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<VirtualizingStackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ItemsControl">
<ScrollViewer Style="{StaticResource HorizontalScrollViewerStyle}" HorizontalSnapPointsType="Mandatory">
<ItemsPresenter />
</ScrollViewer>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Getting snap points to work for bound collections can be tricky. For snap points to work immediate child of ScrollViewer should implement IScrollSnapPointsInfo interface. ItemsControl doesn't implement IScrollSnapPointsInfo and consequently you wouldn't see snapping behaviour.
To work around this issue you got couple options:
I've implemented former approach and can confirm that it works, but in your case custom style could be a better choice.
Ok, here is the simplest (and standalone) example for horizontal ListView with binded items and correctly working snapping (see comments in following code).
xaml:
<ListView x:Name="YourListView"
ItemsSource="{x:Bind Path=Items}"
Loaded="YourListView_OnLoaded">
<!--Set items panel to horizontal-->
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<ItemsStackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ListView.ItemsPanel>
<!--Some item template-->
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
background code:
private void YourListView_OnLoaded(object sender, RoutedEventArgs e)
{
//get ListView
var yourList = sender as ListView;
//*** yourList style-based changes ***
//see Style here https://msdn.microsoft.com/en-us/library/windows/apps/mt299137.aspx
//** Change orientation of scrollviewer (name in the Style "ScrollViewer") **
//1. get scrollviewer (child element of yourList)
var sv = GetFirstChildDependencyObjectOfType<ScrollViewer>(yourList);
//2. enable ScrollViewer horizontal scrolling
sv.HorizontalScrollMode =ScrollMode.Auto;
sv.HorizontalScrollBarVisibility = ScrollBarVisibility.Auto;
sv.IsHorizontalRailEnabled = true;
//3. disable ScrollViewer vertical scrolling
sv.VerticalScrollMode = ScrollMode.Disabled;
sv.VerticalScrollBarVisibility = ScrollBarVisibility.Disabled;
sv.IsVerticalRailEnabled = false;
// //no we have horizontally scrolling ListView
//** Enable snapping **
sv.HorizontalSnapPointsType = SnapPointsType.MandatorySingle; //or you can use SnapPointsType.Mandatory
sv.HorizontalSnapPointsAlignment = SnapPointsAlignment.Near; //example works only for Near case, for other there should be some changes
// //no we have horizontally scrolling ListView with snapping and "scroll last item into view" bug (about bug see here http://stackoverflow.com/questions/11084493/snapping-scrollviewer-in-windows-8-metro-in-wide-screens-not-snapping-to-the-las)
//** fix "scroll last item into view" bug **
//1. Get items presenter (child element of yourList)
var ip = GetFirstChildDependencyObjectOfType<ItemsPresenter>(yourList);
// or var ip = GetFirstChildDependencyObjectOfType<ItemsPresenter>(sv); //also will work here
//2. Subscribe to its SizeChanged event
ip.SizeChanged += ip_SizeChanged;
//3. see the continuation in: private void ip_SizeChanged(object sender, SizeChangedEventArgs e)
}
public static T GetFirstChildDependencyObjectOfType<T>(DependencyObject depObj) where T : DependencyObject
{
if (depObj is T) return depObj as T;
for (var i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
{
var child = VisualTreeHelper.GetChild(depObj, i);
var result = GetFirstChildDependencyObjectOfType<T>(child);
if (result != null) return result;
}
return null;
}
private void ip_SizeChanged(object sender, SizeChangedEventArgs e)
{
//3.0 if rev size is same as new - do nothing
//here should be one more condition added by && but it is a little bit complicated and rare, so it is omitted.
//The condition is: yourList.Items.Last() must be equal to (yourList.Items.Last() used on previous call of ip_SizeChanged)
if (e.PreviousSize.Equals(e.NewSize)) return;
//3.1 get sender as our ItemsPresenter
var ip = sender as ItemsPresenter;
//3.2 get the ItemsPresenter parent to get "viewable" width of ItemsPresenter that is ActualWidth of the Scrollviewer (it is scrollviewer actually, but we need just its ActualWidth so - as FrameworkElement is used)
var sv = ip.Parent as FrameworkElement;
//3.3 get parent ListView to be able to get elements Containers
var yourList = GetParent<ListView>(ip);
//3.4 get last item ActualWidth
var lastItem = yourList.Items.Last();
var lastItemContainerObject = yourList.ContainerFromItem(lastItem);
var lastItemContainer = lastItemContainerObject as FrameworkElement;
if (lastItemContainer == null)
{
//NO lastItemContainer YET, wait for next call
return;
}
var lastItemWidth = lastItemContainer.ActualWidth;
//3.5 get margin fix value
var rightMarginFixValue = sv.ActualWidth - lastItemWidth;
//3.6. fix "scroll last item into view" bug
ip.Margin = new Thickness(ip.Margin.Left,
ip.Margin.Top,
ip.Margin.Right + rightMarginFixValue, //APPLY FIX
ip.Margin.Bottom);
}
public static T GetParent<T>(DependencyObject reference) where T : class
{
var depObj = VisualTreeHelper.GetParent(reference);
if (depObj == null) return (T)null;
while (true)
{
var depClass = depObj as T;
if (depClass != null) return depClass;
depObj = VisualTreeHelper.GetParent(depObj);
if (depObj == null) return (T)null;
}
}
About this example.
Most of checks and errors handling is omitted.
If you override ListView Style/Template, VisualTree search parts must be changed accordingly
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With