Home   Cover Cover Cover Cover
 

Mischen und Speichern von DataSets (Client-Server-Szenario)


Zu Abschnitte 5.4.1 und 5.4.2 des Buchs

Dieses Beispiel zeigt das Mischen von DataSets und das Speichern der Daten in der Datenbank. Es zeigt auch, wie eine einfache verteilte Kommunikation mit .NET realisiert wird (siehe Beispiele zu Kapitel 4). Weiters werden Daten von der Datenbank geladen und auf der Konsole ausgegeben (siehe die restlichen Beispiele zu Kapitel 5).

Es kann vorkommen, dass die gleichen Daten in verschiedenen DataSets verändert werden. Dann müssen diese wieder zu einem einzigen DataSet zusammengemischt werden. Angenommen, die Daten aus der Contact-Datenbank sind auf einem Server (Schicht2) zwischengelagert, werden aber am Client geändert und sollen anschließend an den Server zur Validierung und Speicherung in der Datenbank (Schicht 3) zurückgesendet werden.

Um das Beispiel so einfach wie möglich zu halten, bietet der Server das Interface IServer an, welches nur zwei Methoden besitzt:

  • bool MergeData(DataSet changedDS): Mischt den übergeben DataSet mit dem DataSet am Server und speichert die Daten in der DB
  • DataSet GetData(): liefert alle Daten an den Client
Der Client besitzt die Methode SendDataToServer, mit der er die geänderten Daten mittels eines "Proxies" an den Server schickt. Wenn zwei Clients gleichzeitig IServer.MergeData aufrufen, so werden die Änderungen am Server nacheinander durchgeführt. Eine Benachrichtigung der Clients (Ereignis), das sich die Daten verändert haben, wurde nicht implementiert.

Von Client und Server gemeinsam genutzte Typen

./common/5-4-2IServer.cs
using System.Data;

namespace Chapter5.CS.Common {

  public interface IServer {
    bool MergeData(DataSet changedDS);  
    DataSet GetData();
  }
}

Neben dem IServer Interface, wird für die Ausgabe von DataSets auf der Konsole die Klasse Printer verwendet.

./common/5-4-2Printer.cs
using System;
using System.Data;

namespace Chapter5.CS.Common {

  public class Printer {

    public static void Print(DataSet ds) {
      Console.WriteLine("DataSet {0}:", ds.DataSetName);
      Console.WriteLine();
      foreach(DataTable t in ds.Tables) {
        Print(t);
        Console.WriteLine();
      }
    }
    
    public static void Print(DataTable t) {
      //---- Tabellenkopf
      PrintHeader(t);   
      //---- Daten
      int nrOfCols = t.Columns.Count;
      foreach(DataRow row in t.Rows) {
        Print(row);
      }
    }

    public static void Print(DataRow row) {
      object[] items = row.ItemArray;
      for(int i=0; i < items.Length; i++) {
        Console.Write(items[i]); Console.Write(" | ");
      }
      Console.WriteLine();
    }

    public static void PrintHeader(DataTable t) {
      Console.WriteLine("Tabelle {0}:", t.TableName);
      foreach(DataColumn col in t.Columns) {
        Console.Write(col.ColumnName + " | ");
      }
      Console.WriteLine();
      for (int i=0; i < 40; i++) {Console.Write("-");}
      Console.WriteLine();
    }

  }
}

Client

Der Client benutzt das Interface IServer und nicht die Serverimplementierung selbst. Zur Kommunikation wird ein Proxy (siehe Klasse ServerProxy) verwendet, der das Serverinterface implementiert. Die Methode MergeData schickt nicht den ganzen DataSet zum Server, sondern nur die veränderten Daten.

./client/5-4-2Client.cs
using System.Data;
using System;
using Chapter5.CS.Common; //enthaelt die Typen IServer, Printer etc.

namespace Chapter5.CS.Client {

  public class Client {
    DataSet ds; //Daten
    IServer serverProxy; //Server

