【问题标题】:WPF binding to the same property of multiple objects in a collectionWPF 绑定到集合中多个对象的相同属性
【发布时间】:2016-02-28 11:20:27
【问题描述】:

我正在尝试使用 WPF 创建一个界面,该界面可以一次显示和修改多个选定对象的属性。我知道这一定是可能的(Visual Studio 中的属性网格可以做到),但我无法找到任何有关如何实现它的信息或示例。我找到了很多关于 MultiBinding 的信息,但它的规范用例似乎是将一个 UI 字段绑定到同一个对象上的多个属性,而我试图做相反的事情 - 将一个 UI 字段绑定到同一个属性在多个对象上。

更明确地说,我想要创建的行为是这样的:

  • 如果选择了单个对象,则该对象的属性为 显示
  • 如果选择了多个对象,属性将根据以下逻辑显示:
    • 如果所有选定对象在该属性中具有相同的值,则显示该值
    • 如果所选对象在该属性中具有不同的值,则显示“[Multi]”(或类似)
  • 输入值后,所有选定对象的绑定属性都设置为该值。

例如,这是我的一个旧的 WinForms 形式,它做同样的事情,我或多或少地尝试在 WPF 中重新创建它。在那种情况下,我在没有数据绑定的情况下在代码隐藏中处理它,我并不特别热衷于重复这种体验。

选择一项:

选择了几个项目(元素类型、材料和 Beta 角度属性相同,其他不同):

针对我的特定用例的一些其他注意事项:

  • 我的应用程序的整个 UI 几乎都需要以这种方式工作, 所以越容易重复越好
  • 所选项目的数量可能在 1 到 100000 之间(但通常会在几十个左右 - 如果不会变得无法使用,那么在大量选择时稍微滞后可能是可以的)
  • 我希望将几种不同类型的数据设为可编辑,每种数据都有自己的定制界面(即,我实际上并不想要通用的属性网格解决方案)
  • 我绑定到的数据类型是在一个单独的、公开可用的库中定义的,我(部分)编写了该库,但其他几个人和项目都在使用该库。因此,如果绝对需要,我可以修改这些类型,但我不想对它们做任何过于激烈的事情。

我目前对如何做到这一点的最佳猜测是使用 MultiBinding(或它的自定义子类),跟踪基础集合中的更改,并以编程方式将绑定到每个对象上的属性添加或删除到MultiBinding Bindings 集合,然后编写一个 IMultiValueConverter 来确定显示值。但是,这似乎有点麻烦,实际上并不是 MultiBindings 的设计目的,而且互联网舆论似乎不赞成使用 MultiBindings,除非在绝对必要的情况下(尽管我不完全确定为什么)。有没有更好/更直接/标准的方法来做到这一点?

【问题讨论】:

  • 对我来说听起来很简单。具有专用属性来编辑 ViewModel 中的值并处理多选。然后在每个选定项目的设置器中设置值。 When selection changed you may want to trigger notification of that property, so that getter may provide value to be displayed (not sure which one you want, it can be e.g. average value of all items selected).是的,如果您还没有,请查看 MVVM。

标签: c# .net wpf xaml multibinding


【解决方案1】:

在我看来,对象封装在这里真的会帮助你,而不是试图让 MultiBinding 做一些它并没有真正具备处理能力的事情。

所以,在没有看到你的代码的情况下,我会做出几个假设:

  1. 您有一个代表每个对象的ViewModel。我们称之为ObjectViewModel
  2. 您有一个代表页面状态的顶级ViewModel。我们称之为PageViewModel

ObjectViewModel 可能具有以下属性:

string Name { get; set; }
string ElementType { get; set; }
string SelectionProfile { get; set; }
string Material { get; set; }
... etc

PageViewModel 可能有以下内容:

// Represents a list of selected items
ObjectSelectionViewModel SelectedItems { get; }

请注意新类ObjectSelectionViewModel,它不仅代表您选择的项目,而且允许您绑定到它,就好像它是一个单独的对象一样。它可能看起来像这样:

public class ObjectSelectionViewModel : ObjectViewModel
{
    // The current list of selected items.
    public ObservableCollection<ObjectViewModel> SelectedItems { get; }

    public ObjectSelectionViewModel()
    {
        SelectedItems = new ObservableCollection<ObjectViewModel>();
        SelectedItems.CollectionChanged += (o, e) =>
        {
             // Pseudo-code here
             if (items were added)
             {
                  // Subscribe each to PropertyChanged, using Item_PropertyChanged
             }
             if (items were removed)
             {
                 // Unsubscribe each from PropertyChanged
             }                   
        };
    }

