在不复制项目的情况下在 UWP ListView 中实现数据虚拟化

Implement data virtualisation in a UWP ListView without duplicating items

我有一个很大的 ListView,它主要是 InkCanvas 对象,事实证明 ListView 实现了数据虚拟化以 "cleverly" 在视图中卸载和加载项目取决于视图中的可见项目。这样做的问题是 ListView 多次缓存项目,当添加新项目时,它实际上复制了已添加到视图中的项目。因此,在我的例子中,如果用户向 Inkcanvas 添加一个笔划,然后向 ListView 添加一个新的 InkCanvas,则新的 canvas 包含来自先前 canvas 的笔划。据报道 here 这是因为数据虚拟化。我的ListView实现如下:

    <Grid HorizontalAlignment="Stretch">
        <ListView x:Name="CanvasListView" IsTapEnabled="False"
                  IsItemClickEnabled="False"
                  ScrollViewer.ZoomMode="Enabled"
                  ScrollViewer.HorizontalScrollMode="Enabled"
                  ScrollViewer.VerticalScrollMode="Enabled"
                  ScrollViewer.HorizontalScrollBarVisibility="Auto"
                  ScrollViewer.VerticalScrollBarVisibility="Visible"
                  HorizontalAlignment="Stretch">

            <!-- Make sure that items are not clickable and centered-->
            <ListView.ItemContainerStyle>
                <Style TargetType="ListViewItem">
                    <Setter Property="HorizontalContentAlignment" Value="Center"/>
                </Style>
            </ListView.ItemContainerStyle>

            <ListView.ItemTemplate>
                <DataTemplate>
                    <StackPanel>
                        <local:CanvasControl Margin="0 2"
                                             VerticalAlignment="Stretch"
                                         HorizontalAlignment="Stretch" 
                                         MinWidth="1000" MinHeight="100" MaxHeight="400"
                                         Background="LightGreen"/>
                        <Grid HorizontalAlignment="Stretch" Background="Black" Height="2"></Grid>
                    </StackPanel>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
        <StackPanel Orientation="Horizontal" HorizontalAlignment="Stretch">
            <InkToolbar x:Name="inkToolbar" 
                    VerticalAlignment="Top"
                    Background="LightCoral"/>
            <StackPanel HorizontalAlignment="Right">
                <Button x:Name="AddButton" Content="Add Page" Click="Button_Click"/>
                <TextBlock x:Name="PageCountText" />
            </StackPanel>
        </StackPanel>
    </Grid>

可以找到完整的示例 here and here is a video of the issue。 事实上,如果我关闭数据虚拟化(或切换到 ItemsControl),一切都会很好地运行。然而,问题是对于一个非常大的列表,这种方法对性能有很大的影响(60 多个 InkCanvas 控件应用程序只是崩溃)。那么有没有办法在保留数据虚拟化的同时避免物品重复呢?我试过 VirtualizationMode.Standard 但项目仍然重复。

要解决这个问题,首先要了解为什么会出现这个问题。

ListView里面有一个reuse container,它不会无限创建新的列表项,而是会回收。

在大多数情况下,这样的回收不是问题。但它对 InkCanvas 来说很特别。 InkCanvas 是一个有状态的控件。当您在 InkCanvas 上绘图时,笔迹会保留并显示在 UI.

如果你的控件是TextBlock,则不会出现这个问题,因为我们可以直接绑定值给TextBlock.Text,但是对于InkCanvas的Stroke,我们不能直接绑定bind,会造成所谓的residue.

所以为了避免这种情况,我们需要清除state,即每次创建或重新加载InkCanvas时,都会重新InkCanvas中的笔画-渲染。

1.在ViewModel

中创建保存笔画信息的列表
public class ViewModel : INotifyPropertyChanged
{
    // ... other code
    public List<InkStroke> Strokes { get; set; }
    public ViewModel()
    {
        Strokes = new List<InkStroke>();
    }
}

2。改变CanvasControl

的内部结构

xaml

<Grid>
    <InkCanvas x:Name="inkCanvas" 
               Margin="0 2"
               MinWidth="1000" 
               MinHeight="300"
               HorizontalAlignment="Stretch" >
    </InkCanvas>
</Grid>

xaml.cs

public sealed partial class CanvasControl : UserControl
{
    public CanvasControl()
    {
        this.InitializeComponent();
        // Set supported inking device types.
        inkCanvas.InkPresenter.InputDeviceTypes =
            Windows.UI.Core.CoreInputDeviceTypes.Mouse |
            Windows.UI.Core.CoreInputDeviceTypes.Pen;

    }

    private void StrokesCollected(InkPresenter sender, InkStrokesCollectedEventArgs args)
    {
        if (Data != null)
        {
            var strokes = inkCanvas.InkPresenter.StrokeContainer.GetStrokes().ToList();
            Data.Strokes = strokes.Select(p => p.Clone()).ToList();
        }
    }

    public ViewModel Data
    {
        get { return (ViewModel)GetValue(DataProperty); }
        set { SetValue(DataProperty, value); }
    }

    public static readonly DependencyProperty DataProperty =
        DependencyProperty.Register("Data", typeof(ViewModel), typeof(CanvasControl), new PropertyMetadata(null,new PropertyChangedCallback(Data_Changed)));

    private static void Data_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if(e.NewValue!=null && e.NewValue is ViewModel vm)
        {
            var strokes = vm.Strokes.Select(p=>p.Clone());
            var instance = d as CanvasControl;
            instance.inkCanvas.InkPresenter.StrokesCollected -= instance.StrokesCollected;
            instance.inkCanvas.InkPresenter.StrokeContainer.Clear();
            try
            {
                instance.inkCanvas.InkPresenter.StrokeContainer.AddStrokes(strokes);
            }
            catch (Exception)
            {

            }

            instance.inkCanvas.InkPresenter.StrokesCollected += instance.StrokesCollected;
        }
    }
}

这样,我们的条目就可以保持稳定。