Laden...

Aktualisierung eines Labels aus einer anderen Klasse heraus

Erstellt von vita85 vor 12 Jahren Letzter Beitrag vor 12 Jahren 7.137 Views
V
vita85 Themenstarter:in
20 Beiträge seit 2011
vor 12 Jahren
Aktualisierung eines Labels aus einer anderen Klasse heraus

Hallo Community,
ich hänge seit ziemlich genau 2 Tagen an einem Problem und würde mittlerweile am liebsten den Rechner an die Wand werfen und nach Hause gehen.
Ich sitze zur Zeit an meinem IHK-Abschlussprojekt. Die Aufgabe ist es, eine Management Konsole für Citrix Server zu entwickeln, da die von Citrix bereitgestellte Konsole nicht genug kann.
Mein Auftraggeber legt EXTREM viel Wert auf Kleinigkeiten. Eine dieser Kleinigkeiten macht mir total zu schaffen -> nämlich ein Label in der Statusleiste.

Um zu zeigen, dass das Programm noch reagiert und bei längeren Operationen nicht abgestürzt ist, soll unter Anderem ein Label im Sekundentakt aktualisiert werden und die Zeit in Sekunden anzeigen, die bereits verstrichen ist. Dazu habe ich mir eine Klasse "StatusbarUpdate" als statische Klasse geschrieben:


    public static class StatusbarUpdate {

        private static Action EmptyDelegate = delegate() { };
        private const string defaultMessage = "Ready";
        private static bool start;
        private static Thread t;
        public static int i = 0;

        public static void startTimer() {
            start = true;

            while (start) {
                Thread.Sleep(1000);
                i++;
            }
        }
        
        public static void startThread() {
            t = new Thread(new ThreadStart(startTimer));
            t.Start();
        }

        public static void stopThread() {
            start = false;
            t.Abort();
        }

        public static void Update(this Label uiElement, string _message) {
            // Labelcontent wird auf uebergebenen String gesetzt
            uiElement.Content = _message;
            
            // Hier wird das Label neu gerendert, also sprich: Die Anzeige wird sofort aktualisiert.
            uiElement.Dispatcher.Invoke(DispatcherPriority.Render, EmptyDelegate);
        }


        public static void UseDefault(this Label uiElement) {
            Update(uiElement, defaultMessage);
        }


    }

Der Code zu meiner MainForm sieht dann so aus:


StatusbarUpdate.startThread();

            //Dies kann nicht funktionieren, aber das ist das Label um das es geht.
            //Ich brauche eine Möglichkeit das Label aus der "startTimer()" Methode der Klasse "StatusbarUpdate" zu aktualisieren
            labelActualStatus.Update("Running script getFarmname.ps1 " + StatusbarUpdate.i);

            string farmname = PowershellInvoker.RunScript(@"PowerShell_Scripts\getFarmname.ps1");
            //Unwichtiges Label
            label28.Content = farmname;
            
            //Label wird nach Ablauf des PowerShell Skripts wieder auf Standard gesetzt
            labelActualStatus.UseDefault();

            StatusbarUpdate.stopThread();

Ich hoffe der Code ist verständlich genug.
Aber nochmal zum Ablauf an diesem konkreten Codebeispiel: Es soll der Name einer Citrix Serverfarm angezeigt werden. Dazu wird ein PowerShell Skript durchlaufen (weil die SDK für Citrix Xenapp 6 fast nur noch Windows PowerShell unterstützt). Solange dieses PowerShell Skript aktiv ist, soll das Label "labelActualStatus" im Sekundentakt aktualisiert werden.

Bei Google hab ich schon gesucht, aber quasi ohne Erfolg.
Den Eintrag in der FAQ ([FAQ] Controls von Thread aktualisieren lassen (Control.Invoke/Dispatcher.Invoke)) hab ich auch gefunden, aber der hilft mir leider nicht weiter 😦.
Die Leute, für die ich das Programm schreibe und generell die ganze Abteilung ist eine reine Administratorentruppe ohne Programmierhintergrund.

Das hier ist quasi meine letzte Anlaufstelle 8). Bitte um Hilfe.

1.552 Beiträge seit 2010
vor 12 Jahren

Hallo vita85,