    public Client(IServer server) {
      this.serverProxy = server;
      ds = server.GetData();
    }

    // Methode am Client
    void SendDataToServer(DataSet ds) {
      // Annahme: ds.AcceptChanges wurde noch nicht aufgerufen!
      DataSet changedDS = ds.GetChanges(); // liefert nur die veraenderten DataRows
      bool ok = serverProxy.MergeData(changedDS);
      if (ok) ds.AcceptChanges(); else ds.RejectChanges();
    }

    void ModifyData() {
      if (ds != null) {
        Console.WriteLine("Loaded data from server:");
        Printer.Print(ds);
        Console.WriteLine("Modified data from server: ");
        DataTable pTable = ds.Tables["Person"];
        pTable.Rows[1]["Name"] = pTable.Rows[1]["Name"] + "1";
        Printer.PrintHeader(pTable);
        Printer.Print(pTable.Rows[1]);
      } else {
        Console.WriteLine("no data avialable!");
      }
    }

    void UpdateData() {
      if (this.ds != null) {
        this.SendDataToServer(this.ds);
        Console.WriteLine("\nData sent to server!");
      }
    }

    public static void Main() {
      IServer p = new ServerProxy("localhost", 5000);
      Client c = new Client(p);
      c.ModifyData();
      c.UpdateData();
    }

  }
}
./client/5-4-2ServerProxy.cs
using System.Data;
using System;
using System.Net;
using System.Net.Sockets;
using System.Collections;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;

using Chapter5.CS.Common;

namespace Chapter5.CS.Client {

  public class ServerProxy : IServer {
    string server;
    int port;

    public ServerProxy (string server, int port) {
      this.server = server; this.port = port;
    }

    public bool MergeData(DataSet changedDS) {
      Console.WriteLine("Proxy:MergeData");
      //Protokoll besteht aus Request und Response
      //Request: Array mit Methodenname (string) sowie n Parameter (object)
      //Response: Array mit Statuscode: 0 (== Fehler) oder 1 (== ok) sowie n Rueckgabeparameter
      bool ok = false;
      try {
        ArrayList request = new ArrayList();
        ArrayList response = null;
        request.Add("MergeData");
        request.Add(changedDS);

        response = SendReceive(request);
        
        int code = (int)response[0];
        if (code == 1) {
          ok = (bool)response[1];
        }
      } catch (Exception e) {
        Console.WriteLine(e);
      }
      return ok;
    }

    public DataSet GetData() {
      Console.WriteLine("Proxy:GetData");
      DataSet ds = null;
      //Protokoll besteht aus Request und Response
      //Request: Array mit Methodenname (string) sowie n Parameter (object)
      //Response: Array mit Statuscode: 0 (== Fehler) oder 1 (== ok) sowie n Rueckgabeparameter
      try {
        ArrayList request = new ArrayList();
        ArrayList response = null;
        request.Add("GetData");

        response = SendReceive(request);

        int code = (int)response[0];
        if (code == 1) {
          ds = (DataSet)response[1];
        }
      } catch (Exception e) {
        Console.WriteLine(e);
      }
      return ds;
    }

    ArrayList SendReceive(ArrayList request) {
      BufferedStream stream = Connect();
      BinaryFormatter formatter = new BinaryFormatter();
      formatter.Serialize(stream, request);
      return (ArrayList)formatter.Deserialize(stream);
    }

    BufferedStream Connect() {
      TcpClient tcpClient = new TcpClient();
      BufferedStream s = null;
      try{
        tcpClient.Connect(server, port);
        NetworkStream networkStream = tcpClient.GetStream();

        if(networkStream.CanWrite && networkStream.CanRead){
          s = new BufferedStream(networkStream);
        }
      }
      catch (Exception e ) {
        Console.WriteLine(e.ToString());
      }
      return s;
    }
  }
}

Server

Der Server implementiert das IServer interface und für die Kommunikation mit den Clients wird die Klasse ClientListener verwendet. Die Methode StoreTable speichert eine DataTable in der Datenbank.

