【问题标题】:Two-way binding not working in ContentControl bound to generic value双向绑定在 ContentControl 中不起作用,绑定到通用值
【发布时间】:2016-09-27 11:37:05
【问题描述】:

我正在尝试构建一个允许编辑列表中项目的用户控件。 首先,我将向您展示我拥有的代码,并在代码下方解释我的问题。如果您想重现该问题,您可以创建一个新项目并将所有代码复制到其中。我将包括所有需要的课程。

用户控件将绑定到的列表的代码:

using System.Collections.Generic;

    namespace TestApplicationWPF
    {
      public class SettingList : List<ISetting>
      {
      }
    }

ISetting接口:

namespace TestApplicationWPF
{
  public class ISetting
  {
    string Name { get; set; }
  }
}

以及我写入列表的通用对象:

using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace TestApplicationWPF
{
  public class Setting<T> : ISetting, INotifyPropertyChanged
  {
    public string Name { get; set; }

    private T value;

    public T Value
    {
      get
      {
        return value;
      }
      set
      {
        this.value = value;
        OnPropertyChanged();
      }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    private void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
      PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
  }
}

现在用户控件的目标是绑定到 SettingList 的实例,并允许用户编辑列表中每个“设置”实例的“值”属性。为此,用户控件根据设置中的 T 类型显示特定控件。例如,字符串显示在文本框中,日期时间值将显示在 DatePicker 中。

用户控件的代码如下所示:

<UserControl x:Class="TestApplicationWPF.SettingEditor"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:TestApplicationWPF"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
  <UserControl.Resources>
    <local:TypeOfConverter x:Key="TypeOfConverter" />
    <Style x:Key="TypedValueStyle" TargetType="{x:Type ContentControl}">
      <Setter Property="Width" Value="200" />
      <Setter Property="ContentTemplate">
        <Setter.Value>
          <DataTemplate>
            <TextBox Text="{Binding Path=.}" IsReadOnly="true" Background="LightGray"/>
          </DataTemplate>
        </Setter.Value>
      </Setter>
      <Style.Triggers>
        <DataTrigger Binding="{Binding Converter={StaticResource TypeOfConverter},Path=Value}" 
                     Value="String">
          <Setter Property="ContentTemplate">
            <Setter.Value>
              <DataTemplate>
                <TextBox Text="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Content, Mode=TwoWay}"                  
                         HorizontalAlignment="Stretch"/>
              </DataTemplate>
            </Setter.Value>
          </Setter>
        </DataTrigger>
      </Style.Triggers>
    </Style>
  </UserControl.Resources>
  <Grid>
    <Grid.RowDefinitions>
      <RowDefinition Height="80*"/>
      <RowDefinition Height="20*"/>
      <RowDefinition Height="20*"/>
    </Grid.RowDefinitions>
    <ListView Name="LvPluginSettings" 
              ItemsSource="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:SettingEditor}}, Path=Settings, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
              Grid.Row="0">
      <ListView.View>
        <GridView>
          <GridViewColumn Header="Name" Width="100">
            <GridViewColumn.CellTemplate>
              <DataTemplate>
                <Label Content="{Binding Path=Name}"/>
              </DataTemplate>
            </GridViewColumn.CellTemplate>
          </GridViewColumn>
          <GridViewColumn Header="Value" 
                          Width="Auto">
            <GridViewColumn.CellTemplate>
              <DataTemplate>
                <ContentControl Content="{Binding Path=Value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Style="{StaticResource TypedValueStyle}" />
              </DataTemplate>
            </GridViewColumn.CellTemplate>
          </GridViewColumn>
        </GridView>
      </ListView.View>
    </ListView>
    <Button Name ="BtnSetValue" Grid.Row="1" Content="Set the value" Click="BtnSetValue_Click"/>
    <Button Name ="BtnGetValue" Grid.Row="2" Content="What's the value?" Click="BtnGetValue_Click"/>
  </Grid>
</UserControl>

用户控件的 .cs 代码:

using System.Windows;
using System.Windows.Controls;

namespace TestApplicationWPF
{
  public partial class SettingEditor : UserControl
  {

    public SettingList Settings
    {
      get { return (SettingList)GetValue(PluginSettingsProperty); }
      set { SetValue(PluginSettingsProperty, value); }
    }

    public static readonly DependencyProperty PluginSettingsProperty = DependencyProperty.Register(
                "Settings",
                typeof(SettingList),
                typeof(SettingEditor),
                new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault)
    );

    public SettingEditor()
    {
      InitializeComponent();
    }

    private void BtnSetValue_Click(object sender, RoutedEventArgs e)
    {
      ((Setting<string>)Settings[0]).Value = "Different value now.";
    }

    private void BtnGetValue_Click(object sender, RoutedEventArgs e)
    {
      MessageBox.Show(((Setting<string>)Settings[0]).Value);
    }
  }
}

