none
ProgressBar in einer Foreach-Schleife befüllen RRS feed

  • Frage

  • Hallo zusammen,

    ich bin relativ neu in der PowerShell Programmierung und arbeite derzeit an einem Support-Tool mit GUI, das verschiedene Abfragen in das Firmennetzwerk bereitstellen soll. Dafür stehen diverse Buttons, eine Eingabe und eine Ausgabe Textbox zur Verfügung. Bei einigen Suchabfragen müssen mehrere Server angesprochen werden, was die Antwortzeit der entsprechenden Abfrage ordentlich hinauszögern kann.

    • Für mein Problem nutze ich die Abfrage, ob und auf welchem Scope ein Gerät eine DHCPv4 Reservierung hat. (Da es für dieses Gerät mehrere Reservierungen geben kann, müssen alle Scopes abgefragt werden. Also kein Abbruch, sobald es ein Ergebnis gab!)
    • Die Progressbar soll nach jedem durchsuchten Scope aktualisiert werden, aktuell ~300 Stück.
    • Mein Fehler bei dem ich nicht weiterkomme: Die ProgressBar aktualisiert sich erst beim Abschluss der Schleife. Wie kann Sie nach jeder Schleife aktualisiert werden?
    • (Nebenproblem: wie setze ich diese zurück und kann ich das zusammen mit dem Säubern der Ausgabe-Textbox in eine eigene Funktion packen, die dann von allen Button-Clicks ausgeführt wird?)

    Unten stehender Code gehört zu meinen Events:

    $btnWorkstation.Add_Click{
        $i = 0
        $counter = 0
        $txbOutput.Clear()
        $feed = $txbInput.Text
        $scopes = Get-DhcpServerv4Scope -ComputerName $dhcpserver
        Foreach($scope in $scopes){
            $counter = $counter + 1
            $reservation = Get-DhcpServerv4Reservation -ComputerName $dhcpserver -ScopeId $scope.scopeId | where Name -like $feed* -EA SilentlyContinue
            if ($reservation) {
                $i = 1
                $txbOutput.Text += (
                    "Name : $($reservation.Name)`r",
                    "IP : $($reservation.IPAddress)`r",
                )  
            }
            $a = $counter/$scopes.Count*100
            Write-Host "$a of all Scopes"
            $ProgressBar.Dispatcher.Invoke([action]{
                $ProgressBar.Value = $a
            }, "Normal")**
        }
        if($i -eq 0){
            $txbOutput.Text += (
                "Couldn't find DHCP Reservation for $feed."
            )
        }
    }
    Montag, 23. Dezember 2019 09:54

Antworten

  • Die Progressbar kann in der foreach-Schleife mit

    $ProgressBar.Value = $a
    Start-Sleep -Milliseconds 10 [System.Windows.Forms.Application]::DoEvents()

    befüllt werden.

    • Als Antwort markiert PaddiDAce Freitag, 27. Dezember 2019 10:46
    Freitag, 27. Dezember 2019 10:46

