Этот тип CollectionView не поддерживает изменения в SourceCollection из потока, отличного от потока Dispatcher

У меня есть DataGrid, который заполняет данные из ViewModel асинхронным методом. Мой DataGrid:

<DataGrid ItemsSource="{Binding MatchObsCollection}"  x:Name="dataGridParent" 
                      Style="{StaticResource EfesDataGridStyle}" 
                      HorizontalGridLinesBrush="#DADADA" VerticalGridLinesBrush="#DADADA" Cursor="Hand" AutoGenerateColumns="False" 
                      RowDetailsVisibilityMode="Visible"  >

Я использую http://www.amazedsaint.com/2010/10/asynchronous-delegate-command-for-your.html для реализации асинхронного способа в моей модели просмотра.

Вот мой код viewmodel:

public class MainWindowViewModel:WorkspaceViewModel,INotifyCollectionChanged
    {        

        MatchBLL matchBLL = new MatchBLL();
        EfesBetServiceReference.EfesBetClient proxy = new EfesBetClient();

        public ICommand DoSomethingCommand { get; set; }
        public MainWindowViewModel()
        {
            DoSomethingCommand = new AsyncDelegateCommand(
                () => Load(), null, null,
                (ex) => Debug.WriteLine(ex.Message));           
            _matchObsCollection = new ObservableCollection<EfesBet.DataContract.GetMatchDetailsDC>();                

        }       

        List<EfesBet.DataContract.GetMatchDetailsDC> matchList;
        ObservableCollection<EfesBet.DataContract.GetMatchDetailsDC> _matchObsCollection;

        public ObservableCollection<EfesBet.DataContract.GetMatchDetailsDC> MatchObsCollection
        {
            get { return _matchObsCollection; }
            set
            {
                _matchObsCollection = value;
                OnPropertyChanged("MatchObsCollection");
            }
        }        
        //
        public void Load()
        {            
            matchList = new List<GetMatchDetailsDC>();
            matchList = proxy.GetMatch().ToList();

            foreach (EfesBet.DataContract.GetMatchDetailsDC match in matchList)
            {
                _matchObsCollection.Add(match);
            }

        }

Как вы можете видеть в моем методе Load() в моей модели ViewModel, сначала я получаю matchList (который является списком класса DataContract) из моего сервиса. Затем через цикл foreach я вставляю элементы matchList в свой _matchObsCollection (который это ObservableCollection of DataContract Class)). Теперь я получаю вышеуказанную ошибку (как показано в заголовке). "Этот тип CollectionView не поддерживает изменения в SourceCollection из потока, отличного от потока Dispatcher", enter image description here

Может ли кто-нибудь предложить мне какое-либо решение. Если возможно, мне хотелось бы знать, как связать мой DataGrid в представлении, а также обновлять его асинхронно, если есть лучший способ.

Ответ 1

Так как ваш ObservableCollection создан в потоке пользовательского интерфейса, его можно изменить только из потока пользовательского интерфейса, а не из других потоков. Это называется привязкой к потоку.

Если вам когда-либо понадобится обновлять объекты, созданные в потоке пользовательского интерфейса из другого потока, просто put the delegate on UI Dispatcher, и это будет работать для вас делегирования его в поток пользовательского интерфейса. Это будет работать -

    public void Load()
    {
        matchList = new List<GetMatchDetailsDC>();
        matchList = proxy.GetMatch().ToList();

        foreach (EfesBet.DataContract.GetMatchDetailsDC match in matchList)
        {
            App.Current.Dispatcher.Invoke((Action)delegate // <--- HERE
            {
                _matchObsCollection.Add(match);
            });
        }
    }

Ответ 2

Если я не ошибаюсь, в WPF 4.5 вы можете сделать это без проблем.

Теперь, чтобы решить эту проблему, вы должны использовать контекст синхронизации. Перед запуском потока вам нужно сохранить контекст синхронизации в потоке ui.

var uiContext = SynchronizationContext.Current;

Затем вы используете его в своем потоке:

uiContext.Send(x => _matchObsCollection.Add(match), null);

Взгляните на эту туточку http://www.codeproject.com/Articles/31971/Understanding-SynchronizationContext-Part-I

Ответ 3

Вы можете сделать это:

App.Current.Dispatcher.Invoke((System.Action)delegate
             {
               _matchObsCollection.Add(match)
             });

Для .NET 4.5+: вы можете следовать ответам Даниэля. В своем примере вы даете ответчику издателю, что им нужно вызвать или вызвать в правильной теме:

var uiContext = SynchronizationContext.Current;
uiContext.Send(x => _matchObsCollection.Add(match), null);

Или вы можете поставить ответственность за свой сервис /viewmodel/whatever и просто включить CollectionSynchronization. Таким образом, если вы звоните, вам не нужно беспокоиться о том, на каком потоке вы находитесь, и по которому вы совершаете вызов. Ответственность не для издателя. (Это может дать вам небольшую служебную нагрузку, но это делается в центральной службе, это может сэкономить вам много исключений и упростить обслуживание приложений.)

private static object _lock = new object();

public MainWindowViewModel()
{
    // ...
    _matchObsCollection = new ObservableCollection<EfesBet.DataContract.GetMatchDetailsDC>();
    BindingOperations.EnableCollectionSynchronization(_matchObsCollection , _lock);
} 

Дополнительная информация: https://msdn.microsoft.com/en-us/library/system.windows.data.bindingoperations.enablecollectionsynchronization(v=vs.110).aspx

В Visual Studio 2015 (Pro) перейдите в Debug → Windows → Threads, чтобы легко отладить и посмотреть, в каких потоках вы находитесь.

Ответ 4

В моем случае (я заполняю ObservableCollection асинхронными задачами и не имею доступа к экземпляру App). Я использую TaskScheduler.FromCurrentSynchronizationContext() для очистки коллекции при сбое:

        // some main task
        Task loadFileTask = Task.Factory.StartNew(...);

        Task cleanupTask = loadFileTask.ContinueWith(
            (antecedent) => { CleanupFileList(); },
            /* do not cancel this task */
            CancellationToken.None,
            /* run only if faulted main task */
            TaskContinuationOptions.OnlyOnFaulted,
            /* use main SynchronizationContext */
            TaskScheduler.FromCurrentSynchronizationContext());

Ответ 5

Если вы используете BackgroundWorker, вы должны поднять событие в том же потоке пользовательского интерфейса.

Например, если у вас есть два вида A и B, а следующий код внутри A вызывает событие WakeUpEvent

//Code inside codebehind or viewmodel of A
    var worker = new BackgroundWorker();
    worker.DoWork += WorkerDoWork; //<-- Don't raise the event WakeUpEvent inside this method
    worker.RunWorkerCompleted += workerRunWorkerCompleted; // <-- Raise the event WakeUpEvent inside this method instead
    worker.RunWorkerAsync();

//Code inside codebehind or viewmodel of view B
    public ViewB () {
        WakeUpEvent += UpdateUICallBack;
    }
    private void UpdateUICallBack() {
        //Update here UI element
    }

Метод WorkerDoWork выполняется в потоке, который не совпадает с пользовательским интерфейсом.