为了确定列表中每个项目的“值”属性的类型,我使用了“TypeOfConverter”。转换器如下所示:

using System;
using System.Windows.Data;

namespace TestApplicationWPF
{
  public class TypeOfConverter : IValueConverter
  {
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
      return (value == null) ? null : value.GetType().Name;
    }
    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
      throw new NotImplementedException();
    }
  }
}

最后,为了能够完全重现问题,我为您提供了使用用户控件的 MainWindow,它是 ViewModel: 窗口:

<Window x:Class="TestApplicationWPF.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:TestApplicationWPF"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
  <Window.Resources>
    <local:MainWindowViewModel x:Key="MainWindowViewModel" />
  </Window.Resources>
    <Grid DataContext="{StaticResource MainWindowViewModel}">
    <local:SettingEditor Settings="{Binding Path=Settings}" />
  </Grid>
</Window>

视图模型:

namespace TestApplicationWPF
{
  public class MainWindowViewModel
  {
    public SettingList Settings { get; set; }

    public MainWindowViewModel()
    {
      Settings = new SettingList();
      Settings.Add(new Setting<string> { Name="Name of setting", Value = "HelloWorld" });
    }

  }
}

我的问题是设置实例的值属性的绑定。当我启动应用程序时,它会向我显示非常好的值。我得到一个带有“HelloWorld”的文本框。此外,当背景中的值更改时,它将更新到文本框。 但是,当我将光标设置到文本框中,将文本更改为其他内容并离开文本框时,它不会在绑定的“值”-属性中更改。此外,在我尝试编辑文本框中的文本后,在后台所做的更改不再影响文本框。

如果有人能帮助我解决这个问题,我将不胜感激。即使是对可能出错的最小提示也会对我有很大帮助。

您好, 斯文

