Listbox live sort using CollectionViewSource on ObservableCollection - c#

I would like to enable live sorting of ListBox items bound to an ObservableCollection; I would also like to enable the live sorting exclusively via my XAML markup (if possible). As things stand, the list is properly sorted on application startup, but items are simply appended (not sorted) to the ListBox when new items are added to the ObservableCollection.
In my viewmodel I have the following public property:
public ObservableCollection<Equipment> EquipmentList { get; set; }
Equipment is an auto-generated class from Entity Framework which contains a public string property named 'Description'. This is my sort target.
My XAML has the following DataTemplate which is intended to enable the live sorting:
<DataTemplate x:Key="EquipmentDescriptionTemplate"
DataType="{x:Type e:Equipment}">
<DataTemplate.Resources>
<CollectionViewSource x:Key="SortedEquipmentList"
Source="{Binding Path=Description,
Mode=OneWay,
UpdateSourceTrigger=PropertyChanged}"
IsLiveSortingRequested="True">
<CollectionViewSource.SortDescriptions>
<scm:SortDescription PropertyName="Description"
Direction="Ascending"/>
</CollectionViewSource.SortDescriptions>
<CollectionViewSource.LiveSortingProperties>
<clr:String>Description</clr:String>
</CollectionViewSource.LiveSortingProperties>
</CollectionViewSource>
</DataTemplate.Resources>
<TextBlock Text="{Binding Path=Description}" />
</DataTemplate>
And finally the XAML ListBox item:
<ListBox x:Name="EquipmentList"
ItemsSource="{Binding Path=EquipmentList, UpdateSourceTrigger=PropertyChanged, Mode=OneWay}"
ItemTemplate="{StaticResource EquipmentDescriptionTemplate}"
IsSynchronizedWithCurrentItem="True"
SelectedItem="{Binding EquipmentSelection, UpdateSourceTrigger=PropertyChanged}"
Grid.ColumnSpan="2" Grid.Row="1" Margin="5,5,5,5"/>
There's a lot of extra attributes which I added in my desperate attempts to get the live sorting to work (when in doubt, guess wildly!). I've left them in so folks can see what I've tried, and snicker.
How do I enable live ListBox sorting via XAML?

You'll want to bind the ItemsSource to the CollectionViewSource, not to the underlying collection:
<ListBox x:Name="EquipmentList"
ItemsSource="{StaticResource SortedEquipmentList}"
...
/>
Place the CollectionViewSource somewhere above, in your page's resource dictionary. Bind its source to the underlying collection ("EquipmentList"):
<CollectionViewSource x:Key="SortedEquipmentList"
Source="{Binding EquipmentList.View}"
IsLiveSortingRequested="True">
<CollectionViewSource.SortDescriptions>
<scm:SortDescription PropertyName="Description" Direction="Ascending"/>
</CollectionViewSource.SortDescriptions>
<CollectionViewSource.LiveSortingProperties>
<clr:String>Description</clr:String>
</CollectionViewSource.LiveSortingProperties>
</CollectionViewSource>

Related

Sorting Horizontal ListView

