Introduction
Microsoft sealed the VirtualizingPanel
and all its derivatives from OrientedVirtualizingPanel
which are very useful to its implementations in VirtualizingStackPanel
and CarouselPanel
. Yet, there are some relatively simple tactics which can be used to emulate the functionality of a VirtualizingWrapPanel
using the VirtualizingStackPanel
and an intermediary grouping strategy.
Background
WinRT, LINQ, C# and XAML knowledge required.
Using the Code
First, we take a look at the XAML necessary to support this:
<Page
x:Name="TopLevel"
x:Class="VirtualizingWrapPanel.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:VirtualizingWrapPanel"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Page.Resources>
<CollectionViewSource x:Name="MyItems"
Source="{Binding Path=ViewModel.MyItems, Mode=OneWay}">
</CollectionViewSource>
<DataTemplate x:Key="MyTemplate">
<ItemsControl ItemsSource="{Binding Items}"
ItemTemplate="{StaticResource MyItemTemplate}"
MaxWidth="{Binding ElementName=TopLevel,
Path=UIChanger.MaxWidth, Mode=OneWay}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical"
Width="auto" Height="auto"></StackPanel>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</DataTemplate>
<DataTemplate x:Key="WrapTemplate">
<ItemsControl ItemsSource="{Binding RenderItems}"
ItemTemplateSelector="{StaticResource MyDataTemplateSelector}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"
Height="auto"></StackPanel>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.Template>
<ControlTemplate TargetType="ItemsControl">
<ItemsPresenter/>
</ControlTemplate>
</ItemsControl.Template>
</ItemsControl>
</DataTemplate>
</Page.Resources>
<Grid Background="White">
<Grid.RowDefinitions>
<RowDefinition Height="*"></RowDefinition>
</Grid.RowDefinitions>
<ItemsControl Background="White" x:Name="MainControl"
Grid.Row="0" ItemTemplate="{StaticResource WrapTemplate}"
ItemsSource="{Binding Mode=OneWay, Source={StaticResource MyItems}}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel Background="White"
FlowDirection="RightToLeft"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.Template>
<ControlTemplate TargetType="ItemsControl">
<ScrollViewer HorizontalScrollMode="Disabled"
VerticalScrollBarVisibility="Auto" ZoomMode="Enabled">
<ItemsPresenter/>
</ScrollViewer>
</ControlTemplate>
</ItemsControl.Template>
</ItemsControl>
<ProgressRing Name="LoadingRing" IsActive="True"
HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch"
VerticalAlignment="Stretch"
VerticalContentAlignment="Stretch"></ProgressRing>
</Grid>
</Page>
We define an ItemsControl
using a VirtualizingStackPanel
presented in a ScrollViewer
. Now the trick will be to divide up the items into groups that will be displayed in the horizontal oriented StackPanel
.
With this tactic, we take full advantage of the professionally developed VirtualizingStackPanel
logic while only having to know how to make proper measurement calculations and doing it carefully:
namespace VirtualizingWrapPanel
public sealed partial class MainPage : Page
{
public MainPage()
{
this.DataContext = this;
this.ViewModel = new VirtualizingWrapPanelAdapter();
UIChanger = new MyUIChanger();
this.InitializeComponent();
this.SizeChanged += OnSizeChanged;
}
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
{
UIChanger.MaxWidth = ActualWidth;
this.ViewModel.RegroupRenderModels(ActualWidth);
}
public MyUIChanger UIChanger { get; set; }
public VirtualizingWrapPanelAdapter ViewModel { get; set; }
public static double CalculateWidth(string text, string FontFamily,
float FontSize, float maxWidth, float maxHeight)
{
SharpDX.DirectWrite.Factory factory = new SharpDX.DirectWrite.Factory();
SharpDX.DirectWrite.TextFormat format =
new SharpDX.DirectWrite.TextFormat(factory, FontFamily, FontSize);
SharpDX.DirectWrite.TextLayout layout =
new SharpDX.DirectWrite.TextLayout(factory, text, format, maxWidth, maxHeight);
double width = layout.Metrics.WidthIncludingTrailingWhitespace + layout.Metrics.Left;
layout.Dispose();
format.Dispose();
factory.Dispose();
return width;
}
}
public class MyUIChanger : INotifyPropertyChanged
{
private double _MaxWidth;
public double MaxWidth
{
get
{
return _MaxWidth;
}
set
{
_MaxWidth = value;
if (PropertyChanged != null) PropertyChanged
(this, new PropertyChangedEventArgs("MaxWidth"));
}
}
#region Implementation of INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
#endregion
}
public class MyRenderItem : INotifyPropertyChanged
{
public MyRenderItem()
{
}
public double MaxWidth { get; set; }
private double CalculateWidth()
{
}
private List<object> _Items;
public IEnumerable<object> Items { get { return _Items; }
set { _Items = value.ToList(); if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("Items")); } }
#region Implementation of INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
#endregion
}
public class MyRenderModel : INotifyPropertyChanged
{
public MyRenderModel(IEnumerable<MyRenderItem> NewRenderItems)
{
RenderItems = NewRenderItems;
MaxWidth = CalculateWidth();
}
private double _MaxWidth;
public double MaxWidth { get { return _MaxWidth; }
set { _MaxWidth = value; if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("MaxWidth")); } }
private double CalculateWidth()
{
return RenderItems.Select((Item) => Item.MaxWidth).Sum();
}
private List<MyRenderItem> _RenderItems;
public IEnumerable<MyRenderItem> RenderItems {
get { return _RenderItems; } set { _RenderItems = value.ToList();
if (PropertyChanged != null) PropertyChanged(this,
new PropertyChangedEventArgs("RenderItems")); } }
#region Implementation of INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
#endregion
}
public class VirtualizingWrapPanelAdapter : INotifyPropertyChanged
{
private List<MyRenderModel> _RenderModels;
public List<MyRenderModel> RenderModels
{
get
{
return _RenderModels;
}
set
{
_RenderModels = value;
PropertyChanged(this, new PropertyChangedEventArgs("RenderModels"));
}
}
public void RegroupRenderModels(double maxWidth)
{
if (_RenderModels != null) { GroupRenderModels
(_RenderModels.SelectMany((Item) => Item.RenderItems).ToList(), maxWidth); }
}
public void GroupRenderModels(List<MyRenderItem> value, double maxWidth)
{
double width = 0.0;
int groupIndex = 0;
List<int[]> GroupIndexes = new List<int[]>();
value.FirstOrDefault((Item) => { double itemWidth =
Math.Min(Item.MaxWidth, maxWidth); if (width + itemWidth > maxWidth)
{ width = itemWidth; groupIndex++; } else { width += itemWidth; }
GroupIndexes.Add(new int[] { groupIndex, GroupIndexes.Count });
return false; });
RenderModels = GroupIndexes.GroupBy((Item) => Item[0],
(Item) => value.ElementAt(Item[1])).Select((Item) =>
new MyRenderModel(Item)).ToList();
}
}
}
Notice that the SizeChanged
event should be captured and responded to and a ViewModel
should contain the MaxWidth
property which is bound to the item template of the horizontal stack panel. A custom calculation routine for text is shown using SharpDX as DirectWrite is going to be the best fast and accurate way to simulate your width calculation which should be cached. The RenderItem
and corresponding template must be implemented to proceed and all calculations of the MaxWidth
which is perhaps better though equally called the desired width must be coded using fast, efficient and proper code through knowledge of text, fonts, borders, margins, padding and so forth.
Points of Interest
Other options to do this would require deriving from the Panel
control and writing a lot of controls to basically reproduce the source code of VirtualizingPanel
and OrientedVirtualizingPanel
and most of VirtualizingStackPanel
with the specific modifications to do the 2 dimensional arrangement and which accounting for the generality that Microsoft has would be very difficult. It would be nice if Microsoft would provide the control for us at some point as it is largely mimicking HTML inline layout in an efficient way. Or perhaps Microsoft may unseal these classes at some point though they chose to do this quite deliberately for whatever reasons.
There is a WrapPanel
in WinRTXamlToolkit
that is a good port from WPF, but not virtualized for smaller usages. It is a starting point for virtualizing.
Microsoft provides the source to WPF and .NET open sourced yet WinRT is closed source compiled in assembly language likely written with C++. OrientedVirtualizingPanel
only exists in WinRT so there would not be a mere translation to get that base programmed in.
History