【问题讨论】:

    标签: c# wpf generics data-binding user-controls


    【解决方案1】:

    我发现了问题。

    在用户控件中,我有一个这样的 ContentControl:

    <ContentControl Content="{Binding Path=Value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Style="{StaticResource TypedValueStyle}" />
    

    控件的 DataTemplate 如下所示:

    <DataTemplate>
          <TextBox Text="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Content, Mode=TwoWay}"                  
                   HorizontalAlignment="Stretch"/>
    </DataTemplate>
    

    问题在于数据绑定。 ContentControl 的内容绑定到我的对象中的值属性。然而,DataTemplate 绑定到 ContentControl 的 Content-property。为了使绑定起作用,我需要专门将 Data-Template 中的控件绑定设置为 Value-property。

    现在工作的 ContentControl 如下所示:

    <ContentControl DataContext="{Binding}" 
                                Content="{Binding Path=Value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" 
                                Style="{StaticResource TypedValueStyle}" />
    

    DataTemplate 看起来像这样:

    <DataTemplate>    
          <TextBox Text="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ContentControl}}, Path=DataContext.Value, Mode=TwoWay}"           
          HorizontalAlignment="Stretch"/>
    </DataTemplate>
    

    【讨论】:

      【解决方案2】:

      欢迎来到 SO !

      这是一种利用 ObservableCollectionDataTemplateSelector 和一些 C# subtelties 的方法:D

      注意事项:

      • 使用ObservableCollection&lt;T&gt; 通知订阅者其中的更改
      • 不用UserControl,而是用ItemsControl 代替
      • Setting 是有目的的抽象,强制使用 Setting&lt;T&gt;
      • 但是可观察的集合包含 Setting 对象
      • 项目展示器使用模板选择器根据第一个泛型类型参数返回正确的模板

      等等...玩一下代码,看看它可以有效地工作而不会很麻烦:D

      C#

      using System;
      using System.Collections.Generic;
      using System.Collections.ObjectModel;
      using System.ComponentModel;
      using System.Diagnostics;
      using System.Globalization;
      using System.Linq;
      using System.Runtime.CompilerServices;
      using System.Windows;
      using System.Windows.Controls;
      
      namespace WpfApplication4
      {
          public partial class MainWindow
          {
              public MainWindow()
              {
                  InitializeComponent();
      
                  var collection =
                      new SettingCollection(new Setting[]
                      {
                          new Setting<bool> {Name = "boolean"},
                          new Setting<string> {Name = "string"}
                      });
                  DataContext = collection;
              }
      
              private void Button1_Click(object sender, RoutedEventArgs e)
              {
                  var collection = (SettingCollection) DataContext;
                  var setting = collection.OfType<Setting<bool>>().FirstOrDefault();
                  if (setting != null)
                  {
                      setting.Value = !setting.Value;
                  }
              }
      
              private void Button2_Click(object sender, RoutedEventArgs e)
              {
                  var collection = (SettingCollection) DataContext;
                  var setting = collection.OfType<Setting<string>>().FirstOrDefault();
                  if (setting != null)
                  {
                      setting.Value = DateTime.Now.ToString(CultureInfo.InvariantCulture);
                  }
              }
      
              private void Button3_Click(object sender, RoutedEventArgs e)
              {
                  var collection = (SettingCollection) DataContext;
                  Debugger.Break();
              }
          }
      
          public sealed class SettingCollection : ObservableCollection<Setting>
          {
              public SettingCollection(List<Setting> list) : base(list)
              {
              }
      
              public SettingCollection(IEnumerable<Setting> collection) : base(collection)
              {
              }
      
              public SettingCollection()
              {
              }
          }
      
          public abstract class Setting : INotifyPropertyChanged
          {
              private object _value;
              public string Name { get; set; }
      
              public object Value
              {
                  get { return _value; }
                  set
                  {
                      if (Equals(value, _value)) return;
                      _value = value;
                      OnPropertyChanged();
                  }
              }
      
              public event PropertyChangedEventHandler PropertyChanged;
      
              protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
              {
                  PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
              }
          }
      
          public sealed class Setting<T> : Setting
          {
              private T _value;
      
              public new T Value
              {
                  get { return _value; }
                  set
                  {
                      if (Equals(value, _value)) return;
                      _value = value;
                      OnPropertyChanged();
                  }
              }
          }
      
          public class SettingTemplateSelector : DataTemplateSelector
          {
              public override DataTemplate SelectTemplate(object item, DependencyObject container)
              {
                  var element = container as FrameworkElement;
                  if (element != null && item != null)
                  {
                      var type = item.GetType();
                      var types = type.GenericTypeArguments;
                      var type1 = types[0];
      
                      if (type1 == typeof(bool))
                      {
                          var findResource = element.FindResource("SettingBoolTemplate");
                          var dataTemplate = findResource as DataTemplate;
                          return dataTemplate;
                      }
      
                      if (type1 == typeof(string))
                      {
                          var findResource = element.FindResource("SettingStringTemplate");
                          var dataTemplate = findResource as DataTemplate;
                          return dataTemplate;
                      }
                  }
                  return null;
              }
          }
      }
      

      XAML:

      <Window x:Class="WpfApplication4.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:WpfApplication4"
              mc:Ignorable="d"
              Title="MainWindow" Height="350" Width="525">
          <Grid>
              <StackPanel Margin="20">
                  <Button Content="Set first Setting&lt;bool&gt; to something" Click="Button1_Click" />
                  <Button Content="Set first Setting&lt;string&gt; to something" Click="Button2_Click" />
                  <Button Content="Debugger break" Click="Button3_Click"></Button>
                  <ItemsControl ItemsSource="{Binding}">
                      <ItemsControl.Resources>
                          <DataTemplate x:Key="SettingBoolTemplate" DataType="local:Setting">
                              <CheckBox IsChecked="{Binding Value}" />
                          </DataTemplate>
                          <DataTemplate x:Key="SettingStringTemplate" DataType="local:Setting">
                              <TextBox Text="{Binding Value, UpdateSourceTrigger=PropertyChanged}" />
                          </DataTemplate>
                          <local:SettingTemplateSelector x:Key="SettingTemplateSelector" />
                      </ItemsControl.Resources>
                      <ItemsControl.ItemTemplate>
                          <DataTemplate DataType="local:Setting">
                              <Border Margin="5" BorderBrush="DodgerBlue" Padding="2" BorderThickness="1">
                                  <StackPanel>
                                      <TextBlock Text="{Binding Name}" />
                                      <ContentControl Content="{Binding}"
                                                      ContentTemplateSelector="{StaticResource SettingTemplateSelector}" />
                                  </StackPanel>
                              </Border>
                          </DataTemplate>
                      </ItemsControl.ItemTemplate>
                  </ItemsControl>
              </StackPanel>
      
          </Grid>
      </Window>
      

      【讨论】:

      • 非常感谢您的回复。我在我的代码中发现了问题并自己发布了一个答案。不过我还是会试试你的代码,因为它看起来比我的好一点。
      猜你喜欢
      • 2012-02-14
      • 2012-12-20
      • 2012-02-13
      • 2014-04-10
      • 2011-10-14
      • 2014-03-12
      • 1970-01-01
      相关资源
      最近更新 更多