Implementierung

Aufgrund der sich steteig ändernden GUI ist es nicht ratsam, den GUI-Quellcode von Primalforms mit der eigenen Logik zu verbinden. Die verschiedenen Bestandteile des Skriptes sollten in eigenen Dateien (.ps1) gespeichert werden, um sie vor Überschreiben zu schützen und eine Wiederverwendbarkeit zu erreichen.

Für unser Beispiel ist zunächst folgende Vorgehensweise sinnvoll.

  • View.ps1:

    Handelt die GUI sowie den Aufrufhandler für die eigenen Funktionen

  • Model.ps1:

    Regelt alle Zugriffe auf die Datenbank

  • Control.ps1:

    Vermittelt zwischen dem Quellcode von View.ps1 und Model.ps1 Implementiert die notwendige Verarbeitungslogik

Mit dieser Dreiteilung wird ein klassischs Muster der Programmierung umgesetzt, das sog. MVC-Muster. Prinzipiell geht es darum, die unterschiedlichen Aufgabenbereiche eines Programmes zu unterscheiden und damit auch austauschbar zu machen.

         ======>             ======>
View.ps1         Control.ps1           Model.ps1
         <======              <======

Runde 1

In der ersten Runde geht es darum, eine Verbindung mit dem Datenbankserver herzustellen und das DataGrid des Hauptfensters mit den Daten der Tabelle tblReports zu füllen.

View

Beim Starten des Formulars soll im DataGrid eine Liste der letzten 20 Einträge gezeigt werden. Das von PF generiete Skript muss im Form_Load-Ereignis einen Datenbankabruf vornehmen. Der Eventhandler des Formulars ruft deshalb im Controler eine entsprechende Funktion get_reports auf, die die Daten aus der Datenbank holt (r1_model.ps1). Der Controler bindet anschließend die Ergebnisse an das DataGrid der View.

