Dear Dr. WPF,
As of late, a serein has come upon me in the world of wpf and databinding, hence I’m writing to you in my desperate search for some kind of remedy before there will be a hey rube.
Whilst having an ObservableCollection of “Person” objects, and creating a collectionview for this and also adding a SortDescription on a “Name” property on the “Person” object, I noticed that if I change the “Name” property on one of the objects (they are shown in a listbox, having the “Name” property databound to a textblock), and thereafter calling Refresh() on the CollectionView, the refresh will actually take more than a second with only about 50 items.
Ok, if I measure the call to Refresh() it doesn’t take more than 100 ms or so, but like the call sets in motion alot of other things that will happen after my method has exited, which is understandable since the documentation states that the whole view is recreated and therefore I assume the whole tree of items of the visualtree concerned is re-created too.
If I instead just remove the item, and just re-insert it, it’s as close as I can get to instantaneous. perfect! BUT! Since I’m a quidnunc I won’t be satisfied with this! So I’m asking the great Dr, that has given us all such great in depth knowledge in the past, what can we do about this in a more generic fashion? Can we speed up the “Refresh()” somehow? What would you suggest I do if I have a collection of a type that I don’t know about at compile time? How can I remove / add an item then!
Or am I simply lost in my path to enlightenment ?
I shall not dissert anymore, but hope to hear from the good Dr, perhaps in a blogpost touching this subject as I feel it to be of greater interest to the public health in the land of wpf.
Best regards,
Patient-X
Dear Patient-X,
I’ve been wondering what I could file under the letter ‘E’ in this series, and now you’ve afforded me the perfect opportunity! 🙂
No, you are not lost in your path to enlightenment. The true path will lead you to a new feature in .NET 3.5 SP1 called an “editable” collection view. More on that momentarily… First, let’s examine exactly what is happening in your scenario.
Understanding the ListCollectionView Class
You are binding to a list of items. More specifically, you are binding to a collection of Person objects (namely an ObservableCollection<Person>) that, in turn, implements the IList interface. The binding engine will automatically generate a ListCollectionView for such a collection. The ListCollectionView class provides support for sorting, grouping, and filtering your collection when it is the ItemsSource for an ItemsControl. (See ‘C’ is for Collection for more information.)
For the purposes of this discussion, let’s look at a similar scenario using the following collection of Character objects (borrowed from ‘I’ is for Item Container):
<src:CharacterCollection x:Key="Characters">
<src:Character First="Homer" Last="Simpson"
Gender="Male" Image="images/homer.png" />
<src:Character First="Marge" Last="Bouvier"
Gender="Female" Image="images/marge.png" />
<src:Character First="Bart" Last="Simpson"
Gender="Male" Image="images/bart.png" />
<src:Character First="Lisa" Last="Bouvier"
Gender="Female" Image="images/lisa.png" />
<src:Character First="Maggie" Last="Simpson"
Gender="Female" Image="images/maggie.png" />
</src:CharacterCollection>
To demonstrate grouping and sorting for our collection, we can define a CollectionViewSource object in markup, as shown below:
<CollectionViewSource x:Key="CharacterCollectionView"
Source="{Binding Source={StaticResource Characters}}">
<CollectionViewSource.GroupDescriptions>
<PropertyGroupDescription PropertyName="Gender"/>
</CollectionViewSource.GroupDescriptions>
<CollectionViewSource.SortDescriptions>
<cm:SortDescription PropertyName="First" />
</CollectionViewSource.SortDescriptions>
</CollectionViewSource>
When this CollectionViewSource is set as the ItemsSource of a ListBox, it will yield a view of the collection (namely, a ListCollectionView) in which the characters are grouped by gender and sorted by first name. Here is what the ListBox declaration might look like:
<ListBox Name="lb" HorizontalAlignment="Center" VerticalAlignment="Center"
ItemsPanel="{StaticResource HorizontalItemsPanel}"
ItemsSource="{Binding Source={StaticResource CharacterCollectionView}}"
ItemContainerStyle="{StaticResource CharacterContainerStyle}">
<ListBox.GroupStyle>
<GroupStyle Panel="{StaticResource HorizontalItemsPanel}" />
</ListBox.GroupStyle>
</ListBox>
The image below depicts our ListBox with grouping and sorting.
You can download the sample for this post to see the complete item template, item container style, and group item style.
Dynamically Updating Items is Problematic
Ideally, applying a sort order to a CollectionView would result in a grouped and sorted view of the collection that is fully maintained even when items within the collection are changed. Unfortunately, a ListCollectionView only groups and sorts its items when the view is first created (or when new items are added to the source collection, assuming that the source collection is observable). A ListCollectionView will *not* monitor changes to properties on each existing item within the collection.
For example, if you execute code at runtime to dynamically change Bart’s name to “Mandy”, the ListCollectionView will be unaware of that change and the item will not appear in its proper sorted location after the change. Furthermore, if Bart finally gets the operation he deserves (and secretly desires) and his gender is updated accordingly, the changed item will not move to its proper group. Here is some simple code to perform these changes:
Character bart = lb.Items[0] as Character;
bart.First = "Mandy";
bart.Gender = Gender.Female;
The image below depicts the sorting and grouping errors that are present after these changes:
Note, that the item template did indeed update according to the name and gender change (because the Character object raises the appropriate change notifications for these properties), but the ListCollectionView did not update its view of the items. As a result, the changed item is no longer in the correct sort location or group.
Refreshing a CollectionView
If you are using .NET 3.5 or earlier, you can force a CollectionView to be updated at runtime by calling its Refresh() method. Then the items will once again be properly grouped and sorted. Here is a routine that includes the Refresh() call:
Character bart = lb.Items[0] as Character;
bart.First = "Mandy";
bart.Gender = Gender.Female;
(lb.ItemsSource as ListCollectionView).Refresh();
After calling Refresh(), the modified character now appears in the correct group and sort location, as shown here:
Unfortunately, the Refresh() method results in a complete regeneration of the view. It is a rather drastic operation, to say the least. Furthermore, when a Refresh() occurs within the view, it raises a CollectionChanged notification and supplies the Action as “Reset”. The ItemContainerGenerator for the ListBox receives this notification and responds by discarding all the existing visuals for the items. It then completely regenerates new item containers and visuals. (See ‘I’ is for Item Container and ‘G’ is for Generator for more information on item containers and container generation.) Again, this is a drastic (and often very expensive) operation.
Increasing Refresh Performance is Difficult
You raised the question, “Can we speed up the “Refresh()” somehow?”
Unfortunately, the very nature of the refresh operation makes this difficult. For the mundane reasons, keep reading… otherwise, feel free to skip ahead to A Better Option.
Theoretically, you could derive your own custom collection view from the ListCollectionView class and override its Refresh() method. Then you could perhaps implement a routine that intelligently regenerates the view in a minimalistic manner by comparing the source collection to the view collection.
Such a routine would need to move existing items to their new locations in the view, add items that were not previously present, and remove items that should no longer be present. All of these changes would need to be made per the view’s sorting, grouping, and filter criteria. For each change, the view would need to raise a very specific CollectionChanged notification (Add, Remove, or Move) for consumption by the ItemContainerGenerator of the owning ItemsControl.
Not only would such a synchronization routine be complex, but it is also likely to be even more expensive than simply resetting the entire collection. There will be some number, n, where discrete Add, Remove, and Move change notifications for n or fewer changes is indeed more performant than a single Reset notification. Unfortunately, it will be very difficult to heuristically determine the appropriate value for n.
A Better Option: Remove and Re-Add
A better approach, especially if you are stuck on pre-3.5 SP1 bits, is to do precisely what you describe in your email. Namely, do not call Refresh() at all, but instead, remove the target item from the observable collection, modify its properties, and then re-add it to the collection, as shown here:
Character bart = lb.Items[0] as Character;
CharacterCollection characters = FindResource("Characters") as CharacterCollection;
characters.Remove(bart);
bart.First = "Mandy";
bart.Gender = Gender.Female;
characters.Add(bart);
The ListCollectionView will detect the removal of the old item and raise the appropriate Remove notification so that the ItemContainerGenerator can remove the associated container and visuals. It will also detect the addition of the modified item and insert it at the correct location within its view of the source collection (again, per the grouping, sorting, and filter criteria). Then it will raise the necessary Add notification so that a new item container and item visuals can be generated.
This solution is obviously not perfect! One problem that may not be immediately evident is that adding and removing an item from the source collection effects the ListCollectionView’s notion of currency (“current item”). For example, if the Bart character was the current item before it was removed from the collection, that state will be lost. If this is important to your scenario, you must add additional code around the remove/re-add routine to save and restore currency.
An Even Better Option: IEditableObject
As mentioned earlier, .NET 3.5 SP1 enables an even better approach that solves many of the problems present in the Refresh() and Remove/Re-Add solutions. Namely, it allows you to implement an IEditableObject interface on your datamodel objects (or on your viewmodel objects, if you are using the model-view-viewmodel pattern). This new interface allows for transactional changes (edit, cancel, and commit) on the object.
The framework supports the transactional model by implementing another new interface called IEditableCollectionView on the existing list-based CollectionView classes (ListCollectionView and BindingListCollectionView). This interface works in conjunction with collection items that implement the IEditableObject interface.
In this new approach, when you need to change an item, you can first call the EditItem() method of the collection view, then make the required changes to the item, and finally, call the CommitEdit() method of the collection view. If you decide that you don’t want to commit the changes, you can call the CancelEdit() method of the collection view.
It is worth noting that the ItemCollection class also implements the IEditableCollectionView interface. Since the Items property of an ItemsControl is of type ItemCollection (see ‘C’ is for Collection), it provides handy access to the methods of IEditableCollectionView. As long as you know the source collection is an IList, you can safely call the IEditableCollectionView methods directly on the Items property and the ItemCollection class will delegate the call to the appropriate underlying collection view.
How it Works
In practice, the properties of an “editable” object will rarely be updated directly in code behind. Rather, these properties will be updated via bindings to properties on controls within the UI.
In WPF, it is fairly common to present data using a “view template”. Such a template will often present a readonly view of the data using elements like TextBlock, Image, etc. Then, when it is necessary to edit the data, the view will switch to an “edit template” that contains editable controls like TextBox, ComboBox, CheckBox, etc.
The IEditableObject interface provides three transactional methods: BeginEdit(), CancelEdit(), and EndEdit(). I prefer to expose an additional readonly property on the editable object called IsInEditMode that can be used as a trigger for swapping between the view template and the edit template.
Below is my typical implementation of IEditableObject, as it would be implemented on the Character class in our example:
#region IEditableObject support public Character _cachedCopy = null; public void BeginEdit() { // save object state before entering edit mode _cachedCopy = new Character(); _cachedCopy._first = _first; _cachedCopy._last = _last; _cachedCopy._image = _image; _cachedCopy._gender = _gender; // ensure edit mode flag is set IsInEditMode = true; } public void CancelEdit() { // restore original object state if (_cachedCopy != null) { First = _cachedCopy._first; Last = _cachedCopy._last; Image = _cachedCopy._image; Gender = _cachedCopy._gender; } // clear cached data _cachedCopy = null; // ensure edit mode flag is unset IsInEditMode = false; } public void EndEdit() { // clear cached data _cachedCopy = null; // ensure edit mode flag is unset IsInEditMode = false; } private bool _isInEditMode = false; public bool IsInEditMode { get { return _isInEditMode; } private set { if (_isInEditMode != value) { _isInEditMode = value; RaisePropertyChanged("IsInEditMode"); } } } #endregion
You can download the complete sample for this post to see the IEditableObject and IEditableCollectionView interfaces in action. When you run the sample, simply double-click an item (or press F2) to enter edit mode.
If you do not wish to commit your changes, press Escape. If you do wish to commit your changes, press Enter or simply select another character. When the changes are committed, you will notice that the item immediately moves to its proper location within the view.
Is IEditableObject Absolutely Required?
You posed the question, “What would you suggest I do if I have a collection of a type that I don’t know about at compile time?”
First, I would advise you to avoid that situation if at all possible. The model-view-viewmodel pattern typically involves wrapping or replicating items (and collections) of known types within observable classes (classes that provide change notifications like INotifyPropertyChanged and INotifyCollectionChanged) and adding additional interfaces (like IEditableObject) to either the data model objects or the viewmodel objects to allow them to play nicely with the view. That is always my preferred approach.
Having said that, there are certainly times when you may not have the option of modifying the entity within your collection. If you simply cannot add the IEditableObject interface to the data model or viewmodel class, you needn’t fret. The IEditableCollectionView interface is still your friend (as long as you are targeting .NET 3.5 SP1, of course). You won’t get all of the transactional goodness of IEditableObject, but you can still call EditItem() followed by CommitEdit() to update the view’s notion of a specific item.
Consider the earlier example where the characters were not correctly sorted or grouped after changing Bart’s name and gender. It turns out that you can simply add the lines below to cause that specific item to be moved to the correct location within the view:
   Character bart = lb.Items[0] as Character;
   bart.First = "Mandy";
   bart.Gender = Gender.Female;
   (lb.Items as IEditableCollectionView).EditItem(bart);
   (lb.Items as IEditableCollectionView).CommitEdit();
Voîla! There is no longer a need to explicitly refresh the entire view!
Order now and receive these bonus gifts!
The downloadable sample included with this post covers the specific questions posed by Patient-X, but there is more to IEditableCollectionView. It also provides a convenient way to add and commit new items to the source collection. You can even use it to remove existing items from the source collection. By leveraging members of the IEditableCollectionView interface for such changes, you can ensure that your view of the collection will remain accurate with respect to grouping, sorting, and filtering.
To see some of the other features of IEditableCollectionView, check out Vincent Sibal’s introduction to IEditableCollectionView, He also includes a nice code sample with his post.
Cheers,
Dr. WPF
P.S. For anyone who is hoping to get a published response to a WPF question, you should note (as Patient-X has) that the doctor is easily manipulated by wordsmithery. If you use lots of obscure terms like serein and hey rube, there is a much greater chance you will receive a public response, as it appeals to my invented persona’s inflated sense of perspicacity! 😉