was mir als erster aufällt: Thread.Sleep(1000) 8o
Sowas macht man nicht. Nimm irgend einen Timer.

Die Frage ist jedoch: Was geht nicht? Zeigt das Label nichts an oder wie.
Siehe unter anderem [FAQ] Warum blockiert mein GUI?

WPF und Label direkt ansprechen: sind zwei Sachen die nicht getan werden sollten.
In WPF wird normalerweise alles über DataBinding erledigt.

Aber ich denke dass sich alles mehr oder weniger auf Stopwatch.Elapsed live anzeigen bezieht.

Gruß
Michael

Mein Blog
Meine WPF-Druckbibliothek: auf Wordpress, myCSharp

V
vita85 Themenstarter:in
20 Beiträge seit 2011
vor 12 Jahren

Hallo MURO

was mir als erster aufällt: Thread.Sleep(1000) 8o
Sowas macht man nicht. Nimm irgend einen Timer.

Das ist halt leider der Nachteil, den man als Programmier-Azubi in einer Abteilung voller nicht-Programmierer hat. Ich kann nicht sagen ob es guter oder schlechter Stil ist. Solange ich was funktionstüchtiges auf die Beine stelle, ist es für meinen Ausbilder schon guter Stil 😄.
Wie dem auch sei: Ich behebe das zu einem späteren Zeitpunkt noch. Danke dir für den Hinweis.

Die Frage ist jedoch: Was geht nicht? Zeigt das Label nichts an oder wie.

Das Label zeigt was an. Wenn ich den gesamten Code, den ich oben aufgeführt habe in die Main packe, also so, dass wirklich alles in einer einzigen Klasse steckt, dann funktioniert die Aktualisierung auch. Die GUI blockiert also schonmal nicht.
Es ist halt einfach die Sache, dass ich durch die logische Aufteilung in zwei Klassen meine GUI-Elemente nicht mehr ansprechen kann.

WPF und Label direkt ansprechen: sind zwei Sachen die nicht getan werden sollten.
In WPF wird normalerweise alles über DataBinding erledigt.

Ich hab hier leider absolut keine Idee, wie ich das über DataBinding realisieren könnte 😕.

Aber ich denke dass sich alles mehr oder weniger auf
>
bezieht.

Das was in dem Thread gefragt ist funktioniert bei mir schon. Den Tipp, den du dem Threadstarter da gegeben hast, habe ich bei mir schon selbst umgesetzt (siehe Code oben).

Was mein Problem ist (ich habe mich eben wohl unverständlich ausgedrückt):
Es müssen zwei Dinge parallel laufen, nämlich die Ausführung des PowerShell Skripts, sowie die sekündliche Aktualisierung des Labels "labelActualStatus".
Die Aktualisierung und die Anzeige des Labels an sich funktioniert. GUI blockiert nicht.
Was ich bräuchte wäre in der Methode "startTimer()" irgendetwas, was das Statuslabel ansprechen kann. Also quasi:


        public static void startTimer() {
            start = true;

            while (start) {
                Thread.Sleep(1000); //wird noch geändert
                i++;
                //labelActualStatus.Update("Skript wird ausgeführt", i.ToString());
            }
        }

Das funktioniert leider nicht, da das entsprechende Label nicht statisch ist und ich es auch irgendwie nicht hinbekomme eine entsprechende Set-Methode dafür zu schreiben 😦.
Ich kann klassenübergreifend einfach nicht mit dem Label arbeiten.

6.862 Beiträge seit 2003
vor 12 Jahren

Hallo,

ich denke ebenfalls das du es ganz analog zum von xxMUROxx genannten Thread machen kannst. Dein jetziger Code funktioniert vorne und hinten nicht richtig.

Wenn du einen Timer willst, dann nimm auch einen Timer und nicht eine alles blockierende Schleife...

Außerdem lagerst du die Ausgabe des Status auf die GUI in nem Thread aus, die eigenliche Arbeit (Powershell Skript ausführen) lässt du dort aber? Andersrum machts doch viel mehr Sinn. Die eigentliche Arbeit gehört in nem Thread.

        public static void Update(this Label uiElement, string _message) {
            // Labelcontent wird auf uebergebenen String gesetzt
            uiElement.Content = _message;

            // Hier wird das Label neu gerendert, also sprich: Die Anzeige wird sofort aktualisiert.
            uiElement.Dispatcher.Invoke(DispatcherPriority.Render, EmptyDelegate);
        }

