0

I am currently working on a C# System.Windows.Controls.DataGrid that needs to generate the columns dynamically depending on the data. It can add and/or remove columns during runtime.

I am using a Thread in the ViewModel class to update the ObservableCollection that feeds the DataGrid.

I have read that post which explains the best solution I have found for my problem. Although, the Columns.CollectionChanged Delegate from the DataGridExtension class throws a InvalideOperationException : The calling thread cannot access this object because a different thread owns it.

Heres some code to picture it all :
The 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);
        }
    }
  }
}

DataGridExtension class

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

The section where I put /// This is where I tried to fix the exception. /// is where the exception is getting thrown, exactly at myGrid.add(...);

The myGrid object does not allow me to add that column to be added to the collection of columns of the DataGrid. Which is why I surrounded it with a Dispatcher.Invoke. Strangely, if I execute myGrid.Columns.Add(new DataGridTextColumn()); it works and I can see the empty columns getting added in the view but myGrid.Columns.Add(temp); throws the exception.

There must be something I don't catch with this thing.
Please HELP!!!!

EDIT following Stipo suggestion

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

2 回答 2

4

将 DataGridColumn 创建代码移动到调度程序委托中。

问题的发生是因为 DataGridColumn 继承自 DispatcherObject,其中有一个字段说明 DispatcherObject 是在哪个线程上创建的,并且在构造 DataGridColumn 时,该字段将设置为您的工作线程。

当该列被添加到 DataGrid.Columns 集合时,将引发异常,因为 DataGridColumn 不是在创建 DataGrid 的默认 GUI 线程上创建的。


新解决方案

在玩弄了您的代码之后,我决定实施不同的解决方案来解决您的问题并使您的视图模型更清晰,因为它不再包含 GUI 成员(DataGridColumns)。

新的解决方案使用 ItemProperty 类抽象视图模型层中的 DataGridColumn,而 DataGridExtension 类负责将 ItemProperty 实例转换为 WPF 的 Dispatcher 线程中的 DataGridColumn 实例。

这是一个带有测试示例的完整解决方案(我建议您创建一个空的 WPF 应用程序项目并在其中插入代码来测试解决方案):

项目属性.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;
        }
    }
}

数据网格扩展.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;
                    }
                }
            });
        }
    }
}

MainWindow.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();
        }
    }
}
于 2013-05-23T23:22:27.947 回答
1

WPF 扩展(在 codeplex 中找到)有一个扩展版本的 ObservableCollection,称为DispatchedObservableCollection here,这里很理想。值得一看并进行相应的定制。

于 2013-05-23T15:22:15.963 回答