    void Item_PropertyChanged(object sender, NotifyPropertyChangedArgs e)
    {
         // Notify that the local, group property (may have) changed.
         NotifyPropertyChanged(e.PropertyName);
    }

    public override string Name
    {
        get 
        {
            if (SelectedItems.Count == 0)
            {
                 return "[None]";
            }
            if (SelectedItems.IsSameValue(i => i.Name))
            {
                 return SelectedItems[0].Name;
            }
            return string.Empty;
        }
        set
        {
            if (SelectedItems.Count == 1)
            {
                SelectedItems[0].Name = value;
            }
            // NotifyPropertyChanged for the traditional MVVM ViewModel pattern.
            NotifyPropertyChanged("Name");
        }           
    }

    public override string SelectionProfile
    {
        get 
        {
            if (SelectedItems.Count == 0)
            {
                 return "[None]";
            }
            if (SelectedItems.IsSameValue(i => i.SelectionProfile)) 
            {
                return SelectedItems[0].SelectionProfile;
            }
            return "[Multi]";
        }
        set
        {
            foreach (var item in SelectedItems)
            {
                item.SelectionProfile = value;
            }
            // NotifyPropertyChanged for the traditional MVVM ViewModel pattern.
            NotifyPropertyChanged("SelectionProfile");
        }           
    }

    ... etc ...
}

// Extension method for IEnumerable
public static bool IsSameValue<T, U>(this IEnumerable<T> list, Func<T, U> selector) 
{
    return list.Select(selector).Distinct().Count() == 1;
}

您甚至可以在该类上实现IList&lt;ObjectViewModel&gt;INotifyCollectionChanged,将其转换为您可以直接绑定的全功能列表。

【讨论】:

  • 这是一个很好的答案,但它稍微曲解了这个问题 - 如果所选对象的值不同,我只想显示“[Multi]”,因此只需检查 SelectedItems 的计数还不够好,我需要遍历集合,直到找到不同的值,并且我需要在每个 getter 中都这样做。如问题中所述,我还有很多不同类型的 ObjectViewModel,我需要编写一个 ObjectSelectionViewModel 来包装每个属性的属性,与 MultiBinding 选项相比,这似乎是一个缺点。
  • 此外,这不会检测在 UI 之外进行的选定对象的属性更改(这可能会发生) - 我需要在每个选定对象上订阅该事件并将其向上冒泡以捕获那些变化,MultiBinding 可以有效地为我做。
  • 根据您的 cmets 进行编辑。 1. 添加 IsSameValue() 来处理唯一性 2. 不同类型的 ObjectViewModel - 当您选择不同类型的对象时,您的 UI 会是什么样子? ViewModel 是模型到视图,因此如果您的视图发生变化,您可能需要 (a) 将您的属性组合到一个地方(如上所示)或 (b) 为每种类型使用不同的集合对象对象视图模型。 3. 为添加到 SelectedItems 列表中的每个项目添加属性更改通知(伪代码)。请注意,我没有时间编译/测试。这都是理论:)
  • 对于不同的对象类型,@Liero 的#3 答案似乎值得研究。
【解决方案2】:

这个功能在 WPF 中不是开箱即用的,但是有一些选项可以实现这个:

  1. 使用一些第 3 方控件,支持一次编辑多个对象,例如PropertyGrid from Extended WPF Toolkit

  2. 创建与您的对象具有相同属性但包装对象集合的包装对象。然后绑定到这个包装类。

    public class YourClassMultiEditWrapper{
        private ICollection<YourClass> _objectsToEdit;
    
        public YourClassMultiEditWrapper(ICollection<YourClass> objectsToEdit)
            _objectsToEdit = objectsToEdit;
    
        public string SomeProperty {
           get { return _objectsToEdit[0].SomeProperty ; } 
           set { foreach(var item in _objectsToEdit) item.SomeProperty = value; }
        }
    }
    
    public class YourClass {
       public property SomeProperty {get; set;}
    }
    

    优点是做起来很简单。缺点是您需要为要编辑的每个类创建包装器。

3.您可以使用自定义TypeDescriptor来创建通用包装类。在您的自定义 TypeDescriptor 覆盖 GetProperties() 方法中,它将返回与您的对象相同的属性。您还需要使用重写的GetValueSetValue 方法创建自定义PropertyDescriptor,以便它与您要编辑的对象集合一起使用

    public class MultiEditWrapper<TItem> : CustomTypeDescriptor {
      private ICollection<TItem> _objectsToEdit;
      private MultiEditPropertyDescriptor[] _propertyDescriptors;

      public MultiEditWrapper(ICollection<TItem> objectsToEdit) {
        _objectsToEdit = objectsToEdit;
        _propertyDescriptors = TypeDescriptor.GetProperties(typeof(TItem))
          .Select(p => new MultiEditPropertyDescriptor(objectsToEdit, p))
          .ToArray();  
      }

      public override PropertyDescriptorCollection GetProperties()
      {
        return new PropertyDescriptorCollection(_propertyDescriptors);
      }
    }

