本文记录一个 WPF 已知问题,在 ObservableCollection 的 CollectionChanged 事件里面,绕过 ObservableCollection 的异常判断逻辑,强行修改集合内容,修改之后的 UI 层将不能符合预期。本文将告诉大家此问题的复现方法和修复方法

在 UI 绑定的 ObservableCollection 修改时,给此集合列表添加新的项目,此时 UI 绑定的数据是对的但是界面显示错误。简单的复现方法如下

先在后台代码定义好绑定模型 Model 类,代码如下

public class Model
{
public string? Name { get; set; } public override string? ToString() => Name;
}

接着在 MainWindow 里添加一个 ObservableCollection<Model> 属性用于让 XAML 绑定,这里不加入一个 ViewModel 只是为了让代码简单

    public ObservableCollection<Model> List { get; } = new ObservableCollection<Model>();

在 XAML 里代码如下

<Window x:Class="BekuhalnoKawairlunee.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:BekuhalnoKawairlunee"
mc:Ignorable="d"
x:Name="Root"
Title="MainWindow" Height="450" Width="800">
<Grid>
<ListBox x:Name="ListBox" ItemsSource="{Binding ElementName=Root,Path=List}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"></TextBlock>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Window>

在 MainWindow 构造函数给 List 加上测试内容

    public MainWindow()
{
for (int i = 0; i < 3; i++)
{
var model = new Model()
{
Name = i.ToString()
}; List.Add(model);
} List.CollectionChanged += List_CollectionChanged; InitializeComponent(); Loaded += MainWindow_Loaded;
}

以上的代码的 List.CollectionChanged += List_CollectionChanged; 是为了在集合变更时加入一项用来修改集合。监听 Loaded 用来模拟删除 ObservableCollection 的内容,用来触发 CollectionChanged 事件

先不要实现 List_CollectionChangedMainWindow_Loaded 方法的内容,先看看此时界面显示,修复构建运行代码可以看到如下图

在 Loaded 事件里面,将 List 的第 1 项删除,代码如下

    private async void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
await Task.Delay(100); List.RemoveAt(1);
}

删除之后将会进入 List_CollectionChanged 方法,在 List_CollectionChanged 方法里面,如果直接修改 List 的内容,如以下代码,将会抛出 InvalidOperationException 异常

    private void List_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
//List.CollectionChanged -= List_CollectionChanged; List.Add(new Model()
{
Name = "xx"
});
}

抛出的异常如下

System.InvalidOperationException:“Cannot change ObservableCollection during a CollectionChanged event.”

一个绕过的方法是在进入 List_CollectionChanged 减等事件,但是绕过是存在坑的,原本预期的列表顺序应该是 0 2 xx 的顺序,然而实际的界面显示如下

以上就是最简单的方法让大家了解到问题

修复的方法有两个:

第一个方法是推荐的,使用 Dispatcher.InvokeAsync 延迟执行,修改 List_CollectionChanged 代码如下

    private void List_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
if (_changed)
{
return;
}
_changed = true; Dispatcher.InvokeAsync(() =>
{
List.Add(new Model()
{
Name = "xx"
});
});
} private bool _changed;

以上的 _changed 字段只是让代码不会多次进入而已,因为添加元素也会触发集合变更事件,如果在集合变更事件里面再次添加元素,那就无限进入集合变更

可以看到界面显示符合预期

第二个方法是强行刷 ItemsSource 内容,强行刷不能在 List_CollectionChanged 立即调用,否则将会抛出 InvalidOperationException 异常

    private void List_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
List.CollectionChanged -= List_CollectionChanged; List.Add(new Model()
{
Name = "xx"
}); ListBox.ItemsSource = null;
ListBox.ItemsSource = List;
}

抛出的异常代码如下

System.InvalidOperationException:“某个 ItemsControl 与它的项源不一致。\n请参见内部异常以获取更多信息。”

