2013-10-01 2 views
7

Je dois créer une interface utilisateur WPF qui s'aligne sur les mises à jour Fx Rate (Currency + rate) en temps réel et les affiche dans une grille (environ 1000 mises à jour par seconde, ce qui signifie que chaque ligne de la grille mise à jour jusqu'à 1000 fois par seconde). La grille aurait au moins 50 lignes à tout moment. Pour cela, j'ai créé un Viewmodel qui s'abonne aux événements de mise à jour et stocke ces mises à jour dans un dictionnaire concurrent avec la clé comme symbole et comme valeur dans un objet RateViewModel. Ensuite, j'ai une autre collection observable qui a tous ces objets rateviewmodel, et lier cela à une grille.Mise à jour de l'interface utilisateur en temps réel

code:

public class MyViewModel 
    { 
     private readonly IRatesService ratesService; 

     private readonly ConcurrentDictionary<string, RateViewModel> rateDictionary; 
     private object _locker = new object(); 

     public MyViewModel(IRatesService ratesService) 
     { 
      this.ratesService = ratesService; 
      this.ratesService.OnUpdate += OnUpdate; 
      rateDictionary = new ConcurrentDictionary<string, RateViewModel>(); 
      RateViewModels = new ObservableCollection<RateViewModel>();    
     } 

     private void OnUpdate(object sender, RateUpdateEventArgs e) 
     { 
      RateViewModel exisistingRate; 
      if (!rateDictionary.TryGetValue(e.Update.Currency, out exisistingRate)) 
      { 
       exisistingRate = new RateViewModel(new Rate(e.Update.Currency, e.Update.Rate)); 
       rateDictionary.TryAdd(e.Update.Currency, exisistingRate);     
       return; 
      } 

      lock (_locker) 
      { 
       exisistingRate.UpdateRate(e.Update.Rate);     
      } 

      Application.Current.Dispatcher.BeginInvoke(new Action(() => SearchAndUpdate(exisistingRate))); 
     } 

     public ObservableCollection<RateViewModel> RateViewModels { get; set; } 

     private void SearchAndUpdate(RateViewModel rateViewModel) 
     { 
      //Equals is based on Currency 
      if (!RateViewModels.Contains(rateViewModel)) 
      { 
       RateViewModels.Add(rateViewModel); 
       return; 
      } 

      var index = RateViewModels.IndexOf(rateViewModel); 
      RateViewModels[index] = rateViewModel; 
     }  
    } 

J'ai 4 questions sur ce:

  • Est-il possible que je peux éliminer le ObservableCollection, comme il est conduit à 2 différents datastructures stockant les mêmes éléments - mais ai-je encore mes mises à jour relayées à l'interface utilisateur? J'ai utilisé le dictionnaire concurrent, qui conduit à verrouiller l'opération de mise à jour entière. Y a-t-il une autre façon intelligente de gérer cela plutôt que de bloquer tout le dictionnaire ou d'ailleurs toute infrastructure de données? Ma méthode UpdateRate se verrouille également - toutes mes propriétés sur mon RateviewModel sont en lecture seule sauf le prix, car cela est mis à jour. Y at-il un moyen de rendre cette atomique, s'il vous plaît noter que le prix arrive en double.

  • Y a-t-il un moyen d'optimiser la méthode SearchAndUpdate, c'est en quelque sorte lié au 1er. En ce moment je crois que c'est une opération O (n).

En utilisant .NET 4.0 et ont omis INPC par souci de concision.

* EDIT: * Pourriez-vous s'il vous plaît m'aider à réécrire cela d'une meilleure manière en tenant compte de tous les 4 points? Psuedocode fera l'affaire.

Merci, -Mike

Répondre

4

1) Je ne vous inquiétez environ 50 refs supplémentaires flottant autour

2) Oui, les structures de données sont faisables sans serrure. Interlocked Est-ce votre ami ici et ils sont à peu près tous à un seul point. ReaderWriterLock est une autre bonne option si vous ne changez pas souvent les éléments de votre dictionnaire. 3) En règle générale, si vous manipulez plus de données que l'interface utilisateur ne peut en gérer plus de données, vous devrez effectuer les mises à jour en arrière-plan, ne déclencher que INPC sur le thread UI et surtout supprimer les mises à jour de l'interface utilisateur (tout en mettant à jour le champ de sauvegarde).Approche de base va être quelque chose comme:

  1. faire une Interlocked.Exchange sur le terrain de support
  2. Utilisez Interlocked.CompareExchange pour définir un champ privé à 1, si cela retourne 1 sortie becuase il y a encore une mise à jour de l'interface utilisateur en attente
  3. Si Interlocked.CompareExchange retourné 0, invoquez à l'interface utilisateur et le feu de votre propriété événement a changé et mise à jour de champ limitation à 0 (techniquement il y a plus que vous devez faire si vous vous souciez non x86)

4) SearchAndUpdate Semble superf luous ... UpdateRate devrait bouillonner vers l'interface utilisateur et il vous suffit d'invoquer le thread d'interface utilisateur si vous devez ajouter ou supprimer un élément à la collection observable.

Mise à jour: voici un exemple d'implémentation ... les choses sont un peu plus compliquées car vous utilisez des doubles qui n'obtiennent pas d'atomicité gratuitement sur les processeurs 32 bits.

class MyViewModel : INotifyPropertyChanged 
{ 
    private System.Windows.Threading.Dispatcher dispatcher; 

    public MyViewModel(System.Windows.Threading.Dispatcher dispatcher) 
    { 
     this.dispatcher = dispatcher; 
    } 


