【问题标题】:Creating custom virtualized controls in WinRT/UWP在 WinRT/UWP 中创建自定义虚拟化控件
【发布时间】:2016-03-01 21:04:48
【问题描述】:

在 WPF 中,FrameworkElement 派生类可以通过 AddVisualChild 提供自己的子类。这样就可以实现您自己的虚拟化控件,这些控件只生成可见的子元素。您也可以在没有后备集合的情况下生成子代。

我想使用这种技术将几个控件从 WPF 移植到 Windows 10 UWP,但不清楚如何在 WinRT UI 中正确实现虚拟化。因为在对我最初版本的问题的评论中指出,对于 Stack Overflow 而言,询问实现技术过于笼统,所以我创建了一个简约示例来解释我试图涵盖的关键特性,这些特性是

  • 从数据模型动态生成子控件
  • 为生成的子控件执行自定义布局逻辑

我做了以下考虑:

  • 据我所知,自定义控件无法像在 WPF 中那样管理自己的子控件
  • 我排除了Panel 子类,因为当我的自定义控件被(由其他人)使用时,很容易出错。面板子项应该由包含的 XAML 控制,而不是由面板控制。
  • 我正在排除 ItemsControl 子类,因为提供支持集合是不可能的(数据虚拟化是一项要求)

(请注意,排除它们可能是错误的,如果是,请指出。)

以下 WPF 代码创建一个无限滚动的日期带,但仅具体化当前可见的单元格。我故意让它尽可能简约,所以它没有多大意义,但它确实展示了我上面提到的两个关键特性,我需要了解如何在 WinRT 中实现这些特性。

所以我的问题是:是否可以在 WinRT 中创建这样一个控件,动态构建其子级以显示无限滚动条?请记住,它需要是独立的,以便放置在任意页面上,而页面不必包含额外的代码(否则它根本就不是可重用的控件)。

如果您已经知道如何实现虚拟化并且可以给我一些提示,我认为它足以概述如何在 WinRT 中完成它。

WPF 来源:

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>

【问题讨论】:

  • “据我所知,因为控件无法控制他们的孩子”——这也是我的理解,虽然我远非 Winrt 专家,所以可能有一些东西我忽略了。当我有与您在这里相同的目标(将带有自定义控件的 WPF 移植到 Winrt)时,我发现的唯一解决方案是将自定义控件实现为现有容器的子类(在我的情况下为 Grid)然后管理子元素作为完整的 UI 元素(与 WPF 实现中的 Visual 相反)。可以想象,这远非理想。 :(
  • 我会警告你,你所说的问题对于 Stack Overflow 来说是“太宽泛”的边界。它非常笼统。如果你能提供一个很好的minimal reproducible example 来准确显示你在做什么,以及一个特定的问题陈述,给出一个相对于你目前所做工作的狭隘目标,那将会有所帮助。我觉得这个问题很有用,但你可能需要改进它才能得到好的答案(假设你得到了答案)。
  • @PeterDuniho 我不知道为什么它“太宽泛”,我在问如何将 WPF 中的常见虚拟化技术转换为 WinRT 中可用的任何虚拟化技术。我已经编写了一个最小代码示例来表示 WPF 中的技术。除此之外,我不确定如何改进这个问题,如果你能更直接地指出我不清楚的地方,我可能会更详细地解释它。
  • “我不知道为什么它“太宽泛””——因为可能的答案太多了。一个全面的回复会太长,并且包含太多不同的场景,不适合 Stack Overflow 模型。至于您提供的代码示例,这很有帮助,但您确实应该提供一个很好的minimal reproducible example,展示您在 Winrt 版本中尝试过的内容,并准确解释您正在尝试的 特定 问题在那个版本中解决。
  • @PeterDuniho 我重新提出了这个问题,并基于示例代码。我无法为 WinRT 展示任何东西,因为问题的重点是首先弄清楚如何在 WinRT 中进行虚拟化。

标签: c# windows-store-apps winrt-xaml uwp


【解决方案1】:

根据评论中的要求,我发布了我最终得到的解决方案。我只是想出了使用某种 Panel 子类的解决方案,所以我想出了将控件分成两部分的折衷方案,以避免控件的用户不小心弄乱子集合。

所以我实际上有两个主要类,一个 Control 子类公开公共 API(如依赖属性)并支持主题,一个 Panel 子类实现实际的虚拟化。两者都通过 XAML 模板链接,如果有人在预期控制之外使用它,Panel 子类将拒绝执行任何工作。

完成后,虚拟化非常简单,与您在 WPF 中的执行方式没有太大区别 - 只需修改面板的子项,例如在 MeasureOverride 中。

为了说明,我已将问题中的代码移植到 UWP,如下所示:

UWP 来源:

[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 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 主题:

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

【讨论】:

    【解决方案2】:

    我会选择 TemplatedControl,您可以在其中定义所需的 XAML 中的控制结构,这会容易得多,

    我在 Codeplex 上构建可视化图表库时正是这样做的

    【讨论】:

    • 请注意这与问题有什么关系,因为我在问如何动态生成代表可见区域的子项(也称为虚拟化)。由于 XAML 是静态的,因此我无法在其中创建模板来设置控件样式。这一切都很好,我当然会这样做,但这并不能回答如何创建和布局孩子的问题。
    • 你可以重写 DefaultStyleKey 方法,并获取父控件并开始在其中添加你自己的控件,它会在运行时更新 Xaml 内容
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-09-06
    • 2019-12-14
    • 1970-01-01
    • 1970-01-01
    • 2016-09-11
    相关资源
    最近更新 更多