WPF 多线程下跨线程处理 ObservableCollection 数据
本文告诉大家几个不同的方法在 WPF 里,使用多线程修改或创建 ObservableCollection 列表的数据
需要明确的是 WPF 框架下,非 UI 线程直接或间接访问 UI 是不合法的,设计如此。如此设计可以极大规避新手使用多线程造成的多线程安全问题,由于多线程安全的问题难以定位,以及解决多线程问题需要较多的专业知识。一个优秀的框架从设计上,一定需要满足不同层次开发者接入的需求。大部分微软出品的库和框架都是十分照顾到初学者的,因此默认只开单线程模型的 WPF 框架,将在开发者没有经过 Dispatcher 调度器而直接或间接访问或修改 UI 时,抛出异常
理解了以上这一点,也就了解了为什么跨线程处理 ObservableCollection 数据,大多数时候都会抛出 System.NotSupportedException:“该类型的 CollectionView 不支持从调度程序线程以外的线程对其 SourceCollection 进行的更改。”
等异常
在开始之前,还需要理清另一个概念,那就是 ObservableCollection 是非线程安全的。非线程安全与是否不允许非 UI 线程访问 UI 元素是完全两回事。非线程安全的类型,推荐是单一的时刻,仅有单个线程进行处理,也就是单个线程进行读写等。而 非 UI 线程访问 UI 元素是限制只有 UI 线程才能合法访问 UI 线程创建的元素。具体来说就是 ObservableCollection 是可以在任意线程创建和修改的,但是由于 ObservableCollection 是非线程安全的,因此推荐是单一的时刻,仅有单个线程进行处理。如果 ObservableCollection 被 UI 元素捕获,例如加入到 ItemsSource 里面,那么此时的 ObservableCollection 不仅只能被单一线程处理,还要求这个线程是 UI 线程
根据以上描述,可以了解到,在 WPF 里面,如果有较多数据量,想要多线程处理 ObservableCollection 集合,可以采用在非 UI 的后台线程创建 ObservableCollection 对象和修改或添加数据,完成之后再加入到 UI 线程
为了方便说明,本文新建了一个项目,本文的所有代码都可以在本文后面找到获取方法
添加一个简单的界面来方便说明,代码如下
<Grid>
<Grid.RowDefinitions>
<RowDefinition></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
<Grid>
<ListView x:Name="ListView">
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock Margin="10,10,10,10" Text="{Binding}"></TextBlock>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
<StackPanel Grid.Row="1" Orientation="Horizontal">
<Button x:Name="Button1" Margin="10,10,10,10" Click="Button1_Click">方式一</Button>
<Button x:Name="Button2" Margin="10,10,10,10" Click="Button2_Click">方式二</Button>
<Button x:Name="Button3" Margin="10,10,10,10" Click="Button3_Click">方式三</Button>
</StackPanel>
</Grid>
以上的每个按钮分别代表不同的方法,第一个按钮就是对应开始说的第一个方法。先在后台线程创建 ObservableCollection 对象,然后在后台线程完成处理逻辑,最后赋值给 ListView 的 ItemsSource 属性,实现更新界面逻辑
private async void Button1_Click(object sender, RoutedEventArgs e)
{
var list = await Task.Run(() =>
{
ObservableCollection<string> data = new ObservableCollection<string>();
for (int i = 0; i < 100; i++)
{
data.Add(Random.Shared.Next(1000).ToString());
}
return data;
});
// 以上代码使用 await 等待,可以自动切回主线程
ListView.ItemsSource = list;
}
如以上代码,在按钮点击时,进入按钮点击的是 UI 线程。此时在 UI 线程里面,通过 Task.Run 来切换到后台线程,在后台线程完成 list 变量的初始化逻辑。然后再赋值给 ListView 的 ItemsSource 属性
上面代码符合了上文说的逻辑条件,首先 ObservableCollection 非线程安全,单一的时刻,只有一个线程进行访问。上面代码先是后台线程创建和处理 ObservableCollection 对象,接下来后台线程执行完成,通过 await 自动依靠同步上下文调度到主线程,将后台线程创建的 ObservableCollection 对象赋值给 list 变量,此时的后台线程退出对 ObservableCollection 对象的任何访问,也就是在此单一的时刻,只有后台线程一个线程在访问。接下来进入 ListView.ItemsSource = list
也就是将 list 交给 UI 线程,在此单一的时刻,也只有 UI 线程,一个线程在访问
在将 ObservableCollection 关联到 UI 线程之前,对 ObservableCollection 的任何处理都不会涉及到访问 UI 元素,因此也就没有了非 UI 线程不能访问 UI 元素的限制。只有在调用 ListView.ItemsSource = list
代码之后,才将 ObservableCollection 关联到 UI 线程。在此代码执行之后,就不能通过后台线程去修改 list 变量对应的对象了,因为此时的修改将会间接在后台线程访问到 UI 元素
那如果期望是在后台线程处理原有 UI 线程关联的 ObservableCollection 呢?这就是本文的第二个方法。读取 ObservableCollection 的列表元素内容,不会涉及到访问 UI 元素,因此可以在后台线程进行读取列表元素,读取列表元素也就是等于可以对原有的列表拷贝一份
这里需要再次说明 ObservableCollection 非线程安全,单一的时刻,只有一个线程进行访问才是安全的。换句话说,虽然代码层面上,可以在后台线程拷贝和 UI 线程关联的 ObservableCollection 的列表元素内容,但是此时毕竟 UI 线程和后台线程都拥有访问相同的一个 ObservableCollection 列表的能力,必须从业务上确保只有后台线程在访问,而 UI 线程不会对 ObservableCollection 列表进行任何的改动
在确保 UI 线程不会改动到 ObservableCollection 列表的时候,可以采用如下方法,在后台线程拷贝一份作为新的 ObservableCollection 对象,然后对此新的对象进行处理。完成之后,再将新的 ObservableCollection 对象赋值给到 UI 进行绑定
private async void Button2_Click(object sender, RoutedEventArgs e)
{
// 假定 ListView.ItemsSource 存在源了
if (ListView.ItemsSource is not ObservableCollection<string> list)
{
// 如果假设失败,强行给一个源
list = new();
ListView.ItemsSource = list;
}
var newList = await Task.Run(() =>
{
var data = new ObservableCollection<string>(list);
// 模拟对原有的列表进行处理
if (data.Count > 0)
{
for (int i = 0; i < 100; i++)
{
data.Move(Random.Shared.Next(data.Count), Random.Shared.Next(data.Count));
}
}
return data;
});
ListView.ItemsSource = newList;
}
以上方法可以实现在后台线程对现有的和 UI 绑定的 ObservableCollection 的更改,由于是放在后台线程执行,基本上不需要担心拷贝的耗时
第三个方法是自己实现一个类似 ObservableCollection 的类型。在 WPF 里面,只要一个集合类型的对象继承了 INotifyCollectionChanged 接口,即可在集合变更的时候,通过 WPF 框架监听 CollectionChanged 事件重新更新 UI 元素,自己实现的代码大概如下
public class FooList<T> : Collection<T>, INotifyCollectionChanged
{
protected override void InsertItem(int index, T item)
{
base.InsertItem(index, item);
Application.Current.Dispatcher.InvokeAsync(() =>
{
CollectionChanged?.Invoke(this,
new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, index));
});
}
protected override void RemoveItem(int index)
{
var item = this[index];
base.RemoveItem(index);
Application.Current.Dispatcher.InvokeAsync(() =>
{
CollectionChanged?.Invoke(this,
new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, index));
});
}
protected override void SetItem(int index, T item)
{
var oldItem = this[index];
base.SetItem(index, item);
Application.Current.Dispatcher.InvokeAsync(() =>
{
CollectionChanged?.Invoke(this,
new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, item, oldItem, index));
});
}
public event NotifyCollectionChangedEventHandler? CollectionChanged;
}
如上面代码可以看到,在集合变更的代码里面,都通过 Dispatcher 调度到 UI 线程触发事件用来通知。依靠此机制可以实现在后台线程处理时,依然是让此 FooList 对应的对象是绑定在 UI 线程上
使用 FooList 的例子如下
private async void Button3_Click(object sender, RoutedEventArgs e)
{
if (ListView.ItemsSource is not FooList<string> list)
{
list = new FooList<string>();
ListView.ItemsSource = list;
}
await Task.Run(() =>
{
for (int i = 0; i < 100; i++)
{
list.Add(Random.Shared.Next(100).ToString());
}
});
await Task.Delay(TimeSpan.FromSeconds(1));
await Task.Run(() =>
{
for (int i = 0; i < 10; i++)
{
list.RemoveAt(i);
}
});
await Task.Delay(TimeSpan.FromSeconds(1));
await Task.Run(() =>
{
for (int i = 0; i < 10; i++)
{
list[i] = i.ToString();
}
});
}
以上的 FooList 只是一个例子,用于告诉大家可以使用 INotifyCollectionChanged 的方式自己实现在集合变更的时候通知主线程,而集合的处理本身可以放在其他的线程。但是这个方法在使用的时候,必须关注线程安全问题。例如以上的代码,如果没有关注线程安全,在通知 UI 线程集合变更之后,刚好 UI 线程去读取此集合新的值的时候,集合本身就被其他线程更改了内容,那么此时的逻辑就不是符合预期的
可以通过如下方式获取本文的源代码,先创建一个空文件夹,接着使用命令行 cd 命令进入此空文件夹,在命令行里面输入以下代码,即可获取到本文的代码
git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin df7d9da863047fa0a46bc97e782b054da63fc394
以上使用的是 gitee 的源,如果 gitee 不能访问,请替换为 github 的源
git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
获取代码之后,进入 LeejurkawbaicarkeNayqechurcear 文件夹
关于 UWP 部分,请看 win10 uwp 通知列表
WPF 多线程下跨线程处理 ObservableCollection 数据的更多相关文章
- C#利用委托跨线程更新UI数据
转:http://www.2cto.com/kf/201206/136587.html 在使用C#的过程中,难免会用到多线程,而用多线程之后,线程如何与界面交互则是一个非常头疼的问题.其实不仅仅是界面 ...
- wpf(怎么跨线程访问wpf控件)
在编写代码时,我们经常会碰到一些子线程中处理完的信息,需要通知另一个线程(我这边处理完了,该你了). 但是当我们通知WPF的UI线程时需要用到Dispatcher. 首先我们需要想好在UI控件上需要显 ...
- [转]C#利用委托跨线程更新UI数据
在使用C#的过程中,难免会用到多线程,而用多线程之后,线程如何与界面交互则是一个非常头疼的问题.其实不仅仅是界面,一般情况下,我们往往需要获得线程的一些信息来确定线程的状态.比较好的方式是用委托实现, ...
- WinForm与WPF下跨线程调用控件
Winform下: public delegate void UpadataTextCallBack(string str,TextBox text); public void UpadtaText( ...
- WPF 中那些可跨线程访问的 DispatcherObject(WPF Free Threaded Dispatcher Object)
原文 WPF 中那些可跨线程访问的 DispatcherObject(WPF Free Threaded Dispatcher Object) 众所周知的,WPF 中多数对象都继承自 Dispatch ...
- 对于多线程下Servlet以及Session的一些理解
今天,小伙伴突然问到了Servlet是不是线程安全的问题.脑子当时一卡壳,只想到了单实例多线程.这里做一些总结. Servlet体系是建立在Java多线程的基础之上的,它的生命周期是由Tomcat来维 ...
- 使用ThreadLocal在线程内部传递数据
最近在项目中使用到了JDK提供的线程池,遇到了在多线程环境下在线程内部共享数据的问题 使用ThreadLocal 来解决线程内部共享数据的问题 定义BO package com.unicom.uclo ...
- WPF / Win Form:多线程去修改或访问UI线程数据的方法( winform 跨线程访问UI控件 )
WPF:谈谈各种多线程去修改或访问UI线程数据的方法http://www.cnblogs.com/mgen/archive/2012/03/10/2389509.html 子线程非法访问UI线程的数据 ...
- (转).NET 4.5中使用Task.Run和Parallel.For()实现的C# Winform多线程任务及跨线程更新UI控件综合实例
http://2sharings.com/2014/net-4-5-task-run-parallel-for-winform-cross-multiple-threads-update-ui-dem ...
- Linux多线程实践(4) --线程特定数据
线程特定数据 int pthread_key_create(pthread_key_t *key, void (*destr_function) (void *)); int pthread_key_ ...
随机推荐
- tableau日常使用小技巧
一.设置数值自定义格式为万 0"."0,"万" #"."#, 万 二.
- RMI反序列化分析
RMI介绍 RMI全程Remote Method Invocation (远程方法引用),RMI有客户端和服务端,还有一个注册中心,在java中客户端可以通过RMI调用服务端的方法,流程图如下: 服务 ...
- linux 安装mysql8.0.11
1.使用系统的root账户 2.切换到/use/local 目录下 3.下载mysql ?wget https://dev.mysql.com/get/Downloads/MySQL-8.0/mysq ...
- KingbaseES Collate排序规则对结果集的影响
背景 前端在客户现场遇到一个问题,模糊查询报错:error:invalid multibyte charactor for locale pg the server LC_TYPE locale is ...
- HashMap对key或value进行排序--Java--小白必懂2
HashMap对key进行排序 public static void main (String[]args){ HashMap<String, Integer> map = new Has ...
- Java 中的异常处理机制的简单原理和应用。
Java 中的异常处理机制的简单原理和应用. 异常是指 java 程序运行时(非编译)所发生的非正常情况或错误. Java 对异常进行了分类,不同类型的异常分别用不同的 Java 类表示,所有异常的根 ...
- python 国家标准行业编码标准格式化处理
代码在上次的基础上做了一点优化,之前对项目要的最终结果理解有些偏差: 原始数据的那一列行业编码是存在三位数和四位数的,我上次理解的三位数就是分割成两位数进行查找,其实三位数的编码是由于第一位的0没有显 ...
- #状压dp#JZOJ 3853 帮助Bsny
题目 一共有\(n\)本书,混乱值是连续相同高度书本的段数. 可以取出\(k\)本书随意放回,问最小混乱值,高度\([25\sim 32]\) 分析 设\(f[i][j][k][mask]\)表示前\ ...
- #分治,决策单调性dp#CF868F Yet Another Minimization Problem
题目 给定一个序列 \(a\),要把它分成 \(k\) 个子段.(\(n\leq 10^5,k\leq 20\)) 每个子段的费用是其中相同元素的对数.求所有子段的费用之和的最小值. 分析 有一个很明 ...
- #Tarjan,SPFA,差分约束系统#BZOJ 2330 AcWing 368 银河
题目 分析 首先这明显是一道差分约束题,但是无解的情况确实比较恶心, 考虑它的边权为0或1,无解当且仅当某个强连通分量内的边至少一条边边权为1, 那么用有向图的Tarjan缩点后跑SPFA就可以了 代 ...