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

问题描述 投票:0回答:1

我制作了一个从 JSON 生成表单的生成器。我遇到的问题是,这些表单是为我的程序中的几乎每个元素生成的,并且从未被 GC 过。经过一些研究后,我发现这很可能是由于我生成的视图元素中的绑定所致。

[编辑]更新了转换器(仍未完全脱离绑定)

                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);
    }

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

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

  1. 进入并递归地抛出每个元素来寻找绑定并将其删除。
  2. 调用某种已经存在的自动执行的函数?

[编辑]添加附加信息

<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 基本上包含 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
}

自定义KeyValuePairs.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 中也没有任何项目。

wpf data-binding memory-leaks
1个回答
1
投票

以下模式允许动态创建内容。该框架将根据提供的

DatatTemplate
定义自动生成每个项目的内容。在 XAML 中创建视图比使用 C# 更容易且更具可读性。您的代码更加干净(请参见下面的示例),因为您没有混合与视图相关的代码,例如带有数据相关代码的布局,例如将 JSON 转换为 POCO。例如数据绑定的语法更直接。

为每个出现的数据类型定义隐式

DataTemplate
。由于您有不同的数据用例,例如显示值列表或编辑单个值,因此您应该相应地创建数据结构层次结构,从而产生两个数据模型(基于您提供的示例)。

引入一个通用接口是件好事,它允许在单个集合中存储不同的实现,并且还可以实现多态性和其他 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()
    }
  }

  ...
}

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>
   
© www.soinside.com 2019 - 2024. All rights reserved.