I have a ListView
that allows me to select items, which are then added to an ObservableCollection
. This ObservableCollection
is bound to a TabControl
, so I get new tabs as I add items. What I would like is for each new tab to become selected and show its content automatically.
From a content perspective, this is working. I am able to see the newly-added item's content as expected, however from the looks of the TabControl
itself, all tabs have been "deselected," or moved to the background.
Here's what I'm doing: (Note that I'm setting SelectedItem. I experience similar results when setting SelectedIndex or SelectedValue. Not sure what's best here.)
void MyCollection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
List<MyClass> newStuff = e.NewItems.Cast<MyClass>().ToList<MyClass>();
if (newStuff.Count == 1)
myTabControl.SelectedItem = newStuff[0];
}
}
Using Console.WriteLine
and the TabControl's SelectionChanged event, I can verify that the TabControl.SelectedIndex
is changing as expected. With each new item however, all tabs appear to move to the background.
I always see the content for the new item, but I must manually click the tab in order for it to visually appear as the foreground tab.
Whenever I click the background tab that is supposed to already be in the foreground, my console often displays a redundant debug message.
For example, if I start with zero items in the collection and then add one, I get one background tab, one console message, and I see the item's content. If I click on the tab, it comes to the foreground and the SelectionChanged method fires(!!!), repeating the same console message while I stare at the same item content:
myTabControl now has SelectedIndex of 0
myTabControl now has SelectedIndex of 0
How can I both programmatically show the content of my new tab, and see that tab as the foreground tab?
The only way I can bring ANY tab to the foreground programmatically is by setting myTabControl.SelectedIndex = -1
. Apparently this is not an option while the ObservableCollection
contains items, so this forces the TabControl
to move the first tab (content, header and all) to the foreground. What am I missing here?
Correction: With this method (setting SelectedItem
) I do get my very first tab in the foreground, however creating tab #2 and all subsequent tabs deselects all tabs, while showing the content from the most recent addition.
Some off-by-one bug was occurring here.
The method MyCollection_CollectionChanged was in my View's code-behind (the .xaml.cs file) rather than in the ViewModel. Using the Snoop tool (snoopwpf.codeplex.com) I was able to determine that the index was being set out-of-range somehow. For example, if I added item #4, the item would be placed at index 3, and I would attempt to change to this index. My console log messages would say that the selected index was successfully changed to 3, however the Snoop tool would say the index is 4(!) while all four tabs appeared to be in the background. Truly bizarre. Other collection objects might throw an out-of-range exception, but not this TabControl.
When I discovered this off-by-one bug, I attempted to set the index not to the actual index, but to the index minus one. The result was not a fix, but expected behavior instead. If I tried to set the index to what it should be, it would land on index + 1, but if I tried setting it to index - 1, it would actually change it to index - 1. This meant I could never target the desired tab, but always go one to the left, or one to the right (out-of-range).
Rather than use the View's code-behind to dig the MyClass object out of the NotifyCollectionChangedEventArgs so that it could be used to change tabs (which probably violates the MVVM pattern in the first place), I decided to use the ViewModel to change tabs. This meant passing the TabControl to the ViewModel, so that it could to add to the collection and then immediately change tabs on the next line of code, like so:
MyCollection.Add(newThing);
_tabControl.SelectedItem = newThing;
Works like a charm! Keep this fix in mind if you ever see a TabControl with no tabs selected. The index change may be taking place in the wrong thread, or some such thing.