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:
With several items selected (Element Type, Material and Beta Angle properties the same, others different):
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?
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:
ViewModel
that represents each object. Let's call this ObjectViewModel
.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.