Laden...

Wie kann ich aus einer Klasse eine Fortschrittsanzeige am UI anzeigen lassen?

Erstellt von bert21 vor 6 Jahren Letzter Beitrag vor 6 Jahren 3.019 Views
B
bert21 Themenstarter:in
24 Beiträge seit 2010
vor 6 Jahren
Wie kann ich aus einer Klasse eine Fortschrittsanzeige am UI anzeigen lassen?

Ich habe ein kleines Architekturproblem:

Ein Prozess soll einen Stapel Aufträge erledigen (z.B. Mails verschicken). Dazu soll es eine Fortschrittsanzeige geben. Momentan löse ich das wie folgt:

  • Eine Service Klasse stellt die notwendigen Funktionen bereit (FetchAll(), SumbitMail() etc.)
  • Ein WPF UserControl enthält die Fortschrittsanzeige
  • Ich starte im UC den Prozess in einem neuen Thread und nutzte dann Dispatcher.Invoke für das Aktualisieren der Fortschrittsanzeige.

Dabei muss ich aber den eigentlichen Durchlaufvorgang im View anlegen:


MailService srv = new MailService();
List<Mail> mails = srv.FetchAllOpen();
foreach(Mail mail in mails) {
 srv.SubmitMail();
 Dispatcher.BeginInvoke(DispatcherPriority.Background, (SendOrPostCallback)delegate
            {
                lbl1.SetValue(Label.ContentProperty, iCount.ToString() + " von " + iEnd.ToString());
            },
            null);
} 

Das ist nun ein einfacher Prozess, ich habe aber auch komplexere, z.B.

  • PDF erstellen
  • dann auf einen Webserver laden
  • dann eine Mail verschicken

und finde das gehört eigentlich in den Service. Wenn ich aber die Serviceklasse GUI unabhängig haben will, weiß ich nicht, wie ich den Fortschritt weitergeben soll. Es über Observer zu lösen hab ich auch nicht hinbekommen.

Wie löst ihr sowas?

16.842 Beiträge seit 2008
vor 6 Jahren

Mit Hangfire.

Einfach Hangfire das ganze Verschicken etc übernehmen lassen und Deine UI nur zur Visualisierung verwenden.
Wir haben dafür auch ein WPF Tool, das sich immer wieder den Status der Hangfire-DB holt und das visualisiert.

Kannst aber auch sowohl das Erstellen von Mail-Jobs wie auch das Verarbeiten direkt in Deiner .NET Anwendung laufen lassen; brauchst kein extra Service/ASP.NET Runner, wenn ihr das nicht wollt.

Ansonsten ist das, was Du da fabrizierst, eine klassische Schichtenverletzung.
[Artikel] Drei-Schichten-Architektur

4.942 Beiträge seit 2008
vor 6 Jahren

Hallo bert21,

ein Service sollte nichts von der UI wissen (bzw. Dinge an die UI delegieren) - dafür gibt es Ereignisse, s. [FAQ] Eigenen Event definieren / Information zu Events (Ereignis/Ereignisse).
Und mit WPF sollte man ein ViewModel benutzen (MVVM) - nicht direkt ein Control ansprechen.

B
bert21 Themenstarter:in
24 Beiträge seit 2010
vor 6 Jahren

Das ist mir soweit schon klar, aber wie gebe ich denn aus der Service Klasse den Fortschritt weiter, so dass es das UC anzeigen kann.

16.842 Beiträge seit 2008
vor 6 Jahren

Siehe Antwort von Th69

B
bert21 Themenstarter:in
24 Beiträge seit 2010
vor 6 Jahren
Lösung

Also ich hab es jetzt so gelöst:

public class MeinService
    {
        public delegate void CallbackHandler(object sender, CallbackFortschrittArgs e);
        public event CallbackHandler _myCallbackFortschritt;

        public void CallbackFortschritt(Int32 step, Int32 von)
        {
            if (_myCallbackFortschritt != null)
            {
                CallbackFortschrittArgs daten = new CallbackFortschrittArgs(step, von);
                _myCallbackFortschritt(this, daten);
            }
        }

        public void MachWasProcess()
        {
            Int32 von = 15;
            for(Int32 step=0; step<=von; step++)
            {
                this.CallbackFortschritt(step, von);
                Thread.Sleep(1 * 1000);
            }
        }        
    }

