Paul Jeffries
Paul Jeffries

Reputation: 95

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:

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:

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?

Upvotes: 3

Views: 5837

Answers (4)

Doug
Doug

Reputation: 5338

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.

Upvotes: 3

user3230660
user3230660

Reputation:

I don't think you can make bindings work the way you want them to out of the box. But you can make the PropertyChanged event work in your favor by handling it in a wrapper class of items of your type. In the code below The MultiEditable class handles the PropertyChanged event for the EditItem property. If you have a form where users are editing properties of, say, a beam you will want to bind input controls on the form to properties of EditItem. You will need to override _EditItem_PropertyChanged as shown and from there you can update properties of selected items as properties of EditItem are changed. Don't forget to un-handle events.

Edit: I forgot to add code for checking if all properties are the same as some value. That is easy enough to do - just check the collection and compare the property in question for all items against the same property of EditItem. If they are all the same return true, otherwise "Multi" or whatever you need. Also you can reference MultiEditable in your code - just update EditItem properties and selected items and visuals will all be updated.

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

Upvotes: 1

Liero
Liero

Reputation: 27328

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

Upvotes: 2

Sinatr
Sinatr

Reputation: 21969

Something like this should work (in 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;
    }
}

Then just bind SomeProperty to whatever have to display/edit its value and you are done.

Upvotes: 1

Related Questions