...
$handler_form1_Load=
{

    Write-Debug "in Handler_form_load"
    getReports
 ...

Controler

Der Controler leitet den Aufruf der View an das Model weiter und sorgt anschließend für das Anzeigen der Daten im DataGrid des View-Skriptes. Dazu wird dessen Fähigkeit genutzt, sich per DataSource-Eigenschaften an die “Tabellen” des DataSets zu binden

. .\r1_model.ps1

function getReports
{
    getReportModel
    updateReport
}

....

function updateReport()
{
    $dataGridWER.DataSource = $null;
    $dataGridWER.DataSource = $DataSet.Tables[0];
    $dataGridWER.DataBind
    $form1.refresh()
}

Model

Im Model werden alle ausgeführten Datenbankzugriffe implementiert. Die entsprechend notwendigen Objekte wie Connection, Command, DataSet und DataAdapter werden erzeugt und mit den daten gefüllt. Zum Füllen des DataGrids wird mit Hilfe eines DataAdapters ein DataSet erstellt. Dies stellt sozusagen eine “virtuelle” Tabelle im Speicher dar und kann von vielen Steuerlementen des .NET-Frameworks genutzt werden. Funktionalitäten wie das Öffnen und Schließen von Datenbankverbindungen werden ausgelagert, da sie in Zukunft noch häufiger genutzt werden.

#Erste Einrichtung der Datenbankverbindung
#$connstring = "Server=10.161.8.17;Uid='root';Pwd='steinam';Database=dr_watson";
$connstring = "Server=localhost;Uid='root';Pwd='patricia1234';Database=drwatson";


$con = New-Object Mysql.Data.MysqlClient.MysqlConnection;
$con.ConnectionString = $connstring;
$DataSet = New-Object System.Data.DataSet


############################################################################
#Neu: Dataadapter muss bereits hier erzeugt werden, da bei Änderungen etc. auf diesen zurückgegriffen werden
#muss, damit das dann später benutzte Commandobjekt darauf zugreifen kann.
#Siehe: file:///C:/Fp/OpenBooks/C%20Sharp/Visual%20C%20Sharp%202012/1997_35_001.html#dodtp44f9b504-b660-4f2e-bd26-072f4a498332

$SqlAdapter = New-Object MySql.Data.MySqlClient.MySqlDataAdapter

#Verbindung öffnen
function verbindungOeffnen()
{
        $con.Open();
        Write-Debug "Datenbankverbindung geöffnet"
}

#Verbindung schließen
function verbindungschliessen()
{
        $con.Close();
        Write-Debug "Datenbankverbindung geschlossen"
}

#Computerdaten besorgen
function getReportModel()
{
        #Verbindung öffnen
        verbindungOeffnen

        #SQL-Statement eingeben
        $SqlQuery = "select * from tbl_report"

        #Commandobjekt anlegen und Connectionobjekt sowie Abfrage zuordnen
        $SqlCmd = New-Object MySql.Data.MySqlClient.MySqlCommand;
        $SqlCmd.CommandText = $SqlQuery
        $SqlCmd.Connection = $con

        #Datenadapter instantiieren und Commandreferenz zuweisen
        $SqlAdapter.SelectCommand = $SqlCmd

        #Dataset leeren, sonst werden die alten Daten noch angezeigt
        $DataSet.Reset()

        #Dataset instantiieren und füllen lassen
        $SqlAdapter.Fill($DataSet)

        #Verbindung schließen
        verbindungSchliessen
}

Runde 2

Nach dem Füllen des DataGrids sollen die ComboBoxen zur Auswahl der User und Rechner mit den Werten aus den entsprechenden Tabellen gefüllt werden.

View

Da das Füllen der ComboBoxen bereits beim Start erfolgen soll, muss die Form_Load-Handlermethode des Views um entsprechende Aufrufe von Contrer-Funktionen ergänzt werden.

$handler_form1_Load=
{
    Write-Debug "in Handler_form_load"
    getReports

    #neuer Code
    getCboUser
    getCboHosts
}

Controler

Der Controler ruft eine Methode des Models auf. Seine Daten erhält er von dieser Methode in Form eines Arrays. Diese werden über die updateCboUser-Funktion in die Items-Liste der ComboBox übertragen.

function getCboUser()
{
    # Aufruf einer Funktion des Models
    $UserList = getUserModel
    updateCboUser $UserList
}


function updateCbouser($list)
{
    foreach($user in $list)
        {
                $cboUser.Items.Add($user)
        }
}

Model

Im Model werden die Daten mit Hilfe eines SQL-Statements und einem DataReader aus der entsprechenden Tabelle gelesen. Die Daten des Readers werden in ein Array kopiert und an die aufrufende Funktion des Controlers zurückgegeben. Diese Vorgehensweise wird für das Füllen beider ComboBoxen angewendet. Anstellen des Umwandelns in einen Array hätte man auch den DataReader direkt zum Controler zurückgeben können.

function getUserModel
{
    $Userlist = @();

    verbindungOeffnen

    #SQL-Statement eingeben
        $SqlQuery = "select Anmeldename from tbl_user;"

        #Commandobjekt anlegen und Connectionobjekt sowie Abfrage zuordnen
        $SqlCmd = New-Object MySql.Data.MySqlClient.MySqlCommand;
        $SqlCmd.CommandText = $SqlQuery
        $SqlCmd.Connection = $con

        #Datenadaptar instantiieren und Commandreferenz zuweisen
        $reader = $SqlCmd.ExecuteReader();

        while($reader.Read())
        {
                Write-Host $reader["Anmeldename"]
                #$Userlist.Add($reader["Anmeldename"] = $reader["Anmeldename"])
                $Userlist += $reader["Anmeldename"]
        }
        verbindungschliessen
        return $Userlist
}

Runde 3

Im DataGrid sollen nun nach der Auswahl der jeweiligen ComboBox die Daten angezeigt werden.

Aufgabe

Da hierzu grundsätzlich keine neuen Features hinzukommen, müsste dies leicht zu implementieren sein.

View

Im GUI-Teil sind die On_Cklick()-Handler der beiden Buttons zu bearbeiten. Der Wert des markierten Eintrags der ComboBoxen ist an den Controler weiterzugeben.

#r3_view.ps1

$btnSelectUser_OnClick=
{
        #TODO: Code  for dataGrid for selected User
        getReportsForUser $cboUser.SelectedItem
}

...

$btnSelectRechner_OnClick=
{
 getReportsForRechner $cboRechner.SelectedItem
}

Controler

Der Controler nimmt den Wert der ComboBoxen entgegen und reicht ihn an die Model-Funktionen weiter. Da diesmal wieder mit DataSets gearbeitet wird, sorgt er zum Schluss noch für das Updaten des DataGrids

#r3_control.ps1

function getReportsForUser($selectedUser)
{
        getReportsForUserModel $selectedUser
        updatereport
}

function getReportsForRechner($selectedRechner)
{
        getReportsForRechnerModel $selectedRechner
        updatereport
}

Model

Im Model werden die Daten mit Hilfe einer SQL-Query geholt, in ein DataSet verpackt und an das Controler-Skript zur weiteren Verarbeitung zurückgegeben.

Beim SQL-Statement ist darauf zu achten, dass für die Ausgabe der Rechner ein INNER JOIN-Statement zu wählen ist, da die Tabellen tbl_reports und tbl_computer über die MAC-Adressen verknüpft sind - die Auswahl der ComboBox bezieht sich allerdings auf den Hostnamen.

Die Zuordnung des übergebenen Rechner- bzw. Usernamens in den SQL-String erfolgt mit Hilfe der ‘” “’ Schreibweise

#r3_model.ps1

function getReportsForUserModel($user)
{
        verbindungOeffnen
        $SqlQuery = "select * from tbl_report where Anmeldename = '" + $user +"';"
        #Commandobjekt anlegen und Connectionobjekt sowie Abfrage zuordnen
        $SqlCmd = New-Object MySql.Data.MySqlClient.MySqlCommand;
        $SqlCmd.CommandText = $SqlQuery
        $SqlCmd.Connection = $con

        #Datenadapter instantiieren und Commandreferenz zuweisen
        $SqlAdapter.SelectCommand = $SqlCmd

        #Dataset leeren, sonst werden die alten Daten noch angezeigt
        $DataSet.Reset()

        #Dataset instantiieren und füllen lassen
        $SqlAdapter.Fill($DataSet)

        verbindungschliessen
}


function getReportsForRechnerModel($Rechner)
{
        verbindungOeffnen
        #Ein equi join gibt falsche Ergebnisse
        #$SqlQuery = "select r.ReportID, r.Appname, r.Eventtime,
        #r.Anmeldename, r.MAC, r.ReportType "
        #$SqlQuery += "from tbl_report r,tbl_computer c "
        #$SqlQuery += "where r.MAC = c.MAC and c.Hostname = '" + $Rechner +"';"

        #der inner join machts richtig
        $SqlQuery ="select r.ReportID, r.Appname, r.Eventtime, r.Anmeldename, r.MAC, r.ReportType "
        $SqlQuery += "from tbl_report r inner join tbl_computer c "
        $SqlQuery += "on r.MAC = c.MAC "
        $SqlQuery += "where c.Hostname = '" + $Rechner +"';"


        #Commandobjekt anlegen und Connectionobjekt sowie Abfrage zuordnen
        $SqlCmd = New-Object MySql.Data.MySqlClient.MySqlCommand;
        $SqlCmd.CommandText = $SqlQuery
        $SqlCmd.Connection = $con

        #Datenadapter instantiieren und Commandreferenz zuweisen
        $SqlAdapter.SelectCommand = $SqlCmd

        #Dataset leeren, sonst werden die alten Daten noch angezeigt
        $DataSet.Reset()

        #Dataset instantiieren und füllen lassen
        $SqlAdapter.Fill($DataSet)

        verbindungschliessen
}

Runde 4 - Optimierung

Nachdem das Skript lauffähig ist, wird in einer Zwischenrunde zunächst redundanter Code refaktorisiert.

Unter Refaktorisierung versteht man das Umgestaltung von Quellcode zur Vermeidung von redundanten Code-Teilen bzw. generell eine Verbesserung der Code-Strukturen im Hinblick auf die Erweiterung von Skripten.

Model

Im Model-Skript fallen sofort die Funktionen zum Befüllen der KomboBoxen sowie die Funktionen zum Befüllen des DataGrids auf.

Beide unterscheiden sich eigentlich nur in der Formulierung des SQL-Statements sowie in der Art des Parameters.

Refaktorisierung der KomboBoxen

In den beiden Funktionen zum Holen der Daten für die KomboBoxen User und Rechner ist die gleiche Logik implementiert.

_images/vergleich_cbo_user_host_model.jpg

Durch Parametrisierung der Funktion und Einbau einer Fallabfrage kann die Aufgabe mit Hilfe einer einzigen Funktion erledigt werden.

#r4_model.ps1
function getCboModel ($comboBox)
{
        $SqlQuery = ""
        $FeldName = ""

        switch($comboBox)
        {
            "User" {
                        #SQL-Statement zum Befüllen der ComboBox cboUser
                        $SqlQuery = "select Anmeldename from tbl_user;"
                        $Feldname = "Anmeldename";
                        break
                   }

                "Rechner" {
                        #SQL-Staement zum Befüllen der ComboBox
                        $SqlQuery = "select Hostname from tbl_computer;"
                        $Feldname = "Hostname";
                        break
                }
        }

        $list = @();
        verbindungOeffnen

        #Commandobjekt anlegen und Connectionobjekt sowie Abfrage zuordnen
        $SqlCmd = New-Object MySql.Data.MySqlClient.MySqlCommand;
        $SqlCmd.CommandText = $SqlQuery
        $SqlCmd.Connection = $con

        #Datenadaptar instantiieren und Commandreferenz zuweisen
        $reader = $SqlCmd.ExecuteReader();

        while($reader.Read())
        {
                $list += $reader[$Feldname]
        }
        verbindungschliessen
        return $list
}

Das Befüllen des DataGrids wird zur Zeit durch 3 verschiedene Methoden ausgeführt

  • getReportModel()
  • getReportsForUserModel($user)
  • getReportsForRechnerModel($Rechner)

Diese Methoden sind gleich ! Lediglich der verwendete SQL-String unterscheidet sich. Man kann deswegen lediglich eine dieser 3 Methoden (getReportModel()) weiterverwenden; der jeweils notwendige SQL-String wird in den Controler-Methoden generiert und an die Methode übergeben(getReportModel($SqlQuery).

r4_model.ps1

function getReportModel($SqlQuery)
{
        #Verbindung öffnen
        verbindungOeffnen

        #Commandobjekt anlegen und Connectionobjekt sowie Abfrage zuordnen
        $SqlCmd = New-Object MySql.Data.MySqlClient.MySqlCommand;
        $SqlCmd.CommandText = $SqlQuery
        $SqlCmd.Connection = $con

        #Datenadapter instantiieren und Commandreferenz zuweisen
        $SqlAdapter.SelectCommand = $SqlCmd

        #Dataset leeren, sonst werden die alten Daten noch angezeigt
        $DataSet.Reset()

        #Dataset instantiieren und füllen lassen
        $SqlAdapter.Fill($DataSet)

        #Verbindung schließen
        verbindungSchliessen
}

Control

Die beiden Funktionen getCbouser sowie getCboHost könnten in einer Funktion zusammengefasst werden, wenn man den jeweiligen ComboBox-typ als Parameter übergibt.

r4_control.ps1

....
function getCbo($type)
{
        $list = getcboModel $type

        if($type -eq "Rechner")
        {
                updateCboHost $list
        }
        else
        {
                updateCboUser $List
        }
}

Für den Aufruf zum Befüllen des DataGrids im Model werden die SQL-Statements aus den Model in die bereits bestehenden Funktionen des Controlers ausgelagert.

#r4_control.ps1
function getReports
{
    getReportModel "select * from tbl_report"
    updateReport
}



function getReportsForUser($selectedUser)
{
        $SqlQuery = "select * from tbl_report where Anmeldename = '" + $selectedUser +"';"
        getReportModel $SqlQuery
        updatereport
}


function getReportsForRechner($selectedRechner)
{

        $SqlQuery ="select r.ReportID, r.Appname, r.Eventtime, r.Anmeldename, r.MAC, r.ReportType "
        $SqlQuery += "from tbl_report r inner join tbl_computer c "
        $SqlQuery += "on r.MAC = c.MAC "
        $SqlQuery += "where c.Hostname = '" + $selectedRechner +"';"

        getReportModel $sqlQuery
        updatereport

}

View

Der Aufruf der *getCbo.. * - Methoden in der View müssen wie folgt geschrieben werden.

r4_view.ps1

.....

$handler_form1_Load=
{
    getReports  #Erstes Befüllen des DataGrids
    #getCboUser
    #getCboHosts
    getcbo "Rechner" #Aufruf der refaktorisierten Methode und Übergabe
    getcbo "User"    #ds Parameters für die jeweils zu füllende KomboBox
    .....

Fazit

Durch die Refaktorisierung wurde der Quellcode im Controler etwas mehr; es konnten jedoch einige Funktionen eingespart werden.

Im Model wurden erhebliche Zeilen Quellcode eingespart, welches zur besseren Verständlichkeit des Quellcode beiträgt.

Runde 5 - Ändern von Daten in tbl_report

Die Verwendung von Objekten des .NET-Frameworks erhöhen zwar den Komplexitätsgrad des Skriptes; sie machen aber manche Aufgaben realtiv einfach, wie beispielsweise das Ändern von Einträgen im DataGrid und das Abspeichern dieser Änderungen in der Datenbank.

Durch die Verwendung des DataAdapter-Konzeptes zum Befüllen des DataGrids, stellt .NET quasi eine logische Tabelle im Arbeitsspeicher zur Verfügung. Der Adapter hat damit das Wissen um den Aufbau der Tabelle und kann dieses Wissen auch zum Ändern und Löschen von Einträgen in dieser Tabelle verwenden. Das verwendete DataAdapter-Objekt muss dazu einem sog. CommandBuilder übergeben werden, der die ‘lästige’ Arbeit des Formulierens von SQL INSERT-, UPDATE und DELETE-Anweisungen für den DataAdapter übernimmt.

function updateDatenbankModel()
{
   verbindungOeffnen
   $commandbuilder = new-object MySql.Data.MySqlClient.MySqlCommandBuilder $SqlAdapter

   $SqlAdapter.UpdateCommand = $commandbuilder.GetUpdateCommand()
   $SqlAdapter.InsertCommand = $commandbuilder.GetInsertCommand()
   $SqlAdapter.DeleteCommand = $commandbuilder.GetDeleteCommand()

   $null = $SqlAdapter.Update($DataSet)

   verbindungSchliessen
}
_images/grid_before_update_click.jpg _images/grid_after_auswaehlen_click.jpg