Update Controls two-way binding of collections

Developer
Dec 29, 2011 at 11:41 PM
Edited Dec 29, 2011 at 11:47 PM

Well, as you know, in the past I've had to disable list virtualization in order to synchronize the set of selected items correctly. However, this makes the app run slower, and now I'm using a 3rd party list control that I believe would be even slower without virtualization.

In order to synchronize the selection state with the ViewModel without binding to IsSelected directly, it is necessary to synchronize with the control's SelectedItems property. It is not possible to bind to SelectedItems directly, so one must create a derived class of ListBox/ListView or use an attached behavior. I asked the Infragistics people how to synchronize their control's selection state with MVVM and one of them came up with a derived-class solution that defines an new SelectedItems property (e.g. SelectedItems2) that supports data binding.

While I am not satisfied with the performance of that solution, it's probably fast enough when only one or two items are selected (the usual case). So I created a property in my "Object List ViewModel":

public IEnumerable<ViewModel> SelectedItems
{
	get { return _filteredList.Value.Where(vm => vm.IsSelected); }
	set {
		var hashSet = new HashSet<ViewModel>(value);
		foreach (var vm in _filteredList.Value)
			vm.IsSelected = hashSet.Contains(vm);
	}
}
protected Dependent<List<ViewModel>> _filteredList;

And then I tried to bind to it:

<local:XamDataGrid2 SelectedItems2="{Binding SelectedItems, Mode=TwoWay}" ... >

But I got the exception: "Update Controls does not support two-way binding of collection properties." What would it take to allow two-way binding in this scenario, where the user has defined a property setter?

Coordinator
Dec 30, 2011 at 4:13 PM

The reason for this restriction is that collections are dependent. This allows them to be specified with a simple linq query. A typical setter on a collection property doesn't simply impact the contents of the collection, it replaces one collection with another. It's a common point of confusion with ObservableCollection.

To allow two-way binding of a collection property, the implementer would have to determine all of the independent changes based on the items in the list, and the items not in the list.

For example, the SelectedItems getter might be a linq query.

 

from item in _model.Items
where item.IsSelected
select new ItemViewModel(item)


The setter then would take a List<ItemViewModel>. It would have to loop through all items and set the IsSelected property.

 

foreach (item in _model.Items)
  item.IsSelected = value.Any(vm => vm.Item == item);


 

Developer
Dec 30, 2011 at 6:42 PM
Edited Dec 30, 2011 at 6:43 PM

A *typical* setter replaces one collection with another, but you can clearly see that my setter merely scans the collection and updates the relevant IsSelected properties (independents). I think UpdateControls should allow two-way collection binding if the wrapped object offers a property setter. It would probably be a small change to UpdateControls.XAML, I'm just not sure what to change.

Coordinator
Dec 30, 2011 at 10:41 PM

The first thing is to add a "TranslateIncomingValue" abstract method to ObjectPropertyCollection. Make it look like the one in ObjectPropertyAtom, but correct the spelling. :) Implement it in the derived classes just like Atom.

Then change the OnUserInput method in ObjectPropertyCollection to cast the value to an IEnumerable. Create a List containing everything in that enumerable passed through TranslateIncomingValue. Then call ClassProperty.SetObjectValue(ObjectInstance.WrappedObject, theNewList).

This will work with properties of type IEnumerable. But if you want to support type-safe properties, it will take a little bit more work. You'll have to get the actual type from the ClassProperty, use reflection to call its constructor, and then cast that to IList to populate it.

Developer
Dec 30, 2011 at 11:27 PM
Edited Dec 30, 2011 at 11:38 PM

Should I introduce that NotificationGate thingie in TriggerUpdate() and OnUserInput, like ObjectPropertyAtom uses?

In order to call ClassProperty.SetObjectValue, it seems like I shouldn't need to create a new list if the derived type is ObjectPropertyCollectionNative. In that case the original list can be passed to the setter, eh? Maybe TranslateIncomingValue should be called TranslateIncomingCollection and translate the whole collection, and ObjectPropertyCollectionNative just returns its argument.

I think it's generally sufficient that UpdateControls.XAML support property setters whose type matches the type of the DependencyProperty they want to synchronize with. In the case of data binding to SelectedItems, the user must use custom code (either a derived class or attached behavior) so, presumably, the user can customize the code as necessary to ensure that the data type of the GUI's property is compatible with the data type of the ViewModel property. In other words, I doubt we need to magically convert GUI-typed collections to ViewModel-typed collections.