Exception: 针对开发人员的信息(使用文本可视化工具来阅读此内容):
引发此异常的原因是名为“ListBox”的控件“System.Windows.Controls.ListBox Items.Count:3”的生成器已接收到一个 CollectionChanged 事件序列,这些事件与 Items 集合的当前状态不符。 检测到以下差异:
累积计数 2 与实际计数 3 不相同。[累积计数的计算方式为: 上次重置时的计数 + 添加数 - 自上次重置后的删除数。] 以下的一个或多个源可能已引发错误事件:
System.Windows.Controls.ItemContainerGenerator
System.Windows.Controls.ItemCollection
System.Windows.Data.ListCollectionView
* System.Collections.ObjectModel.ObservableCollection`1[[BekuhalnoKawairlunee.Model, BekuhalnoKawairlunee, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]
(标有星号的源被认为更有可能是问题的根源。) 最常见的原因有: (a)在未引发相应事件的情况下更改了集合或集合的计数,(b)引发的事件使用了错误的索引或项参数。 异常的堆栈跟踪将描述不一致情况是如何检测到的,而不是描述不一致情况是如何发生的。 要获得更及时的异常,应将生成器上附加的属性“PresentationTraceSources.TraceLevel”设置为值“High”,然后重新运行该方案。 完成此操作的一个方法是,在“即时”窗口中运行与下面的命令类似的命令:\n System.Diagnostics.PresentationTraceSources.SetTraceLevel(myItemsControl.ItemContainerGenerator, System.Diagnostics.PresentationTraceLevel.High)
。这会使得在发生每个 CollectionChanged 事件之后运行检测逻辑,因此,这将减慢应用程序的运行速度。

通过以上的异常信息也可以了解到为什么 WPF 存在此已知问题,因为原本预期就是开发者不能在集合变更时修改集合,如果在每个集合变更里都需要重新处理状态,将会让 WPF 的性能很差。因此这个问题也是不会在 WPF 里面修复的,只能开发者自己修复

强行刷只能放在其他的时机,例如在界面添加一个按钮,点击按钮强行刷

    private void Button_OnClick(object sender, RoutedEventArgs e)
{
ListBox.ItemsSource = null;
ListBox.ItemsSource = List;
}

运行程序,可以看到开始界面显示错误,在点击按钮之后,界面就符合预期

本文的代码放在githubgitee 欢迎访问

可以通过如下方式获取本文的源代码,先创建一个空文件夹,接着使用命令行 cd 命令进入此空文件夹,在命令行里面输入以下代码,即可获取到本文的代码

git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin f6c0785629f2f73ee190b97cf14699daf8746699

以上使用的是 gitee 的源,如果 gitee 不能访问,请替换为 github 的源。请在命令行继续输入以下代码

git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
git pull origin f6c0785629f2f73ee190b97cf14699daf8746699

获取代码之后,进入 BekuhalnoKawairlunee 文件夹

更多博客,请参阅我的 博客导航

WPF 已知问题 在 ObservableCollection 的 CollectionChanged 修改集合内容将让 UI 显示错误的更多相关文章

  1. Confluence 6 指定日志选项和已知问题

    指定 Confluence 日志选项 这里是一些特定的日志配置,你可能在对问题进行调试的时候需要. 在日志中记录数据库使用的 SQL 查询请求 你可能希望增加日志的中的内容,记录 Confluence ...

  2. 面试题: 已知一个含有n个不同元素的集合,要求打印其所有具有k个元素的子集(不允许有重复的)

    TX面试题2: 已知一个含有n个元素的集合,要求打印其所有具有k个元素的子集(不允许有重复的) 题目分析, 为了便于说明,不妨将问题简化一下: 已知一个盒子中有n个不同的球,分别标记为{a1,a2,. ...

  3. OpenCV开发笔记(六十九):红胖子8分钟带你使用传统方法识别已知物体(图文并茂+浅显易懂+程序源码)

    若该文为原创文章,未经允许不得转载原博主博客地址:https://blog.csdn.net/qq21497936原博主博客导航:https://blog.csdn.net/qq21497936/ar ...

  4. 风口之下,猪都能飞。当今中国股市牛市,真可谓“错过等七年”。 给你一个回顾历史的机会,已知一支股票连续n天的价格走势,以长度为n的整数数组表示,

    转自:http://www.cnblogs.com/ranranblog/p/5845010.html 风口之下,猪都能飞.当今中国股市牛市,真可谓“错过等七年”. 给你一个回顾历史的机会,已知一支股 ...

  5. Delphi 查找标题已知的窗口句柄,遍历窗口控件句柄(转)

    用我的方法来控制其他程序窗体上的窗口控件,必须先了解什么是 回调函数.我的理解是这样的: 回 调函数写出来不是自己的程序去调用的,反而是让其他的东西去调用,比如windows操作系统,比如其他的程序等 ...

  6. 对象布局已知时 C++ 对象指针的转换时地址调整

    在我调试和研究 netscape 系浏览器插件开发时,注意到了这个问题.即,在对象布局已知(即对象之间具有继承关系)时,不同类型对象的指针进行转换(不管是隐式的从下向上转换,还是强制的从上到下转换)时 ...

  7. ARCgis已知线裁剪已知面

    经常遇到需要在ArcGIS中,根据已知线图层(要素)切分已知面图层(要素).经过研究,利用topology拓扑菜单中的construct features可以实现.具体如下 现有用线图层A.面图层B, ...

  8. Java集合-5. (List)已知有一个Worker 类如下: 完成下面的要求 1) 创建一个List,在List 中增加三个工人,基本信息如下: 姓名 年龄 工资 zhang3 18 3000 li4 25 3500 wang5 22 3200 2) 在li4 之前插入一个工人,信息为:姓名:zhao6,年龄:24,工资3300 3) 删除wang5 的信息 4) 利用for 循

    第六题 5. (List)已知有一个Worker 类如下: public class Worker { private int age; private String name; private do ...

  9. JAVA-集合作业-已知有十六支男子足球队参加2008 北京奥运会。写一个程序,把这16 支球队随机分为4 个组。采用List集合和随机数

    第二题 已知有十六支男子足球队参加2008 北京奥运会.写一个程序,把这16 支球队随机分为4 个组.采用List集合和随机数 2008 北京奥运会男足参赛国家: 科特迪瓦,阿根廷,澳大利亚,塞尔维亚 ...

  10. WCF 已知类型和泛型解析程序 KnownType

    数据协定继承 已知类型和泛型解析程序 Juval Lowy 下载代码示例 自首次发布以来,Windows Communication Foundation (WCF) 开发人员便必须处理数据协定继承方 ...

随机推荐

  1. python高级技术(进程二)

    一 进程对象及其他方法 '''一台计算机上面运行着很多进程,那么计算机是如何区分并管理这些进程服务端的呢?计算机会给每一个运行的进程分配一个PID号如何查看 windows电脑 进入cmd输入task ...

  2. C++ Qt开发:QUdpSocket网络通信组件

    Qt 是一个跨平台C++图形界面开发库,利用Qt可以快速开发跨平台窗体应用程序,在Qt中我们可以通过拖拽的方式将不同组件放到指定的位置,实现图形化开发极大的方便了开发效率,本章将重点介绍如何运用QUd ...

  3. 记录--一个好用的轮子 turn.js 实现仿真翻书的效果

    这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 国际惯例,官网链接 官网传送门 Github地址 github上有几个demos例子,介绍了基础用法. 我参考官网的例子,写了一个demo ...

  4. 记录--JavaScript 中有趣的 9 个常用编码套路

    这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 1️⃣ set对象:数组快速去重 常规情况下,我们想要筛选唯一值,一般会想到遍历数组然后逐个对比,或者使用成熟的库比如lodash之类的. ...

  5. 使用Go语言开发一个短链接服务:四、生成code算法

    章节  使用Go语言开发一个短链接服务:一.基本原理  使用Go语言开发一个短链接服务:二.架构设计  使用Go语言开发一个短链接服务:三.项目目录结构设计  使用Go语言开发一个短链接服务:四.生成 ...

  6. archlinux xfce 设置窗口背景颜色,QT背景颜色

    1.使用xfce主题 2.有QT背景不覆盖,使用配置 sudo pacman -S qt5-ct 3.在/etc/environment添加环境变量 QT_QPA_PLATFORMTHEME=qt5c ...

  7. 【已解决】初始化 Hive 元数据库报错slf4j-log4j12-1.7.25.jar包冲突

    错误log描述 [root@hadoop102 hive]# schematool -initSchema -dbType mysql -verboseSLF4J: Class path contai ...

  8. CTFshow pwn53 wp

    PWN53 那么先看保护 虽然没有开canary但是本题在ida打开看见他是模拟了canary的效果,不同的是他的是固定的canary,但是一样要爆破 而且发现还有后门函数 在ctfshow函数我们发 ...

  9. #构造,二分#[AGC006B] [AGC006D] Median Pyramid

    Easy Hard 分析(Easy) 若\(X=1\)或\(X=2n-1\)无解,否则在正中间构造\(X-1,X,X+1\), 其余位置升序铺入剩余数, 若\(X-1\)左侧数大于\(X-1\)那么\ ...

  10. #树链剖分,LCA#洛谷 3398 仓鼠找sugar

    题目 多次询问求树上的两条路径是否有公共点 分析 有公共点当且仅当一条路径的LCA在另一条路径上, 否则一定会形成一个环,那树剖求LCA判断一下LCA是否在另一条路径上即可 代码 #include & ...