I want a list view to sort based on an Object its bounded to
<ListView x:Name="ListView1" Grid.Row="2" Grid.Column="2" VirtualizingStackPanel.IsVirtualizing="True" ScrollViewer.CanContentScroll="False" ItemsSource="{Binding Path=CurrentProductsImages}">
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ListView.ItemsPanel>
<ListView.ItemTemplate>
<DataTemplate>
<Border BorderBrush="Black" BorderThickness="0,0,2,0" >
<Image Source="{Binding Path=Data, Converter={StaticResource ImageSourceConverter}}" RenderOptions.BitmapScalingMode="Fant" Height="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ListView} },Path=ActualHeight, Converter={StaticResource HeightMinusConverter}}" />
</Border>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
So the Binded Item CurrentProductsImages has a variable called "DisplayOrder"
I want to order the Listview off this value but can't find a way. Most vertical methods of sorting use a gridview but as mines stacked horizontally i cant get it to work?
I can sort the object prior to binding but i wanted it to be more on the WPF side. any ideas? Many thanks
The xaml way of sorting would be to use a collectionviewsource. That has sortdescriptions.
<CollectionViewSource x:Key="SortedItems" Source="{Binding UnsortedItems}" >
<CollectionViewSource.SortDescriptions>
<scm:SortDescription PropertyName="Blaa" />
<scm:SortDescription PropertyName="Blaa2" />
</CollectionViewSource.SortDescriptions>
</CollectionViewSource>
The problem being you can't bind those sortdescriptions.
You could potentially do something with a behaviour or custom frameworkelement. That would allow you to write a bunch of code and kind of make this work in the UI. It'd be more complicated than doing the sorting in the viewmodel though.
I suggest you instead add a collectionview to your viewmodel and handle the sorting there in code. You can define a custom icomparer to use or just clear and add sortdescriptions.
Or sort using Linq if that is better suited to your requirement / skillset.
Examples of sorting (and doing other stuff ) with collections:
https://social.technet.microsoft.com/wiki/contents/articles/26673.wpf-collectionview-tips.aspx
There's a working sample linked.
Esentially, with an observablecollection of People, this is bound using:
public CollectionView PeopleView {get; set;}
Set up:
CollectionViewSource cvs = new CollectionViewSource();
cvs.Source = People;
PeopleView = (CollectionView)cvs.View;
Sorted:
PeopleView.SortDescriptions.Clear();
PeopleView.SortDescriptions.Add(new SortDescription("OrganizationLevel", ListSortDirection.Ascending));
PeopleView.SortDescriptions.Add(new SortDescription("LastName", ListSortDirection.Ascending));
PeopleView.SortDescriptions.Add(new SortDescription("FirstName", ListSortDirection.Ascending));

Select different bindings in DataContext based on another binding