Alle Antworten

  • Moin,

    ich kann Dir mit diesem Problem nicht in gewohnter Qualität helfen, möchte aber eine (vielleicht unpopuläre) Meinung äußern:

    Der oben zitierte Code ist zu 95% .NET und zu 5% PowerShell. Warum akzeptiert man es nicht und schreibt seine GUI nicht einfach in C#? Den einen PowerShell-Aufruf (Abfrage des DHCP-Servers) kannst Du mit drei Code-Zeilen in Deine WinForms-GUI in C# einbauen. Visual Studio Community Edition ist kostenlos. Du hast für die C#- und WinForms-Teile einen grafischen Editor und ein echtes Debugging. Am Ende kommt eine EXE raus, die Du im Zweifel einfacher verteilen kannst als ein PowerShell-Skript.

    Als ich zur Schule ging, gab es noch Werkunterricht. Und der Lehrer hat uns gefühlt in jeder Stunde mindestens einmal eingebleut, man soll jedes Werkzeug nach seiner Bestimmung verwenden.

    Amen, und hoffentlich meldet sich einer der PowerShell-WinForms-Jockeys und hilft Dir mit dem konkreten Vorhaben. Aber schön wird der Code, der dann rauskommt, nicht aussehen :-)


    Evgenij Smirnov

    http://evgenij.smirnov.de

    Montag, 23. Dezember 2019 11:14
  • Das liegt daran, dass du Forms keine Zeit dafür gibst.

    Deine Function "Add_Click" wird im Forms-Thread aufgerufen.
    Daher ist dein "$ProgressBar.Dispatcher.Invoke()" gleichwertig zum Direktzugriff "$ProgressBar.Value".
    Der Invoke ist nur erforderlich, wenn der Aufruf von einem anderen Thread aus erfolgt.

    Nun ist es so, dass Veränderungen an Formsobjekten nur ausgegeben werden, wenn ein sog. "Paint"-Ereignis eintritt. Das Verändern von Controls setzt nur das "Invalidate"-Flag, so dass beim nächsten Paint ein Refresh erfolgt. Das "Paint"-Ereignis kann aber erst ausgelöst werden, wenn du "Add_CLick" verlässt und zum Dispatcher zurückkehrst.

    Abhilfe kann ein DoEvents bringen.
    https://docs.microsoft.com/de-de/dotnet/api/system.windows.forms.application.doevents?view=netframework-4.8

    Zu beachten ist, dass du deinen Add-Button dann aber inaktivierst, da während des DoEvents aufgelaufene Clicks ebenso zur Ausführung kommen.

    Besser wäre es, deine Aufgabe mittels Thread auszuführen, da dann der Mainprozess nicht blockiert wird:
    https://docs.microsoft.com/en-us/powershell/module/threadjob/start-threadjob?view=powershell-6

    Somit kannst du ggf. eine Aufgabe abbrechen oder eine Meldung ausgeben, dass die Aufgabe nicht fertig ist.
    Der Invoke ist dann wiederum erforderlich, da ein Cross-Thread-Zugriff in Forms nicht erlaubt ist.

    Übrigens:
    Während ein Button disabled ist und der Thread blockiert wird, laufen Aktionen in der Queue auf.
    Sobald der Dispatcher wieder Zeit bekommt, werden die Meldungen abgearbeitet.
    Klickst du also 10-Mal auf den Add-Button, wird "Add_Click" noch 10x aufgerufen.
    Dies kannst du ebenso nur per Disabled (Enabled=False) und DoEvents auflösen.

    Das Queue-Verhalten betrifft alle Forms-Elemente während einer Thread-Blockierung.

    Montag, 23. Dezember 2019 11:17
  • Besser wäre es, deine Aufgabe mittels Thread auszuführen, da dann der Mainprozess nicht blockiert wird:

    https://docs.microsoft.com/en-us/powershell/module/threadjob/start-threadjob?view=powershell-6

    Vielen Dank für die Antwort!

    Threads sind für mich noch total unbekanntes Terrain. Wenn ich das cmdlet richtig verstehe, muss ich nach

    $btnWorkstation.Add_Click{

    die Schleife in ein Start-ThreadJob einbauen und kann dann mit dem bereits vorhandenen

            $ProgressBar.Dispatcher.Invoke([action]{
                $ProgressBar.Value = $a
            }, "Normal")

    auf den Forms-Thread zugreifen?

    Mit welchen Parametern sollte ich den ThreadJob dann initiieren?


    Montag, 23. Dezember 2019 11:56
  • Man greift gerne auf das zurück, was man kann und bereits hat: die 70 kombinierten Abfragen, bzw. bedingten cmdlets habe ich in PS bereits geschrieben und möchte diese nun mehr oder minder in eine simple GUI kopieren.

    Deinen Einwand kann ich nicht einschätzen, da ich mit C# keine Erfahrung habe. Würde ihn dann aber wahrscheinlich nachvollziehen können ;-)

    Montag, 23. Dezember 2019 12:48
  • Nun ja, die Schönheit eines Codes liegt in der Formatierung.
    Anfang der 80er gab es in einer Computerfachzeitschrift einen monatlichen Wettbewerb, wer den effektivisten Code in 1KB unterbrachte. Da war jeder TAB/NEWLINE schon zuviel. Variablen 1stellig usw.usf.
    Um den Code zu analysieren musste man ihn erst "beautyfyen". Dafür gab es extra Tools.

    Was nun das Mischen von .Net und Powershell angeht so ist das letztlich egal. Ich halte persönlich Python ja auch nicht für eine vernünfigte Programmiersprache und trotzdem werden da ganze App's entwickelt.

    Nun zum Threading:
    Ein Thread wird mit der Übergabe einer Funktionsadresse instantiiert und mit der Start-Anweisung zur Ausführung gebracht. Als Parameter kann ein Objekt übergeben werden, was i.d.R. nur nötig ist, wenn man Thread-Funktionen allgemeinerer Art zur Verfügung stellt und ein Argument auf ein Objekt und dessen Eigenschaften benötigt.

    In diesem Zusammenhang bietet sich daher eher eine separate Funktion an, dessen einziges Argument eine Variable vom Typ Objekt ist. Man kann auch $null übergeben, wenn man auf Grund der Erreichbarkeit von Variablen (Scope) sowieso direktzugriff hat.

    Dies sollte man so ebenso auch mit Ereignis-Funktionen halten. Zumal man auf Grund der Signatur "MyFunction(sender, eventargs)" mehrere verschiedene bzw. gleichartige Objekte mit nur einer Funktion überwachen kann.

    Dienstag, 24. Dezember 2019 11:01
  • Die Progressbar kann in der foreach-Schleife mit

    $ProgressBar.Value = $a
    Start-Sleep -Milliseconds 10 [System.Windows.Forms.Application]::DoEvents()

    befüllt werden.

    • Als Antwort markiert PaddiDAce Freitag, 27. Dezember 2019 10:46
    Freitag, 27. Dezember 2019 10:46
  • Auf den DoEvents habe ich ja hingewiesen;-).
    Den Sleep kannst du dir allerdings schenken, denn der ist absolut unsinnig.
    Damit blockierst du den Thread nur und erzeugst aber keine Ausgabe.
    Diese wird erst mit DoEvents durchgeführt.

    Freitag, 27. Dezember 2019 13:19