Question

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);
        };
Was it helpful?

Solution

Move DataGridColumn creation code into the dispatcher delegate.

The issue happens because DataGridColumn inherits from DispatcherObject which has one field which says on which thread the DispatcherObject was created and when DataGridColumn is constructed this field will be set to your worker thread.

When that column gets added to DataGrid.Columns collection, exception will be thrown because DataGridColumn is not created on default GUI thread on which the DataGrid is created.


NEW SOLUTION

After playing around with your code, I have decided to implement different solution which should solve your problem and make your view model cleaner since it won't have GUI members (DataGridColumns) in it anymore.

New solution abstracts DataGridColumn in view model layer with ItemProperty class and DataGridExtension class takes care of converting ItemProperty instance to DataGridColumn instance in WPF's Dispatcher thread.

Here is a complete solution with test example (I recommend you create an empty WPF Application project and insert code in it to test the solution):

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 (used for testing)

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 (used for testing)

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 (used for testing)

<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 (used for testing)

using System.Windows;

namespace WpfApplication
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void OnTestButtonClicked(object sender, RoutedEventArgs e)
        {
            ((ViewModel)this.DataContext).Test();
        }
    }
}

OTHER TIPS

WPF Extension (found in codeplex) has a extended version of ObservableCollection called DispatchedObservableCollection here , which ideal here. Its worth having a look at it and customize accordingly.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top