Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Creating custom virtualized controls in WinRT/UWP

In WPF it is possible for a FrameworkElement derived class to provide its own children via AddVisualChild. This way it is possible to implement your own virtualized controls which only generate the children which are visible. Also you can generate children without having a backing collection.

I want to port several controls using this technique from WPF to Windows 10 UWP but it is unclear how to properly implement virtualization in WinRT UI. Because in a comment on my original version of the question it was stated that asking about implementation techniques is too general for Stack Overflow I've created a minimalistic example to explain the key features I'm trying to cover, which are

  • dynamically generating child controls from the data model
  • performing custom layout logic for the generated child controls

I've done following considerations:

  • As far as I can see it is not possible for a custom control to manage its own children like in WPF
  • I'm ruling out a Panel subclass because when my custom control is used (by someone else) it is far too easy to make mistakes. The panel children are supposed to be controlled by the containing XAML not by the panel.
  • I'm ruling out ItemsControl subclasses because it is not reasonably possible to provide a backing collection (data virtualization is a requirement)

(Note that ruling them out may be a mistake, so if it is please point it out.)

The following WPF Code creates an infinite scrolling date band but only materializes the currently visible cells. I intentionally kept it as minimalistic as possible so it does not make much sense, but it does present the two key features I mentioned above and which I need to understand how to implement in WinRT.

So my question: is it possible to create such a control in WinRT which dynamically builds its children to display an infinite scrolling band? Keep in mind it needs to be self-contained in order to be placed on arbitrary pages without the page having to contain additional code (otherwise it wouldn't be a reusable control after all).

I'd consider it enough for an answer to outline how it could be done in WinRT, if you already know how to implement virtualization and can just give me some hints.

WPF Source:

using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;

namespace Sandbox
{
    public class DateBand : FrameworkElement
    {
        public static readonly DependencyProperty ScrollOffsetProperty = DependencyProperty.Register(
            nameof(ScrollOffset), typeof(double), typeof(DateBand), new FrameworkPropertyMetadata {
                AffectsMeasure = true,
            });

        public double ScrollOffset
        {
            get { return (double)GetValue(ScrollOffsetProperty); }
            set { SetValue(ScrollOffsetProperty, value); }
        }

        public static readonly DependencyProperty CellTemplateProperty = DependencyProperty.Register(
            nameof(CellTemplate), typeof(DataTemplate), typeof(DateBand), new FrameworkPropertyMetadata {
                AffectsMeasure = true,
            });

        public DataTemplate CellTemplate
        {
            get { return (DataTemplate)GetValue(CellTemplateProperty); }
            set { SetValue(CellTemplateProperty, value); }
        }

        private List<DateCell> _cells = new List<DateCell>();
        private DateTime _startDate = new DateTime(DateTime.Today.Year, DateTime.Today.Month, 1);
        private const double cSlotWidth = 5;
        private const double cSlotHeight = 20;

        protected override int VisualChildrenCount => _cells.Count;
        protected override Visual GetVisualChild(int index) => _cells[index];

        protected override Size MeasureOverride(Size availableSize)
        {
            int usedCells = 0;
            double desiredWidth = 0;
            double desiredHeight = 0;

            if (!double.IsPositiveInfinity(availableSize.Height))
            {
                var index = (int)Math.Floor(ScrollOffset);
                var offset = (index - ScrollOffset) * cSlotHeight;

                while (offset < availableSize.Height)
                {
                    DateCell cell;
                    if (usedCells < _cells.Count)
                    {
                        cell = _cells[usedCells];
                    }
                    else
                    {
                        cell = new DateCell();
                        AddVisualChild(cell);
                        _cells.Add(cell);
                    }
                    usedCells++;

                    var cellValue = _startDate.AddMonths(index);
                    cell._offset = offset;
                    cell._width = DateTime.DaysInMonth(cellValue.Year, cellValue.Month) * cSlotWidth;
                    cell.Content = cellValue;
                    cell.ContentTemplate = CellTemplate;
                    cell.Measure(new Size(cell._width, cSlotHeight));

                    offset += cSlotHeight;
                    index++;

                    desiredHeight = Math.Max(desiredHeight, offset);
                    desiredWidth = Math.Max(desiredWidth, cell._width);
                }
            }

            if (usedCells < _cells.Count)
            {
                for (int i = usedCells; i < _cells.Count; i++)
                    RemoveVisualChild(_cells[i]);

                _cells.RemoveRange(usedCells, _cells.Count - usedCells);
            }

            return new Size(desiredWidth, desiredHeight);
        }

        protected override Size ArrangeOverride(Size finalSize)
        {
            foreach (var cell in _cells)
                cell.Arrange(new Rect(0, cell._offset, cell._width, cell.DesiredSize.Height));

            return finalSize;
        }
    }

    public class DateCell : ContentControl
    {
        internal double _offset;
        internal double _width;
    }

    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void Window_MouseWheel(object sender, MouseWheelEventArgs e)
        {
            Band.SetCurrentValue(DateBand.ScrollOffsetProperty, Band.ScrollOffset - e.Delta / Mouse.MouseWheelDeltaForOneLine);
        }
    }
}

