Share via


Confessions of a ListBox groupie [Using IValueConverter to create a grouped list of items simply and flexibly]

**

This blog has moved to a new location and comments have been disabled.

All old posts, new posts, and comments can be found on The blog of dlaa.me.

See you there!

Comments

  • Anonymous
    March 18, 2010
    Why couldn't you do this: <Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">   <Page.Resources>      <XmlDataProvider x:Key="data">         <x:XData>            <Animals xmlns="">               <Animal name="Dory" Species="Fish" />               <Animal name="Felix" Species="Cat" />               <Animal name="Fluffy" Species="Dog" />               <Animal name="Jake" Species="Snake" />               <Animal name="Mittens" Species="Cat" />               <Animal name="Murtle" Species="Turtle" />               <Animal name="Nemo" Species="Fish" />               <Animal name="Rex" Species="Dog" />               <Animal name="Rover" Species="Dog" />               <Animal name="Toonces" Species="Cat" />            </Animals>         </x:XData>      </XmlDataProvider>   </Page.Resources>   <DockPanel>      <ScrollViewer DockPanel.Dock="Bottom" VerticalScrollBarVisibility="Auto">        <ScrollViewer.Resources>          <CollectionViewSource x:Key="animalsBySpecies" Source="{Binding Source={StaticResource data}, XPath=Animals/Animal}">            <CollectionViewSource.GroupDescriptions>              <PropertyGroupDescription PropertyName="@Species&quot; />            </CollectionViewSource.GroupDescriptions>          </CollectionViewSource>        </ScrollViewer.Resources>        <ItemsControl ItemsSource="{Binding Source={StaticResource animalsBySpecies}}">          <ItemsControl.GroupStyle>            <GroupStyle>              <GroupStyle.ContainerStyle>                  <Style TargetType="{x:Type GroupItem}">                    <Setter Property="Template">                      <Setter.Value>                        <ControlTemplate TargetType="{x:Type GroupItem}">                          <GroupBox Header="{Binding Name}">                            <ItemsPresenter />                          </GroupBox>                        </ControlTemplate>                      </Setter.Value>                    </Setter>                  </Style>              </GroupStyle.ContainerStyle>            </GroupStyle>          </ItemsControl.GroupStyle>          <ItemsControl.ItemTemplate>            <DataTemplate>              <TextBlock Text="{Binding XPath=@name}" />            </DataTemplate>          </ItemsControl.ItemTemplate>        </ItemsControl>      </ScrollViewer>   </DockPanel> </Page>

  • Anonymous
    March 18, 2010
    Because that doesn't work in Silverlight. :) The ItemsControl.GroupStyle property that forms the basis for the example above is not supported by any version of Silverlight (yet). (Neither is XmlDataProvider, as far as I know.) But I think what you were really suggesting was to use that technique for WPF in response to the challenge at the end of my post. In which case I agree with you! :) That's a really nice demonstration of some powerful platform support and it's extra cool that it's a XAML-only solution. Thanks a lot for sharing!

  • Anonymous
    March 18, 2010
    This is great.  I have had many requests for this type of feature in a listbox.  The only thing I have to figure out now is how to collapse a group header.  Something similar to the datalist.

  • Anonymous
    March 18, 2010
    I'm at fault for skimming... my apologies. GroupStyle does not seem to be on the SL 4 list either which is disappointing.

  • Anonymous
    March 18, 2010
    Mustang65, At a certain point, it becomes easier to stop hacking around limitations and switch to a control that does what you need by its very design. :) I'm not sure if you're there yet or why it is that you need to use ListBox, but maybe it's worthwhile to reconsider whether DataGrid could meet your needs - or whether the above WPF-specific solution would (because I think you could do collapse with it fairly easily). Thanks for your comment!

  • Anonymous
    March 18, 2010
    cromwellryan, No need to apologize - yours is a great solution on WPF!

  • Anonymous
    March 18, 2010
    I currently have 2 different views of the same data.  Once view contains a listbox and a dataform, a Master Detail type view.  The other is a datalist view that also allows users to export the data.  Both the listbox and the datalist use the same RIA DomainDataSource.  The DDS has a filter on it which works for the datalist and does somewhat work for the listbox.  By that I mean the data list listed the same way just no collapsable headers.  The listbox is used because the way i am showing the data i dont require or need a column header and have a data template that shows a server name and then under that a current status and location.

  • Anonymous
    March 18, 2010
    Mustang65, I think you can get rid of the headers on DataGrid and I'm pretty sure you could still use your DataTemplate if you switched over. It might be a little bit of work to do things under a slightly different programming model, but you'd get properr grouping and collapse for free. Of course, you know your situation far better than I do - I'm just thinking that you might be happier in the long run with a more officially supported approach. :) Hope this helps!

  • Anonymous
    May 31, 2010
    Drag and Drop with ListBoxDragDropTarget dont works

  • Anonymous
    June 01, 2010
    DragAndDrop, Please have a look at the following post and follow up with the author if you still have questions. It can be a little tricky to set up, but it should work okay in most cases: themechanicalbride.blogspot.com/.../new-with-silverlight-toolkit-drag-and.html FYI, there's an example of ListBoxDragDropTarget in the Toolkit samples project you can try online and look at the source code for: silverlight.net/.../default.html

  • Anonymous
    June 01, 2010
    You made my day with this very inventive solution! Thanks a lot for that! I translated the code to VB  to incorporate it into my silverlight controls library. Here it is for other VB-minded people... Imports System.Windows.Data ''' <summary> ''' Class that implements simple grouping for ItemsControl and its subclasses (ex: ListBox) ''' </summary> Public Class GroupingItemsControlConverter    Implements IValueConverter    ''' <summary>    ''' Modifies the source data before passing it to the target for display in the UI.    ''' </summary>    ''' <param name="value">The source data being passed to the target.</param>    ''' <param name="targetType">The Type of data expected by the target dependency property.</param>    ''' <param name="parameter">An optional parameter to be used in the converter logic.</param>    ''' <param name="culture">The culture of the conversion.</param>    ''' <returns>The value to be passed to the target dependency property.</returns>    Public Function Convert(ByVal value As Object, ByVal targetType As System.Type, ByVal parameter As Object, ByVal culture As System.Globalization.CultureInfo) As Object Implements System.Windows.Data.IValueConverter.Convert        Dim valueAsIEnumerable As System.Collections.IEnumerable = CType(value, System.Collections.IEnumerable)        Dim parameterAsGroupingItemsControlConverterParameter As GroupingItemsControlConverterParameters = CType(parameter, GroupingItemsControlConverterParameters)        ' Validate parameters        If valueAsIEnumerable Is Nothing Then            Throw New ArgumentException("GroupingItemsControlConverter works for only IEnumerable inputs.", "value")        End If        If parameterAsGroupingItemsControlConverterParameter Is Nothing Then            Throw New ArgumentException("Missing required GroupingItemsControlConverterParameter.", "parameter")        End If        Dim groupSelectorAsIGroupingItemsControlConverterSelector As IGroupingItemsControlConverterSelector = CType(parameterAsGroupingItemsControlConverterParameter.GroupSelector, IGroupingItemsControlConverterSelector)        If groupSelectorAsIGroupingItemsControlConverterSelector Is Nothing Then            Throw New ArgumentException("GroupingItemsControlConverterParameter.GroupSelector must be non-null and implement IGroupingItemsControlConverterSelector.", "parameter")        End If        ' Return the grouped results        Return ConvertAndGroupSequence(valueAsIEnumerable.Cast(Of Object)(), parameterAsGroupingItemsControlConverterParameter)    End Function    Public Function ConvertBack(ByVal value As Object, ByVal targetType As System.Type, ByVal parameter As Object, ByVal culture As System.Globalization.CultureInfo) As Object Implements System.Windows.Data.IValueConverter.ConvertBack        Throw New NotImplementedException()    End Function    ''' <summary>    ''' Converts and groups the values of the specified sequence according to the settings of the specified parameters.    ''' </summary>    ''' <param name="sequence">Sequence of items.</param>    ''' <param name="parameters">Parameters for the grouping operation.</param>    ''' <returns>Converted and grouped sequence.</returns>    Private Function ConvertAndGroupSequence(ByVal sequence As IEnumerable(Of Object), ByVal parameters As GroupingItemsControlConverterParameters) As IEnumerable(Of Object)        ' Validate parameters        Dim groupSelector As Func(Of Object, IComparable) = CType(parameters.GroupSelector, IGroupingItemsControlConverterSelector).GetGroupSelector()        Dim lstObjects As New List(Of Object)        If groupSelector Is Nothing Then            Throw New NotSupportedException("IGroupingItemsControlConverterSelector.GetGroupSelector must return a non-null value.")        End If        ' Do the grouping and ordering        Dim groupedOrderedSequence = sequence.GroupBy(groupSelector).OrderBy(Function(g) g.Key)        ' Return the wrapped results        For Each group In groupedOrderedSequence            lstObjects.Add(New ContentControl With {.Content = group.Key, .ContentTemplate = parameters.GroupHeaderTemplate})            For Each item In group                lstObjects.Add(New ContentControl With {.Content = item, .ContentTemplate = parameters.ItemTemplate})            Next        Next        Return lstObjects    End Function End Class ''' <summary> ''' Class that represents the input parameters to the GroupingItemsControlConverter class. ''' </summary> Public Class GroupingItemsControlConverterParameters    Private m_GroupHeaderTemplate As DataTemplate    Private m_ItemTemplate As DataTemplate    Private m_GroupSelector As IGroupingItemsControlConverterSelector    ''' <summary>    ''' Template to use for the header for a group.    ''' </summary>    Public Property GroupHeaderTemplate As DataTemplate        Get            Return m_GroupHeaderTemplate        End Get        Set(ByVal value As DataTemplate)            m_GroupHeaderTemplate = value        End Set    End Property    ''' <summary>    ''' Template to use for the items of a group.    ''' </summary>    Public Property ItemTemplate As DataTemplate        Get            Return m_ItemTemplate        End Get        Set(ByVal value As DataTemplate)            m_ItemTemplate = value        End Set    End Property    ''' <summary>    ''' Selector to use for determining the grouping of the sequence.    ''' </summary>    Public Property GroupSelector As IGroupingItemsControlConverterSelector        Get            Return m_GroupSelector        End Get        Set(ByVal value As IGroupingItemsControlConverterSelector)            m_GroupSelector = value        End Set    End Property End Class ''' <summary> ''' Interface for classes to be used as a selector for the GroupingItemsControlConverterParameters class. ''' </summary> Public Interface IGroupingItemsControlConverterSelector    ''' <summary>    ''' Function that returns the group selector.    ''' </summary>    ''' <returns>Key to use for grouping.</returns>    ReadOnly Property GetGroupSelector As Func(Of Object, IComparable) End Interface

  • Anonymous
    June 01, 2010
    Michael Bakker 1, Thanks for the assistance! :)

  • Anonymous
    October 17, 2010
    The comment has been removed

  • Anonymous
    October 18, 2010
    KCS, It seems like whatever's getting passed into the Convert method the second time by the VS designer isn't an IEnumerable. I might start by adding some debug-time exceptions there to give a little more information: What IS getting passed in? What's its type? What if we ignore the bad type the first time - do we get a good type the next time? Given that it's working for you at run-time, I'm optimistic that this problem can be fairly easily solved once we understand what's special about the second display at design time. :) Hope this helps!

  • Anonymous
    October 21, 2010
    Hello, can you elaborate on how to sort the items in the group via Name?

  • Anonymous
    October 21, 2010
    Hi, Right now each item is outputted like this:                <delay:GroupingItemsControlConverterParameters.ItemTemplate>                    <DataTemplate>                        <ContentControl Content="{Binding Name}"/>                    </DataTemplate>                </delay:GroupingItemsControlConverterParameters.ItemTemplate> However, this only works for single line. If there are multiple lines of data, and a stack panel of controls is used, this will break the <local:IsAnimalConverter x:Key="IsAnimalConverter"/> because each control's content will be an Animal. What's the recommended way around that? ie.                <delay:GroupingItemsControlConverterParameters.ItemTemplate>                    <DataTemplate>                                            <StackPanel Margin="0,0,0,17" Width="432">                                                <TextBlock Text="{Binding Name}" />                                                <TextBlock Text="{Binding Description}" />                                            </StackPanel>                    </DataTemplate>                </delay:GroupingItemsControlConverterParameters.ItemTemplate> TIA

  • Anonymous
    October 22, 2010
    Sort, If I understand your scenario, you want to sort the items within each group as well as having the groups sorted. One approach is to sort the complete list before-hand and rely on the fact that GroupBy maintains the original ordering within each group to give you sorted groups with no code changes required. That's what I've done in my example by having the original list of animals already sorted by name. However, if that's not practical for your scenario, you'd probably want to modify the Linq statement below the comment "// Do the grouping and ordering" to either pre-sort as I just described or else post-sort within each group. I didn't do this myself because in general it requires an additional parameter to identify the sort criteria - but it should be pretty easy to handle this in a custom way for any particular application. Hope this helps!

  • Anonymous
    October 22, 2010
    John Zhu, I'm afraid I don't understand your question. :( The sample XAML you provide is exactly how I'd implement multiple lines of data - and it works fine in the sample project (once I added a simple Description field to the Animal class). Can you please be more specific about what you think won't work?

  • Anonymous
    October 23, 2010
    Hi Delay, Doing the multiple lines of data as above would surely work. But I wasn't sure how you would change the code to disable selection of headers? Surely the code below would have to be modified?        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)        {            var contentControl = value as ContentControl;            return (null != contentControl) && contentControl.Content is Animal;        }

  • Anonymous
    October 23, 2010
    my bad, I was able to answer my own question :)

  • Anonymous
    October 25, 2010
    John Zhu, Great! :)

  • Anonymous
    October 25, 2010
    Hello Delay, Sorry to bother you again, but I have a follow-up question. In using an item template as follows:                <delay:GroupingItemsControlConverterParameters.ItemTemplate>                    <DataTemplate>                        <StackPanel Margin="0,0,0,15" Width="432">                            <TextBlock Text="{Binding Name}" TextWrapping="Wrap" Margin="0,0,0,0" Style="{StaticResource PhoneTextNormalStyle}"  FontSize="30"/>                            <TextBlock Text="{Binding Description}" TextWrapping="Wrap" Margin="0,0,0,0" Style="{StaticResource PhoneTextNormalStyle}"  FontSize="24"/>                        </StackPanel>                    </DataTemplate>                </delay:GroupingItemsControlConverterParameters.ItemTemplate> I noticed that the ability to highlight the text foreground in light blue has disappeared, while it was present if a single contentcontrol is used. I was wondering if you know how to enable that again? I know it's doable for a listboxitem like this link (www.uxpassion.com/.../styling-wpf-listbox-highlight-color), but I wasn't sure how it would work in the context of a textbox in a stackpanel in a listboxitem. Thanks.

  • Anonymous
    October 26, 2010
    John Zhu, The accent-color highlighting of ListBox works by changing the Foreground property of the ListBoxItem that's selected - because Foreground inherits down, it's automatically picked up by the ContentControl by default. However, your template uses PhoneTextNormalStyle which is BasedOn PhoneTextBlockBase which sets the Foreground property itself - thereby overriding the inherited accent color. If you remove the Style setters from your Template above, I think you'll get the accent color back. :)

  • Anonymous
    October 27, 2010
    Hello Delay, That worked. Thanks for your input.

  • Anonymous
    October 28, 2010
    Hello Delay, Just FYI. I have noticed that by using the Setter, my listbox rendering slowed down considerably on an actual device. It's fine on the emulator though. After I commented it out, as below, my performance on the device improve noticeably.                <ListBox SelectionChanged="ListBox_SelectionChanged" Name="ListBoxAll"                ItemsSource="{Binding ListByDatesAll, Converter={StaticResource GroupingItemsControlConverter},                    ConverterParameter={StaticResource FancyGroupingItemsControlConverterParameter}}">                                        <!--<ListBox.ItemContainerStyle>                    </ListBox.ItemContainerStyle>-->                </ListBox>

  • Anonymous
    November 07, 2010
    Hello Delay, I have another question about your code. In my original collectoin, I was using an ObserveableCollection<> to keep my items. Things added/removed from this list shows up in the collection properly through data-binding, but I have noticed that gets broken when the IValueConverter is used, possibly because it doesn't return a ObserveableCollection back. I am currently using NotifyPropertyChanged manually, but I think that re-adds the entire collection whenever a single item is added/removed. Therefore, I would like to determine how we can put CollectionChanged back. Do you have any idea?

  • Anonymous
    November 08, 2010
    John Zhu, The problem with ObservableCollection in this scenario (as you suggest) is that CollectionChanged events don't bubble up and re-run the IValueConverter. Forcing that via INotifyProperty changed is a very reasonable workaround, but quite inefficient as you note. Thinking about this briefly, it seems like the IValueConverter could hook up to the INotifyCollectionChanged.CollectionChanged property if it were present and could merge any subsequent changes into its list - but that would be quite a bit more complex than the initial "here's how you could mock this up" sample I originally posted. :) What I'd suggest considering instead is the LongListSelector in the November 2010 release of the Windows Phone Toolkit - it's a control that's designed to support this scenario "properly" (i.e., without an IValueConverter) and performantly. You can read more about it here: blogs.msdn.com/.../mo-controls-mo-controls-mo-controls-announcing-the-second-release-of-the-silverlight-for-windows-phone-toolkit.aspx Hope this helps!

  • Anonymous
    November 14, 2010
    Thanks for the great post! Just like the above user, ListBox Drag and Drop stopped working  when I added your grouping solution. I had implemented drag and drop for ListBox using the same post you mentioned: themechanicalbride.blogspot.com/.../new-with-silverlight-toolkit-drag-and.html

  • Anonymous
    November 14, 2010
    kamran, I'm sorry to hear that! I'll pass the feedback on to teh author of the drag/drop code to see if he knows why this might be. (Aside: If I had to guess, it'd be that the problem arises because the items in the ListBox doesn't exactly match the items in its ItemsSource (due to the custom IValueConverter)...)

  • Anonymous
    November 16, 2010
    Does this work with a dynamic source (instead of hard coded collection of objects.). I'm trying to implement this pattern, but in my case I'm using a CollectionViewSource in XAML and binding it to an ObservableCollection of Objects that I have to call out to a Web Service to get. I think whats happening is that all these Converters are firing before I have the data (due to the asynchronous behavior of getting data in Silverlight). Once the data comes back I don't think the Converters are firing again. Any thoughts?

  • Anonymous
    November 16, 2010
    I forgot to mention that I'm using a MVVM pattern. The VM is my datacontext and has a property with an ObservableCollection of Objects. When I initialize the ViewModel I make a call to get the data and handle the asynchronous response when it comes back.

  • Anonymous
    November 16, 2010
    Shane, It sounds like you're asking a very similar question to John Zhu - if so, then my reply to him (a page or two up) should apply here as well. :)

  • Anonymous
    November 17, 2010
    Delay - I'm reviewing the LongListSelector tool with WP7 Toolkit and it looks cool, but should I be using that with a pure Silverlight solution (meaning not a WP7 solution).

  • Anonymous
    November 17, 2010
    Shanez, LongListSelector was specifically developed for (and tested on) Windows Phone 7; being able to use it on Silverlight was not a goal. There are a variety of aspects of LongListSelector that are specifically intended to address Windows Phone 7 limitations which may not be present on Silverlight 4. That said, I'd expect it should work prett much as-is (or with only a little bit of tweaking), so this might be an interesting scenario to investigate. Please let us know how it goes! :)

  • Anonymous
    November 17, 2010
    Ok - I'll give it a shot and see if I can get it working in SL4. My main goal is to be able to add GroupBy functionality to an ItemsControl in SL4 after I get data back from a web service call (couldn't get the Converter example to work with an asynchronous call to get data). Hoping this works! Looks like a good control.

  • Anonymous
    January 12, 2011
    Is it possible to achieve two levels of grouping with this solution? Thanks

  • Anonymous
    January 12, 2011
    gbelzile, Yes, it should be! Doing so would require some changes to my code (i.e., supporting a collection of collections, perhaps), but the general concept should extend to two levels without issue. :)

  • Anonymous
    May 22, 2011
    Hi David, Can I use this in my Windows Phone project? Because I get an error that said "System.Windows.AssemblyPart does not contain a definition for Load" Thanks.

  • Anonymous
    May 23, 2011
    Veri, Yes, you can use this idea/code on Windows Phone - and I know of others who have done so. :) The sample is a Silverlight 4 project, but the file you really want to reuse is GroupingItemsControlConverter.cs and it doesn't contain any references to AssemblyPart. It sounds like maybe you tried to reuse a part of the sample that was specific to desktop Silverlight - if you add only GroupingItemsControlConverter.cs to your Windows Phone project, you should be fine. Good luck!

  • Anonymous
    June 22, 2011
    Will generating the listbox in this way allow for Blendability?  I can't seem to get my test data to show up in my grid and I'm wondering if Blend just can't put the logic together for this one, or if I've missed something else I'd need to statically define. Thanks.

  • Anonymous
    June 23, 2011
    Brandon, I think Blend runs IValueConverters on the design surface, so I'd expect this to work. What I'd recommend is removing the GroupingItemsControlConverter to be sure everything else is hooked up right in order to show the test data at design-time; sometimes that can be a little tricky to get right. Once that's working, drop in the GroupingItemsControlConverter and things should work. If not, then try running the app to verify that the run-time behavior is correct - if that doesn't work, then something's not hooked up right which is probably why it's not working. Hope this helps!

  • Anonymous
    June 23, 2011
    Thanks David, I'll give it a shot, it works beautifully runtime, but I'd just like to get Blend to show it to tweak my interface.  I'll try out your suggestions right now.

  • Anonymous
    June 23, 2011
    Wow, seriously, I completely left out the field I'm grouping on from my test data, sorry for wasting your time!  Thank you very much for your help though!

  • Anonymous
    June 23, 2011
    Brandon, No worries - glad you've got it working! :)

  • Anonymous
    December 20, 2012
    Great post. thanks for source code and explanation. David, I implemented the GroupConverter and have one issue. The point were you set the data context, your ex it was a static array. In my case a async job returns an object and I parse throw to make an array of similar structure. But the groupconverter do not wait for the object and fails to show anything. Can you suggest something in this case.

  • Anonymous
    December 20, 2012
    The comment has been removed

  • Anonymous
    December 21, 2012
    Thanks for the suggestion. there was a custom class in middle I changed it and not its working good. David, Can you suggest how can I implement a expand collapse feature on the group headers. Thanks.

  • Anonymous
    December 23, 2012
    The comment has been removed

  • Anonymous
    December 05, 2013
    Hello David, Thank you for your post. I've got a different scenario over here.. collectionViewSource has groups after propertydescriptions have been given. I have few null groups also formed. How can I take the items of the null group and tag them with the group higher than that? Regards, John

  • Anonymous
    December 06, 2013
    John, What I think I would do in your scenario is introduce another property on the view model object which provides a custom property for GroupDescription that returns the same value as whatever you're using now EXCEPT that it maps the problematic null group items into the groups you want them to live in by returning the corresponding value.