0

私は現在、データに応じて列を動的に生成する必要がある C# System.Windows.Controls.DataGrid に取り組んでいます。実行時に列を追加および/または削除できます。

ViewModel クラスで Thread を使用して、DataGrid にフィードする ObservableCollection を更新しています。

私の問題に対して見つけた最良の解決策を説明する投稿を読みました。ただし、DataGridExtension クラスの Columns.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 クラス

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 クラス

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.Add(new 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);
        };
4

2 に答える 2

4

DataGridColumn 作成コードをディスパッチャー デリゲートに移動します。

この問題は、DataGridColumn が、DispatcherObject が作成されたスレッドを示す 1 つのフィールドを持つ DispatcherObject から継承され、DataGridColumn が構築されると、このフィールドがワーカー スレッドに設定されるために発生します。

その列が DataGrid.Columns コレクションに追加されると、DataGrid が作成されるデフォルトの GUI スレッドで DataGridColumn が作成されないため、例外がスローされます。


新しいソリューション

コードをいじった後、GUI メンバー (DataGridColumns) がなくなるため、問題を解決し、ビュー モデルをよりクリーンにする別のソリューションを実装することにしました。

新しいソリューションでは、ItemProperty クラスを使用してビュー モデル レイヤーで DataGridColumn を抽象化し、DataGridExtension クラスは、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;
                    }
                }
            });
        }
    }
}

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 にあります) には、 here と呼ばれる ObservableCollection の拡張バージョンがあり、DispatchedObservableCollection ここで理想的です。それを見て、それに応じてカスタマイズする価値があります。

于 2013-05-23T15:22:15.963 に答える