解决了由于dataBinding导致的生成表单中的WPF内存泄漏



我制作了一个生成器,用于从JSON生成表单。我遇到的问题是,这些表单几乎是为我程序中的每一个元素生成的,从来都不是GC’ed。经过一些研究,我发现这很可能是由于我生成的View元素中的Bindings。

[EDIT]更新的转换器(仍未完全从绑定中移出(

public static (ObservableCollection<CutomKeyValuePairs> keyValuePairs, StackPanel stackPanel) RenderForm(JObject jArray, ObservableCollection<CutomKeyValuePairs> pairs, bool allowEdit, int itteration)
{
StackPanel stackPanel = new StackPanel();
if (pairs == null)
{
pairs = new ObservableCollection<CutomKeyValuePairs>();
}
foreach (JToken element in jArray["properties"].Reverse())
{
Grid grid1 = new Grid();
ColumnDefinition col1 = new ColumnDefinition() { SharedSizeGroup = "gr0" + itteration };
Grid grid2 = new Grid();
ColumnDefinition col2 = new ColumnDefinition() { };
grid1.ColumnDefinitions.Add(col1);
grid2.ColumnDefinitions.Add(col2);
bool containes = true;
CutomKeyValuePairs keyValuePairs = pairs.FirstOrDefault(item => item.Key == element.ToObject<JProperty>().Name);
if (keyValuePairs == null)
{
keyValuePairs = new CutomKeyValuePairs(element.ToObject<JProperty>().Name, null, null);
containes = false;
}
string type;
if (!element.First["type"].HasValues)
{
type = element.First["type"].ToString();
}
else
type = element.First["type"].First.ToString();
TextBlock textBlock = new TextBlock() { Text = element.ToObject<JProperty>().Name + ": ", TextWrapping = TextWrapping.Wrap };
textBlock.Padding = new Thickness() { Top = 5 };
switch (type)
{
case "object":
keyValuePairs.Type = type;
var (tmp, stack) = RenderForm(element.First.ToObject<JObject>(), keyValuePairs.Value as ObservableCollection<CutomKeyValuePairs>, allowEdit, itteration + 1);
keyValuePairs.Value = tmp;
grid1.Children.Add(textBlock);
grid2.Children.Add(stack);
break;
case "boolean":
keyValuePairs.Type = type;
if (keyValuePairs.Value == null)
keyValuePairs.Value = false;
CheckBox checkBox = new CheckBox() { IsEnabled = allowEdit, Margin = new Thickness() { Top = 5, Bottom = 5, Left = 5, Right = 5 } };
checkBox.DataContext = keyValuePairs;
Binding checkBoxBinding = new Binding() { Path = new PropertyPath("Value"), Mode = BindingMode.TwoWay, UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged };
checkBoxBinding.Source = keyValuePairs;
checkBox.SetBinding(CheckBox.IsCheckedProperty, checkBoxBinding);
grid1.Children.Add(textBlock);
grid2.Children.Add(checkBox);
break;
case "integer":
keyValuePairs.Type = type;
grid1.Children.Add(textBlock);
if (allowEdit)
{
if (element.First["enum"] == null)
{
IntegerTextBox textBox = new IntegerTextBox() { TextWrapping = TextWrapping.Wrap, IsEnabled = allowEdit, Margin = new Thickness() { Top = 5, Bottom = 5, Left = 5, Right = 5 }, HorizontalAlignment = HorizontalAlignment.Stretch };
textBox.DataContext = keyValuePairs;
Binding textBoxBinding = new Binding() { Path = new PropertyPath("Value"), Mode = BindingMode.TwoWay, UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged };
textBoxBinding.Source = keyValuePairs;
textBox.SetBinding(TextBox.TextProperty, textBoxBinding);
grid2.Children.Add(textBox);
}
else
{
var list = element.First["enum"].Values<string>().ToList<object>();
keyValuePairs.Enum = list;
ComboBox comboBox = new ComboBox() { Margin = new Thickness() { Top = 5, Bottom = 5, Left = 5, Right = 5 }, HorizontalAlignment = HorizontalAlignment.Stretch };
comboBox.DataContext = keyValuePairs;
Binding selectedEnum = new Binding() { Path = new PropertyPath("Value"), Mode = BindingMode.TwoWay, UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged };
comboBox.ItemsSource = keyValuePairs.Enum;
comboBox.SelectedItem = keyValuePairs.Value;
comboBox.SetBinding(ComboBox.TextProperty, selectedEnum);
grid2.Children.Add(comboBox);
}
}
else
{
TextBlock textBlock2 = new TextBlock() { TextWrapping = TextWrapping.Wrap, Margin = new Thickness() { Top = 5, Bottom = 5, Left = 5, Right = 5 }, HorizontalAlignment = HorizontalAlignment.Left };
textBlock2.DataContext = keyValuePairs;
textBlock2.Text = keyValuePairs.Value as string;
textBlock2.ToolTip = keyValuePairs.Value as string;
grid2.Children.Add(textBlock2);
}
break;
case "number":
keyValuePairs.Type = type;
grid1.Children.Add(textBlock);
if (allowEdit)
{
if (element.First["enum"] == null)
{
DoubleTextBox textBox = new DoubleTextBox() { TextWrapping = TextWrapping.Wrap, IsEnabled = allowEdit, Margin = new Thickness() { Top = 5, Bottom = 5, Left = 5, Right = 5 }, HorizontalAlignment = HorizontalAlignment.Stretch };
textBox.DataContext = keyValuePairs;
Binding textBoxBinding = new Binding() { Path = new PropertyPath("Value"), Mode = BindingMode.TwoWay, UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged };
textBoxBinding.Source = keyValuePairs;
textBox.SetBinding(TextBox.TextProperty, textBoxBinding);
grid2.Children.Add(textBox);
}
else
{
var list = element.First["enum"].Values<string>().ToList<object>();
keyValuePairs.Enum = list;
ComboBox comboBox = new ComboBox() { Margin = new Thickness() { Top = 5, Bottom = 5, Left = 5, Right = 5 }, HorizontalAlignment = HorizontalAlignment.Stretch };
comboBox.DataContext = keyValuePairs;
Binding selectedEnum = new Binding() { Path = new PropertyPath("Value"), Mode = BindingMode.TwoWay, UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged };
comboBox.ItemsSource = keyValuePairs.Enum;
comboBox.SelectedItem = keyValuePairs.Value;
comboBox.SetBinding(ComboBox.TextProperty, selectedEnum);
grid2.Children.Add(comboBox);
}
}
else
{
TextBlock textBlock2 = new TextBlock() { TextWrapping = TextWrapping.Wrap, Margin = new Thickness() { Top = 5, Bottom = 5, Left = 5, Right = 5 }, HorizontalAlignment = HorizontalAlignment.Left };
textBlock2.DataContext = keyValuePairs;
textBlock2.Text = keyValuePairs.Value as string;
textBlock2.ToolTip = keyValuePairs.Value as string;
grid2.Children.Add(textBlock2);
}
break;
case "string":
default:
keyValuePairs.Type = "string";
grid1.Children.Add(textBlock);
if (allowEdit)
{
if (element.First["enum"] == null)
{
TextBox textBox = new TextBox() { IsEnabled = allowEdit, TextWrapping = TextWrapping.Wrap, Margin = new Thickness() { Top = 5, Bottom = 5, Left = 5, Right = 5 }, HorizontalAlignment = HorizontalAlignment.Stretch };
textBox.DataContext = keyValuePairs;
textBox.AcceptsReturn = true;
textBox.Text = keyValuePairs.Value as string;
grid2.Children.Add(textBox);
}
else
{
var list = element.First["enum"].Values<string>().ToList<object>();
keyValuePairs.Enum = list;
ComboBox comboBox = new ComboBox() { Margin = new Thickness() { Top = 5, Bottom = 5, Left = 5, Right = 5 }, HorizontalAlignment = HorizontalAlignment.Stretch };
comboBox.DataContext = keyValuePairs;
Binding selectedEnum = new Binding() { Path = new PropertyPath("Value"), Mode = BindingMode.TwoWay, UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged };
comboBox.ItemsSource = keyValuePairs.Enum;
comboBox.SelectedItem = keyValuePairs.Value;
comboBox.SetBinding(ComboBox.TextProperty, selectedEnum);
grid2.Children.Add(comboBox);
}
}
else
{
TextBlock textBlock2 = new TextBlock() { TextWrapping = TextWrapping.Wrap, Margin = new Thickness() { Top = 5, Bottom = 5, Left = 5, Right = 5 }, HorizontalAlignment = HorizontalAlignment.Left, MaxWidth = 300};
textBlock2.DataContext = keyValuePairs;
textBlock2.Text = keyValuePairs.Value as string;
textBlock2.ToolTip = keyValuePairs.Value as string;
grid2.Children.Add(textBlock2);
}
break;
}
DockPanel pan = new DockPanel() { LastChildFill = true };
pan.Children.Add(grid1);
pan.Children.Add(grid2);
stackPanel.Children.Add(pan);
if (!containes)
pairs.Add(keyValuePairs);
}
return (pairs, stackPanel);
}

这只是代码的一部分,但基本上是它所做的——它递归地进入Json内部,收集所有数据,并将其绑定到表单中新创建的元素

我的问题是:有没有办法自动消除所有这些绑定?如果没有,手动处理它们的最佳方法是什么?

  1. 进入并递归抛出每个元素,寻找绑定并移除它
  2. 调用某种已经存在的函数,它会自动执行吗

[EDIT]添加附加信息

<ListBox Grid.Row="2" ItemsSource="{Binding Fruits, ElementName=uc}"  SelectedItem="{Binding SelectedFruit, ElementName=uc}"
MouseDoubleClick="OnFruitEditClick" MouseDown="ListBoxMouseDown" VirtualizingPanel.IsVirtualizing="True" VirtualizingPanel.ScrollUnit="Pixel" ScrollViewer.CanContentScroll="True"
Grid.IsSharedSizeScope="True" ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate >
<Border BorderBrush="{DynamicResource DefaultForegroundBrush}" BorderThickness="1" Margin="{StaticResource DefaultMargin}" Padding="{StaticResource DefaultMargin}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition MaxHeight="300"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto" MinWidth="300" MaxWidth="500" SharedSizeGroup="col1"/>
<ColumnDefinition MaxWidth="500" Width="*" SharedSizeGroup="col2"/>
</Grid.ColumnDefinitions>
<Viewbox MaxHeight="300" MaxWidth="300">
<Canvas Height="{Binding ActualHeight, ElementName=img}" Width="{Binding ActualWidth, ElementName=img}">
<Image Name="img"  Source="{Binding Thumb}"/>

</Canvas>
</Viewbox>
<TextBlock Grid.Row="1" Grid.Column="0" Text="{Binding Name}" FontWeight="Bold" TextAlignment="Center" Style="{StaticResource DefaultTextBlockStyle}"/>
<c:MetadataView MaxHeight="300" Grid.Column="1" Grid.RowSpan="2" Metadata="{Binding Metadata}" HorizontalAlignment="Stretch" VerticalAlignment="Center" IsEdit="False"/>
</Grid>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>

元数据视图.xaml

<UserControl ** the usual stuff** >
<Grid Name="grid" VerticalAlignment="Center" HorizontalAlignment="Stretch">
<ScrollViewer  VerticalScrollBarVisibility="Visible" HorizontalScrollBarVisibility="Disabled" Focusable="False">
<StackPanel Name="DisplayMode" VerticalAlignment="Center" Grid.IsSharedSizeScope="True"/>
</ScrollViewer>
</Grid>
</UserControl>

MetadataView.xaml.cs基本上包含DataPairs,并在内部创建StackPanel

public partial class MetadataView : UserControl, INotifyPropertyChanged
{
#region Public static fields
public static DependencyProperty MetadataProperty = DependencyProperty.Register("Metadata", typeof(string), typeof(MetadataView), new PropertyMetadata(OnMetadaChanged));
#endregion
#region Private fields
private ObservableCollection<CutomKeyValuePairs> _metadataList;
private bool _isEdit = false;
#endregion
#region Public constructor
public MetadataView()
{
InitializeComponent();
}
#endregion
#region Properties
public string Metadata
{
get => (string)GetValue(MetadataProperty);
set => SetValue(MetadataProperty, value);
}
public bool IsEdit
{
get { return _isEdit; }
set
{
SetProperty(ref _isEdit, value);
if (!value)
{
DeserializeMetadata();
}
}
}
public ObservableCollection<CutomKeyValuePairs> MetadataList
{
get => _metadataList;
set => SetProperty(ref _metadataList, value);
}
#endregion
#region Private methods
private static void OnMetadaChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var target = d as MetadataView;
target.DeserializeMetadata();
}
private ObservableCollection<CutomKeyValuePairs> DeserializeRecursively(string list)
{
var pairs = new ObservableCollection<CutomKeyValuePairs>();
foreach (var prop in JObject.Parse(list))
{
if (prop.Value.HasValues)
{
pairs.Add(new CutomKeyValuePairs(prop.Key, DeserializeRecursively(prop.Value.ToString()), "object"));
}
else
{
pairs.Add(new CutomKeyValuePairs(prop.Key, prop.Value.ToString(), null));
}
}
return pairs;
}
private Dictionary<string, object> SerializeRecursively(ObservableCollection<CutomKeyValuePairs> list)
{
var dict = new Dictionary<string, object>();
foreach (var pair in list)
{
if (pair.Value != null && pair.Value.GetType().IsGenericType && pair.Value.GetType().GetGenericTypeDefinition() == typeof(ObservableCollection<>))
{
dict.Add(pair.Key, SerializeRecursively(pair.Value as ObservableCollection<CutomKeyValuePairs>));
}
else
{
if (pair.Type == "number")
{
dict.Add(pair.Key, Convert.ToDouble(pair.Value as string));
}
else if (pair.Type == "integer")
{
dict.Add(pair.Key, Convert.ToInt32(pair.Value as string));
}
else
dict.Add(pair.Key, pair.Value);
}
}
return dict;
}
#endregion
#region Public methods
public string GetSerializedJson()
{
var dict = new Dictionary<string, object>();
foreach (var pair in MetadataList)
{
if (pair.Value != null && pair.Value.GetType().IsGenericType && pair.Value.GetType().GetGenericTypeDefinition() == typeof(ObservableCollection<>))
{
dict.Add(pair.Key, SerializeRecursively(pair.Value as ObservableCollection<CutomKeyValuePairs>));
}
else
{
if (pair.Type == "number")
{
if ((pair.Value as string)?.Length > 0)
{
dict.Add(pair.Key, Convert.ToDouble(pair.Value as string));
}
else
{
dict.Add(pair.Key, null);
}
}
else if (pair.Type == "integer")
{
if ((pair.Value as string)?.Length > 0)
{
dict.Add(pair.Key, Convert.ToInt32(pair.Value as string));
}
else
{
dict.Add(pair.Key, null);
}
}
else
{
dict.Add(pair.Key, pair.Value);
}
}
}
return JsonConvert.SerializeObject(dict);
}
public void AllowEdit(bool allow)
{
if (allow)
{
IsEdit = true;
DisplayMode.Visibility = Visibility.Visible;
}
else
{
IsEdit = false;
DisplayMode.Visibility = Visibility.Visible;
}
}
public void DeserializeMetadata()
{
try
{
DisplayMode.Children.Clear();
MetadataList = new ObservableCollection<CutomKeyValuePairs>();
if (Metadata != null)
{
foreach (var prop in JObject.Parse(Metadata))
{
if (prop.Value.HasValues)
{
MetadataList.Add(new CutomKeyValuePairs(prop.Key, DeserializeRecursively(prop.Value.ToString()), "object"));
}
else
{
MetadataList.Add(new CutomKeyValuePairs(prop.Key, prop.Value.ToString(), null));
}
}
var schema = ApiContext.Instance.Schema; //TODO: Allow this to be null/empty
var schemaObj = JObject.Parse(schema);
StackPanel pan = null;
(MetadataList, pan) = JsonToFormConverter.RenderForm(schemaObj, MetadataList, IsEdit, 0);
DisplayMode.Children.Add(pan);
}
}
catch (Exception ex)
{
MessageBox.Show("Error: " + ex.Message, "Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
public void DeserializeNew()
{
try
{
DisplayMode.Children.Clear();
MetadataList = new ObservableCollection<CutomKeyValuePairs>();
var schema = ApiContext.Instance.Schema;
var schemaObj = JObject.Parse(schema);
StackPanel pan = null;
(MetadataList, pan) = JsonToFormConverter.RenderForm(schemaObj, MetadataList, IsEdit, 0);
DisplayMode.Children.Add(pan);
}
catch (Exception ex)
{
MessageBox.Show("Error: " + ex.Message, "Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
#endregion
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
private bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
{
if (!object.Equals(storage, value))
{
storage = value;
RaisePropertyChanged(propertyName);
return true;
}
return false;
}
private void RaisePropertyChanged(string propertyname)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyname));
}
#endregion
}

CustomKeyValuePairs.cs

public class CutomKeyValuePairs : INotifyPropertyChanged
{
#region Public constructors
private string _key;
private object _value;
private string _type;
private List<object> _enum;
public CutomKeyValuePairs()
{
}
public CutomKeyValuePairs(string key, object value, string type, List<object> @enum)
{
this.Key = key;
this.Value = value;
this.Type = type;
this.Enum = @enum;
}
public CutomKeyValuePairs(string key, object value, string type)
{
this.Key = key;
this.Value = value;
this.Type = type;
this.Enum = null;
}
#endregion
#region Public properties
public string Key 
{
get => _key;
set => SetProperty(ref _key, value);
}
public object Value
{
get => _value;
set => SetProperty(ref _value, value);
}
public string Type
{
get => _type;
set => SetProperty(ref _type, value);
}
public List<object> Enum
{
get => _enum;
set => SetProperty(ref _enum, value);
}
#endregion
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
private bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
{
if (!object.Equals(storage, value))
{
storage = value;
RaisePropertyChanged(propertyName);
return true;
}
return false;
}
private void RaisePropertyChanged(string propertyname)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyname));
}
#endregion

我发现它的方式是使用.net内存探查器。据我所知:MetadataView.xaml从未被销毁,我很确定DataTemplate中有任何项。

以下模式允许动态创建内容。该框架将根据提供的DatatTemplate定义自动生成每个项目的内容。在XAML中创建视图比使用C#更容易,可读性更强。您的代码要干净得多(见下面的示例(,因为您没有将视图相关的代码(例如布局(与数据相关的代码混合在一起,例如将JSON转换为POCO。例如,数据绑定的语法更直接。

为每个出现的数据类型定义隐式CCD_ 2。由于您有不同的数据用例,如显示值列表或编辑单个值,因此您应该相应地创建数据结构层次结构,从而产生两个数据模型(基于您提供的示例(。

引入一个通用接口是很好的,它允许将不同的实现存储在一个集合中,还允许多态性和其他OO设计改进。

以下代码不会引入内存泄漏,除非您在其他组件中保留对项模型的强引用。如果是这种情况,您必须重新评估对象的生存期,以确保删除对模型的所有引用,以便GC结束其生存期。

为了消除完整的JSON切换和迭代,我建议使用System.Text.Json命名空间,或者引入像Newtonsoft这样的第三方库,将JSON响应对象自动反序列化为C#模型类。您只需要将JSON反序列化为C#,然后将生成的实例添加到源集合中。

请参阅Microsoft文档:数据模板概述

IKeyValuePair.cs

interface IKeyValuePair, INotifyPropertyChanged
{
string Type { get; set; }
}

EnumKeyValuePair.cs

class EnumKeyValuePair : IKeyValuePair
{
public IEnumerable Enum { get; set; }
private string selectedEnum;
public string SelectedEnum
{
get => this.selectedEnum;
set
{
this.selectedEnum = value;
OnPropertyChanged()
}
}
...
}

可编辑KeyValuePair.cs

class EnumKeyValuePair : IKeyValuePair
{
private bool isEditable;
public bool IsEditable
{
get => this.isEditable;
set
{
this.isEditable = value;
OnPropertyChanged()
}
}
private string value;
public string Value
{
get => this.value;
set
{
this.value = value;
OnPropertyChanged()
}
}
...
}

MainWindow.xam.cs

partial class MainWindow : Window
{
public ObservableCollection<IKeyValuePair> KeyValuePairs { get; }
public MainWindow()
{
InitializeComponent();
this.DataContext = this;
this.KeyValuePairs = new ObservableCollection<IKeyValuePair>();
}
private void LoadItems()
{
...
case "string":
if (element.First["enum"] == null)
{
var editableKeyValuePairs = new EditableKeyValuePair()
{
IsEditable = allowEdit,
Type = "string",
Value = "The value"
};
this.KeyValuePairs.Add(editableKeyValuePairs);
}
else
{
var list = element.First["enum"].Values<string>().ToList<object>();
var enumKeyValuePairs = new EnumKeyValuePair()
{
Enum = list,
Type = "string"
};
this.KeyValuePairs.Add(enumKeyValuePairs);
}
break;
}
}

主窗口.xaml

<Window>
<Window.Resources>
<DataTemplate DataType="{x:Type EnumKeyValuePair}">
<ComboBox ItemsSource="{Binding Enum}"
SelectedItem="{Binding SelectedEnum}" />
</DataTemplate>
<DataTemplate DataType="{x:Type EditableKeyValuePairs}">
<TextBox IsReadOnly="{Binding IsEditable}" 
Text="{Binding Value}" />
</DataTemplate>
</Window.Resources>
<ListBox ItemsSource="{Binding KeyValuePairs}" />
</Window>

最新更新