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