Dieser Code macht leider gar keinen Sinn. Das Invoke ist völlig unnütz weil es nichts macht. Wozu überhaupt nen Invoke wenn du vorher eh schon direkt auf die GUI zugreifst und den Content setzt? Schau dir bitte im schon verlinkten FAQ Eintrag an wie man es richtig macht.

Baka wa shinanakya naoranai.

Mein XING Profil.

Gelöschter Account
vor 12 Jahren

Hier mal ein etwas andere Ansatz, aber vielleicht hilft es dir weiter:

Die Statusbar als UserControl, das label bindet auf LabelContent, und LabelContent wird vom Timer immer aktualisiert. => funktioniert wunderbar

View:


<UserControl x:Class="BindingOverControlBorder.Model.Statusbar"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" DataContext="{Binding RelativeSource={RelativeSource Self}}" 
             d:DesignHeight="300" d:DesignWidth="300">
    <Grid Background="GreenYellow">
        <TextBlock Text="{Binding LabelContent}" FontSize="18"></TextBlock>    
    </Grid>
</UserControl>

Code


public partial class Statusbar : UserControl, INotifyPropertyChanged
    {
        #region Fields
        private static String _labelContent;
        private DateTime _startTime;
        private Dispatcher _parentDispatcher = Dispatcher.CurrentDispatcher;
        private DispatcherTimer dt = new DispatcherTimer();
        #endregion
        #region Properties
        public String LabelContent
        {
            get { return _labelContent; }
            set
            {
                if (_labelContent == value)
                    return;
                _labelContent = value;
                Changed("LabelContent");
            }
        }
        #endregion
 
        public Statusbar()
        {
            InitializeComponent();
        }
 
        public void StartTimer()
        {
            dt.Interval = TimeSpan.FromSeconds(1);
            dt.Tick += (s, e) => 
            {
                _parentDispatcher.Invoke((Action)(() => { LabelContent = "Seconds: " + (DateTime.Now - _startTime).Seconds; }));
            };
            _startTime = DateTime.Now;
            dt.Start();
        }
 
        public void StopTimer()
        {
            dt.Stop();
        }
 
 
        #region INotifyPropertyChanged Members
        public event PropertyChangedEventHandler PropertyChanged;
 
        protected void Changed(String propertyName)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
        #endregion
    }


V
vita85 Themenstarter:in
20 Beiträge seit 2011
vor 12 Jahren

Ok, das letzte Beispiel hab ich jetzt verstanden, nachdem ich es in einer einfacheren Form nachprogrammiert habe. Ich kannte die INotifyPropertyChanged Schnittstelle nicht. Das macht es irgendwie einfacher und ich kann die Daten des Timers vernünftig an das Label binden.

Zu dem Programm, was ich für unsere Citrix-Admins schreibe habe ich dennoch eine Frage: ist es design-technisch besser die Statusbar bzw. alle Operationen, die innerhalb der Statusbar ablaufen, in einer eigenen Klasse abzubilden?
Oder soll ich einfach alles in die Mainform reinschreiben?
Denn der Timer ist ja nicht das Einzige, was innerhalb der Statusbar aktualisiert werden soll. Ich soll zusätzlich noch eine fortlaufende Progressbar implementieren und in einem weiteren Label (in textueller Form) ausgeben, welche Operation genau da eigentlich gerade durchgeführt wird.

Wenn ersteres zutreffen sollte, dann muss ich meine Klasse Klasse "StatusbarUpdate" nochmal neu programmieren und mir in der Mainform ein Objekt davon erzeugen.

Habe wie gesagt sonst leider niemanden den ich fragen kann und da dies mein erstes richtiges Projekt ist, würde ich es gern richtig machen. Zumal das das IHK Abschlussprojekt ist und zu einem Drittel in meine Endnote einfließt.

Danke euch allen schonmal soweit.

1.552 Beiträge seit 2010
vor 12 Jahren

ist es design-technisch besser die Statusbar bzw. alle Operationen, die innerhalb der Statusbar ablaufen, in einer eigenen Klasse abzubilden?

