Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 451
Paul Jeffries Avatar asked Nov 22 '25 08:11

Paul Jeffries


2 Answers

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.

like image 88
Doug Avatar answered Nov 24 '25 00:11

Doug


This feature is not in WPF out of the box, however there are some options how to achieve this:

  1. Use some 3rd party control, that support edit of multiple objects at once, e.g. PropertyGrid from Extended WPF Toolkit

  2. Create wrapper object that has the same properties as your objects but is wraps collection of objects. Then bind to this wrapper class.

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

    Advantage is that it is quite simple to do. Disadvantage is that you need to create wrapper for each class you want to edit.

3. You can use custom TypeDescriptor to create generic wrapper class. In your custom TypeDescriptor override GetProperties() method so it will return the same properties as your objects. You will also need to create custom PropertyDescriptor with overridden GetValue and SetValue methods so it works with your collection of objects to edit

    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);
      }
    }
like image 33
Liero Avatar answered Nov 24 '25 00:11

Liero



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!