WPF绑定到集合

时间:2015-11-25 15:30:18

标签: c# .net wpf xaml multibinding

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

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

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

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

选择了一个项目:

enter image description here

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

enter image description here

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

  • 我的应用程序的整个UI都需要以这种方式工作, 所以更容易重复的更好
  • 所选项目的数量范围可以从1-100000(尽管更常见的是大约几十个数量级 - 如果它不会变得无法使用,那么巨大选择的一些轻微延迟可能是正常的)
  • 我想要编辑几种不同类型的数据,每种数据都有自己的定制界面(即我实际上并不需要通用的Property Grid解决方案)。
  • 我绑定的数据类型是在我(部分)编写的单独且公开可用的库中定义的,但是其他几个人和项目都在使用。所以,我可以修改这些类型,如果我必须这样做,但我宁愿不做任何过于激烈的事情。

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

4 个答案:

答案 0 :(得分:2)

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

所以,如果没有看到你的代码,我会做出几个假设:

  1. 您有ViewModel代表每个对象。我们称之为ObjectViewModel
  2. 您有一个代表您网页状态的顶级ViewModel。我们称之为PageViewModel
  3. 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 :(得分:2)

此功能不适用于WPF,但有一些选项可以实现此目的:

  1. 使用一些第三方控件,支持一次编辑多个对象,例如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.     3.您可以使用自定义TypeDescriptor来创建通用包装类。在自定义TypeDescriptor重写GetProperties()方法中,它将返回与对象相同的属性。您还需要使用已覆盖的PropertyDescriptorGetValue方法创建自定义SetValue,以便与您的对象集合进行编辑

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

答案 2 :(得分: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绑定到必须显示/编辑其值的任何内容,您就完成了。

答案 3 :(得分:1)

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

编辑:我忘了添加代码来检查所有属性是否与某个值相同。这很容易做 - 只需检查集合并比较所有项目的属性与EditItem的相同属性。如果它们都是相同的返回true,否则&#34; Multi&#34;或者你需要什么。您还可以在代码中引用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));
    }
}