web-dev-qa-db-fra.com

Mise à niveau vers .NET 4.5: un ItemsControl est incompatible avec la source de ses éléments

Je construis une application qui utilise de nombreux ItemControls (datagrids et listviews). Afin de mettre facilement à jour ces listes à partir de threads d'arrière-plan, j'ai utilisé cette extension pour ObservableCollections, qui a bien fonctionné:

http://geekswithblogs.net/NewThingsILearned/archive/2008/01/16/have-worker-thread-update-observablecollection-that-is-bound-to-a.aspx

Aujourd'hui, j'ai installé VS12 (qui à son tour a installé .NET 4.5), car je souhaite utiliser un composant écrit pour .NET 4.5. Avant même de mettre à niveau mon projet vers .NET 4.5 (à partir de la version 4.0), ma grille de données a commencé à générer une exception InvalidOperationException lors de la mise à jour à partir d'un fil de travail. Message d'exception:

Cette exception a été émise car le générateur de contrôle 'System.Windows.Controls.DataGrid Items.Count: 5' with name '(unnamed)' a reçu une séquence d'événements CollectionChanged qui ne correspondent pas à l'état actuel de la collection Items. Les différences suivantes ont été détectées: Le compte accumulé 4 est différent du compte réel 5. [Le compte accumulé est (Compte lors de la dernière réinitialisation + #Adds - #Removes depuis la dernière réinitialisation).]

Code de repro:

XAML:

<Window x:Class="Test1.MainWindow"
    xmlns="http://schemas.Microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.Microsoft.com/winfx/2006/xaml"
    Title="MainWindow" Height="350" Width="525">
   <Grid>
      <DataGrid ItemsSource="{Binding Items, Mode=OneTime}" PresentationTraceSources.TraceLevel="High"/>       
   </Grid>
</Window>

Code:

public partial class MainWindow : Window
{
    public ExtendedObservableCollection<int> Items { get; private set; }

    public MainWindow()
    {
        InitializeComponent();
        Items = new ExtendedObservableCollection<int>();
        DataContext = this;
        Loaded += MainWindow_Loaded;
    }

    void MainWindow_Loaded(object sender, RoutedEventArgs e)
    {
            Task.Factory.StartNew(() =>
            {
                foreach (var item in Enumerable.Range(1, 500))
                {
                    Items.Add(item);
                }
            });                
    }
}
24
LasseBP

WPF 4.5 fournit de nouvelles fonctionnalités pour accéder à des collections sur des threads non-UI.

WPF vous permet d’accéder et de modifier des collections de données sur des threads Autres que celui qui a créé la collection. Cela vous permet d'utiliser Un thread en arrière-plan pour recevoir des données d'une source externe, telle que Sous forme de base de données, et d'afficher les données sur le thread d'interface utilisateur. En utilisant un autre thread Pour modifier la collection, votre interface utilisateur reste Sensible aux interactions de l'utilisateur.

Cela peut être fait en utilisant la méthode statique EnableCollectionSynchronization sur la classe BindingOperations.

Si vous avez beaucoup de données à collecter ou à modifier, vous pouvez utiliser Un fil d’arrière-plan pour collecter et modifier les données afin que l’interface utilisateur Reste réactive aux données saisies. Pour permettre à plusieurs threads d'accéder à une collection, appelez la méthode EnableCollectionSynchronization. Lorsque vous appelez cette surcharge du EnableCollectionSynchronization (IEnumerable, Object), la méthode Le système verrouille la collection lorsque vous y accédez. Pour spécifier un rappel Afin de verrouiller la collection vous-même, appelez la surcharge EnableCollectionSynchronization (IEnumerable, Object, CollectionSynchronizationCallback).

L'utilisation est la suivante. Créez un objet utilisé comme verrou pour la synchronisation de la collection. Appelez ensuite la méthode EnableCollectionSynchronization de BindingsOperations et transmettez-lui la collection que vous souhaitez synchroniser et l'objet utilisé pour le verrouillage.

J'ai mis à jour votre code et ajouté les détails. Aussi, j'ai changé la collection en ObservableCollection normale pour éviter les conflits.

public partial class MainWindow : Window{
  public ObservableCollection<int> Items { get; private set; }

  //lock object for synchronization;
  private static object _syncLock = new object();

  public MainWindow()
  {
    InitializeComponent();
    Items = new ObservableCollection<int>();

    //Enable the cross acces to this collection elsewhere
    BindingOperations.EnableCollectionSynchronization(Items, _syncLock);

    DataContext = this;
    Loaded += MainWindow_Loaded;
  }

  void MainWindow_Loaded(object sender, RoutedEventArgs e)
  {
        Task.Factory.StartNew(() =>
        {
            foreach (var item in Enumerable.Range(1, 500))
            {
                lock(_syncLock) {
                  Items.Add(item);
                }
            }
        });                
  }
}

Voir aussi: http://10rem.net/blog/2012/01/20/wpf-45-cross-thread-collection-synchronization-redux

37
Jehof

Pour résumer ce sujet, cette AsyncObservableCollection fonctionne avec les applications WPF .NET 4 et .NET 4.5.

using System;
using System.Collections;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;
using System.Windows.Data;
using System.Windows.Threading;

namespace WpfAsyncCollection
{
    public class AsyncObservableCollection<T> : ObservableCollection<T>
    {
        public override event NotifyCollectionChangedEventHandler CollectionChanged;
        private static object _syncLock = new object();

        public AsyncObservableCollection()
        {
            enableCollectionSynchronization(this, _syncLock);
        }

        protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
        {
            using (BlockReentrancy())
            {
                var eh = CollectionChanged;
                if (eh == null) return;

                var dispatcher = (from NotifyCollectionChangedEventHandler nh in eh.GetInvocationList()
                                  let dpo = nh.Target as DispatcherObject
                                  where dpo != null
                                  select dpo.Dispatcher).FirstOrDefault();

                if (dispatcher != null && dispatcher.CheckAccess() == false)
                {
                    dispatcher.Invoke(DispatcherPriority.DataBind, (Action)(() => OnCollectionChanged(e)));
                }
                else
                {
                    foreach (NotifyCollectionChangedEventHandler nh in eh.GetInvocationList())
                        nh.Invoke(this, e);
                }
            }
        }

        private static void enableCollectionSynchronization(IEnumerable collection, object lockObject)
        {
            var method = typeof(BindingOperations).GetMethod("EnableCollectionSynchronization", 
                                    new Type[] { typeof(IEnumerable), typeof(object) });
            if (method != null)
            {
                // It's .NET 4.5
                method.Invoke(null, new object[] { collection, lockObject });
            }
        }
    }
}
11
VahidN

La réponse de Jehof est correcte.

Nous ne pouvons pas encore cibler la version 4.5 et nous avions ce problème avec nos collections observables personnalisées qui autorisaient déjà les mises à jour en arrière-plan (en utilisant Dispatcher lors des notifications d'événement).

Si quelqu'un le trouve utile, j'ai utilisé le code suivant dans notre application qui cible .NET 4.0 pour lui permettre d'utiliser cette fonctionnalité si l'environnement d'exécution est .NET 4.5:

public static void EnableCollectionSynchronization(IEnumerable collection, object lockObject)
{
    // Equivalent to .NET 4.5:
    // BindingOperations.EnableCollectionSynchronization(collection, lockObject);
    MethodInfo method = typeof(BindingOperations).GetMethod("EnableCollectionSynchronization", new Type[] { typeof(IEnumerable), typeof(object) });
    if (method != null)
    {
        method.Invoke(null, new object[] { collection, lockObject });
    }
}
6
Chris

Ceci est pour Windows 10 Version 1607 les utilisateurs utilisant la version de VS 2017 pouvant avoir ce problème.

Microsoft Visual Studio Community 2017
Version 15.1 (26403.3) Release
VisualStudio.15.Release/15.1.0+26403.3
Microsoft .NET Framework
Version 4.6.01586

Vous n'avez pas besoin du lock nor EnableCollectionSynchronization .

<ListBox x:Name="FontFamilyListBox" SelectedIndex="{Binding SelectedIndex, Mode=TwoWay}" Width="{Binding FontFamilyWidth, Mode=TwoWay}"
         SelectedItem="{Binding FontFamilyItem, Mode=TwoWay}"
         ItemsSource="{Binding FontFamilyItems}"
          diag:PresentationTraceSources.TraceLevel="High">
    <ListBox.ItemTemplate>
        <DataTemplate DataType="typeData:FontFamilyItem">
            <Grid>
                <TextBlock Text="{Binding}" diag:PresentationTraceSources.TraceLevel="High"/>

            </Grid>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

public ObservableCollection<string> fontFamilyItems;
public ObservableCollection<string> FontFamilyItems
{
    get { return fontFamilyItems; }
    set { SetProperty(ref fontFamilyItems, value, nameof(FontFamilyItems)); }
}

public string fontFamilyItem;
public string FontFamilyItem
{
    get { return fontFamilyItem; }
    set { SetProperty(ref fontFamilyItem, value, nameof(FontFamilyItem)); }
}

private List<string> GetItems()
{
    List<string> fonts = new List<string>();
    foreach (System.Windows.Media.FontFamily font in Fonts.SystemFontFamilies)
    {
        fonts.Add(font.Source);
        ....
        other stuff..
    }
    return fonts;
}

public async void OnFontFamilyViewLoaded(object sender, EventArgs e)
{
    DisposableFontFamilyViewLoaded.Dispose();
    Task<List<string>> getItemsTask = Task.Factory.StartNew(GetItems);

    try
    {
        foreach (string item in await getItemsTask)
        {
            FontFamilyItems.Add(item);
        }
    }
    catch (Exception x)
    {
        throw new Exception("Error - " + x.Message);
    }

    ...
    other stuff
}
0
Nasheayahu