Developer
Jan 19, 2012 at 12:45 AM
Edited Jan 19, 2012 at 12:54 AM

I've been wondering why you haven't been responding.

I implemented OnUserInput(), and I even included support for viewmodels that use List<T> or one of its interfaces (IEnumerable<T>, IList<T>, ICollection<T>, or IList) as their collection type.

However, I noticed that this problem seems to be more complicated than it first appears. When the UI object (UIElement) changes its collection, it can do so in two ways, and both ways have pitfalls.

  1. When I attach the ForView-wrapped viewmodel, the DependencyProperty in the viewmodel is assigned the value of ObjectPropertyCollection._collection -- not a copy of the ObservableCollection, but the same instance. The UI object (or anyone with access to the DP) could simply modify the collection that is assigned to its DP.  The code I wrote will NOT handle this case, because it does not hook the _collection.CollectionChanged event.
  2. The UI object could create a new collection from scratch, and assign it to its DependencyProperty. In this case ClassProperty.SetValue is called, which calls ObjectPropertyCollection.OnUserInput.  I translate the collection into something the viewmodel can use, then call ClassProperty.SetObjectValue to send it back to the viewmodel. So far, so good. However, when a new collection is assigned to the UI object, we can expect it to cancel its subscription to _collection.CollectionChanged and subscribe to the new collection instead. Therefore, further changes to _collection will have NO EFFECT: so changes to the viewmodel continue to propagate to _collection but they don't propagate to the UIElement. To solve this problem, I did two things:
    1. In OnUserInput, assign _collection = value as ObservableCollection<object>; note that the UI DP is not necessarily ObservableCollection<object>!
    2. If _collection is null in OnUpdateCollection(), set _collection = new ObservableCollection<object>() and fire PropertyChanged.

I couldn't find any guidance on the 'net about which approach is "standard". In my SelectedItems synchronizer code, I used the second approach, and I verified that my program works okay whether the new collection is of type ObservableCollection<object> or merely List<object>.

I've gotta say, synchronizing SelectedItems is by far the most difficult thing I have done with WPF, and that's among a minefield of things that are difficult in WPF.

Coordinator
Jan 19, 2012 at 1:10 AM

Sorry. I wasn't watching the forums.

I only use the NotificationGate when I find that the UI is looping back around and calling my code while I'm trying to update the UI. Events coming from the UI should only be the result of user input, not side-effects from the program's own actions. If you find that you need it, use it. But I wouldn't use it until you do.

I haven't looked into how ListBox uses SelectedItems. But I would guess that it would set it to a new list. If it wanted to modify the list you provided in the getter, then it wouldn't need a setter.

I would recommend an approach that completely severs the get from the set. "Set" means that the user did something. "Get" means that the program wants to display something. The two are opposites, and should never cross paths. At least that's the way it should be.

The practical application of that advice is that you take the collection passed in to set, update program state, and throw it away. The UI shouldn't expect you to take ownership of that collection and start to modify it. And if it does, then you should throw a PropertyChanged back at it to force it to call "get".

Developer
Jan 19, 2012 at 4:11 PM
Edited Jan 19, 2012 at 4:39 PM

Unfortunately, SelectedItems isn't bindable on any controls (even the 3rd party one). To make it bindable you have to either create a derived class with a "SelectedItems2" that synchronizes itself with SelectedItems, or create an "attached behavior" class that does the same thing. Since the developer has to provide bindability manually, there are two different approaches to binding behavior, and I decided to support the second approach (with two variations).

I am only aware of one built-in WPF collection that supports two-way binding, namely Polyline.Points. I tried binding to it in my first-ever WPF program (which was a graph editor), and I had some trouble with it, unless my ViewModel was a DependencyObject... I haven't tried binding Polyline.Points with UpdateControls. It's a slightly different problem, since its type is PointCollection which is not observable.

I'm not sure what you mean about completely severing "get" from "set". Anyway, it took me a full day of coding (following several days of fruitless puzzlement, partly puzzlement about how WPF works and partly about how UC.XAML works (as its classes have no xml-comments) to arrive at the solution I implemented. I'm not eager to do it again.

On the other hand, I have half a mind to create a competitor to WPF that is far more discoverable, memory efficient, performance-conscious, statically typed, simpler and generally better-designed.