public class DialogProzess_Child : DialogProzess
    {
        public DialogProzess_Child()
        {
            //BaseLogger.Block();
            new Thread(
                delegate ()
                {
                    this.starteMeinenProzess();
                }
                ).Start();
        }

        // eigentlicher Prozess, der wird jetzt im B Tread ausgeführt
        private void starteMeinenProzess()
        {
            Dispatcher.BeginInvoke(DispatcherPriority.Background, (SendOrPostCallback)delegate
            {
                lbTitle.SetValue(Label.ContentProperty, "Prozess wird abgearbeitet");
            },
                    null);

            MeinService srv = new MeinService();
            srv._myCallbackFortschritt += new MeinService.CallbackHandler(CallbackFortschritt);
            srv.MachWasProcess();
            
            Dispatcher.BeginInvoke(DispatcherPriority.Background, (SendOrPostCallback)delegate
            {
                btnClose.SetValue(Button.VisibilityProperty, System.Windows.Visibility.Visible);
                lbl1.SetValue(Label.ContentProperty, "Fertig");
            },
            null);
        }

        public void CallbackFortschritt(object sender, CallbackFortschrittArgs e)
        {
            Dispatcher.BeginInvoke(DispatcherPriority.Background, (SendOrPostCallback)delegate
            {
                lbTitle.SetValue(Label.ContentProperty, "Prozess wird abgearbeitet");
                pb1.SetValue(ProgressBar.ValueProperty, (double) e.Step);
                pb1.SetValue(ProgressBar.MinimumProperty, (double)0);
                pb1.SetValue(ProgressBar.MaximumProperty, (double)e.Von);

                lbl1.SetValue(Label.ContentProperty, e.Step.ToString() + " von " + e.Von.ToString());
            },
                    null);
        }
    }

Das das alles noch in ein MVVM sollte ist mir klar, aber zum testen gehts auch so. Ist das so wie Du meintest ?

16.842 Beiträge seit 2008
vor 6 Jahren

Das ist ganz weit weg von dem, wie man es optimalerweise löst 😉
Der Code ist nicht im Ansatz modular oder gar testbar ( [Artikel] Unit-Tests: Einführung in das Unit-Testing mit VisualStudio )

Dein MachWasProzess würde - wenn man es korrekt umgesetzt hat - hier auch Deine UI blockieren.
Denn die Methode an für sich sollte ja blockieren, bis es fertig ist; jedenfalls suggeriert sie das.
Dein Service soll ja gar nicht wissen, ob er blockiert oder nicht. Er weiß ja nicht, ob er von einer WPF-Anwendung, Konsolenanwendung oder Web-Anwendung ausgeführt wird (Thema Software Design).
Eine Methode sollte nicht verstecken, dass sie evtl. einen Thread startet.
Das sollte man direkt der Methode ansehen bzw. direkt asynchron umsetzen (bzw. bei Long Running dann tatsächlich über einen extra Task starten).

Ich persönliche rate immer von solchen "hauptsache man kann es mal testen"-Lösungen ab... denn nichts hält sich so lange wie ein Provisorium.
Wieso nicht Zeit sparen und gleich richtig lösen? 😉

B
bert21 Themenstarter:in
24 Beiträge seit 2010
vor 6 Jahren

Natürlich ist der Code testbar. ich kann die Service Klasse problemlos von einem Komponententest aus aufrufen und da sie die komplette Funktionalität abbildet auch komlett testen.

Wenn Du das anders siehst, lerne ich gern etwas dazu und bin auch für jede sinnvolle Antwort dankbar, aber im Moment hilft mir das was Du sagst nicht weiter. Die Artikel hab ich auch schon gelesen, aber die Theorie in die Praxis umzusetzen find ich nicht immer so einfach.

B
bert21 Themenstarter:in
24 Beiträge seit 2010
vor 6 Jahren

Sorry, ich hab nur die Hälfte gelesen;

Ob ich die GUI blockieren will, oder nicht hängt ja von der Art des Prozesses ab. Der Sinn eines eigenen Threads ist ja auch die Verarbeitung von Vorgängen im Hintergrund. ich könnte also die Fortschrittsanzeige irgendwo im Fuß der GUi einbauen und dann normal weiterarbeiten. Das ist doch genauso denkbar wie letztere zu blockieren.

16.842 Beiträge seit 2008
vor 6 Jahren

Dein Code ist so nicht (wirklich) testbar. Und die Praxis von guter Software ist Testbarkeit.

