在不复制项目的情况下在 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;
}
}
}
这样,我们的条目就可以保持稳定。
我有一个很大的 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;
}
}
}
这样,我们的条目就可以保持稳定。