ObservableCollection.Clear 留下幻影元素
ObservableCollection.Clear leaving phantom elements
我有一组转换器,用于在 canvas 上动态定位方块。这些正方形存储在一个可观察的集合中。 CanvasPositionScaleConverter
将范围内的值转换为0到1之间的值。如果它转换的值超出指定范围,则会抛出Argument异常。
我的问题是,当我清除我的方块集合时,屏幕变空,“幻影”元素似乎被留下,转换器继续对其进行操作。因此,当 canvas 调整大小时,我仍然会抛出异常,即使 squares 集合已被清除。
为什么转换器仍然 运行 在这些已通过 Squares.Clear()
删除的元素上?
备注
使用 Squares = new ObservableCollection<Square>()
而不是 Squares.Clear()
仍然会导致此问题。
更新
如果从未添加 Sqaures
集合并按下 Resize
,则不会发生此问题。仅当元素已从集合中删除时才会发生(显然如果元素尚未从集合中删除但超出有效范围)。
异常详情为
Exception thrown: 'System.ArgumentException' in LateExceptionMcve.dll
An unhandled exception of type 'System.ArgumentException' occurred in LateExceptionMcve.dll
Value cannot be greater than max
MCVE
MainWindow.xaml
<Window x:Class="LateExceptionMcve.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:LateExceptionMcve"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance Type=local:MainWindow, IsDesignTimeCreatable=True}"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Viewbox Grid.Column="0"
Grid.ColumnSpan="2"
Grid.Row="0"
Stretch="Uniform">
<Border BorderThickness="1"
BorderBrush="Black">
<Border.Resources>
<local:CanvasScaleConverter x:Key="CanvasScaleConverter" />
<local:CanvasPositionScaleConverter x:Key="CanvasPositionScaleConverter" />
</Border.Resources>
<ItemsControl ItemsSource="{Binding Squares}"
Width="{Binding Size, Converter={StaticResource CanvasScaleConverter}}"
Height="{Binding Size, Converter={StaticResource CanvasScaleConverter}}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Canvas.Left">
<Setter.Value>
<MultiBinding Converter="{StaticResource CanvasPositionScaleConverter}">
<Binding Path="X" />
<Binding RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type Window}}"
Path="DataContext.Min" />
<Binding RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type Window}}"
Path="DataContext.Max" />
<Binding RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type Window}}"
Path="DataContext.Size"
Converter="{StaticResource CanvasScaleConverter}" />
</MultiBinding>
</Setter.Value>
</Setter>
<Setter Property="Canvas.Top">
<Setter.Value>
<MultiBinding Converter="{StaticResource CanvasPositionScaleConverter}">
<Binding Path="Y" />
<Binding RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type Window}}"
Path="DataContext.Min" />
<Binding RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type Window}}"
Path="DataContext.Max" />
<Binding RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type Window}}"
Path="DataContext.Size"
Converter="{StaticResource CanvasScaleConverter}" />
</MultiBinding>
</Setter.Value>
</Setter>
<Setter Property="Width" Value="50" />
<Setter Property="Height" Value="50" />
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid Background="Red" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Border>
</Viewbox>
<Button Grid.Column="0"
Grid.Row="1"
Content="Clear"
Click="Clear" />
<Button Grid.Column="1"
Grid.Row="1"
Content="Resize"
Click="Resize" />
</Grid>
</Window>
MainWindow.xaml.cs
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Data;
namespace LateExceptionMcve
{
public partial class MainWindow : INotifyPropertyChanged
{
public MainWindow()
{
InitializeComponent();
DataContext = this;
Min = 0;
Max = 10;
Size = Max - Min;
Squares = new ObservableCollection<Square>
{
new Square
{
X = 8,
Y = 7,
},
};
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private void Resize(object sender, RoutedEventArgs e)
{
Min = 0;
Max = 5;
Size = Max - Min;
}
private void Clear(object sender, RoutedEventArgs e)
{
Squares.Clear();
}
private ObservableCollection<Square> _squares;
public ObservableCollection<Square> Squares
{
get => _squares;
set
{
_squares = value;
OnPropertyChanged();
}
}
private double _size;
public double Size
{
get => _size;
set
{
_size = value;
OnPropertyChanged();
}
}
private double _min;
public double Min
{
get => _min;
set
{
_min = value;
OnPropertyChanged();
}
}
private double _max;
public double Max
{
get => _max;
set
{
_max = value;
OnPropertyChanged();
}
}
}
public sealed class Square : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private double _x;
public double X
{
get => _x;
set
{
_x = value;
OnPropertyChanged();
}
}
private double _y;
public double Y
{
get => _y;
set
{
_y = value;
OnPropertyChanged();
}
}
}
public sealed class CanvasScaleConverter : IValueConverter
{
private const double Scale = 100;
public object Convert(
object value,
Type targetType,
object parameter,
CultureInfo culture)
{
if (value is double valueToScale)
{
return valueToScale * Scale;
}
return value;
}
public object ConvertBack(
object value,
Type targetType,
object parameter,
CultureInfo culture)
{
throw new NotSupportedException();
}
}
public sealed class CanvasPositionScaleConverter : IMultiValueConverter
{
public object Convert(
object[] values,
Type targetType,
object parameter,
CultureInfo culture)
{
if (values.Length == 4 &&
values[0] is double value &&
values[1] is double min &&
values[2] is double max &&
values[3] is double canvasWidthHeight)
{
return canvasWidthHeight * RangeToNormalizedValue(min, max, value);
}
return values;
}
private static double RangeToNormalizedValue(
double min,
double max,
double value)
{
if (min > max)
{
throw new ArgumentException("Min cannot be less than max");
}
if (value < min)
{
throw new ArgumentException("Value cannot be less than min");
}
if (value > max)
{
throw new ArgumentException("Value cannot be greater than max");
}
return (value - min) / (max - min);
}
public object[] ConvertBack(
object value,
Type[] targetTypes,
object parameter,
CultureInfo culture)
{
throw new NotSupportedException();
}
}
}
编辑
我已经在 WPF GitHub 存储库中填写了一个 bug report,希望这可以帮助找到这里发生的事情。
来自 miloush on the GitHub issue 的信息。
此问题的原因是即使 Squares
集合已被清除并且对象在屏幕上不再可见,但对象尚未被垃圾回收。
可以通过将 Clear
方法修改为类似
的方法来强制进行垃圾回收
private void Clear(object sender, RoutedEventArgs e)
{
Squares.Clear();
Dispatcher.BeginInvoke(new Action(GC.Collect), DispatcherPriority.ContextIdle);
}
这将(最终)清除对象。但是,这不是立竿见影的。即使在调用 Squares.Clear
之后只是 GC.Collect()
它也不是立即的。
此问题可能会在 WPF 中多次发生,包括在虚拟化等常见操作中。因此,最好更新转换器,以便它可以优雅地处理无效输入。
public sealed class CanvasPositionScaleConverter : IMultiValueConverter
{
public object Convert(
object[] values,
Type targetType,
object parameter,
CultureInfo culture)
{
if (values.Length != 4 ||
!(values[0] is double value) ||
!(values[1] is double min) ||
!(values[2] is double max) ||
!(values[3] is double canvasWidthHeight))
{
return null;
}
try
{
return canvasWidthHeight * RangeToNormalizedValue(min, max, value);
}
catch (ArgumentException e) when (e.Message == ValueLessThanMin)
{
return 0;
}
catch (ArgumentException e) when (e.Message == ValueGreaterThanMax)
{
return canvasWidthHeight;
}
}
// This is in a utility class normally
private const string ValueLessThanMin = "Value cannot be less than min";
private const string ValueGreaterThanMax = "Value cannot be greater than max";
private static double RangeToNormalizedValue(
double min,
double max,
double value)
{
if (min > max)
{
throw new ArgumentException("Min cannot be less than max");
}
if (value < min)
{
throw new ArgumentException(ValueLessThanMin);
}
if (value > max)
{
throw new ArgumentException(ValueGreaterThanMax);
}
return (value - min) / (max - min);
}
public object[] ConvertBack(
object value,
Type[] targetTypes,
object parameter,
CultureInfo culture)
{
throw new NotSupportedException();
}
}
我有一组转换器,用于在 canvas 上动态定位方块。这些正方形存储在一个可观察的集合中。 CanvasPositionScaleConverter
将范围内的值转换为0到1之间的值。如果它转换的值超出指定范围,则会抛出Argument异常。
我的问题是,当我清除我的方块集合时,屏幕变空,“幻影”元素似乎被留下,转换器继续对其进行操作。因此,当 canvas 调整大小时,我仍然会抛出异常,即使 squares 集合已被清除。
为什么转换器仍然 运行 在这些已通过 Squares.Clear()
删除的元素上?
备注
使用 Squares = new ObservableCollection<Square>()
而不是 Squares.Clear()
仍然会导致此问题。
更新
如果从未添加 Sqaures
集合并按下 Resize
,则不会发生此问题。仅当元素已从集合中删除时才会发生(显然如果元素尚未从集合中删除但超出有效范围)。
异常详情为
Exception thrown: 'System.ArgumentException' in LateExceptionMcve.dll
An unhandled exception of type 'System.ArgumentException' occurred in LateExceptionMcve.dll
Value cannot be greater than max
MCVE
MainWindow.xaml
<Window x:Class="LateExceptionMcve.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:LateExceptionMcve"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance Type=local:MainWindow, IsDesignTimeCreatable=True}"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Viewbox Grid.Column="0"
Grid.ColumnSpan="2"
Grid.Row="0"
Stretch="Uniform">
<Border BorderThickness="1"
BorderBrush="Black">
<Border.Resources>
<local:CanvasScaleConverter x:Key="CanvasScaleConverter" />
<local:CanvasPositionScaleConverter x:Key="CanvasPositionScaleConverter" />
</Border.Resources>
<ItemsControl ItemsSource="{Binding Squares}"
Width="{Binding Size, Converter={StaticResource CanvasScaleConverter}}"
Height="{Binding Size, Converter={StaticResource CanvasScaleConverter}}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Canvas.Left">
<Setter.Value>
<MultiBinding Converter="{StaticResource CanvasPositionScaleConverter}">
<Binding Path="X" />
<Binding RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type Window}}"
Path="DataContext.Min" />
<Binding RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type Window}}"
Path="DataContext.Max" />
<Binding RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type Window}}"
Path="DataContext.Size"
Converter="{StaticResource CanvasScaleConverter}" />
</MultiBinding>
</Setter.Value>
</Setter>
<Setter Property="Canvas.Top">
<Setter.Value>
<MultiBinding Converter="{StaticResource CanvasPositionScaleConverter}">
<Binding Path="Y" />
<Binding RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type Window}}"
Path="DataContext.Min" />
<Binding RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type Window}}"
Path="DataContext.Max" />
<Binding RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type Window}}"
Path="DataContext.Size"
Converter="{StaticResource CanvasScaleConverter}" />
</MultiBinding>
</Setter.Value>
</Setter>
<Setter Property="Width" Value="50" />
<Setter Property="Height" Value="50" />
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid Background="Red" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Border>
</Viewbox>
<Button Grid.Column="0"
Grid.Row="1"
Content="Clear"
Click="Clear" />
<Button Grid.Column="1"
Grid.Row="1"
Content="Resize"
Click="Resize" />
</Grid>
</Window>
MainWindow.xaml.cs
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Data;
namespace LateExceptionMcve
{
public partial class MainWindow : INotifyPropertyChanged
{
public MainWindow()
{
InitializeComponent();
DataContext = this;
Min = 0;
Max = 10;
Size = Max - Min;
Squares = new ObservableCollection<Square>
{
new Square
{
X = 8,
Y = 7,
},
};
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private void Resize(object sender, RoutedEventArgs e)
{
Min = 0;
Max = 5;
Size = Max - Min;
}
private void Clear(object sender, RoutedEventArgs e)
{
Squares.Clear();
}
private ObservableCollection<Square> _squares;
public ObservableCollection<Square> Squares
{
get => _squares;
set
{
_squares = value;
OnPropertyChanged();
}
}
private double _size;
public double Size
{
get => _size;
set
{
_size = value;
OnPropertyChanged();
}
}
private double _min;
public double Min
{
get => _min;
set
{
_min = value;
OnPropertyChanged();
}
}
private double _max;
public double Max
{
get => _max;
set
{
_max = value;
OnPropertyChanged();
}
}
}
public sealed class Square : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private double _x;
public double X
{
get => _x;
set
{
_x = value;
OnPropertyChanged();
}
}
private double _y;
public double Y
{
get => _y;
set
{
_y = value;
OnPropertyChanged();
}
}
}
public sealed class CanvasScaleConverter : IValueConverter
{
private const double Scale = 100;
public object Convert(
object value,
Type targetType,
object parameter,
CultureInfo culture)
{
if (value is double valueToScale)
{
return valueToScale * Scale;
}
return value;
}
public object ConvertBack(
object value,
Type targetType,
object parameter,
CultureInfo culture)
{
throw new NotSupportedException();
}
}
public sealed class CanvasPositionScaleConverter : IMultiValueConverter
{
public object Convert(
object[] values,
Type targetType,
object parameter,
CultureInfo culture)
{
if (values.Length == 4 &&
values[0] is double value &&
values[1] is double min &&
values[2] is double max &&
values[3] is double canvasWidthHeight)
{
return canvasWidthHeight * RangeToNormalizedValue(min, max, value);
}
return values;
}
private static double RangeToNormalizedValue(
double min,
double max,
double value)
{
if (min > max)
{
throw new ArgumentException("Min cannot be less than max");
}
if (value < min)
{
throw new ArgumentException("Value cannot be less than min");
}
if (value > max)
{
throw new ArgumentException("Value cannot be greater than max");
}
return (value - min) / (max - min);
}
public object[] ConvertBack(
object value,
Type[] targetTypes,
object parameter,
CultureInfo culture)
{
throw new NotSupportedException();
}
}
}
编辑
我已经在 WPF GitHub 存储库中填写了一个 bug report,希望这可以帮助找到这里发生的事情。
来自 miloush on the GitHub issue 的信息。
此问题的原因是即使 Squares
集合已被清除并且对象在屏幕上不再可见,但对象尚未被垃圾回收。
可以通过将 Clear
方法修改为类似
private void Clear(object sender, RoutedEventArgs e)
{
Squares.Clear();
Dispatcher.BeginInvoke(new Action(GC.Collect), DispatcherPriority.ContextIdle);
}
这将(最终)清除对象。但是,这不是立竿见影的。即使在调用 Squares.Clear
之后只是 GC.Collect()
它也不是立即的。
此问题可能会在 WPF 中多次发生,包括在虚拟化等常见操作中。因此,最好更新转换器,以便它可以优雅地处理无效输入。
public sealed class CanvasPositionScaleConverter : IMultiValueConverter
{
public object Convert(
object[] values,
Type targetType,
object parameter,
CultureInfo culture)
{
if (values.Length != 4 ||
!(values[0] is double value) ||
!(values[1] is double min) ||
!(values[2] is double max) ||
!(values[3] is double canvasWidthHeight))
{
return null;
}
try
{
return canvasWidthHeight * RangeToNormalizedValue(min, max, value);
}
catch (ArgumentException e) when (e.Message == ValueLessThanMin)
{
return 0;
}
catch (ArgumentException e) when (e.Message == ValueGreaterThanMax)
{
return canvasWidthHeight;
}
}
// This is in a utility class normally
private const string ValueLessThanMin = "Value cannot be less than min";
private const string ValueGreaterThanMax = "Value cannot be greater than max";
private static double RangeToNormalizedValue(
double min,
double max,
double value)
{
if (min > max)
{
throw new ArgumentException("Min cannot be less than max");
}
if (value < min)
{
throw new ArgumentException(ValueLessThanMin);
}
if (value > max)
{
throw new ArgumentException(ValueGreaterThanMax);
}
return (value - min) / (max - min);
}
public object[] ConvertBack(
object value,
Type[] targetTypes,
object parameter,
CultureInfo culture)
{
throw new NotSupportedException();
}
}