Unit Tests funktionieren hier gar nicht, denn Dein Code erfüllt die (Mindest-)Anforderungen dafür (eine Unit) gar nicht; es fehlt die Modularität völlig.
Das notwendige Stichwort hier wäre Dependency Injection. Und DI bzw IoC ist eine Mindestanforderung an Software-Architektur heutzutage; völlig egal ob Konsolen-, Desktop-, Webanwendung oder App.
Bei WPF ist Unity, Ninject sehr weit verbreitet oder auch Locator Pattern.. hunderte, wenn nicht tausende Möglichkeiten gibt es hier.
Dahingehend können ja auch Integrationstest gar nicht durchgeführt werden.

Man muss lernen Code modularisieren zu können; nur dann hat man auch die Möglichkeit Code durch Units- und Integrationstest testen zu können.
Wenn diese Mindestvoraussetzung nicht geschaffen ist, kann man jeglichen qualitativen Anspruch vergessen.
Das, was Du da Testen nennst, ist ja eher "ich steppe mal durch den Code und hoffe es klappt" 😉

Und das Thema MVVM haste ja bereits auf dem Schirm - hoffentlich.

PS: auch Du hast ein Edit-Button bei Beiträgen 😉

Ja, Dein Code läuft hier in einem Thread. Aber eigentlich legt man nur das Starten bzw. die das Verarbeiten in einen Thread und nicht alles in einen riesigen Code-Block.

B
bert21 Themenstarter:in
24 Beiträge seit 2010
vor 6 Jahren

Schau mal, ich verstehe es wirklich nicht ...

Das ist ja jetzt mal ein relativ simples Beispiel. In der Praxis hab ich z.B. noch ein Mail Model und einen Datenbank Mapper, der die Schnittstelle zur Datenbank bereitstellt. Die Service Klasse handelt dann nur den Prozess (also holt vom Mapper das Model oder die Models, verschickt die Mails und läßt den Mapper den Erfolg oder Fehler in die DB schreiben).

Ich kann das Model einzeln testen,
Ich kann die Mapper Klasse und damit die Datenbank Funktionen einzen testen,
Ich kann die Service Klasse und damit die Prozesse, dann auch nochmal in Verbindung mit der Datenbank testen.
Wenn ich die Mail Funktion einzeln abbilde, kann ich die auch einzeln testen.

Wenn ich jetzt nur einen einfachen View habe, könnte ich das per MVC Pattern lösen und wäre trozdem in der Lage alles zu testen. Wenn ich im View eine größere Funktionalität brauche, lege ich die in das View Model, das ich dann auch testen kann.

Was fehlt Dir denn da, bzw. wo fehlt da die Modularität?

16.842 Beiträge seit 2008
vor 6 Jahren

Nein, Du kannst das nicht einzeln testen ohne den Code anzufassen.
Dir fehlt die Modularität hier komplett. Du hast darüber hinaus durch das konkrete Nutzen von Implementierungen und Instanzen ein Maximum an Bindung.

Das Lösen von Abhängigkeiten zu konkreten Implementierungen löst man über Interfaces und Dependency Injection.

Deine Logik würde dann nur noch einen _IMailService _kennen; der Logik ist ja egal ob dahinter dann ein GoogleMailService, ein OutlookMailService, ein _GmxMailService _oder ein _TestMailService _steckt, der gar keine Mails versendet sondern beim Send einfach was in die Konsole schreibt.
Die Logik will nur IMailService.Send() - die Implementierung ist an dieser Stelle wurst.

Das gleiche für Datenbanken:
Logik ist es egal, welche Datenbank hinter einem _IMailRepository _steckt.
Das kann ein MssqlMailRepository, ein OracleMailRespository, ein _MysqlMailRepository _oder ein _XmlMailRepository _sein.
Hauptsache die Implementierung der Schnittstelle _IMailRepository _liefert das, was die Logik erwartet.
Das ist Entkopplung.

Und nur entkoppelte Elemente haben eine erhöhte Wiederverwendbarkeit und den Mindestanspruch für Unit-Testing.
Fürs Unit-Testen braucht man eben dann bei der entkoppelten Testweise einfach Interfaces, denn nur Interfaces lassen sich mocken (Mock-Objekt) und liefern so die Basis für eine zuverlässige und deterministische Testergebnisse.

Das steht auch alles inkl. Beispielen in [Artikel] Unit-Tests: Einführung in das Unit-Testing mit VisualStudio

B
bert21 Themenstarter:in
24 Beiträge seit 2010
vor 6 Jahren

Ok, ich schau mir das mal in Ruhe an. Danke, dass Du Dir die Zeit genommen hast.