[原]WPF编程经常遇到一个问题:
某个数组己绑定到主界面某控件中,然后在后台程序中需要对数组增(减)数据,然后程序就会报错,
程序提示:该类型的CollectionView 不支持从调度程序线程以外的线程对其SourceCollection进行的更改。
如下图所示:
既然不能这样操作,就得想一个办法来解决,现在先把把出现错误的程序全部列出来,然后再来根据解决办法进行修改,
本测试程序先建一个学生类:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.ComponentModel; namespace WPF_test { public class student : INotifyPropertyChanged { //定义数据更改事件通知和更改的方法 public event PropertyChangedEventHandler PropertyChanged; public void up(string s) { if (this.PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(s)); } } //姓名 string _name; public string name { get { return _name; } set { _name = value; up("name"); } } //学号 string _id; public string id { get { return _id; } set { _id = value; up("name"); } } public student(string _id, string _name) { id = _id; name = _name; } public student() { } } }
主窗口xaml代码(将students数组绑定到主界面中):
<Window x:Class="WPF_test.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:zz="clr-namespace:WPF_test" Title="MainWindow" Height="350" Width="525" Loaded="Window_Loaded" > <Window.Resources > <zz:VisibilityConverter x:Key="VisibilityConverter"/> </Window.Resources> <DockPanel > <StackPanel Orientation="Horizontal" DockPanel.Dock="Bottom" HorizontalAlignment="Center" Margin="0 10 "> <Button Content="出错测试" Margin="20 0" Click="Button_Click" /> <Button Content="正确测试" Margin="20 0" Click="Button_Click_1" /> </StackPanel> <Grid> <!-- 顶层等待动画--> <Grid Name="g1" Panel.ZIndex="2" Visibility="{Binding isWorking, Converter={StaticResource VisibilityConverter}}"> <Border Background="Black" Height="100" BorderBrush="Gold" Opacity="0.7" BorderThickness="1"> <StackPanel > <TextBlock Text="请稍等" HorizontalAlignment="Center" Foreground="White" Margin="0 10 0 0"></TextBlock> <ProgressBar IsIndeterminate="True" Height="25" Margin="20"></ProgressBar> </StackPanel> </Border> </Grid> <!-- 底层数据显示--> <ListView Name="listview1"> <ListView.View> <GridView > <GridViewColumn Header="学号" Width="80" DisplayMemberBinding="{Binding id}"/> <GridViewColumn Header="姓名" Width="150" DisplayMemberBinding="{Binding name}"/> </GridView> </ListView.View> </ListView> </Grid> </DockPanel> </Window>
等待动画类代码:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.ComponentModel; namespace WPF_test { class witeMe:INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; public void up(string s) { if (this.PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(s)); } } //工作提示状态 private bool _isWorking = false; public bool isWorking { get { return _isWorking; } set { _isWorking = value; up("isWorking"); } } } }
主窗口核心代码:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Shapes; using System.Collections.ObjectModel; using System.Threading; using System.Threading.Tasks; namespace WPF_test { /// <summary> /// MainWindow.xaml 的交互逻辑 /// </summary> public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } ObservableCollection<student> students = new ObservableCollection<student>(); witeMe wm = new witeMe(); private void Window_Loaded(object sender, RoutedEventArgs e) { students.Add(new student("1号","张三")); students.Add(new student("2号", "李四")); listview1.ItemsSource = students; g1.DataContext = wm; } //出错测试 private void Button_Click(object sender, RoutedEventArgs e) { students.Clear(); Task tk = new Task(() => { Action<student, bool> ac1 = (x, y) => students.Add(x); createStudents(ac1); }); tk.Start(); tk.ContinueWith((t) => MessageBox.Show("结束")); } //正确测试 private void Button_Click_1(object sender, RoutedEventArgs e) { students.Clear(); wm.isWorking = true; Task tk = new Task(() => { Action<student, bool> ac2 = (x, y) => addData(x, y); createStudents(ac2); }); tk.Start(); } private void addData(student s, bool k) { ThreadPool.QueueUserWorkItem(delegate { this.Dispatcher.Invoke(new Action(() => { students.Add(s); if (k == true) wm.isWorking = false; // students.Insert(0, s);//这样也会很卡 //listview1.ScrollIntoView(s);//这个不行,大数据时真会卡死了 }), null); }); } //创建学生数组 private void createStudents(Action<student, bool> _ac) { for (int i = 0; i < 100000; i++) { student s = new student(); s.id = (i + 1).ToString(); s.name = "小颗豆" + (i + 1).ToString(); if (i < 99999) _ac(s, false); else _ac(s, true); } } } }
程序运行时:点击"错误测试"就会出现文章前边的错误图示,点击"正确测试"出现下图,一切正常:
正确测试时绑定的数组边修改,画面边显示,而且主窗口也没有卡死,鼠标也能拖动窗口,基本能达到目的了,下面分析一下代码是如何解决的:
//创建学生数组
private void createStudents(Action<student, bool> _ac)
{
for (int i = 0; i < 100000; i++)
{
student s = new student();
s.id = (i + 1).ToString();
s.name = "小颗豆" + (i + 1).ToString();
if (i < 99999)
_ac(s, false);
else
_ac(s, true);
}
}
在以上代码中每增加一个学生成员到数组中都是通过_ac(s,bool)委托进行的,
委托的定义是在异步线程中定义好的.即是ac2
Task tk = new Task(() =>
{
Action<student, bool> ac2 = (x, y) => addData(x, y);
createStudents(ac2);
});
tk.Start();
异步Task里,先定义了委托,每增加一个数组成员时委托addData方法在主界面调用者线程中由线程池去操作即可解决外线程不能更改数组的问题:
private void addData(student s, bool k)
{
ThreadPool.QueueUserWorkItem(delegate
{
this.Dispatcher.Invoke(new Action(() =>
{
students.Add(s);
if (k == true)
wm.isWorking = false;
// students.Insert(0, s);//这样会很卡,如果数据量小时则会显得很流畅
//listview1.ScrollIntoView(s);//这个不行,小数据无所谓,大数据时真会卡死界面了
}), null);
});
}
上边的代码中,
this.Dispatcher.Invoke(new Action(() =>
{
students.Add(s);
}),null);这里是关键的解决办法,主窗体是主线程创建的,每个线程都有一个唯一的调度员,我们的工作就是命令调度员去做相应的工作,这里我们就相当于命令主窗体的线程调度员去增加数组成员,这样做线程是安全的,不会再有错误提示。
以上是本人测试的例子,不足之处请批评指正,高手请飘过。