WPF XAML:

<Window x:Class="Sandbox.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:Sandbox"
        MouseWheel="Window_MouseWheel">
    <DockPanel>
        <ScrollBar x:Name="Scroll" Orientation="Vertical" Minimum="-24" Maximum="+24" ViewportSize="6"/>
        <local:DateBand x:Name="Band" ScrollOffset="{Binding ElementName=Scroll, Path=Value, Mode=OneWay}">
            <local:DateBand.CellTemplate>
                <DataTemplate>
                    <Border BorderBrush="Black" BorderThickness="1" Padding="5,2">
                        <TextBlock Text="{Binding StringFormat='yyyy - MMMM'}"/>
                    </Border>
                </DataTemplate>
            </local:DateBand.CellTemplate>
        </local:DateBand>
    </DockPanel>
</Window>
like image 981
Zarat Avatar asked Nov 27 '15 16:11

Zarat


People also ask

How do I create a custom control in UWP?

Like this we can give the desired control over our custom control to the user by binding them with our created dependency properties. Then go to MyUserControl1. xaml and add textblock and image from toolbox. Now we want to give the control to the user using our custom control so he/she can change text and image.

How to use UWP controls in WPF?

In your WPF project, right-click the Dependencies node and add a reference to the UWP class library project. In the UWP app project you configured earlier, right-click the References node and add a reference to the UWP class library project. Rebuild the entire solution and make sure all the projects build successfully.

What are XAML Islands?

XAML Islands is a technology that enables Windows developers to use new pieces of UI from the Universal Windows Platform (UWP) on their existing Win32 Applications, including Windows Forms and WPF technologies.


1 Answers

As requested in a comment I'm posting the solution I ended up with. I only figured out solutions which use some kind of Panel subclass, so I came up with the compromise of splitting the control into two parts, to avoid users of the control accidently messing with the child collection.

So I actually have two main classes, one Control subclass exposing the public API (like dependency properties) and supporting theming, and a Panel subclass implementing the actual virtualization. Both are linked through the XAML template and the Panel subclass will refuse to perform any work if someone should use it outside the expected control.

Having done that, the virtualization is pretty straightforward and doesn't differ very much from how you would do it in WPF - just modify the Children of the Panel, for example in the MeasureOverride.

For illustration I've ported the code from the question to UWP as follows:

UWP Source:

[TemplatePart(Name = PanelPartName, Type = typeof(DateBandPanel))]
public class DateBand : Control
{
    private const string PanelPartName = "CellPanel";

    public static readonly DependencyProperty ScrollOffsetProperty = DependencyProperty.Register(
        nameof(ScrollOffset), typeof(double), typeof(DateBand), new PropertyMetadata(
            (double)0, new PropertyChangedCallback((d, e) => ((DateBand)d).HandleScrollOffsetChanged(e))));

    private void HandleScrollOffsetChanged(DependencyPropertyChangedEventArgs e)
    {
        _panel?.InvalidateMeasure();
    }

    public double ScrollOffset
    {
        get { return (double)GetValue(ScrollOffsetProperty); }
        set { SetValue(ScrollOffsetProperty, value); }
    }

    public static readonly DependencyProperty CellTemplateProperty = DependencyProperty.Register(
        nameof(CellTemplate), typeof(DataTemplate), typeof(DateBand), new PropertyMetadata(
            null, new PropertyChangedCallback((d, e) => ((DateBand)d).HandleCellTemplateChanged(e))));

    private void HandleCellTemplateChanged(DependencyPropertyChangedEventArgs e)
    {
        _panel?.InvalidateMeasure();
    }

    public DataTemplate CellTemplate
    {
        get { return (DataTemplate)GetValue(CellTemplateProperty); }
        set { SetValue(CellTemplateProperty, value); }
    }

    private DateBandPanel _panel;

    public DateBand()
    {
        this.DefaultStyleKey = typeof(DateBand);
    }

    protected override void OnApplyTemplate()
    {
        if (_panel != null)
            _panel._band = null;

        base.OnApplyTemplate();

        _panel = GetTemplateChild(PanelPartName) as DateBandPanel;

        if (_panel != null)
            _panel._band = this;
    }
}