【讨论】:

    【解决方案3】:

    这样的事情应该可以工作(在 ViewModel 中):

    ObservableCollection<Item> _selectedItems;
    // used to handle multi selection, the easiest is to set it from View in SelectionChanged event
    public ObservableCollection<Item> SelectedItems
    {
        get { return _selectedItems; }
        set
        {
            _selectedItems = value;
            OnPropertyChanged();
            // this will trigger View updating value from getter
            OnPropertyChanged(nameof(SomeProperty));
        }
    }
    
    // this will be one of your properties to edit, you'll have to do this for each property you want to edit
    public double SomeProperty
    {
        get { return SelectedItems.Average(); } // as example
        set
        {
            foreach(var item in SelectedItems)
                item.SomeProperty = value;
        }
    }
    

    然后只需将SomeProperty 绑定到必须显示/编辑其值的任何内容,您就完成了。

    【讨论】:

      【解决方案4】:

      我不认为您可以让绑定按照您希望它们开箱即用的方式工作。但是您可以通过在您类型的项目的包装类中处理 PropertyChanged 事件来使它对您有利。在下面的代码中 MultiEditable 类处理 EditItem 属性的 PropertyChanged 事件。如果您有一个表单,其中用户正在编辑梁的属性,那么您将希望将表单上的输入控件绑定到 EditItem 的属性。您将需要覆盖 _EditItem_PropertyChanged,如图所示,并且当 EditItem 的属性发生更改时,您可以从那里更新所选项目的属性。不要忘记取消处理事件。

      编辑:我忘记添加代码来检查所有属性是否与某个值相同。这很容易做到 - 只需检查集合并将所有项目的相关属性与 EditItem 的相同属性进行比较。如果它们都相同,则返回 true,否则返回“Multi”或您需要的任何内容。您还可以在代码中引用 MultiEditable - 只需更新 EditItem 属性,所选项目和视觉效果都会全部更新。

      public interface ISelectable
      {
          bool IsSelected { get; set; }
      }
      
      public abstract class MultiEditable<T> : ObservableCollection<T> where T:class,ISelectable,INotifyPropertyChanged
      {
          private T _EditItem;
          public T EditItem 
          {
              get { return _EditItem; }
              set 
              { 
                  if(_EditItem != value)
                  {
                      _EditItem = value;
                      _EditItem.PropertyChanged += _EditItem_PropertyChanged;
                  }
              }
          }
      
          public bool AreMultipleItemsSelected
          {
              get { return this.Count(x => x.IsSelected) > 1; }
          }
      
          public virtual void _EditItem_PropertyChanged(object sender, PropertyChangedEventArgs e)
          {
      
          }
      }
      
      public class MultiEditableBeams : MultiEditable<Beam> 
      {
          public override void _EditItem_PropertyChanged(object sender, PropertyChangedEventArgs e)
          {
              base._EditItem_PropertyChanged(sender, e);
      
              foreach (Beam beam in this.Where(x => x.IsSelected))
              {
                  if (e.PropertyName == "Material")
                      beam.Material = EditItem.Material;
                  else if (e.PropertyName == "Length")
                      beam.Length = EditItem.Length;
      
              }
          }
      }
      
      public class Beam : ISelectable, INotifyPropertyChanged
      {
          private bool _IsSelected;
          public bool IsSelected 
          {
              get { return _IsSelected; }
              set
              {
                  if (_IsSelected != value)
                  {
                      _IsSelected = value;
                      RaisePropertyChanged();
                  }
              }
          }
      
          private string _Material;
          public string Material
          {
              get { return _Material; }
              set
              {
                  if (_Material != value)
                  {
                      Material = value;
                      RaisePropertyChanged();
                  }
              }
          }
      
          private int _Length;
          public int Length
          {
              get { return _Length; }
              set
              {
                  if (_Length != value)
                  {
                      _Length = value;
                      RaisePropertyChanged();
                  }
              }
          }
      
      
          public event PropertyChangedEventHandler PropertyChanged;
      
          private void RaisePropertyChanged([CallerMemberName] string propertyName = "")
          {
              if (PropertyChanged != null)
                  PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
          }
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2011-09-02
        • 1970-01-01
        • 2020-04-01
        • 2012-07-03
        • 2010-12-27
        • 1970-01-01
        • 2014-10-05
        相关资源
        最近更新 更多