【问题标题】:Binding ListView to an ObservableCollection and two visual controls将 ListView 绑定到 ObservableCollection 和两个可视控件
【发布时间】:2014-12-29 08:02:34
【问题描述】:

我想将 ListView 绑定到字符串的 ObservableCollection,我想使用来自两个可视控件的数据(例如,这些字符串的前缀和后缀)进行修改。

一个简化的例子:

XAML:

<TextBox Name="tbPrefix"/>
<TextBox Name="tbPostfix"/>

<ListView Name="lvTarget"/>

C#:

public ObservableCollection<string> sources = GetFromSomewhere();

public IEnumerable<string> Items()
{
    foreach (var source in sources) 
    {
        yield return tbPrefix.Text + source + tbPostfix.Text;
    }
}

为了让 ListView 保持更新,我目前只是在 CollectionChanged 事件上重置其 ItemsSource:

void sources_CollectionChanged(...)
{
    lvTarget.ItemsSource = Items();
}

但我还希望 ListView 绑定到其三个来源中的任何中的更改:集合和前缀/后缀控件。我想我想要一个 MultiBinding 或 MultiDataTrigger,但我不能完全理解语法和我能找到的所有示例将控件绑定到其他控件,同时我也有那个 ObservableCollection 作为来源。

附:对不起,如果它简单明了,这只是我使用 WPF 的第三天,我有点不知所措!谢谢!

