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无法收到属性变更通知,不会主动刷新价格显示,才让你误以为必须重建整个集合才能更新数据。
修复方案
- 让
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)); } } - 初始化时只创建一次
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; } } } - 初始化填充数据时,直接往
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)); } } }; - 如果WebSocket推送频率较高,可以给
ListView附加VirtualizingStackPanel.IsVirtualizing="True"和VirtualizingStackPanel.VirtualizationMode="Recycling"属性,复用列表项容器,进一步降低渲染开销,彻底消除闪烁。
内容的提问来源于stack exchange,提问作者EmilAv
相关产品推荐
相关产品推荐