I have a fairly dynamic ObservableCollection of view models that is used by two different ListBox elements in XAML. Each view model contains properties for two different model objects of type Card called Primary and Secondary, as well as other properties. In one ListBox I'd like to display properties from Primary and in the other I'd like to display properties from Secondary. I'd like to use the same XAML UserControl file when displaying the ListBoxItems for both.
My first thought was to create an entry in UserControl.Resources that gives a name to the "right" card based on a RelativeSource reference from the parent view model which indicates Primary or Secondary, but I've not created an entry like that before. Is this the right approach? If so, what would the entry look like?
I've made up some XAML to help illustrate (may have typos). First, the Primary ListBox control:
<UserControl x:Class="Project.Cards.ListPrimary" d:DataContext="{Binding Main.Cards.Primary, Source={StaticResource Locator}}">
<UserControl.Resources>
<ResourceDictionary>
<DataTemplate DataType="{x:Type vms:CardViewModel}">
<views:Card />
</DataTemplate>
</ResourceDictionary>
</UserControl.Resources>
<Grid>
<ListBox x:Name="CardListBox"
SelectedItem="{Binding SelectedCard}"
ItemsSource="{Binding Cards}" />
</Grid>
</UserControl>
And the secondary:
<UserControl x:Class="Project.Cards.ListSecondary" d:DataContext="{Binding Main.Cards.Secondary, Source={StaticResource Locator}}">
... (same) ...
</UserControl>
And the card view (where I need to replace "Primary.Direction" with something that lets me select Primary/Secondary):
<UserControl x:Class="Project.Cards.Card">
<UserControl.Resources>
... perhaps something here ...
</UserControl.Resources>
<StackPanel>
<TextBlock Text="{Binding Name}" />
<TextBlock Text="{Binding Description}" />
<TextBlock Text="{Binding Primary.Direction}" />
</StackPanel>
</UserControl>
If you want two instances of the same UserControl that differ in one respect, you figure out how to parameterize that. There are a couple of ways, but the simplest I thought of that fits your case was to just bind the differing value to a property of the View. This moves the specification of the different value to the owner.
We'll do that by defining a dependency property on the UserControl. It's a string, though it could be an object, and in the future you might want to make it one. Since we're using the view in a DataTemplate, we can bind a property of the DataContext to it there.
public partial class Card : UserControl
{
public Card()
{
InitializeComponent();
}
public String Direction
{
get { return (String)GetValue(DirectionProperty); }
set { SetValue(DirectionProperty, value); }
}
public static readonly DependencyProperty DirectionProperty = DependencyProperty.Register("Direction",
typeof(String), typeof(Card), new PropertyMetadata(null));
}
...and we'll use that in the UserControl like this:
<Grid>
<StackPanel>
<TextBlock Text="{Binding Name}" />
<TextBlock Text="{Binding Description}" />
<TextBlock
Text="{Binding Direction, RelativeSource={RelativeSource AncestorType=UserControl}}"
/>
</StackPanel>
</Grid>
The RelativeSource stuff tells the Binding to look for that Direction property on the UserControl object itself, rather than on the DataContext as it would otherwise do by default.
If Card.Direction were object instead of string, you'd make that TextBox a ContentControl and bind to its Content property. Then you could put anything in there -- XAML, a whole other viewmodel, literally anything that XAML can figure out how to display.
And here's how it looks in the wild:
<DataTemplate DataType="{x:Type vms:CardViewModel}">
<views:Card Direction="{Binding Primary.Direction}" />
</DataTemplate>
And here's my whole mainwindow content from my test code. I didn't bother creating user controls for the listboxes; the above template is an exact match for the way you're doing it.
<Window.Resources>
<DataTemplate x:Key="PrimaryItemTemplate" DataType="{x:Type vms:CardViewModel}">
<views:Card Direction="{Binding Primary.Direction}" />
</DataTemplate>
<DataTemplate x:Key="SecondaryItemTemplate" DataType="{x:Type vms:CardViewModel}">
<views:Card Direction="{Binding Secondary.Direction}" />
</DataTemplate>
</Window.Resources>
<Grid>
<StackPanel Orientation="Horizontal">
<ListBox
SelectedItem="{Binding SelectedCard}"
ItemsSource="{Binding Cards}"
ItemTemplate="{StaticResource PrimaryItemTemplate}"
/>
<ListBox
SelectedItem="{Binding SelectedCard}"
ItemsSource="{Binding Cards}"
ItemTemplate="{StaticResource SecondaryItemTemplate}"
/>
</StackPanel>
</Grid>
I originally thought of a more elaborate scheme where you give the view a DataTemplate instead, and it worked, but this is simpler. On the other hand, that was more powerful. I actually used that in the first version of the answer, before I came to my senses; it's in the edit history.
Thanks for a fun little projectlet.

Combobox not showing groupings

I'm trying to organize the items in a combobox into groups. To do this I've created an object that has project and group name strings. I then set the GroupStyle and ItemTemplate to display these values. However, Currently, only the project string is displayed in the combobox (and the box has a red border, indicating some kind of error).
Here's the xaml for my combobox:
<ComboBox x:Name="comboBoxProjects" Margin="165,90,28,0" Grid.Column="0" VerticalAlignment="Top" Height="25"
IsSynchronizedWithCurrentItem="True" SelectedIndex="0" Style="{StaticResource ComboBoxDefault}"
ItemsSource="{Binding Path=ProjectClientSelections.ProjectGroupItems,Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
SelectedItem="{Binding Path=ProjectClientSelections.SelectedProject, UpdateSourceTrigger=PropertyChanged}">
<ComboBox.GroupStyle>
<GroupStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate>
<TextBlock Text="{Binding GroupName}"/>
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
</ComboBox.GroupStyle>
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Project}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
Does anyone see where I'm going wrong?
In GroupStyle, the DataContext is not your item (the type contained in your ItemsSource), but a CollectionViewGroup object, which is formed based on the collection of items that you have grouped. Because of this you have to declare a binding path to one of the properties in CollectionViewGroup, for example, based on your code you probably want to use Name property. See MSDN CollectionViewGroup Class
Change your GroupStyle.HeaderTemplate to this:
<DataTemplate>
<TextBlock Text="{Binding Name}" />
</DataTemplate>
You don't show how you have formed your GroupDescriptions. If you have not grouped the items already, you can do it in following way (assuming the XAML you have provided is contained inside Window and Window's and GroupBox's DataContext is the same):
<Window.Resources>
<CollectionViewSource
Source="{Binding ProjectClientSelections.ProjectGroupItems}"
x:Key="GroupedProjectItems">
<CollectionViewSource.GroupDescriptions>
<PropertyGroupDescription
PropertyName="GroupName" />
</CollectionViewSource.GroupDescriptions>
</CollectionViewSource>
</Window.Resources>
After this change GroupBox ItemSource binding to the following (directly to CollectionViewSource resource):
ItemsSource="{Binding Source={StaticResource GroupedProjectItems}}"