【问题讨论】:

    标签: c# wpf xaml listview data-binding


    【解决方案1】:

    observable 集合用于在集合中的项目发生更改时通知,例如添加、删除、移动等……如果您更改字符串中的文本,它不会通知。 WPF 中第一个使用 ViewModel 绑定属性而不是隐藏代码的最佳实践规则。您可以通过以下方式解决此问题:
    1- 创建一个名为 SomethingViewModel 的新类
    2- 添加所有需要绑定到视图的属性:

    public class SomethingViewModel : INotifyPropertyChanged
    {
        private string _prefix;
        private string _postfix;
    
        public SomethingViewModel()
        {
            Sources = new ObservableCollection<string>(/*pass initial data of the list*/);
            Sources.CollectionChanged += (sender, args) => OnPropertyChanged("Items");
        }
    
        public event PropertyChangedEventHandler PropertyChanged;
    
        protected virtual void OnPropertyChanged(string propertyName = null)
        {
            var handler = PropertyChanged;
            if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
        }
    
        private ObservableCollection<string> Sources { get; set; }
    
        public IList<string> Items
        {
            get { return Sources.Select(x => string.Format("{0}{1}{2}", Prefix, x, Postfix)).ToList(); }
        }
    
        public string Prefix
        {
            get
            {
                return _prefix;
            }
            set
            {
                if (_prefix == value) return;
                _prefix = value;
                OnPropertyChanged("Prefix");
                OnPropertyChanged("Items");
            }
        }
    
        public string Postfix
        {
            get
            {
                return _postfix;
            }
            set
            {
                if (_postfix == value) return;
                _postfix = value;
                OnPropertyChanged("Postfix");
                OnPropertyChanged("Items"); // we will notify that the items list has changed so the view refresh its items
            }
        }
    }
    

    3- 在视图的构造函数中放入以下代码来初始化视图的数据上下文:

    public MainWindow()
    {
        this.DataContext= new SomethingViewModel();
    }
    

    4-最后将视图元素绑定到视图模型属性:

    <TextBox Text={Binding Prefix,Mode=TwoWay}/>
    <TextBox  Text={Binding Postfix,Mode=TwoWay}/>
    
    <ListView ItemsSource={Binding Items}/>
    

    5-如果要更改源中的项目,请不要初始化新对象,只需使用方法:

    Sources.Clear();
    Sources.Add();
    

    【讨论】:

    • 我最终按照您展示的方式实现了 ViewModel。唯一让我烦恼的是许多几乎相同的样板代码,它们围绕着定义触发 OnPropertyChanged 的​​ Prefix 和 Postfix 等属性(我实际上有 3 个)。我想我可以为他们制作一个通用类?
    • @isagalaev OnPropertyChanged 需要将您的视图模型中的修改反映到视图中......但是您可以做一个实现 INotifyPropertyChanged 的​​基类,然后让您的所有视图模型都继承自它。这样可以减少编写与类中更改的属性(事件和方法)相关的代码。
    【解决方案2】:

    免责声明:请注意,如果您只需要修改项目的视觉外观,则使用 DataTemplate(如 Clemens 的回答)将是更好的解决方案。如果您想实际将组合字符串作为项目,请使用 ViewModel 方式。以下解决方案不是最佳实践,而是试图演示 MultiBindings 的工作原理。


    这个问题最好在你的 ViewModel 中解决。转换器(尤其是 MultiConverters)只应在绝对必要时使用。

    但由于这是您使用 WPF 的第三天,您不应该为 MVVM 烦恼。

    1. 让你的 Window 类成为它自己的 DataContext:

      public MainWindow()
      {
          InitializeComponent();
          this.DataContext = this;
      }
      

      这将让我们使用从 Window 到定义在其上的属性的数据绑定。

    2. 我们可以为我们的项目使用默认属性,但是当属性值发生变化时,WPF 不会注意到。我们现在将使用DependencyProperty

      public static readonly DependencyProperty ItemsProperty
          = DependencyProperty.Register("Items", typeof (IEnumerable<string>), typeof (MainWindow));
      
      public IEnumerable<string> Items
      {
          get { return (IEnumerable<string>) GetValue(ItemsProperty); }
          set { SetValue(ItemsProperty, value); }
      }
      
      public MainWindow()
      {
          InitializeComponent();
          this.DataContext = this;
      
          this.Items = new[] {"sdf", "fdsa", "tgrg"};
      }
      

      每当我们调用此属性的设置器时,WPF 都会注意到它并更新与此属性的所有绑定。我们还更新了构造函数以加载一些初始值。

      我们也可以实现INotifyPropertyChanged - 事实上,ViewModel 使用这种模式 - 和/或使用 ObservableCollection。不幸的是,ObservableCollection 更改不会重新触发 MultiBinding,因此我们只使用 IEnumerable&lt;string&gt; 作为类型。

    3. 现在让我们在 XAML 中添加绑定:

      <StackPanel>
          <TextBox Name="prefixTextBox" />
          <TextBox Name="postfixTextBox" />
          <ListBox>
              <ListBox.ItemsSource>
                  <MultiBinding>
                      <MultiBinding.Converter>
                          <wpfApplication1:PrefixPostfixConverter />
                      </MultiBinding.Converter>
                      <Binding Path="Items" />
                      <Binding ElementName="prefixTextBox" Path="Text" />
                      <Binding ElementName="postfixTextBox" Path="Text" />
                  </MultiBinding>
              </ListBox.ItemsSource>
          </ListBox>
      </StackPanel>
      

      好的,我们使用 MultiBinding 设置 ItemsSource。这基本上是在做您之前在更改处理程序中所做的事情:每当其子绑定之一发生更改时,它都会调用指定的转换器并使用其结果更新 ItemsSource。但这是什么PrefixPostfixConverter

    4. 添加 PrefixPostfixConverter 类:

      public class PrefixPostfixConverter : IMultiValueConverter 
      {
          public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
          {
              if (values == null || values.Length != 3)
                  throw new ArgumentException("values");
      
              var items = values[0] as IEnumerable;
              var prefix = values[1] as string;
              var postfix = values[2] as string;
      
              if (items == null || prefix == null || postfix == null)
                  return null;
      
              return items.Cast<object>()
                          .Select(i => prefix + i + postfix)
                          .ToArray();
          }
      
          public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
          {
              throw new NotSupportedException();
          }
      }
      

      这会从您的三个绑定中获取输入(作为 values 参数传入)并将组合值创建为数组。

    【讨论】:

    • 谢谢!我是否正确地将 DependencyProperty 视为一种模拟触发单个属性的 PropertyChanged 事件的快捷方式,就像我在自定义 ViewModel 代码中所做的那样?
    • MainWindow 是一个“视图”。因此,它的基类层次结构中有 DependencyObject,并且可以使用依赖属性。无需实现 INotifyPropertyChanged。尽管 ViewModel 通常不是从 DependencyObject 派生的。 ViewModel 尝试忽略“查看”代码。许多 ViewModel 可以在不了解任何 System.Windows 命名空间的情况下存在,这使得它们易于测试。他们实现 INotifyPropertyChange 以通知绑定系统的更改。
    【解决方案3】:

    您不需要任何代码。只需为 ListView 项创建适当的 DataTemplate:

    <ListView ItemsSource="{Binding Items}">
        <ListView.ItemTemplate>
            <DataTemplate>
                <StackPanel Orientation="Horizontal">
                    <TextBlock Text="{Binding Text, ElementName=tbPrefix}"/>
                    <TextBlock Text="{Binding}"/>
                    <TextBlock Text="{Binding Text, ElementName=tbPostfix}"/>
                </StackPanel>
            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>
    

    还要注意Items 在这里是公共属性,而不是方法,您应该在视图模型类中声明它。此视图模型类的实例将分配给您的视图的DataContext(例如 MainWindow)。

    【讨论】:

    • 看起来我用我过于简化的例子误导了你,对不起。实际的任务不是简单地连接字符串,它需要一个图片文件名列表,读取它们的图片拍摄时间,并基于字符串模板和计数器构造一个新的文件名。所以我真的需要在幕后进行一些实际的计算。
    • 这一切都应该在您的视图模型中完成。您将拥有一个带有返回构造文件名的属性的项目数据类,而不是字符串项目。
    猜你喜欢
    • 2012-05-26
    • 2018-05-21
    • 2017-09-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-08-21
    相关资源
    最近更新 更多