Apr 19, 2011

ItemsControl hidden feature when binding to a list

I've been battling (and finally, at last, solved) a particularly annoying problem lately which I was convinced for the longest time must be a bug. As usual, it was instead an undocumented feature. I tried to wait to write this so I wouldn't still be fuming after having torn my hair out for hours on end. I'll attempt civility, but if you detect a tinge of frustration you'll know why.

I was attempting to use the Windows Phone 7 Pivot control in a more dynamic way, one that I'm sure is actually fairly common when attempting an MVVM pattern. I didn't want the shell page to depend on (or "know about") the exact implementations of the specific pages on the Pivot control, nor how many there may be. The smart ones amongst you are already ahead of me: bind a list to the ItemsSource property of the Pivot control!

And you're right, that's the way to go. Given an ObservableCollection of arbitrary objects you can bind them to the Pivot control and have it display them as the content of each page. This way I can create any number of different View implementations I want and have them display without the shell part housing the Pivot container needing to know about them.* Voilà!

<Grid x:Name="LayoutRoot" Background="Transparent">
    <controls:Pivot x:Name="PivotMenu" Title="{Binding ApplicationName, Mode=OneWay}" ItemsSource="{Binding ListViews}" SelectedItem="{Binding ActiveListView, Mode=TwoWay}">
                <TextBlock Text="{Binding DataContext.ListName}"/>

Except it doesn't work. Not without a trick, which I've finally come up with.

When you pass the UserControl instances in, you'll likely see a variety of different design-time and run-time errors (also depending on if you're using Visual Studio or Expression Blend):

  • ArgumentException : Value does not fall within the expected range
  • InvalidOperation : Element is already the child of another element

These errors generally don't show up with a HeaderTemplate defined but then you get type names and other weirdness for the Pivot headers.

I must have fought this for awhile before trying out a few variations. To test, I created a brand new WP7 Pivot project in Blend. I modified the default classes such that it would pass a list in and bind it to the Pivot control. This time, however, I passed a list of ViewModel instances instead of UserControl-derived objects. This worked beautifully. The headers of each Pivot page were appropriately titled and the content displayed via an extra (but simple) ItemTemplate. This added a view dependency, though, but it did work. So what's so different?

After much searching, I'm fairly convinced it has to do with an obscure behavior of the ItemsControl class, which Pivot inherits from. I found this helpful SO question and an associated blog entry by this amazing WPF fellow (who goes by, appropriately, Dr. WPF) that covers it.

The gist is this, as taken from his blog post:

If a UIElement is added to the Items collection of an explicit ItemsControl instance (as opposed to an instance of a derived class like ListBox), it will become a direct child of the items panel. If a non-UIElement is added, it will be wrapped within a ContentPresenter.

He claims that derived types, such as ListBox and TabControl don't necessarily do this, just the base ItemsControl type. I'm willing to believe him on that, but it seems like the Pivot control must not have overridden this behavior. (His article was written long before WP7 was announced and does not include anything about it specifically.)

This behavior is overridable, should you desire to sub-class Pivot (it ain’t sealed!), via the following methods:

I didn't want to create a whole new derived control just to fiddle with that behavior, so I didn't, but I imagine this could work. Instead, I approached it from another way. If the detection of UIElements is causing radically different behavior, how can we prevent that at the design/XAML level without polluting the ViewModel? This is, after all, a presentation concern.

Hey, isn't this exactly what IValueConverters are for?

public class PivotItemConverter : IValueConverter
{ public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { IEnumerable<object> values = (IEnumerable<object>)value; ObservableCollection<object> newValues = new ObservableCollection<object>(); foreach (object obj in values) { PivotItem pvItem = new PivotItem(); pvItem.Content = obj; if (obj is Control) pvItem.Header = (obj as Control).DataContext; newValues.Add(pvItem); } return newValues; } public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { throw new NotImplementedException(); } }

Just quick and dirty. The converter is applied to the ItemSource property binding of the Pivot control, so it will receive a value parameter of type ObservableCollection<object>. These are the UserControl Views to display, but we need to convert them so that the Pivot control will play nice. I do this simply by creating a new list of PivotItems. The content is the object (UserControl) and for the Header I toss in the control's DataContext. You can manually set it to whatever string property on the DataContext you want here if you like but this way you can use the HeaderTemplate to style things accordingly plus you don’t have to sniff the DataContext’s type.

All in all, a fairly simple workaround once you kinda get what’s going on. It seems though that this behavior isn’t very well defined and it’s important to note that my original attempt (seen above) worked fine with a TabControl in WPF. It was a simple matter of setting a few styles on the ItemContainerStyle and it had no trouble with just a straight list of objects bound to it.

You can see the fruits of this labor and more in this GitHub project. I’ll have a more formal post soon about it but for now you can peruse the included readme info when you get there.

* Some might ask “Why not use an ItemTemplateSelector?” It's possible. This won't work in the end, though, because the ItemTemplate processing part is bypassed when a list of UIElements is given, so the point is moot.

No comments:

Post a Comment