CollectionViewSource sorting only the first time it is bound to a source
I'm using a DataGrid bound to a CollectionViewSource (players), itself bound to the currently selected item of a ListBox (levels), each item containing a collection to be sorted/displayed in the DataGrid:
<ListBox Name="lstLevel"
DisplayMemberPath="Name"
IsSynchronizedWithCurrentItem="True" />
...
<!-- DataGrid source, as a CollectionViewSource to allow for sorting and/or filtering -->
<CollectionViewSource x:Key="Players"
Source="{Binding ElementName=lstLevel,
Path=SelectedItem.Players}">
<CollectionViewSource.SortDescriptions>
<scm:SortDescription PropertyName="Name" />
</CollectionViewSource.SortDescriptions>
<开发者_JAVA百科/CollectionViewSource>
...
<DataGrid Name="lstPlayers" AutoGenerateColumns="False"
CanUserSortColumns="False"
ItemsSource="{Binding Source={StaticResource Players}}">
<DataGrid.Columns>
<DataGridTextColumn Header="Name"
Binding="{Binding Path=Name, Mode=TwoWay}"
Width="*" />
<DataGridTextColumn Header="Age"
Binding="{Binding Path=Age, Mode=TwoWay}"
Width="80">
</DataGridTextColumn>
</DataGrid.Columns>
</DataGrid>
(whole C# code here, XAML code here, entire test project here - in addition to the DataGrid I've added a simple ListBox for the players, to make sure it wasn't a DataGrid issue)
The problem is that the players are sorted the first time they are shown, but as soon as I select another level from the ListBox, they are not sorted anymore. Also, modifying names the first time players are shown will sort them accordingly to the changes, but not anymore once the level has been changed.
So it looks like changing the source of the CollectionViewSource somehow breaks the sort feature, but I have no idea why, nor how to fix it. Does anyone know what I'm doing wrong?
(I did a test with a filter, but that one kept working as expected)
The framework is .NET 4.
Great question and an interesting observation. Upon closer inspection, it appears that the DataGrid clears sort descriptions of a previous ItemsSource before a new one is set. Here is its code for OnCoerceItemsSourceProperty:
private static object OnCoerceItemsSourceProperty(DependencyObject d, object baseValue)
{
DataGrid grid = (DataGrid) d;
if ((baseValue != grid._cachedItemsSource) && (grid._cachedItemsSource != null))
{
grid.ClearSortDescriptionsOnItemsSourceChange();
}
return baseValue;
}
This behavior only happens on a DataGrid. If you used a ListBox instead (to display the "Players" collection above), the behavior will be different and the SortDescriptions will still remain after selecting different items from the parent datagrid.
So I guess the solution to this is to somehow re-apply the sort descriptions of the Players collection whenever the selected item in the parent DataGrid (i.e. "lstLevel") changes.
However, I'm not 100% sure about this and probably needs more testing/investigation. I hope I was able to contribute something though. =)
EDIT:
As a suggested solution, you can put a handler for lstLevel.SelectionChanged in your constructor, before setting the lstLevel.ItemsSource property. Something like this:
lstLevel.SelectionChanged +=
(sender, e) =>
{
levels.ToList().ForEach((p) =>
{
CollectionViewSource.GetDefaultView(p.Players)
.SortDescriptions
.Add(new SortDescription("Name", ListSortDirection.Ascending));
});
};
lstLevel.ItemsSource = levels;
EDIT2:
In response to the problems you're encountering with regards to keyboard navigation, I suggest that instead of handling the "CurrentChanged" event, you handle the lstLevel.SelectionChanged event instead. I'm posting the necessary updates you need to make below. Just copy-paste to your code and see if it works fine.
XAML:
<!-- Players data, with sort on the Name column -->
<StackPanel Grid.Column="1">
<Label>DataGrid:</Label>
<DataGrid Name="lstPlayers" AutoGenerateColumns="False"
CanUserSortColumns="False"
ItemsSource="{Binding ElementName=lstLevel, Path=SelectedItem.Players}">
<DataGrid.Columns>
<DataGridTextColumn Header="Name"
Binding="{Binding Path=Name, Mode=TwoWay}"
Width="*" />
<DataGridTextColumn Header="Age"
Binding="{Binding Path=Age, Mode=TwoWay}"
Width="80">
</DataGridTextColumn>
</DataGrid.Columns>
</DataGrid>
</StackPanel>
<StackPanel Grid.Column="2">
<Label>ListBox:</Label>
<ListBox ItemsSource="{Binding ElementName=lstLevel, Path=SelectedItem.Players}" DisplayMemberPath="Name" />
</StackPanel>
Code-behind (constructor):
lstLevel.SelectionChanged +=
(sender, e) =>
{
levels.ToList().ForEach((p) =>
{
CollectionViewSource.GetDefaultView(p.Players)
.SortDescriptions
.Add(new SortDescription("Name", ListSortDirection.Ascending));
});
};
lstLevel.ItemsSource = levels;
A better workaround: CollectionViewSource sorting only the first time it is bound to a source
Implement your own DataGrid:
public class SDataGrid : DataGrid { static SDataGrid() { ItemsControl.ItemsSourceProperty.OverrideMetadata(typeof(SDataGrid), new FrameworkPropertyMetadata((PropertyChangedCallback)null, (CoerceValueCallback)null)); } }
The only thing coerce callback does in the current implementation is clear the sort descriptions. You can simply "cut" this code by overriding metadata. Not viable on Silverlight: OverrideMetadata API is not public. Though I'm not sure Silverlight is affected by this bug. Other risks and side effects may apply.
I was able to fix this by simply calling PropertyChanged on the property that exposes the view, letting the view refresh (and clear out the sort) and then adding the sort descriptions.
精彩评论