Search code examples
c#.netwpfxamlmultibinding

WPF binding to the same property of multiple objects in a collection


I'm trying to create an interface using WPF that can display and modify the properties of multiple selected objects at once. I know this must be possible (the property grid in Visual Studio does it) but I haven't been able to find any information or examples on how to achieve it. I have found a lot of information on MultiBinding, but the canonical use case of that appears to be to bind one UI field to multiple properties on the same object while I'm trying to do the opposite - binding a UI field to the same property on multiple objects.

To be more explicit, the behaviour I want to create is this:

  • If a single object is selected, the properties of that object are displayed
  • If more than one object is selected, properties are displayed according to the following logic:
    • If all selected objects have the same value in that property, display the value
    • If the selected objects have different values in that property, display '[Multi]' (or similar)
  • When a value is entered, all selected object have the bound property set to that value.

By way of example, here is an old WinForms form of mine that does the same thing and which I am more-or-less trying to recreate in WPF. In that case I dealt with it in the code-behind without data binding, an experience I'm not particularly keen to repeat.

With one item selected:

enter image description here

With several items selected (Element Type, Material and Beta Angle properties the same, others different):

enter image description here

Some other considerations for my particular use-case:

  • Pretty much the whole UI of my application needs to work in this way, so the more easily repeatable the better
  • The number of selected items could range from 1-100000 (though will more typically be around the order of a few dozen - some slight lag on huge selections is probably OK provided it doesn't become unusable)
  • There will be several different types of data I'll want to make editable, each with its own bespoke interface (i.e. I don't actually want a generic Property Grid solution)
  • The datatypes I'm binding to are defined in a separate and publically-available library that I (partly) write but that several other people and projects use. So, I can modify those types if I absolutely have to but I'd rather not do anything too drastic to them.

My current best-guess for how to do this would be to use a MultiBinding (or a custom sub-class of it), track changes in the underlying collection and programmatically add or remove bindings to the properties on each object to the MultiBinding Bindings collection, then write an IMultiValueConverter to determine the display value. However, that seems like a bit of a fiddle, not really what MultiBindings were designed for and internet opinion appears to disfavour using MultiBindings except for where absolutely necessary (although I'm not entirely sure why). Is there a better/more straightforward/standard way of doing this?


Solution

  • It seems to me that object encapsulation would really help you here, rather than trying to make MultiBinding do something it's not really equipped to handle.

    So, without seeing your code, I'll make a couple of assumptions:

    1. You have a ViewModel that represents each object. Let's call this ObjectViewModel.
    2. You have a top-level ViewModel that represents the state of your page. Let's call this PageViewModel.

    ObjectViewModel might have the following properties:

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

    and PageViewModel might have the following:

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

    Notice the new class ObjectSelectionViewModel, which would not only represent your selected items, but allow you to bind to it as if it were a single object. It might look something like this:

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

    You could even implement IList<ObjectViewModel> and INotifyCollectionChanged on this class to turn it into a full-featured list that you could bind to directly.