web-dev-qa-db-fra.com

Ce type de CollectionView ne prend pas en charge les modifications apportées à son SourceCollection à partir d'un thread différent du thread Dispatcher.

J'ai un DataGrid qui remplit les données de ViewModel par une méthode asynchrone. Mon DataGrid est:

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

J'utilise http://www.amazedsaint.com/2010/10/asynchronous-delegate-command-for-your.html pour implémenter de manière asynchrone dans mon viewmodel.

Voici mon code 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);
            }

        }

Comme vous pouvez le voir dans ma méthode Load () dans mon ViewModel, je reçois d'abord matchList (qui est la liste d'une classe DataContract) de mon service. Puis, par boucle boucle foreach, j'insère mes éléments matchList dans mon _matchObsCollection (qui est un ObservableCollection de l'erreur DataContract Class)). Maintenant, l'erreur suivante s'affiche (comme je l'ai montré dans le titre) "Ce type de CollectionView ne prend pas en charge les modifications apportées à sa SourceCollection à partir d'un thread différent du thread Dispatcher" enter image description here

De plus, si cela est possible, j'aimerais savoir comment lier mon DataGrid dans View et l'actualiser de manière asynchrone, le cas échéant.

111
Anindya Chatterjee

Comme votre ObservableCollection est créé sur un thread d'interface utilisateur, vous ne pouvez le modifier qu'à partir d'un thread d'interface utilisateur et non d'autres threads. Cela s'appelle affinité du fil .

Si vous avez besoin de mettre à jour des objets créés sur un thread d'interface utilisateur à partir d'un thread différent, il vous suffit de put the delegate on UI Dispatcher et cela fonctionnera pour vous en le déléguant à un thread d'interface utilisateur. Cela fonctionnera -

    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);
            });
        }
    }
194
Rohit Vats

Si je ne me trompe pas, dans WPF 4.5, vous devriez pouvoir le faire sans problème.

Maintenant, pour résoudre ce problème, vous devez utiliser le contexte de synchronisation. Avant de lancer le thread, vous devez stocker le contexte de synchronisation dans le thread ui.

var uiContext = SynchronizationContext.Current;

Ensuite, vous l'utilisez dans votre fil de discussion:

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

Jetez un oeil à cette tuto http://www.codeproject.com/Articles/31971/Understanding-SynchronizationContext-Part-I

51
Daniel

Tu peux le faire:

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

Pour .NET 4.5+: Vous pouvez suivre la réponse de Daniel. Dans son exemple, vous donnez à l'éditeur la responsabilité d'appeler ou d'invoquer sur le fil approprié:

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

Ou vous pouvez en attribuer la responsabilité à votre service/viewmodel/peu importe et simplement activer CollectionSynchronization. Ainsi, si vous passez un appel, vous n'avez pas à vous soucier du fil sur lequel vous vous trouvez ni sur celui sur lequel vous passez l'appel. La responsabilité n’incombe plus à l’éditeur .(Cela peut vous donner un léger surcoût de performances, mais si vous effectuez cette opération dans un service central, vous éviterez de nombreuses exceptions et vous faciliterz la maintenance des applications.)

private static object _lock = new object();

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

Plus d'infos: https://msdn.Microsoft.com/en-us/library/system.windows.data.bindingoperations.enablecollectionsynchronization(v=vs.110).aspx

Dans Visual Studio 2015 (Pro), accédez à Debug -> Windows -> Threads pour déboguer facilement et voir sur quels threads vous êtes.

41
juFo

J'ai rencontré le même problème une fois et l'ai résolu avec AsyncObservableCollection ( http://www.thomaslevesque.com/2009/04/17/wpf-binding-to-an-asynchronous-collection/ ).

3
mnyarar

Si vous utilisez BackgroundWorker, vous devez déclencher l'événement dans le même fil de l'interface utilisateur.

Par exemple, si vous avez deux vues A et B et que le code suivant à l'intérieur de A déclenche l'événement 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
    }

La méthode WorkerDoWork est exécutée dans un thread différent de l'interface utilisateur.

2
Gianluca Conte

Dans mon cas (je remplis ObservableCollection avec des tâches asynchrones et je n'ai pas accès à l'instance App), j'utilise TaskScheduler.FromCurrentSynchronizationContext() pour nettoyer la collection en défaut:

        // 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());
2
Vladislav

J'ai trouvé une solution ici: https://www.thomaslevesque.com/2009/04/17/wpf-binding-to-an-asasynchronous- collection/ . classe et l'utiliser à la place de ObservableCollection. Cela a fonctionné pour moi.

public class AsyncObservableCollection<T> : ObservableCollection<T>
{
    private SynchronizationContext _synchronizationContext = SynchronizationContext.Current;

    public AsyncObservableCollection()
    {
    }

    public AsyncObservableCollection(IEnumerable<T> list)
        : base(list)
    {
    }

    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
        if (SynchronizationContext.Current == _synchronizationContext)
        {
            // Execute the CollectionChanged event on the current thread
            RaiseCollectionChanged(e);
        }
        else
        {
            // Raises the CollectionChanged event on the creator thread
            _synchronizationContext.Send(RaiseCollectionChanged, e);
        }
    }

    private void RaiseCollectionChanged(object param)
    {
        // We are in the creator thread, call the base implementation directly
        base.OnCollectionChanged((NotifyCollectionChangedEventArgs)param);
    }

    protected override void OnPropertyChanged(PropertyChangedEventArgs e)
    {
        if (SynchronizationContext.Current == _synchronizationContext)
        {
            // Execute the PropertyChanged event on the current thread
            RaisePropertyChanged(e);
        }
        else
        {
            // Raises the PropertyChanged event on the creator thread
            _synchronizationContext.Send(RaisePropertyChanged, e);
        }
    }

    private void RaisePropertyChanged(object param)
    {
        // We are in the creator thread, call the base implementation directly
        base.OnPropertyChanged((PropertyChangedEventArgs)param);
    }
}
1
Istvan Heckl

J'avais aussi cette erreur:

"Ce type de CollectionView ne prend pas en charge les modifications apportées à son SourceCollection à partir d'un thread différent du thread Dispatcher"

Il s’est avéré que j’avais créé une nouvelle configuration appelée "Version Android" qui était une copie de la configuration "Version" et que je l’utilisais pour créer la nouvelle version dans Archive Manager. Je suis revenu à la configuration "Release" et tout a bien été construit. Plus d'erreur.

J'espère que ça aide quelqu'un.

0
Shane