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>
Grid
, in my case) and then manage the child elements as full UI elements (as opposed toVisual
, as in the WPF implementation). As you can imagine, this is far from ideal. :( – Amabelle