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

WPF绑定ObservableCollection到ListView出现闪烁、滚动重置问题

问题描述

我将包含约190个条目的ObservableCollection绑定至ListView控件,所有条目数据通过WebSocket持续推送实时更新,目前遇到两类异常表现:

  • 鼠标在列表项上移动悬停时,ListView出现明显闪烁;
  • 使用键盘操作滚动列表时,每一次数据更新后列表都会自动跳转回首项位置。

我最初推测问题诱因是每次收到更新时都重新创建新的ObservableCollection实例,导致ListView经历“完整加载→清空(对应闪烁现象)→重新加载完整列表”的过程,但实际测试发现:点击选中某一项后,该项焦点可稳定保持、无闪烁现象,且数据可正常更新。以下是我的实现代码:

ViewModel代码

public List<Tickers> ListTickers = new List<Tickers>();

private WSFuturesResponse _futuresResponse;

public WSFuturesResponse FuturesResponse
{
    get
    {
        return _futuresResponse;
    }
    set
    {
        _futuresResponse = value;
        OnPropertyChanged("FuturesResponse");
        foreach (var future in ListTickers)
        {
            if (future.Market == FuturesResponse.market)
            {
                future.Price = FuturesResponse.data.last;
            }
        }
        Tickers = new ObservableCollection<Tickers>(ListTickers);
    }
}
private ObservableCollection<Tickers> _tickers;
public ObservableCollection<Tickers> Tickers
{
    get
    {
        return _tickers;
    }
    set
    {
        _tickers = value;
        OnPropertyChanged("Tickers");
    }
}
// 填充ListTickers的方法
private async void StartConnection(Ticker wsApi, Client client, FtxRestApi api)
{
    // 从API获取所有合约数据
    var futures = await api.GetAllFuturesAsync();
    // 解析数据
    ApiFuturesResponse all_futures = JsonConvert.DeserializeObject<ApiFuturesResponse>(futures);

    wsApi.OnWebSocketConnect += () =>
    {
        wsApi.SendCommand(FtxWebSocketRequestGenerator.GetAuthRequest(client));
        foreach (ApiFuturesData future in all_futures.result)
        {
            if (future.name.Contains("PERP"))
            {
                ListTickers.Add(new Tickers(future.name));
                wsApi.SendCommand(FtxWebSocketRequestGenerator.GetSubscribeRequest("ticker", future.name));
            }
        }
    };

    await wsApi.Connect();
}

// 处理属性更新的消息接收事件
public void WebsocketOnMessageReceive(object o, MessageReceivedEventArgs messageReceivedEventArgs)
{
    FuturesResponse = JsonConvert.DeserializeObject<WSFuturesResponse>(messageReceivedEventArgs.Message);
}

XAML代码

<ListView Grid.Row="2"
          Grid.Column="1"
          ItemsSource="{Binding Tickers}">
    <ListView.ItemTemplate>
        <DataTemplate>
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="*"/>
                    <ColumnDefinition Width="*"/>
                    <ColumnDefinition Width="*"/>
                </Grid.ColumnDefinitions>
                <TextBlock x:Name="TextBlockMarket" Grid.Column="0" Width="100" Text="{Binding Market}"/>
                <TextBlock x:Name="TextBlockPrice" Grid.Column="1" Width="100" Text="{Binding Price}"/>
            </Grid>
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>
根本原因

问题核心就是每次WebSocket收到推送时,你都为Tickers属性赋值了全新的ObservableCollection实例:

  • 当ListView检测到ItemsSource被整体替换为新集合时,会先销毁所有已生成的列表项容器,再根据新集合重新生成全部190个条目容器,销毁+重建的过程就是闪烁的直接来源。
  • 容器重建完成后,ListView默认会将滚动位置重置到列表顶部。你观察到选中项时不跳回顶部、也无明显闪烁,是WPF的默认选中项保活逻辑在生效——重建完成后会自动将选中项滚入可视范围,跳过了顶部渲染阶段,才造成了“选中就正常”的假象。
  • 你之所以选择每次重建集合,本质是因为你的Tickers类没有实现INotifyPropertyChanged接口:你在替换集合前虽然已经修改了对应条目的Price值,但UI无法收到属性变更通知,不会主动刷新价格显示,才让你误以为必须重建整个集合才能更新数据。
修复方案
  1. 让Tickers类实现INotifyPropertyChanged接口,属性修改时主动触发变更通知,无需靠重建集合刷新UI:
    public class Tickers : INotifyPropertyChanged
    {
        private string _market;
        private decimal _price;
    
        public Tickers(string market)
        {
            Market = market;
        }
    
        public string Market
        {
            get => _market;
            set
            {
                _market = value;
                OnPropertyChanged();
            }
        }
    
        public decimal Price
        {
            get => _price;
            set
            {
                _price = value;
                OnPropertyChanged();
            }
        }
    
        public event PropertyChangedEventHandler PropertyChanged;
        protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
    
  2. 初始化时只创建一次ObservableCollection,后续收到WebSocket消息仅修改对应条目的属性,删除每次更新重新实例化集合的逻辑,同时删掉冗余的ListTickers字段,避免维护两份集合数据:
    // 初始化时仅实例化一次集合,后续不再重新赋值
    public ObservableCollection<Tickers> Tickers { get; } = new ObservableCollection<Tickers>();
    
    private WSFuturesResponse _futuresResponse;
    public WSFuturesResponse FuturesResponse
    {
        get => _futuresResponse;
        set
        {
            _futuresResponse = value;
            OnPropertyChanged();
            // 直接修改集合内对应条目的属性即可触发UI刷新
            var target = Tickers.FirstOrDefault(x => x.Market == FuturesResponse.market);
            if (target != null)
            {
                target.Price = FuturesResponse.data.last;
            }
        }
    }
    
  3. 初始化填充数据时,直接往Tickers集合添加条目即可:
    wsApi.OnWebSocketConnect += () =>
    {
        wsApi.SendCommand(FtxWebSocketRequestGenerator.GetAuthRequest(client));
        foreach (ApiFuturesData future in all_futures.result)
        {
            if (future.name.Contains("PERP"))
            {
                Tickers.Add(new Tickers(future.name));
                wsApi.SendCommand(FtxWebSocketRequestGenerator.GetSubscribeRequest("ticker", future.name));
            }
        }
    };
    
  4. 如果WebSocket推送频率较高,可以给ListView附加VirtualizingStackPanel.IsVirtualizing="True"和VirtualizingStackPanel.VirtualizationMode="Recycling"属性,复用列表项容器,进一步降低渲染开销,彻底消除闪烁。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.30 21:00:56