./server/5-4-2Server.cs
using System.Data;
using System.Data.SqlClient;
using System;

using Chapter5.CS.Common; // enthaelt IServer etc.


namespace Chapter5.CS.Server {

  public class Server : IServer {
    DataSet origDS;

    public Server() {  }

    public void Init() {
      try {
        Console.WriteLine("loading data from database ...");
        origDS = LoadData();
      } catch (Exception e) {
        HandleError(e, "ERROR: Could not load data from database");
      }
    }

    static void HandleError(Exception e, string msg) {
      Console.WriteLine(msg);
      Console.WriteLine(e);
    }

    static IDbCommand GetSelectAllCmd() {
      SqlCommand cmd = new SqlCommand();
            cmd.Connection = new SqlConnection(
                "data source=localhost\\SQLEXPRESS; initial catalog=NETBOOK; Integrated Security=true;"
                );
      cmd.CommandText = "SELECT * FROM Person; SELECT * FROM Contact";
      return cmd;
    }

    static DataSet LoadData() {
      DataSet ds = new DataSet("PersonContacts");
      IDbDataAdapter adapter = new SqlDataAdapter();
      adapter.SelectCommand = GetSelectAllCmd();
      //----- im DataSet befinden sich noch keine Tabellen, also fuege sie bei Fill hinzu!
      adapter.MissingSchemaAction = MissingSchemaAction.AddWithKey;
      //----- Automatisch erzeugte Tabellen umbenennen
      adapter.TableMappings.Add("Table", "Person");
      adapter.TableMappings.Add("Table1", "Contact");
      //----- Daten aus der Datenbank laden
      adapter.Fill(ds);
      if (ds.HasErrors) ds.RejectChanges(); else ds.AcceptChanges();
      if (adapter is IDisposable) ((IDisposable)adapter).Dispose();
      return ds;
    }


    //---- interface IServer
    // DataSet vom Client mit dem vom Server mischen
    public bool MergeData(DataSet changedDS) {
      // sind Aenderungen vorhanden?
      if (!changedDS.HasChanges(DataRowState.Modified)) return true;
      //----- Tabellen Person und Contact speichern
      StoreTable(changedDS, "Person");
      if (!changedDS.HasErrors) StoreTable(changedDS, "Contact");
      if (changedDS.HasErrors) return false;
      //----- changedDs zum Original-DataSet origDS hinzufuegen
      origDS.Merge(changedDS);
      bool ok = !origDS.HasErrors;
      if (ok) origDS.AcceptChanges(); else origDS.RejectChanges();
      return ok;
    }
    
    // speichert die Datensaetze einer Tabelle
    static void StoreTable(DataSet ds, string tableName) {
            SqlConnection con = new SqlConnection("data source=localhost\\SQLEXPRESS; initial catalog=NETBOOK; Integrated Security=true;"
                
            );
      //----- SelectCommand setzen, damit der OleDbCommandBuilder automatisch
      // Insert-, Update- und Delete-Kommandos generieren kann
      SqlDataAdapter adapter =
        new SqlDataAdapter("SELECT * FROM " + tableName, con);
      SqlCommandBuilder cmdBuilder = new SqlCommandBuilder(adapter);
      //----- Daten speichern!
      try {
        adapter.Update(ds, tableName);
      } catch (DBConcurrencyException) {
        // Datenbank wurde auch von einer anderen Transaktion geaendert!
        // ... Auf Fehler reagieren! z.B.: DataSet neu laden!
      }
      adapter.Dispose();
    }

    //---- interface IServer
    // liefert Daten 
    public DataSet GetData() {
      return this.origDS;
    }

    // Main
    public static void Main() {
      Server s = new Server();
      s.Init();
      
      // fuer die Kommunikation mit einem Client
      ClientListener l = new ClientListener(s, 5000);
      Console.WriteLine("Server is up and listening at port {0}", l.port);
      Console.WriteLine("Press Ctrl+C to stop the server :-).");
      l.Start(); // warte auf Anfragen vom Client
    }
    
  }
      
}

