在 UWP 列表视图中实现数据虚拟化,而无需重复项目



我有一个很大ListView,它主要InkCanvas对象组成,事实证明,ListView实现了数据虚拟化,以根据视图中的可见项目"巧妙地"卸载和加载视图中的项目。这样做的问题是,很多时候ListView会缓存项目,并且在添加新项目时,它基本上会复制视图中已添加的项目。因此,就我而言,如果用户向 Inkcanvas 添加笔画,然后将新的 InkCanvas 添加到 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>

可以在此处找到完整示例,这是该问题的视频。 事实上,如果我关闭数据虚拟化(或切换到ItemsControl(,一切都会很好地工作。然而,问题在于,对于非常大的列表,这种方法对性能有严重影响(使用 60+ InkCanvas 控制应用程序只是崩溃(。那么,有没有办法在保留数据虚拟化的同时避免重复项目呢?我已经尝试过VirtualizationMode.Standard但项目仍然重复。

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

ListView里面有一个重用容器,它不会无休止地创建新的列表项,而是会回收。

在大多数情况下,这种回收不是问题。但这对InkCanvas来说很特别.InkCanvas是有状态控件。在InkCanvas上绘制时,手写内容将保留并显示在 UI 上。

如果你的控件是TextBlock,则不会出现此问题,因为我们可以直接将值绑定到TextBlock.Text,但是对于InkCanvas的笔画,我们不能直接绑定,这就会导致所谓的残留

所以为了避免这种情况,我们需要 清除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;
}
}
}

通过这种方式,我们可以保持我们的条目稳定。

最新更新