Ja eindeutig. Die Logik sollte weitestgehend von der Oberfläche getrennt werden. Krass gesagt: Die Oberfläche zeigt nur das an was ihr die "Logik" sagt was sie zeigen soll.
Für WPF siehe MVVM, ist anfags etwas heftig aber im Nachhinein nicht mehr wegzudenken.
Generelle Stichwörter: MVVM, MVC,...

Oder soll ich einfach alles in die Mainform reinschreiben?

Nein, dort steht idealerweise neben dem XAML-Code in der C# Datei nur eine Zeile im Konstructor und die ist InitializeComponent();
Aber bis es dahin kommt rate ich dir bezüglich MVVM Tutorials anzusehen bevor du den ganzen Code umkrempelst. Denn es ist viel Arbeit bestehenden Code der FormKlasse in eine separate Klasse auszulagern.

Mein Blog
Meine WPF-Druckbibliothek: auf Wordpress, myCSharp

V
vita85 Themenstarter:in
20 Beiträge seit 2011
vor 12 Jahren

Der Code von MusiuminCapitiss funktioniert bei mir irgendwie nicht richtig. Das Label zeigt nichts an.

Habe jetzt alles folgendermaßen testweise in eine Klasse gepackt und 2 Buttons zum starten bzw. stoppen hinzugefügt, aber das Laben zeigt nichts an.

Hier der Code (sicherheitshalber):


public partial class MainWindow : Window, INotifyPropertyChanged {

        public MainWindow() {
            InitializeComponent();
        }

        private static String _labelContent;
        private DateTime _startTime;
        private Dispatcher _parentDispatcher = Dispatcher.CurrentDispatcher;
        private DispatcherTimer dt = new DispatcherTimer();
        public event PropertyChangedEventHandler PropertyChanged;

        public string LabelContent {
            get { return _labelContent; }
            set {
                if (_labelContent == value)
                    return;

                _labelContent = value;
                if (PropertyChanged != null) {
                    PropertyChanged(this, new PropertyChangedEventArgs("LabelContent"));
                }
            }
        }

        public void StartTimer() {
            dt.Interval = TimeSpan.FromSeconds(1);
            dt.Tick += (s, e) => {
                _parentDispatcher.Invoke((Action)(() => { LabelContent = "Seconds: " + (DateTime.Now - _startTime).Seconds; }));
            };
            _startTime = DateTime.Now;
            dt.Start();
        }

        public void StopTimer() {
            dt.Stop();
        }

        private void Button_Start(object sender, RoutedEventArgs e) {
            StartTimer();
        }

        private void Button_Stop(object sender, RoutedEventArgs e) {
            StopTimer();
        }
    }

Und der XAML-Code:


<Window x:Class="Stoppuhr_WPF_Datenbindung.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="114" Width="176">
    <Grid>
        <Button Content="Start" Height="23" HorizontalAlignment="Left" Margin="12,12,0,0" Name="button1" VerticalAlignment="Top" Width="75" Click="Button_Start"/>
        <Button Content="Stop" Height="23" HorizontalAlignment="Left" Margin="12,41,0,0" Name="button2" VerticalAlignment="Top" Width="75" Click="Button_Stop"/>
        <Label Name="label1" Content="{Binding LabelContent}" Margin="93,0,0,0" />
    </Grid>
</Window>

1.552 Beiträge seit 2010
vor 12 Jahren

Ich bin mir nicht sicher ob der DataContext per default schon auf das aktuelle Fenster gesetzt wird. Aber probier mal im Konstructor folgende Zeile:
DataContext = this;

Mein Blog
Meine WPF-Druckbibliothek: auf Wordpress, myCSharp

V
vita85 Themenstarter:in
20 Beiträge seit 2011
vor 12 Jahren

Super, es läuft.
Vielen Dank. Dann starte ich den morgigen Tag mal motiviert... Nicht so wie die letzten beiden Tage 😁

5.742 Beiträge seit 2007
vor 12 Jahren

Verwende bei Control-Properties aber besser eine DepenencyProperty - INotifyPropertyChanged gehört nicht in ein DependencyObject!

Oder noch besser: Lagere den Code einfach in ein ViewModel aus, dass du im einfachsten Fall im Konstruktor des Windows instanzierst.