2010-04-13 3 views
2

Première question ici donc bonjour tout le monde.Comment mettre à jour le thread/la classe de l'interface utilisateur graphique à partir du thread/de la classe de travail?

L'exigence sur laquelle je travaille est une petite application de test qui communique avec un périphérique externe via un port série. La communication peut prendre beaucoup de temps et l'appareil peut renvoyer toutes sortes d'erreurs.

Le périphérique est bien abstrait dans sa propre classe que le thread graphique commence à s'exécuter dans son propre thread et possède les fonctions de base habituelles d'ouverture/fermeture/lecture de données/écriture. L'interface graphique est également assez simple - choisissez le port COM, ouvrez, fermez, affichez les données lues ou les erreurs du périphérique, autorisez la modification et la réécriture etc.

La question est simplement de savoir comment mettre à jour l'interface graphique de la classe d'unité? Il existe plusieurs types distincts de données que le périphérique gère. J'ai donc besoin d'un pont relativement générique entre la classe de l'interface graphique/la classe de threads et la classe/thread de l'unité de travail. Dans le GUI vers le périphérique, tout fonctionne correctement avec [Begin] Appeler des appels pour ouvrir/fermer/lire/écrire etc. sur divers événements générés par l'interface graphique.

J'ai lu le fil here (How to update GUI from another thread in C#?) où l'on suppose que l'interface graphique et le thread de travail sont dans la même classe. Les recherches effectuées par Google expliquent comment créer un délégué ou comment créer un arrière-plan classique, mais ce n'est pas du tout ce dont j'ai besoin, même si cela peut faire partie de la solution. Donc, y a-t-il une structure simple mais générique qui peut être utilisée?

Mon niveau de C# est modéré et j'ai programmé toute ma vie professionnelle, étant donné un indice je vais comprendre (et poster) ... Merci d'avance pour toute aide.

Répondre

6

Vous pouvez exposer une méthode publique sur votre classe d'interface utilisateur que la classe d'unités peut appeler sur le thread d'arrière-plan avec toutes les informations dont elle a besoin pour passer à l'interface utilisateur. Cette méthode publique sera exécutée dans le contexte du thread d'arrière-plan, mais comme elle appartient à la classe d'interface utilisateur, vous pouvez désormais utiliser les techniques de marshaling d'appel que vous avez lues.

