Epigraph:
In reality, everything is not as in fact.
- Stanisław Jerzy Lec
Contents
Introduction
Let’s define the scope of the problem a bit more accurately. The problem is not just the problem of some particular list control, such as System.Windows.Controls.ListBox
; this is a problem of all System.Windows.Controls.ItemsControl
classes.
Likewise, the problem appears not just with strings used as the control’s items type; it appears with a wide set of classes, first of all, primitive and enumeration types, most value types and some reference types.
I’ll explain this a bit later, so now we can jump to the discussion of the problem, but first I need to explain how to observe the behavior on the test cases I prepared.
Demo Application
Before discussing the problem itself, I want to introduce my testing facility. I will experiment with only one demo/test application I called “NeverEverAddStringsToWPFLists.exe”. Here is the idea: we can use the same instance of System.Windows.Controls.ListBox
. This control, as well as any other ItemsControl
, is agnostic to the types of its items. The application navigates to different test cases, on a tree view shown on the right of the application’s main window. Each test case initially populates the list box in different ways and provides different ways of adding new items and extracting string representation of an item to be shown as a selected item.
To abstract out such behavior, let’s define an abstract base class for this purpose:
internal abstract class ItemsControlHandlerBase {
internal virtual void Populate(ListBox listBox) {
listBox.ItemsSource = null;
listBox.Items.Clear();
}
internal abstract string GetSelectedItem(ListBox listBox);
internal abstract void AddNewItem(ListBox listBox, string value);
internal virtual bool FilterTextBoxInput(string badText) { return false; }
internal abstract string Help { get; }
}
The classes representing different test cases (some of them representing the manifestation of the problem, other cases demonstrating the solution) are derived from this base class and override five virtual methods; two of these methods are pseudo-abstract, not abstract
. Below, in the sections The Problem and Solutions, I’ll show only one overridden method Populate
, which is enough to reveal the problem.
The solution is provided for Visual Studio 2008 which targets .NET v. 3.5, and Visual Studio 2015 targeting .NET v. 3.5 to v. 4.6.1, to make sure it covers most of the frameworks and development tools supporting WPF. Later versions of Visual Studio provide automatic conversion to later solution and project types. It can be built in one click on the batch file “build.bat”.
The Problem
To observe the abnormal behavior, run the demo application. It will navigate to the first test case and show some instructions on the bottom panel of the main window.
First, clicking on the list items in certain order will eventually show the situation when more than one items are marked as selected, as shown in the picture. It’s important to understand that there is nothing like multiple selection here. It’s just that the major control behavior is badly broken.
Another abnormality can be revealed in scrolling. The demo application code always scrolls to bring the newly added item into view. It always works if the newly added item is distinct. Let’s say, its string (or integer, in next test case) content is the same as of some other item already in view, no scrolling occurs.
As we could reasonably expect, nothing wrong happens when all the items on the list view are distinct.
String List Items
The problem appears if at least two list items of string type have identical values. Let’s add two pair of identical strings:
internal override void Populate(ListBox listBox) {
base.Populate(listBox);
listBox.Items.Add("one");
listBox.Items.Add("two");
listBox.Items.Add("one");
listBox.Items.Add("two");
}
Same Problem with Integers
It’s hard to expect that integer type could make the situation better. In fact, the abnormal behavior with types like primitive or enumeration types is easier to explain than with strings. Let’s try this:
internal override void Populate(ListBox listBox) {
base.Populate(listBox);
listBox.Items.Add(1);
listBox.Items.Add(2);
listBox.Items.Add(1);
listBox.Items.Add(2);
}
Same Problem with Data Binding
Will Data Binding help to solve the problem? Let’s try:
StringObservableCollection list = new StringObservableCollection();
internal override void Populate(ListBox listBox) {
base.Populate(listBox);
list.Clear();
list.Add("one");
list.Add("two");
list.Add("one");
list.Add("two");
listBox.ItemsSource = list;
}
Not really — same thing; and it’s not too hard to figure out why.
The Problem per .NET Version
I’ve tested the problem with different .NET versions. The testing shows that the second problems, the one with bringing an item into view by scrolling still persists in all .NET versions.
Explanation
As the problem is manifested only if some of the items are identical, it should be obvious that the problem is related to the object identity.
The problem starts when we add an item. What item, where? Apparently, this is the point where an item is added to any instance of any of the ItemsControl
types, which happens in the method System.Windows.Controls. ItemCollection.Add(object)
.
From this API, it looks apparent, that any objects can be added, as System.Object
is the base type of all .NET types. Indeed, this is almost true, and we all should know that the devil of fine print is often hidden under the word “almost”. In fact™ :-), there should be one silent rule: all objects are assumed to be unique.
Unfortunately, the parameter of the method Add
is not generic, and if it was, we could not find a suitable generic parameter constraint mechanism in present-day .NET. In my opinion (in agreement with the number of other software developers), this is one of the major design defects of WPF.
What is “almost”, in this context? This is the uniqueness, in the scope of the set of items of an instance of some ItemControl
object, in regards to the equivalence relation. In turn, this is the relation fully defined by the (possibly overridden) method System.Object.Equals(object)
.
If this rule is not observed, the behavior of the control can be extremely messy. It’s hard to explain all possible scenarios, so let’s just explain abnormal scrolling behavior. Let’s say, when we add an item to the end of the list and try to scroll to the newly added item. Let’s consider the moment of time when we set selection on the object “one”, and we have several objects “two” below; and the last of them is on the bottom of the list, beyond the visible portion if item. What happens if we add another “two” and try to scroll to it using the method ScrollIntoView
? This function will try to find the identical object to scroll to (the one identical to the object
parameter passed to this method), and, quite apparently, will stop at the very first “two” object. But this object is already in view, so nothing will happen.
It’s quite obvious that the problem will be the same with wide set of types: all primitive and enumeration types, most value types. It’s also obvious enough that the problem won’t appear with reference types, but not all of them. Let’s see…
For the reference types, by default, this method simply returns ReferenceEquals. This is what most of the reference types do, but the type string
is one of the exclusions. String
objects, even referentially different, return true
from Equals
if the contents of the two compared objects are identical. As if it wasn’t enough, identical string
objects are quite rarely even referentially different. First of all, the strings are immutable. There is a special mechanism used to reuse identical string objects via the string intern pool.
The explanation of this quite delicate mechanism would lead us too far from the topic of the present article, but this is something which is good to know. See also: String.Intern
, String.IsInterned
. Unfortunately, I could not find a review or technological articles in official MSDN documentation, only some unofficial article, for example, this one.
That said, to have the problem solved, we need to make sure that all the items are unique. But how to guarantee that, taking in consideration that the user can always add second “one” or second “two”. To make sure that two different “two” objects are unique, it’s enough to wrap them in some reference type with default equality method. And then, I’ll show how to reproduce the problem by overriding TObject.Equals
.
Let’s do exactly that.
Solutions
To illustrate the solution, let’s deal with just the string
type. As we make sure it works, it’s quite obvious that the mechanism will work with all other types. Let’s start with the simplest case.
ListBoxItem
With Strings
internal override void Populate(ListBox listBox) {
base.Populate(listBox);
string[] items = new string[] { "one", "two", "one", "two", };
foreach (var item in items) {
ListBoxItem lbitem = new ListBoxItem();
lbitem.Content = item;
listBox.Items.Add(lbitem);
}
}
With Data Binding
Data Binding won’t change anything:
internal override void Populate(ListBox listBox) {
base.Populate(listBox);
list.Clear();
string[] items = new string[] { "one", "two", "one", "two", };
foreach (var item in items) {
ListBoxItem lbitem = new ListBoxItem();
lbitem.Content = item;
list.Add(lbitem);
}
listBox.ItemsSource = list;
}
With Custom Item Type
Actually, the WPF class ListBoxItem
does not do any special, so there is absolutely no need to use it — well, not in the logical tree. Rather, it is the type of the objects automatically created when the visual tree is built. In two previous solutions, I simply reused already available type; also, this type will be suggested by the Intellisense during XAML programming.
In fact, any custom type can play the same role. More exactly, it should be the type with a kind of unique identity: two objects created via two different constructor calls should be considered non-identical. That said, even a reference type could behave poorly and cause the same exact problem, if its identity rules are overridden in certain way; this is the problem we encountered with the string
type. For any reference type, default identity rule uses just the referential identity, so such type, without redefined identity rules, will work as a perfect item type.
The only problem is: how the instance of such item type will be shown in a list? The simplest solution is really very simple: the method System.Object.ToString()
should be overridden to show desired string value. Here is the example of such solution:
class MyItem {
internal MyItem(string content) { this.Content = content; }
internal string Content { get; set; }
public override string ToString() { return Content; }
}
internal override void Populate(ListBox listBox) {
base.Populate(listBox);
string[] items = new string[] { "one", "two", "one", "two", };
foreach (var item in items)
listBox.Items.Add(new MyItem(item));
}
Note that such solution has a lot of benefits. First of all, the Content
type does not have to be untyped (System.Object
with ListBoxItem
). It means that the data (content) can be read, assigned to, or manipulated without potentially unsafe typecast. This is a very important advantage.
With Custom Item Type and Data Binding
This is how the same custom item type MyItem
will work with Data Binding:
internal override void Populate(ListBox listBox) {
base.Populate(listBox);
list.Clear();
string[] items = new string[] { "one", "two", "one", "two", };
foreach (var item in items)
list.Add(new MyItem(item));
listBox.ItemsSource = list;
}
How to Reproduce the Problem?
Armed with our knowledge, we can intentionally reproduce the problem, to pin-point it precisely and confirm our understanding of its nature. Let’s override System.Object.Equals
this way:
class MyItem {
internal MyItem(string content) { this.Content = content; }
internal string Content { get; set; }
public override string ToString() { return Content; }
public override bool Equals(object obj) {
if (obj == null) return false;
MyItem myItem = obj as MyItem;
if (myItem == null) return false;
return myItem.Content == Content;
}
public override int GetHashCode() {
return Content.GetHashCode();
}
}
This will immediately bring us to the same situation as with bare strings.
Final Notes
Initially, I planned to make a Tip/Trick article, but during writing I clearly realized that it would not make a consistent and useful tip. In this case, the focus should be on the fundamental understanding of what’s going on in WPF in general. In other words, the real tip is not “Never Ever Add Strings to WPF Lists“. It could be used as a rule of thumb or just to make some article title.
The real tip would be:
Don't let WPF foolish you; read the fine print. Especially if the fine print is not available; in this case, read between lines.
In essence, the present article is that kind of fine print written to fill the gap. Even more serious tip is formulated in the last paragraph of my humorous 1st of April article. :-)
I’ll gladly try to answer any questions and consider all critical comments. Hope this matter can be useful.