You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

数据虚拟化:通知DataGrid更新时抛出「指定方法不支持」异常

问题解答:WPF DataGrid虚拟异步数据源异常与重复访问问题

问题背景

要构建一个从异步源获取数据的随机访问虚拟数据源,目前正在开发基础验证原型——从文件读取数据并在DataGrid中展示,为此自定义了实现IList和INotifyCollectionChanged接口的BufferRandomAccessVirtualiser集合类。

相关代码

XAML中的DataGrid定义

<controls:DataGrid controls:DockPanel.Dock="Right" x:Name="HexDump" ItemsSource="{Binding DataRows, Mode=OneWay}" AutoGenerateColumns="false" CanUserReorderColumns="False" CanUserResizeColumns="False" CanUserSortColumns="False" DataFetchSize="32">
    <controls:DataGrid.Columns>
        <controls:DataGridTextColumn Header="Addr" Binding="{Binding startAdr, Converter={StaticResource x4Converter}}"/>
        <controls:DataGridTextColumn Header="Bytes" Binding="{Binding data, Converter={StaticResource x2Converter}}"/>
        <controls:DataGridTextColumn Header="Chars" Binding="{Binding data, Converter={StaticResource cConverter}}"/>
    </controls:DataGrid.Columns>
</controls:DataGrid>

BufferRandomAccessVirtualiser部分实现

public class BufferRandomAccessVirtualiser : IList, INotifyCollectionChanged 
{
    // INotifyCollectionChanged impl
    public event NotifyCollectionChangedEventHandler CollectionChanged;

    // IList impl
    Dictionary<int, BufferedBlock> tempBlocks = new Dictionary<int, BufferedBlock>();

    public BufferedBlock this[int index] {
        get {
            if (index >= Count) throw new IndexOutOfRangeException();

            if (tempBlocks.ContainsKey(index)) {
                Debug.Write(index); Debug.Write("& ");
                return tempBlocks[index]; 
            }

            Debug.Write(index); Debug.Write("? ");
            var oldItem = tempBlocks[index] = new BufferedBlock(); // placeholder
            loadRowsTask.ContinueWith(rowsResult => { 
                var row = index % rowsPerPage;
                var newItem = new BufferedBlock(rowsResult[row]);
                
                Debug.Write(index); Debug.Write("<");
                tempBlocks[index] = newItem;
                CollectionChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, newItem, oldItem, index));
                Debug.Write("> ");
            });
            return tempBlocks[index];
        } 
        set => throw new NotImplementedException();
    }

    // other parts of IList impl - just boilerplate really
}

遇到的问题

  • 初始化阶段,索引0的元素被反复调用,调试输出显示多次访问;
  • 触发CollectionChanged的Replace动作时,DataGrid调用索引器验证新值后抛出System.NotSupportedException(提示「指定方法不支持」),移除所有列后问题仍存在,已排除绑定转换器问题。

一、System.NotSupportedException异常原因

核心问题

  1. IList接口实现不全:DataGrid在处理Replace类型的集合变更通知时,会调用IList的Contains、IndexOf等方法做验证。如果这些方法是抛出NotImplementedException的样板代码,就会触发该异常。
  2. 跨线程访问问题:异步任务的ContinueWith默认在非UI线程执行,直接触发CollectionChanged事件属于跨线程操作UI控件,会引发隐藏异常,也可能间接导致不支持的操作提示。

修复方案

  • 完善IList核心方法:至少实现Contains和IndexOf方法,返回合理值而非抛出异常:
    public bool Contains(object value)
    {
        if (value is BufferedBlock block)
        {
            return tempBlocks.Values.Contains(block);
        }
        return false;
    }
    
    public int IndexOf(object value)
    {
        if (value is BufferedBlock block)
        {
            foreach (var pair in tempBlocks)
            {
                if (pair.Value == block) return pair.Key;
            }
        }
        return -1;
    }
    
  • 切换到UI线程触发变更事件:使用Dispatcher确保CollectionChanged在UI线程执行:
    loadRowsTask.ContinueWith(rowsResult => {
        var row = index % rowsPerPage;
        var newItem = new BufferedBlock(rowsResult[row]);
        
        Application.Current.Dispatcher.Invoke(() => {
            tempBlocks[index] = newItem;
            CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, newItem, oldItem, index));
        });
    });
    

二、初始化时反复调用索引0的原因

核心原因

  1. DataGrid布局测量需求:初始化时,DataGrid会多次访问索引0的元素——第一次用于测量行高,第二次用于验证数据源有效性;
  2. 占位符无属性变更通知:返回的空BufferedBlock未实现INotifyPropertyChanged,DataGrid无法感知到后续数据更新,会重复调用索引器确认值是否变化;
  3. 异步加载时序问题:首次返回占位符后,若变更事件触发不及时,DataGrid会再次请求索引0的数据。

修复方案

  • 让BufferedBlock实现INotifyPropertyChanged接口,异步加载完成后更新属性并触发PropertyChanged事件,而非直接替换整个对象。这样DataGrid会通过属性变更更新UI,无需反复调用索引器,也能避免集合Replace动作带来的验证问题。

额外注意事项

  • 你使用的DataGrid带有DataFetchSize属性,属于自定义虚拟化DataGrid(如WPF Toolkit版本),需确认控件文档中对虚拟数据源的要求,是否需要额外实现ICollectionView等接口;
  • 在索引器中添加加载状态标记,避免同一索引被多次触发异步加载逻辑,防止重复请求数据。

内容的提问来源于stack exchange,提问作者Jules May

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 00:13:14