Click here to Skip to main content
13,664,877 members
Click here to Skip to main content
Add your own
alternative version

Tagged as

Stats

5.6K views
87 downloads
7 bookmarked
Posted 13 Mar 2017
Licenced CPOL

Never Ever Add Strings to WPF Lists

Rate this:
Please Sign up or sign in to vote.
The same advice applies to many other item types, and not just with lists. More exactly, such items can be used, but unexpected behavior may make it a nightmare.

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();
   } //Populate
   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; }
} //PreparationBase

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");
} //Populate

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

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;
} //Populate

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);
    } //loop
} //Populate

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);
    } //loop
    listBox.ItemsSource = list;
} //Populate

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; }
} //class MyItem

// ...

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

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;
} //Populate} //Populate

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; }
    
    // this is what can be used to reproduce the problem:
    public override bool Equals(object obj) {
        if (obj == null) return false;
        MyItem myItem = obj as MyItem; // dynamic cast
        if (myItem == null) return false;
        return myItem.Content == Content;
    } //Equals

    // when we override Equals, we also have to override GetHashCode:
    public override int GetHashCode() {
        return Content.GetHashCode();
    } //GetHashCode

} //class MyItem

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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Sergey Alexandrovich Kryukov
Architect
United States United States
No Biography provided

You may also be interested in...

Comments and Discussions

 
SuggestionYou should point out that... Pin
SledgeHammer0113-Mar-17 13:32
memberSledgeHammer0113-Mar-17 13:32 
GeneralWrong! Did you check it up yourself? Pin
Sergey Alexandrovich Kryukov13-Mar-17 14:42
memberSergey Alexandrovich Kryukov13-Mar-17 14:42 
GeneralRe: Wrong! Did you check it up yourself? Pin
JustWatchLittle12-Dec-17 3:28
professionalJustWatchLittle12-Dec-17 3:28 
GeneralRe: Wrong! Did you check it up yourself? Pin
Sergey Alexandrovich Kryukov13-Dec-17 9:59
memberSergey Alexandrovich Kryukov13-Dec-17 9:59 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Cookies | Terms of Use | Mobile
Web04-2016 | 2.8.180810.1 | Last Updated 14 Mar 2017
Article Copyright 2017 by Sergey Alexandrovich Kryukov
Everything else Copyright © CodeProject, 1999-2018
Layout: fixed | fluid