Reputation: 623
I made a generator that generates form from JSON. The problem I'm having is the fact, that these forms are generated for nearly every element in my program and are never GC'ed. After doing some research I figured out that that is most likely due to Bindings in my generated View elements.
[EDIT] updated converter (still not fully moved from binding)
public static (ObservableCollection<CutomKeyValuePairs> keyValuePairs, StackPanel stackPanel) RenderForm(JObject jArray, ObservableCollection<CutomKeyValuePairs> pairs, bool allowEdit, int iteration)
{
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" + iteration };
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, iteration + 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);
}
This is just a part of the code, but basically what it does - it goes inside the Json recursively and collects all the data, and binds it to the newly created elements that go into the form.
My question is: is there any way to automatically get rid of all these bindings and if not - what is the best approach of dealing with them manually?
[EDIT] adding additional information
<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>
MetadataView.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 basically contains the DataPairs and creates StackPanel inside itself
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
The way I found about it was using .net memory profiler. As far as I managed to determine: The MetadataView.xaml is never destroyed and I'm pretty sure nether is any item in the DataTemplate.
Upvotes: 0
Views: 1581
Reputation: 28948
The following pattern allows to create content dynamically. The framework will generate the content for each item automatically based on the provided DatatTemplate
definitions. Creating the view in XAML is much easier and readable than using C#. Your code is much cleaner (see example below), as you don't mix view related code e.g. layout with data related code e.g. converting JSON to POCO. The syntax e.g for data binding is more straight forward.
An implicit DataTemplate
is defined for each occurring data type. Since you have different data use cases like displaying a list of values or editing a single value, you should create your data structure hierarchy accordingly, resulting in two data models (based on your provided example).
It's good to introduce a common interface, which allows to store different implementations in a single collection and also enables polymorphism and other OO design improvements.
The following code doesn't introduce memory leaks, unless you would keep a strong reference to the item models in other components. If this is the case you must re-evaluate your object lifetimes to ensure that all references to the models are removed in order to let the GC end their lifetime.
To eliminate the complete JSON switch and iteration I recommend to use the System.Text.Json
namespace or introducing a third party library like Newtonsoft to deserialize the JSON response object automatically to a C# model class. You would only have to deserialize JSON to C# and add the resulting instance to the source collection.
See Microsoft Docs: Data Templating Overview
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()
}
}
...
}
EditableKeyValuePair.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;
}
}
MainWindow.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>
Upvotes: 1