CollectionViewSource在与数据源绑定时仅第一次排序。

20

我正在使用一个绑定到CollectionViewSource(players)的DataGrid,CollectionViewSource(players)本身绑定到ListBox(levels)当前选择的项,每个项包含一个要在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>
</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>

(完整的 C# 代码在这里,XAML 代码在这里,整个测试项目在这里 - 除了 DataGrid,我还添加了一个简单的 ListBox 来显示玩家信息,以确保问题不是出在 DataGrid 上)

问题是当玩家数据首次展现时它们已经排序,但是只要我从 ListBox 中选择另一个关卡,它们就不再排列。此外,在首次展示玩家并修改名称后,它们将根据更改排序,但是一旦更改了关卡,就不再工作。

所以看起来更改 CollectionViewSource 的来源会破坏排序功能,但我不知道为什么,也不知道如何解决。有人知道我做错了什么吗?

(我进行了筛选测试,但那个测试一直按预期工作)

框架是 .NET 4。


我以前也遇到过同样的问题——你能否不每次都创建一个新对象,而是删除并重新插入其内容呢? - Dave
除了额外的工作,那会通过创建/释放对象不必要地分割托管堆,如果可以的话,我宁愿避免它。 - RedGlyph
3个回答

16

好问题,有趣的观察。仔细查看后,似乎DataGrid在设置新的ItemsSource之前会清除先前ItemsSource的排序描述。以下是它在OnCoerceItemsSourceProperty中的代码:

private static object OnCoerceItemsSourceProperty(DependencyObject d, object baseValue)
{
    DataGrid grid = (DataGrid) d;
    if ((baseValue != grid._cachedItemsSource) && (grid._cachedItemsSource != null))
    {
        grid.ClearSortDescriptionsOnItemsSourceChange();
    }
    return baseValue;
}
这种行为只会在DataGrid上发生。如果您使用ListBox(用于在上面显示“Players”集合),行为将是不同的,并且在选择父DataGrid中的不同项后,SortDescriptions仍将保留。
因此,我想这个问题的解决方法是在父DataGrid(即“lstLevel”)更改选定项时,以某种方式重新应用Players集合的排序描述。
但是,我并不确定这一点,可能需要进行更多的测试/调查。希望我能做出一些贡献。
编辑:作为一个建议的解决方案,您可以在构造函数中为lstLevel.SelectionChanged添加处理程序,然后设置lstLevel.ItemsSource属性。类似这样:
lstLevel.SelectionChanged +=
    (sender, e) =>
    {
        levels.ToList().ForEach((p) =>
        {
            CollectionViewSource.GetDefaultView(p.Players)
                .SortDescriptions
                .Add(new SortDescription("Name", ListSortDirection.Ascending));
        });
    };

lstLevel.ItemsSource = levels;

编辑2:

针对您在键盘导航方面遇到的问题,我建议您不要处理“CurrentChanged”事件,而是处理lstLevel.SelectionChanged事件。我在下面发布了您需要进行的必要更新。只需将其复制粘贴到您的代码中,看看是否正常工作即可。

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>

代码后台(构造函数):

lstLevel.SelectionChanged +=
    (sender, e) =>
    {
        levels.ToList().ForEach((p) =>
        {
            CollectionViewSource.GetDefaultView(p.Players)
                .SortDescriptions
                .Add(new SortDescription("Name", ListSortDirection.Ascending));
        });
    };
lstLevel.ItemsSource = levels;

++,非常有见地!我添加了一个单独的ListBox,因为我不信任DataGrid,但我没有尝试在没有DataGrid的情况下单独测试ListBox。我使用这个控件的时间越长,它就越古怪(最近2天已经报告了3个错误)。我会从建议开始并让您知道。谢谢! - RedGlyph
1
进行了一些修改(原来的代码无法正常工作,但很接近),并更新了问题,附上解决方法。再次感谢! - RedGlyph
很高兴能帮忙。是的,我发现DataGrid在幕后清除了SortDescriptions,这让我感到相当惊讶。我不太确定为什么会这样,但可能有一个很好的原因...或者,也许这是一个错误。=) - ASanch
小更新,解决方法似乎破坏了(或未能修复)列表中的键盘导航,只有通过用鼠标选择它们才能到达其中的某些行... - RedGlyph
我认为在CurrentChanged事件中添加处理程序并不是一个好的解决方案,因为每次更改子DataGrid(即lstPlayers)中的选定项时都会触发该事件。实际上,您只需要在父DataGrid(lstLevel)中的选定项更改时更新SortDescriptions。 - ASanch
没错,我写那段代码的时候可能已经很晚了;-)你的代码运行得非常好,再次感谢!因此,使用ListCollectionView而不是中间的CollectionViewSource似乎表现得更好。 - RedGlyph

5
一个更好的解决方法:CollectionViewSource排序仅在第一次绑定到源时进行。该链接提供了有关如何解决与CollectionViewSource的排序问题的详细说明。

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.


尽管有报告称此解决方案无法工作,但在我的代码中它似乎能够胜任其工作。 - linac
1
通常微软的链接已经失效了。 :/ - Seanba

5

我能够通过在暴露视图的属性上简单地调用PropertyChanged来解决此问题,让视图刷新(并清除排序),然后添加排序描述。


这对我也有效。最简单的解决方案,没有任何代码后端的麻烦。谢谢! - Anttu

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接