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);
        };

WPF MVVM动态列多线程

将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的扩展版本,在这里是理想的。