Ainsi, la conception serait alors plus simple:

  • ajouter une méthode à votre classe d'interface utilisateur (par exemple MyUIForm) appelé quelque chose comme UpdateUI() qui prend toutes les structures de données que vous utilisez pour transmettre les données de l'appareil à l'interface utilisateur que vous utilisez. Vous pouvez déclarer cette méthode dans une interface (par exemple, IUIForm), si vous souhaitez ultérieurement prendre en charge DI/IoC et que le formulaire l'implémente.
  • sur le thread A (le thread de l'interface utilisateur), votre classe d'interface utilisateur crée la classe de périphérique, initialise tous les paramètres nécessaires et démarre son thread d'arrière-plan. Il passe également un pointeur sur lui-même.
  • sur le thread B, le périphérique collecte les données et appelle MyUIForm.UpdateUI() (ou IUIForm.UpdateUI()).
  • UpdateUI fait Invoke ou BeginInvoke selon le cas.

Notez que cela présente l'avantage secondaire d'encapsuler toute l'interface utilisateur et la logique de présentation dans votre classe d'interface utilisateur. Votre classe d'appareil peut désormais se concentrer sur le matériel.

Mise à jour: Pour répondre à vos problèmes d'évolutivité -

Peu importe combien votre application se développe et combien de classes UI que vous avez, vous voulez toujours traverser la limite de fil à l'aide du BeginInvoke pour la classe particulière de l'interface utilisateur vous vouloir mettre à jour.(Cette classe d'interface utilisateur peut être un contrôle spécifique ou la racine d'un arbre visuel particulier, cela n'a pas vraiment d'importance) La raison principale est que si vous avez plus d'un thread d'interface utilisateur, vous devez vous assurer que la mise à jour cette interface utilisateur particulière a été créée, en raison de la façon dont la messagerie Windows et windows fonctionnent. Par conséquent, la logique actuelle de traverser le fil de la frontière devrait être encapsulée dans la couche d'interface utilisateur.

Votre classe de périphérique ne devrait pas avoir à se soucier des classes d'interface utilisateur et du thread à mettre à jour. En fait, je rendrais l'appareil totalement ignorant de toute interface utilisateur et j'exposerais simplement des événements sur lesquels différentes classes d'interface utilisateur peuvent s'abonner.

Notez que la solution alternative consiste à rendre le thread entièrement encapsulé dans la classe de périphérique et à rendre l'interface utilisateur ignorante de l'existence d'un thread de renseignements. Cependant, le croisement des limites de threads devient alors la responsabilité de la classe d'unité et devrait être contenu dans sa logique, vous ne devriez donc pas utiliser l'interface utilisateur pour traverser les threads. Cela signifie également que votre classe d'appareils est liée à un thread d'interface utilisateur particulier.

+0

C'est probablement la méthode la plus simple, et ça marche bien au début et je l'ai déjà utilisé, mais ça a l'inconvénient qu'une fois l'interface graphique agrandie et constituée de plus de classes ou UserControls (ce qui semble inévitable) ça ne va pas bien. Peut-être aurais-je dû préciser dans ma question que la raison pour laquelle je cherche une solution générique est que je m'attends à ce que cette application grandisse avec le temps et qu'elle aimerait quelque chose qui évolue. – 0xDEADBEEF

0

Il s'agit d'une version avec un gestionnaire d'événements.
Il est simplifié donc il n'y a pas de contrôles d'interface utilisateur dans le formulaire et pas de propriétés dans la classe SerialIoEventArgs.

  1. Placez votre code pour mettre à jour l'interface utilisateur sous le commentaire // Mise à jour l'interface utilisateur
  2. Placez votre code pour lire IO série sous le commentaire // Lecture de série IO
  3. Ajouter des champs/propriétés à SerialIoEventArgs classe et le peupler dans la méthode OnReadCompleated.
public class SerialIoForm : Form 
{ 
    private delegate void SerialIoResultHandlerDelegate(object sender, SerialIoEventArgs args); 
    private readonly SerialIoReader _serialIoReader; 
    private readonly SerialIoResultHandlerDelegate _serialIoResultHandler; 

    public SerialIoForm() 
    { 
     Load += SerialIoForm_Load; 
     _serialIoReader = new SerialIoReader(); 
     _serialIoReader.ReadCompleated += SerialIoResultHandler; 
     _serialIoResultHandler = SerialIoResultHandler; 
    } 

    private void SerialIoForm_Load(object sender, EventArgs e) 
    { 
     _serialIoReader.StartReading(); 
    } 
    private void SerialIoResultHandler(object sender, SerialIoEventArgs args) 
    { 
     if (InvokeRequired) 
     { 
      Invoke(_serialIoResultHandler, sender, args); 
      return; 
     } 
     // Update UI 
    } 
} 
public class SerialIoReader 
{ 
    public EventHandler ReadCompleated; 
    public void StartReading() 
    { 
     ThreadPool.QueueUserWorkItem(ReadWorker); 
    } 
    public void ReadWorker(object obj) 
    { 
     // Read from serial IO 

     OnReadCompleated(); 
    } 

    private void OnReadCompleated() 
    { 
     var readCompleated = ReadCompleated; 
     if (readCompleated == null) return; 
     readCompleated(this, new SerialIoEventArgs()); 
    } 
} 

public class SerialIoEventArgs : EventArgs 
{ 
} 
0

Ainsi, après quelques recherches sur la base des réponses ci-dessus, plus Google recherche et demander à un collègue qui connaît un peu C# ma solution choisie au problème est ci-dessous. Je reste intéressé par les commentaires, les suggestions et les améliorations. D'abord quelques détails supplémentaires sur le problème, qui est en fait assez générique dans le sens où l'interface graphique contrôle quelque chose, qui doit rester totalement abstrait, à travers une série d'événements auxquels l'interface graphique doit réagir. Il y a quelques problèmes distincts:

  1. Les événements eux-mêmes, avec différents types de données. Les événements seront ajoutés, supprimés, modifiés au fur et à mesure de l'évolution du programme.
  2. Comment faire le pont entre plusieurs classes qui composent l'interface graphique (différents UserControls) et les classes qui résument le matériel.
  3. Toutes les classes peuvent produire et consommer des événements et doivent rester découplées autant que possible.
  4. Le compilateur doit repérer le codage cockups dans la mesure du possible (par exemple. Un événement qui envoie un type de données, mais un comsumer qui attend une autre)

La première partie de c'est les événements. Comme l'interface utilisateur graphique et le périphérique peuvent générer plusieurs événements, avec éventuellement des types de données différents associés, un répartiteur d'événements est pratique.Cela doit être générique dans les deux événements et les données, donc:

// Define a type independent class to contain event data 
    public class EventArgs<T> : EventArgs 
    { 
    public EventArgs(T value) 
    { 
     m_value = value; 
    } 

    private T m_value; 

    public T Value 
    { 
     get { return m_value; } 
    } 
} 

// Create a type independent event handler to maintain a list of events. 
public static class EventDispatcher<TEvent> where TEvent : new() 
{ 
    static Dictionary<TEvent, EventHandler> Events = new Dictionary<TEvent, EventHandler>(); 

    // Add a new event to the list of events. 
    static public void CreateEvent(TEvent Event) 
    { 
     Events.Add(Event, new EventHandler((s, e) => 
     { 
      // Insert possible default action here, done every time the event is fired. 
     })); 
    } 

    // Add a subscriber to the given event, the Handler will be called when the event is triggered. 
    static public void Subscribe(TEvent Event, EventHandler Handler) 
    { 
     Events[Event] += Handler; 
    } 

    // Trigger the event. Call all handlers of this event. 
    static public void Fire(TEvent Event, object sender, EventArgs Data) 
    { 
     if (Events[Event] != null) 
      Events[Event](sender, Data); 

    } 
} 

Maintenant, nous avons besoin de certains événements et qui viennent du monde C, j'aime énumérations, donc je définir certains événements que l'interface graphique soulèvera:

public enum DEVICE_ACTION_REQUEST 
    { 
    LoadStuffFromXMLFile, 
    StoreStuffToDevice, 
    VerifyStuffOnDevice, 
    etc 
    } 

maintenant partout dans le champ (espace de noms, généralement) de la classe statique du EventDispatcher il est possible de définir un nouveau répartiteur:

 public void Initialize() 
     { 
     foreach (DEVICE_ACTION_REQUEST Action in Enum.GetValues(typeof(DEVICE_ACTION_REQUEST))) 
      EventDispatcher<DEVICE_ACTION_REQUEST>.CreateEvent(Action); 
     } 

Cela crée un gestionnaire d'événements pour chaque événement dans la salle um.

et consommé en vous inscrivant à l'événement comme ce code dans le constructeur de l'objet consommation de l'appareil:

 public DeviceController() 
    { 
     EventDispatcher<DEVICE_ACTION_REQUEST>.Subscribe(DEVICE_ACTION_REQUEST.LoadAxisDefaults, (s, e) => 
      { 
       InControlThread.Invoke(this,() => 
       { 
        ReadConfigXML(s, (EventArgs<string>)e); 
       }); 
      }); 
    } 

Lorsque le InControlThread.Invoke est une classe abstraite qui enveloppe tout simplement l'appel Invoke.

événements peuvent être déclenchés par l'interface utilisateur graphique simple:

 private void buttonLoad_Click(object sender, EventArgs e) 
     { 
      string Filename = @"c:\test.xml"; 
      EventDispatcher<DEVICE_ACTION_REQUEST>.Fire(DEVICE_ACTION_REQUEST.LoadStuffFromXMLFile, sender, new EventArgs<string>(Filename)); 
     } 

Ceci a l'avantage que si l'événement de collecte et de consommer les types correspondent pas (ici la chaîne Nom du fichier) le compilateur grogner.

Il existe de nombreuses améliorations qui peuvent être apportées, mais c'est le cœur du problème. Cela m'intéresserait, comme je l'ai dit dans les commentaires, surtout s'il y a des omissions ou des bogues flagrants ou des lacunes. J'espère que cela aide quelqu'un.

+0

Il me semble que vous essayez de placer la sémantique C sur C#. Ce n'est pas productif. Vous pourriez simplement ajouter des événements à la classe ComSer et .BeginInvoke (asynchrone) eux et le formulaire s'y serait abonné etc. Maintenant vous avez cette chose hideuse avec votre propre sémantique que personne venant nativement de C# background ne voit de raison. Je suis désolé que ce soit dur et aucune offense signifiée. –

+0

Merci pour le commentaire. Bien que vous puissiez évidemment fournir des critiques au nom de l'ensemble du monde C# J'ai googlé les points pertinents de votre suggestion, je ne suis toujours pas le plus sage quant à la façon dont ils aideraient. – 0xDEADBEEF

Questions connexes