ComboBox with grouping and custom group headers

I would like to create a grouped ComboBox with a group header which is bound to not only the group's name, but other properties of the values by which the 'ItemsSource' of the ComboBox is grouped as well.
This is the CollectionViewSource which is used as the ComboBoxes ItemsSource:
<CollectionViewSource x:Key="Tools" Source="{Binding AvailableTools}">
<CollectionViewSource.GroupDescriptions>
<PropertyGroupDescription PropertyName="Toolbox" />
</CollectionViewSource.GroupDescriptions>
</CollectionViewSource>
The property by which I group, is not a string property, but an object of a custom type. The CollectionViewSource obviously creates groups and gives each group the name of [object by which is group].ToString(), yielding the type name of the property Toolbox in my example.
Here's the combo:
<ComboBox
SelectedItem="{Binding SelectedTool, Mode=TwoWay}"
Margin="10,0,0,0"
Width="40"
ItemsSource="{Binding Source={StaticResource Tools}}">
<ComboBox .GroupStyle>
<GroupStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate DataType="CollectionViewGroup">
<Grid Background="{StaticResource LighterBackgroundBrush}">
<TextBlock Text="{Binding Name}"/>
</Grid>
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
</ComboBox .GroupStyle>
<ComboBox.ItemTemplate>
<DataTemplate DataType="viewModels1:ToolSelectionItem">
<StackPanel Orientation="Horizontal">
<Image Source="{Binding Tool.ToolIcon}" Width="16" />
<TextBlock Text="{Binding ToolName}" Margin="10,0,0,0"/>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
In the header template, I can bind to the groups name but nothing else. What I would like to achieve, is a binding to the object by which a group was created and not just it's ToString() result. I.e. the values of the Toolbox property by which I group contain an Icon property of type BitmapImage which I would like to bind to an Image inside the header template to display this icon in the group header. Can this be done at all and if so, how?
The issue is a bit difficult to explain, please ask, if I am not being clear enough...

Sort an editable ComboBox in XAML

I would like to sort an editable combobox in pure WPF, after the addition of a new record. After some research I found some tips, which used a CollectionViewSource.SortDescription, but it does not work for me correctly. What am I doing wrong? The DataContext of the item which used my DataTemplate works fine, but the binding between the DataTemplate and the resource part to sort my entry list doesn't.
My XAML Part
<DataTemplate x:Key="Document">
<DataTemplate.Resources>
<CollectionViewSource x:Key="SortedLabels" Source="{Binding Parent.Labels}">
<CollectionViewSource.SortDescriptions>
<scm:SortDescription PropertyName="Items"/>
</CollectionViewSource.SortDescriptions>
</CollectionViewSource>
</DataTemplate.Resources>
<Grid>
...
<ComboBox Name="cbLabel"
ItemsSource="{Binding Source={StaticResource SortedLabels}}"
IsEditable="True"
LostFocus="cbLabel_LostFocus"
KeyUp="cbLabel_KeyUp"
Visibility="{Binding Path=IsUndiscovered, Converter={StaticResource ResourceKey=BooleanToVisibilityConverter}}"/>
...
</Grid>
</DataTemplate>
EDIT
The Collection Parent.Labels are of type ObservableCollection<String>.
Hard to say since you didn't show what the collection is you're trying to sort, but my guess is that each object in Parent.Labels doesn't have a Items property. The PropertyName refers so a property on the individual objects in the list that it will look at and try to sort. I'd guess you want something more along the lines of "Name" or some other property.

Categories

Resources