WPF MVVM动态列多线程
本文关键字:多线程 动态 MVVM WPF | 更新日期: 2023-09-27 18:15:15
我目前正在开发一个c# System.Windows.Controls.DataGrid,它需要根据数据动态生成列。它可以在运行时添加和/或删除列。
我在ViewModel类中使用一个线程来更新为DataGrid提供数据的ObservableCollection。
我读了那篇文章,它解释了我为我的问题找到的最佳解决方案。尽管列。来自datagriextension类的CollectionChanged Delegate抛出InvalideOperationException:调用线程不能访问该对象,因为它属于另一个线程。
这里有一些代码来描绘这一切:
View XAML
<DataGrid ItemsSource="{Binding CollectionView, Source={StaticResource ViewModel}}" local:DataGridExtension.Columns="{Binding DataGridColumns, Source={StaticResource ViewModel}}" AutoGenerateColumns="False" Name="dataGrid">
ViewModel Class
public ObservableCollection<DataGridColumn> DataGridColumns
{
get { return columns; }
set { columns = value; }
}
private void getViewData()
{
while (true)
{
Thread.Sleep(1000);
foreach (DataObject data in dataObjects)
{
int index = -1;
foreach (DataGridColumn c in columns)
{
if (c.Header.Equals(column.Header))
index = columns.IndexOf(c);
}
DataGridColumn column = new DataGridTextColumn();
... Creating the column based on data from DataObject ...
DataGridExtension._currentDispatcher = Dispatcher.CurrentDispatcher;
if (index == -1)
{
this.columns.Add(column);
}
else
{
this.columns.RemoveAt(index);
this.columns.Add(column);
}
}
}
}
datagriextensionclass
public static class DataGridExtension
{
public static Dispatcher _currentDispatcher;
public static readonly DependencyProperty ColumnsProperty =
DependencyProperty.RegisterAttached("Columns",
typeof(ObservableCollection<DataGridColumn>),
typeof(DataGridExtension),
new UIPropertyMetadata(new ObservableCollection<DataGridColumn>(), OnDataGridColumnsPropertyChanged));
private static void OnDataGridColumnsPropertyChanged(DependencyObject iObj, DependencyPropertyChangedEventArgs iArgs)
{
if (iObj.GetType() == typeof(DataGrid))
{
DataGrid myGrid = iObj as DataGrid;
ObservableCollection<DataGridColumn> Columns = (ObservableCollection<DataGridColumn>)iArgs.NewValue;
if (Columns != null)
{
myGrid.Columns.Clear();
if (Columns != null && Columns.Count > 0)
{
foreach (DataGridColumn dataGridColumn in Columns)
{
myGrid.Columns.Add(dataGridColumn);
}
}
Columns.CollectionChanged += delegate(object sender, NotifyCollectionChangedEventArgs args)
{
if (args.NewItems != null)
{
UserControl control = ((UserControl)((Grid)myGrid.Parent).Parent);
foreach (DataGridColumn column in args.NewItems.Cast<DataGridColumn>())
{
/// This is where I tried to fix the exception. ///
DataGridColumn temp = new DataGridTextColumn();
temp.Header = column.Header;
temp.SortMemberPath = column.SortMemberPath;
control.Dispatcher.Invoke(new Action(delegate()
{
myGrid.Columns.Add(temp);
}), DispatcherPriority.Normal);
////////////////////////////////////////////////////
}
}
if (args.OldItems != null)
{
foreach (DataGridColumn column in args.OldItems.Cast<DataGridColumn>())
{
myGrid.Columns.Remove(column);
}
}
};
}
}
}
public static ObservableCollection<DataGridColumn> GetColumns(DependencyObject iObj)
{
return (ObservableCollection<DataGridColumn>)iObj.GetValue(ColumnsProperty);
}
public static void SetColumns(DependencyObject iObj, ObservableCollection<DataGridColumn> iColumns)
{
iObj.SetValue(ColumnsProperty, iColumns);
}
}
我放置///的部分这是我试图修复异常的地方。///是抛出异常的地方,正好在myGrid.add(…);
myGrid对象不允许我将该列添加到DataGrid的列集合中。这就是为什么我用dispatcher。invoke包围了它。奇怪的是,如果我执行myGrid.Columns。添加(新DataGridTextColumn ());它的工作原理,我可以看到在视图中添加的空列,但myGrid.Columns.Add(temp);抛出异常。
这东西一定有什么地方我不明白。
请帮助! !
编辑Stipo建议
UserControl control = ((UserControl)((Grid)myGrid.Parent).Parent);
Columns.CollectionChanged += delegate(object sender, NotifyCollectionChangedEventArgs args)
{
control.Dispatcher.Invoke(new Action(delegate()
{
if (args.NewItems != null)
{
foreach (DataGridColumn column in args.NewItems.Cast<DataGridColumn>())
{
DataGridColumn temp = new DataGridTextColumn();
temp.Header = column.Header;
temp.SortMemberPath = column.SortMemberPath;
myGrid.Columns.Add(temp);
}
}
if (args.OldItems != null)
{
foreach (DataGridColumn column in args.OldItems.Cast<DataGridColumn>())
{
myGrid.Columns.Remove(column);
}
}
}), DispatcherPriority.Normal);
};
将DataGridColumn创建代码移动到分派器委托中。
这个问题的发生是因为DataGridColumn继承了DispatcherObject, DispatcherObject有一个字段,它说DispatcherObject是在哪个线程上创建的,当DataGridColumn被构造时,这个字段将被设置为你的工作线程。
当列被添加到DataGrid时。,因为DataGridColumn不是在创建DataGrid的默认GUI线程上创建的,因此将抛出异常。
新解决方案
玩弄你的代码后,我决定实现不同的解决方案,应该解决你的问题,使你的视图模型更干净,因为它不会有GUI成员(DataGridColumns)在它了。
新的解决方案在视图模型层用ItemProperty类抽象了DataGridColumn, datagriextension类负责在WPF的Dispatcher线程中将ItemProperty实例转换为DataGridColumn实例。
下面是一个完整的解决方案和测试示例(我建议您创建一个空的WPF应用程序项目,并在其中插入代码来测试解决方案):
ItemProperty.cs
using System;
namespace WpfApplication
{
// Abstracts DataGridColumn in view-model layer.
class ItemProperty
{
public Type PropertyType { get; private set; }
public string Name { get; private set; }
public bool IsReadOnly { get; private set; }
public ItemProperty(Type propertyType, string name, bool isReadOnly)
{
this.PropertyType = propertyType;
this.Name = name;
this.IsReadOnly = isReadOnly;
}
}
}
DataGridExtension.cs
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Markup;
using System.Windows.Threading;
namespace WpfApplication
{
static class DataGridExtension
{
private static readonly DependencyProperty ColumnBinderProperty = DependencyProperty.RegisterAttached("ColumnBinder", typeof(ColumnBinder), typeof(DataGridExtension));
public static readonly DependencyProperty ItemPropertiesProperty = DependencyProperty.RegisterAttached(
"ItemProperties",
typeof(ObservableCollection<ItemProperty>),
typeof(DataGridExtension), new PropertyMetadata((d, e) =>
{
var dataGrid = d as DataGrid;
if (dataGrid != null)
{
var columnBinder = dataGrid.GetColumnBinder();
if (columnBinder != null)
columnBinder.Dispose();
var itemProperties = e.NewValue as ObservableCollection<ItemProperty>;
dataGrid.SetColumnBinder(new ColumnBinder(dataGrid.Dispatcher, dataGrid.Columns, itemProperties));
}
}));
[AttachedPropertyBrowsableForType(typeof(DataGrid))]
[DependsOn("ItemsSource")]
public static ObservableCollection<ItemProperty> GetItemProperties(this DataGrid dataGrid)
{
return (ObservableCollection<ItemProperty>)dataGrid.GetValue(ItemPropertiesProperty);
}
public static void SetItemProperties(this DataGrid dataGrid, ObservableCollection<ItemProperty> itemProperties)
{
dataGrid.SetValue(ItemPropertiesProperty, itemProperties);
}
private static ColumnBinder GetColumnBinder(this DataGrid dataGrid)
{
return (ColumnBinder)dataGrid.GetValue(ColumnBinderProperty);
}
private static void SetColumnBinder(this DataGrid dataGrid, ColumnBinder columnBinder)
{
dataGrid.SetValue(ColumnBinderProperty, columnBinder);
}
// Takes care of binding ItemProperty collection to DataGridColumn collection.
// It derives from TypeConverter so it can access SimplePropertyDescriptor class which base class (PropertyDescriptor) is used in DataGrid.GenerateColumns method to inspect if property is read-only.
// It must be stored in DataGrid (via ColumnBinderProperty attached dependency property) because previous binder must be disposed (CollectionChanged handler must be removed from event), otherwise memory-leak might occur.
private class ColumnBinder : TypeConverter, IDisposable
{
private readonly Dispatcher dispatcher;
private readonly ObservableCollection<DataGridColumn> columns;
private readonly ObservableCollection<ItemProperty> itemProperties;
public ColumnBinder(Dispatcher dispatcher, ObservableCollection<DataGridColumn> columns, ObservableCollection<ItemProperty> itemProperties)
{
this.dispatcher = dispatcher;
this.columns = columns;
this.itemProperties = itemProperties;
this.Reset();
this.itemProperties.CollectionChanged += this.OnItemPropertiesCollectionChanged;
}
private void Reset()
{
this.columns.Clear();
foreach (var column in GenerateColumns(itemProperties))
this.columns.Add(column);
}
private static IEnumerable<DataGridColumn> GenerateColumns(IEnumerable<ItemProperty> itemProperties)
{
return DataGrid.GenerateColumns(new ItemProperties(itemProperties));
}
private void OnItemPropertiesCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
// CollectionChanged is handled in WPF's Dispatcher thread.
this.dispatcher.Invoke(new Action(() =>
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
int index = e.NewStartingIndex >= 0 ? e.NewStartingIndex : this.columns.Count;
foreach (var column in GenerateColumns(e.NewItems.Cast<ItemProperty>()))
this.columns.Insert(index++, column);
break;
case NotifyCollectionChangedAction.Remove:
if (e.OldStartingIndex >= 0)
for (int i = 0; i < e.OldItems.Count; ++i)
this.columns.RemoveAt(e.OldStartingIndex);
else
this.Reset();
break;
case NotifyCollectionChangedAction.Replace:
if (e.OldStartingIndex >= 0)
{
index = e.OldStartingIndex;
foreach (var column in GenerateColumns(e.NewItems.Cast<ItemProperty>()))
this.columns[index++] = column;
}
else
this.Reset();
break;
case NotifyCollectionChangedAction.Reset:
this.Reset();
break;
}
}));
}
public void Dispose()
{
this.itemProperties.CollectionChanged -= this.OnItemPropertiesCollectionChanged;
}
// Used in DataGrid.GenerateColumns method so that .NET takes care of generating columns from properties.
private class ItemProperties : IItemProperties
{
private readonly ReadOnlyCollection<ItemPropertyInfo> itemProperties;
public ItemProperties(IEnumerable<ItemProperty> itemProperties)
{
this.itemProperties = new ReadOnlyCollection<ItemPropertyInfo>(itemProperties.Select(itemProperty => new ItemPropertyInfo(itemProperty.Name, itemProperty.PropertyType, new ItemPropertyDescriptor(itemProperty.Name, itemProperty.PropertyType, itemProperty.IsReadOnly))).ToArray());
}
ReadOnlyCollection<ItemPropertyInfo> IItemProperties.ItemProperties
{
get { return this.itemProperties; }
}
private class ItemPropertyDescriptor : SimplePropertyDescriptor
{
public ItemPropertyDescriptor(string name, Type propertyType, bool isReadOnly)
: base(null, name, propertyType, new Attribute[] { isReadOnly ? ReadOnlyAttribute.Yes : ReadOnlyAttribute.No })
{
}
public override object GetValue(object component)
{
throw new NotSupportedException();
}
public override void SetValue(object component, object value)
{
throw new NotSupportedException();
}
}
}
}
}
}
Item.cs(用于测试)
using System;
namespace WpfApplication
{
class Item
{
public string Name { get; private set; }
public ItemKind Kind { get; set; }
public bool IsChecked { get; set; }
public Uri Link { get; set; }
public Item(string name)
{
this.Name = name;
}
}
enum ItemKind
{
ItemKind1,
ItemKind2,
ItemKind3
}
}
ViewModel.cs(用于测试)
using System;
using System.Collections.ObjectModel;
using System.Threading;
namespace WpfApplication
{
class ViewModel
{
public ObservableCollection<Item> Items { get; private set; }
public ObservableCollection<ItemProperty> ItemProperties { get; private set; }
public ViewModel()
{
this.Items = new ObservableCollection<Item>();
this.ItemProperties = new ObservableCollection<ItemProperty>();
for (int i = 0; i < 1000; ++i)
this.Items.Add(new Item("Name " + i) { Kind = (ItemKind)(i % 3), IsChecked = (i % 2) == 1, Link = new Uri("http://www.link" + i + ".com") });
}
private bool testStarted;
// Test method operates on another thread and it will first add all columns one by one in interval of 1 second, and then remove all columns one by one in interval of 1 second.
// Adding and removing will be repeated indefinitely.
public void Test()
{
if (this.testStarted)
return;
this.testStarted = true;
ThreadPool.QueueUserWorkItem(state =>
{
var itemProperties = new ItemProperty[]
{
new ItemProperty(typeof(string), "Name", true),
new ItemProperty(typeof(ItemKind), "Kind", false),
new ItemProperty(typeof(bool), "IsChecked", false),
new ItemProperty(typeof(Uri), "Link", false)
};
bool removing = false;
while (true)
{
Thread.Sleep(1000);
if (removing)
{
if (this.ItemProperties.Count > 0)
this.ItemProperties.RemoveAt(this.ItemProperties.Count - 1);
else
removing = false;
}
else
{
if (this.ItemProperties.Count < itemProperties.Length)
this.ItemProperties.Add(itemProperties[this.ItemProperties.Count]);
else
removing = true;
}
}
});
}
}
}
主窗口。xaml(用于测试)
<Window x:Class="WpfApplication.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication">
<Window.DataContext>
<local:ViewModel/>
</Window.DataContext>
<DockPanel>
<Button DockPanel.Dock="Top" Content="Test" Click="OnTestButtonClicked"/>
<DataGrid ItemsSource="{Binding Items}" local:DataGridExtension.ItemProperties="{Binding ItemProperties}" AutoGenerateColumns="False"/>
</DockPanel>
</Window>
MainWindow.xaml.cs(用于测试)
using System.Windows;
namespace WpfApplication
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void OnTestButtonClicked(object sender, RoutedEventArgs e)
{
((ViewModel)this.DataContext).Test();
}
}
}
WPF扩展(在codeplex中找到)有一个名为DispatchedObservableCollection
的ObservableCollection的扩展版本,在这里是理想的。