首页 文章

WPF绑定到集合中多个对象的相同属性

提问于
浏览
2

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

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

  • 如果选择单个对象,则显示该对象的属性

  • 如果选择了多个对象,则会根据以下逻辑显示属性:

  • 如果所有选定对象在该属性中具有相同的值,则显示该值

  • 如果所选对象在该属性中具有不同的值,则显示'[Multi]'(或类似)

  • 输入值时,所有选定对象都将bound属性设置为该值 .

举例来说,这是我的一个旧的WinForms形式,它做同样的事情,我或多或少尝试在WPF中重新创建 . 在那种情况下,我在没有数据绑定的代码隐藏中处理它,这是一种我并不特别渴望重复的经验 .

选择一个项目:

enter image description here

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

enter image description here

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

  • 几乎我的应用程序的整个UI都需要以这种方式工作,因此更容易重复更好

  • 所选项目的数量范围可以从1-100000(尽管通常会在几十个左右) - 如果不会变得无法使用,那么巨大选择的一些轻微延迟可能是正常的

  • 我将会有几种不同类型的数据实际上需要一个通用的Property Grid解决方案

  • 我绑定的数据类型是在一个单独的,可公开使用的库中定义的,我(部分)编写但是其他几个人和项目使用 . 所以,如果我必须这样做,我可以修改这些类型,但我不想对它们做太过激烈的事情 .

我目前关于如何执行此操作的最佳猜测是使用MultiBinding(或其自定义子类),跟踪基础集合中的更改,并以编程方式将对每个对象的属性的绑定添加或移除到MultiBinding Bindings集合,然后编写一个IMultiValueConverter来确定显示值 . 然而,这似乎是一个小提琴,不是真正的MultiBindings设计和互联网意见似乎不喜欢使用MultiBindings除了绝对必要的地方(虽然我不完全确定为什么) . 这样做有更好/更直接/标准的方式吗?

4 回答

  • 2

    在我看来,对象封装在这里真的会帮助你,而不是试图让MultiBinding做一些它没有真正能够处理的东西 .

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

    • 您有一个代表每个对象的 ViewModel . 我们称之为 ObjectViewModel .

    • 您有一个代表页面状态的顶级 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<ObjectViewModel>INotifyCollectionChanged ,将其转换为可以直接绑定的功能齐全的列表 .

  • 1

    这个功能不是WPF开箱即用的,但有一些选项如何实现:

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

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

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

    像这样的东西应该工作(在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 绑定到必须显示/编辑其值的任何内容,您就完成了 .

  • 2

    我不认为您可以按照您希望它们开箱即用的方式进行绑定 . 但是,您可以通过在类型的包装类中处理它来使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));
        }
    }
    

相关问题