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);
        };
Community
  • 1
  • 1
ncourcy84
  • 101
  • 1
  • 10

2 Answers2

4

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();
        }
    }
}
Stipo
  • 4,566
  • 1
  • 21
  • 37
  • Doing this result in the exception getting thrown at line temp.Header = column.Header; – ncourcy84 May 24 '13 at 13:07
  • Then the code is obviously not executing on Dispatcher's thread. WPF disallows to even access property value of DispatcherObject-derived class on another thread, not just changing it. Try moving entire CollectionChanged delegate to Dispatcher. – Stipo May 24 '13 at 15:13
  • I have edited my question with your suggestion. Is it what you meant? If yes, same result as above. – ncourcy84 May 24 '13 at 15:33
  • I have managed to reproduce the issue in my test application, but currently I don't have time to give you a complete answer, I will give it tomorrow. Your problem is this: you create column in background thread which can only be accessed in background thread (because it derives from DispatcherObject) and then you want to make a clone of it in WPF's main thread. Solution to your problem is that background thread column must be serialized (for example, save its properties into Dictionary) so it can be accessed from WPF's main thread. Also Columns attached property must be accessed on WPF thread. – Stipo May 24 '13 at 19:05
  • Thank you Stipo for the time spent doing this example. But I'm having a hard time trying to understand the conversion from ItemProperty to DataGridColumn. Can I put any DataGridColumn properties in the ItemProperty object? Like Binding... – ncourcy84 May 27 '13 at 16:30
  • DataGridColumn.Binding will be automatically created from ItemProperty.Name. Conversion is little more complex because I wanted to use already existing conversion logic which is contained in method DataGrid.GenerateColumns (http://msdn.microsoft.com/en-us/library/system.windows.controls.datagrid.generatecolumns.aspx). You should check out DataGrid.GenerateColumns with Reflector to see which DataGridColumn properties are set by the conversion process. In short, ItemProperty.PropertyType is used to generate appropriate DataGridColumn-derived instance... – Stipo May 27 '13 at 16:46
  • If ItemProperty.PropertyType is string, generated column will be DataGridTextColumn, if it is Enum, it will be DataGridComboBoxColumn, if it is bool, it will be DataGridCheckBoxColumn, if it is Uri, it will be DataGridHyperlinkColumn. If ItemProperty.PropertyType doesn't implement IComparable, DataGridColumn.CanUserSort will be set to false. DataGridBoundColumn.Binding will be set from ItemProperty.Name. DataGridColumn.IsReadOnly and DataGridBoundColumn.Binding.Mode will be set to true and BindingMode.OneWay if ItemProperty.IsReadOnly is true... – Stipo May 27 '13 at 16:50
  • I have concluded all this by inspecting code of DataGrid.GenerateColumns method with Reflector. If you need to set additional properties on DataGridColumn, it can also be done, but it will have to be done in my code, not that of DataGrid.GenerateColumns anymore. Let me know which properties do you need and I will update my solution. – Stipo May 27 '13 at 16:53
  • Yeah I'm reading the code of DataGridColumn.cs and it seems to be working as you say. But I could generate the columns myself from the ItemProperty object, why DataGrid.GenerateColumns? It's even not suggested by msdn to use it. – ncourcy84 May 27 '13 at 16:55
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/30700/discussion-between-ncourcy84-and-stipo) – ncourcy84 May 27 '13 at 16:58
1

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.

S.N
  • 4,910
  • 5
  • 31
  • 51
  • 1
    Why do you think this would solve the problem? In my understanding it is the call myGrid.Columns.Add that is problematic. So the concerned ObservableCollection is the Columns collection in the DataGrid class. I can't change that right? – ncourcy84 May 23 '13 at 17:37