Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / XAML

Creating a VirtualizingWrapPanel in XAML WinRT

5.00/5 (1 vote)
27 Feb 2016CPOL2 min read 12.3K  
Deriving VirtualizingPanel and OrientedVirtualizingPanel is not possible in WinRT but using some calculation strategies, an efficient emulation is possible

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:

XML
 <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:

C#
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

  • Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)