Kommunikation

Die Kommunikation (d.h. ein entfernter Methodenaufruf) besteht aus Request und Response. Weiters wird nach jedem Methodenaufruf die Verbindung zum Client wieder getrennt. Da Sockets und TCP/IP verwendet werden, könnte die Verbindung zum Client auch bestehen bleiben.

Aufbau eines Methodenaufrufes (Request / Response):

  • Request: ArrayList mit Methodennamen (Typ string, Index 0) gefolgt von den Aktualparametern (Typ object).
  • Response: ArrayList mit dem Status (Typ int, Index 0) gefolgt von den Rückgabeparametern (Typ object).
Natürlich kann das Protokoll auch anders aussehen bzw. .NET-Remoting, oder SOAP (siehe Beispiele zu Kapitel 7) verwendet werden.

./server/5-4-2ClientListener.cs
using System.Data;
using System;
using System.Net;
using System.Net.Sockets;
using System.Collections;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;

using Chapter5.CS.Common; // enthaelt IServer etc.


namespace Chapter5.CS.Server {

  //---- Uebernimmt die Netzwerkkommunikation mit dem Client
  public class ClientListener {
    IServer server;
    public int port;

    public ClientListener(IServer s, int port) {
      server = s; this.port = port;
    }
    //---- Started das "Warten auf Client-Requests"
    public void Start() {
      TcpListener tcpListener = new TcpListener(this.port);
      tcpListener.Start();
      Console.WriteLine("Waiting for a connection....");
 
      try{
        while (true) {
          // blockiert, bis Client etwas sendet
          TcpClient tcpClient = tcpListener.AcceptTcpClient();
          Console.WriteLine("Connection accepted.");
          // Client-Request verarbeiten
          this.UnmarshallCall(tcpClient);
        }
      } catch (Exception e) {
        Console.WriteLine(e.ToString());
      }
      tcpListener.Stop();
    }

    //---- ruft die entsprechende IServer-Methode auf
    void UnmarshallCall(TcpClient tcpClient) {
      //Protokoll besteht aus Request und Response
      //Request: Array mit Methodenname (string) sowie n Parameter (object)
      //Response: Array mit Statuscode: 0 (== Fehler) oder 1 (== ok) sowie n Rueckgabeparameter
      
      NetworkStream networkStream = tcpClient.GetStream();
      BufferedStream stream = new BufferedStream(networkStream);
      BinaryFormatter formatter = new BinaryFormatter(); //wandelt Objekte in byte Array um

      ArrayList request = (ArrayList) formatter.Deserialize(stream);
      ArrayList response = new ArrayList();

      //Request: Verstehe nur 2 Methoden: "MergeData" oder "GetData"
      string methName = request[0].ToString();
      Console.WriteLine("Client sent \"{0}\" ", methName);
      
      if (methName == "MergeData") {
        DataSet changedDS = (DataSet)request[1];
        Printer.Print(changedDS);
        //eigentliche IServer-Methode aufrufen und Ergebnis in response verpacken
        bool ok = this.server.MergeData(changedDS);
        response.Add(1); response.Add(ok);
      } else if (methName == "GetData") {
        //eigentliche IServer-Methode aufrufen und Ergebnis in response verpacken
        DataSet ds = this.server.GetData();
        response.Add(1); response.Add(ds);
      } else { //Nachricht des Clients wurde nicht verstanden
        response.Add(0);
      }

      //Response als byte-Array an Client zurueckschicken
      formatter.Serialize(stream, response);

      //Client-Verbindung trennen
      stream.Close();
    }

  }

      
}

Testen der Anwendung

Zuerst wird die Server-Anwendung gestarted. Dann wird der Client gestartet. Die folgenden Screenshots zeigen die Ausgaben des Server und des Client.

Konsole 1 (Server):
Konsolenausgabe

Konsole 2 (Client):
Konsolenausgabe