public class DateBandPanel : Panel
{
    internal DateBand _band;
    private List<DateCell> _cells = new List<DateCell>();
    private DateTime _startDate = new DateTime(DateTime.Today.Year, DateTime.Today.Month, 1);
    private const double cSlotWidth = 5;
    private const double cSlotHeight = 26;

    protected override Size MeasureOverride(Size availableSize)
    {
        int usedCells = 0;
        double desiredWidth = 0;
        double desiredHeight = 0;

        if (!double.IsPositiveInfinity(availableSize.Height) && _band != null)
        {
            var index = (int)Math.Floor(_band.ScrollOffset);
            var offset = (index - _band.ScrollOffset) * cSlotHeight;

            while (offset < availableSize.Height)
            {
                DateCell cell;
                if (usedCells < _cells.Count)
                {
                    cell = _cells[usedCells];
                }
                else
                {
                    cell = new DateCell();
                    Children.Add(cell);
                    _cells.Add(cell);
                }
                usedCells++;

                var cellValue = _startDate.AddMonths(index);
                cell._offset = offset;
                cell._width = DateTime.DaysInMonth(cellValue.Year, cellValue.Month) * cSlotWidth;
                cell.Content = new CellData(cellValue);
                cell.ContentTemplate = _band.CellTemplate;
                cell.Measure(new Size(cell._width, cSlotHeight));

                offset += cSlotHeight;
                index++;

                desiredHeight = Math.Max(desiredHeight, offset);
                desiredWidth = Math.Max(desiredWidth, cell._width);
            }
        }

        if (usedCells < _cells.Count)
        {
            for (int i = usedCells; i < _cells.Count; i++)
                Children.Remove(_cells[i]);

            _cells.RemoveRange(usedCells, _cells.Count - usedCells);
        }

        return new Size(desiredWidth, desiredHeight);
    }

    protected override Size ArrangeOverride(Size finalSize)
    {
        foreach (var cell in _cells)
            cell.Arrange(new Rect(0, cell._offset, cell._width, cell.DesiredSize.Height));

        return finalSize;
    }
}

public class CellData
{
    public DateTime Date { get; }
    public CellData(DateTime date) { this.Date = date; }
}

public class DateCell : ContentControl
{
    internal double _offset;
    internal double _width;
}

public class FormattingConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, string language)
    {
        if (value == null)
            return null;

        if (parameter == null)
            return value.ToString();

        return ((IFormattable)value).ToString((string)parameter, CultureInfo.CurrentCulture);
    }

    object IValueConverter.ConvertBack(object value, Type targetType, object parameter, string language)
    {
        throw new NotSupportedException();
    }
}

public sealed partial class MainPage : Page
{
    public MainPage()
    {
        this.InitializeComponent();
    }

    private void Page_PointerWheelChanged(object sender, PointerRoutedEventArgs e)
    {
        Scroll.Value -= e.GetCurrentPoint(this).Properties.MouseWheelDelta / 120.0;
    }
}

UWP XAML Page:

<Page x:Class="Sandbox.MainPage"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:local="using:Sandbox"
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      mc:Ignorable="d"
      PointerWheelChanged="Page_PointerWheelChanged">
    <Page.Resources>
        <local:FormattingConverter x:Key="FormattingConverter"/>
    </Page.Resources>
    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="auto"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <ScrollBar x:Name="Scroll" Grid.Column="0" Orientation="Vertical" IndicatorMode="MouseIndicator" Minimum="-24" Maximum="+24" ViewportSize="6"/>
        <local:DateBand x:Name="Band" Grid.Column="1" ScrollOffset="{Binding ElementName=Scroll, Path=Value, Mode=OneWay}">
            <local:DateBand.CellTemplate>
                <DataTemplate x:DataType="local:CellData">
                    <Border BorderBrush="Black" BorderThickness="1" Padding="5,2">
                        <TextBlock Text="{x:Bind Path=Date, Converter={StaticResource FormattingConverter}, ConverterParameter='yyyy - MMMM'}"/>
                    </Border>
                </DataTemplate>
            </local:DateBand.CellTemplate>
        </local:DateBand>
    </Grid>
</Page>

UWP XAML Theme:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:local="using:Sandbox">
    <Style TargetType="local:DateBand" >
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="local:DateBand">
                    <Border Background="{TemplateBinding Background}"
                            BorderBrush="{TemplateBinding BorderBrush}"
                            BorderThickness="{TemplateBinding BorderThickness}">
                        <local:DateBandPanel Name="CellPanel"/>
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>
like image 139
Zarat Avatar answered Oct 04 '22 19:10

Zarat