    int myPropertyUpdating; //needs to be marked volatile if you care about non x86 
    double myProperty; 
    double MyPropery 
    { 
     get 
     { 
      // Hack for Missing Interlocked.Read for doubles 
      // if you are compiled for 64 bit you should be able to just do a read 
      var retv = Interlocked.CompareExchange(ref myProperty, myProperty, -myProperty); 
      return retv; 
     } 
     set 
     { 
      if (myProperty != value) 
      { 
       // if you are compiled for 64 bit you can just do an assignment here 
       Interlocked.Exchange(ref myProperty, value); 
       if (Interlocked.Exchange(ref myPropertyUpdating, 1) == 0) 
       { 
        dispatcher.BeginInvoke(() => 
        { 
         try 
         { 
          PropertyChanged(this, new PropertyChangedEventArgs("MyProperty")); 
         } 
         finally 
         { 
          myPropertyUpdating = 0; 
          Thread.MemoryBarrier(); // This will flush the store buffer which is the technically correct thing to do... but I've never had problems with out it 
         } 
        }, null); 
       } 

      } 
     } 
    } 

    public event PropertyChangedEventHandler PropertyChanged = delegate {}; 


}  
+0

Pourriez-vous poster un pseudo code pour le point 3 s'il vous plaît? – Mike

+0

Je ne suis pas très clair à ce sujet "Utiliser Interlocked.CompareExchange pour définir un champ privé à 1, si cela renvoie 1 sortie car il y a encore une mise à jour de l'interface utilisateur Si Interlocked.CompareExchange retourné 0, invoquez l'interface utilisateur et déclenchez votre la propriété a changé d'événement et vous met à jour le champ de limitation à 0 (techniquement il y a plus que vous devez faire si vous vous souciez de non x86) "--- Pouvez-vous poster un court extrait de code s'il vous plait? – Mike

+0

@Mike J'ai ajouté du code montrant la technique – Yaur

3

Mike -

j'aborder ce un peu différemment. Vous n'avez vraiment pas besoin d'une collection observable sauf si de nouvelles lignes Fx sont ajoutées. La collection Observable, comme vous le savez, vous donne uniquement une notification de modification intégrée dans ce scénario. Si vous avez une liste de 50 lignes (par exemple) et l'objet Fx (qui représente chaque ligne individuelle) est mis à jour 1000 fois par seconde - alors vous pouvez très bien utiliser INotifyPropertyChanged sur les propriétés Fx sur l'objet et laisser ce mécanisme se mettre à jour l'interface utilisateur comme ils changent. Ma ligne de pensée est - c'est une approche plus simple pour les mises à jour de l'interface utilisateur plutôt que de les déplacer d'une collection à l'autre

Maintenant en ce qui concerne votre deuxième point - 1000 mises à jour en une seconde (à un objet FX existant) - ce qui techniquement est illisible à partir d'une perspective UI - l'approche que j'ai prise est geler et décongeler - ce qui signifie que vous interceptez essentiellement le InotifyPropertyChanged (comme son tir à l'interface utilisateur) et maintenez la fréquence - par exemple - toutes les 1 sec - quel que soit mon statut de tous les objets FX sont (rafraîchir l'interface utilisateur). Maintenant, au cours de cette seconde - quelles que soient les mises à jour apportées aux propriétés FX - elles continuent de s'écraser sur elles-mêmes - et la dernière valeur/correcte à l'intervalle d'une seconde - est affichée à l'interface utilisateur. De cette façon, les données affichées à l'interface utilisateur sont toujours correctes et pertinentes lorsqu'elles sont affichées dans l'interface utilisateur.

+0

Lorsque vous choisissez un intervalle de mise à jour, gardez à l'esprit que l'utilisateur peut trier par colonne fréquemment mise à jour, auquel cas non seulement les chiffres changeront, mais les lignes changeront aussi de position (plus difficile à suivre visuellement). Je pense que Patrick est mort avec les mises à jour différées INPC, et vous pourriez être en mesure d'optimiser davantage en réduisant les notifications à travers les propriétés (lever un seul événement PropertyChanged avec 'null' pour le nom de la propriété). La plupart des grilles gèrent correctement ceci en actualisant la rangée entière, et quelques grilles (comme DevExpress) le feraient pour chaque changement de propriété de toute façon. –

0

Il y a plusieurs facteurs à prendre en compte, en particulier si le nombre de débits affichés change de manière dynamique. Je suppose que les 1000 mises à jour par seconde proviennent d'un thread autre que le thread de l'interface utilisateur. Le premier est que vous aurez besoin de marshall les mises à jour à l'UI thread - fait pour vous pour les mises à jour à un ViewModel existant, pas fait pour vous pour les ViewModels nouveaux/supprimés. Avec 1000 mises à jour par seconde, vous voudrez probablement contrôler la granularité du marshalling sur le thread UI et le changement de contexte que cela implique. Ian Griffiths a écrit un grand blog series à ce sujet. La seconde est que si vous voulez que votre interface utilisateur reste réactive, vous devez probablement éviter autant de collectes de mémoire de génération 2 que possible, ce qui signifie minimiser la pression sur le CPG. Cela peut être un problème dans votre cas lorsque vous créez une nouvelle mise à jour de l'objet Rate pour chaque mise à jour. Une fois que vous commencerez à avoir quelques écrans qui font la même chose, vous voudrez trouver un moyen de résumer ce comportement de mise à jour dans un composant commun. Sinon, vous saupoudrez le code de threading à travers votre ViewModels qui est sujet aux erreurs.

J'ai créé un projet open source, ReactiveTables, qui répond à ces trois préoccupations et ajoute quelques autres fonctionnalités telles que le fait de pouvoir filtrer, trier et joindre vos collections de modèles. Il y a aussi des démos montrant comment l'utiliser avec des grilles virtuelles pour obtenir les meilleures performances. Peut-être que cela peut vous aider/inspirer.

Questions connexes