Willkommen auf myCSharp.de! Anmelden | kostenlos registrieren
 | Suche | FAQ

Hauptmenü
myCSharp.de
» Startseite
» Forum
» Suche
» Regeln
» Wie poste ich richtig?

Mitglieder
» Liste / Suche
» Wer ist online?

Ressourcen
» FAQ
» Artikel
» C#-Snippets
» Jobbörse
» Microsoft Docs

Team
» Kontakt
» Cookies
» Spenden
» Datenschutz
» Impressum

Tree(Node) Collection als Zwischenschicht bauen [und grundsätzliches zur 3-Schichten-Architektur]
matze72
myCSharp.de - Member



Dabei seit:
Beiträge: 3

Themenstarter:

Tree(Node) Collection als Zwischenschicht bauen [und grundsätzliches zur 3-Schichten-Architektur]

beantworten | zitieren | melden

Hallo,
ich möchte einen TreeView an eine Datenbank anbinden. Soweit ist alles klar.
Damit ich später flexibler bin, möchte ich das ganze in drei Schichten erstellen.
UI -> Tree
Zwischenschicht -> ein Objekt , dass Eigenschaften und Verhalten analog zum TreeView hat und Nodes enthält, die wiederum Nodes enthalten können.
DAL: Zugriff auf die Datenbank

Mein Problem:
Ich kriege die Zwischenschicht nicht hin :-( .


Was ich bisher probiert habe:
Ich habe mir eine Nodeklasse gebaut und eine Nodes - Collection die Nodes Aufnehmen kann.
Dass Problem im Moment ist , dass das original Treeview-Control einmal auf die Nodes-Collection mit Nodes zugreift :

trv1.Nodes.Add
und zum andern auf das Array Nodes

trv1.Nodes[1].Text
mit dem selben Namen zugreift.
Ich habe schon alles Mögliche versucht um das nachzubauen und merke , dass mir einfach das Wisseen fehlt


Kann mir irgendjemand sagen kann wie man so etwas realisiert ?

Besser noch: Gibt es im Netz irgendwelche Beispiele zum Thema die ich mir anschauen kann? (Hab schon ziemich viel gegoogelt , aber leider nichts vernünftiges gefunden).
private Nachricht | Beiträge des Benutzers
herbivore
myCSharp.de - Experte

Avatar #avatar-2627.gif


Dabei seit:
Beiträge: 52329
Herkunft: Berlin

beantworten | zitieren | melden

Hallo matze72,

TreeView.Nodes ist einfach eine Eigenschaft (=Property) und ist wie folgt implementiert:


public TreeNodeCollection Nodes
{
      get
      {
            if (this.nodes == null)
            {
                  this.nodes = new TreeNodeCollection(this.root);
            }
            return this.nodes;
      }
}
Das musst/kannst du in deiner Klasse entsprechend nachbauen.

herbivore
private Nachricht | Beiträge des Benutzers
matze72
myCSharp.de - Member



Dabei seit:
Beiträge: 3

Themenstarter:

beantworten | zitieren | melden

Hi herbivore,

erst mal danke für die schnelle Antwort.
Deine Lösungsansatz hilft mir leider nicht weiter bei der Frage wie ich realisieren kann, dass ich zum einen über Nodes auf Methoden wie Add und Insert zugreifen kann, und gleichzeitig über Nodes[1] auf den zweiten Node der Collection zugreifen kann.
Es kann aber auch ganz gut sein, dass ich nur nicht verstehe wie deine Antwort mein Problem löst , da ich noch ziemlich wenig Ahnung habe.

Gruss Matze
private Nachricht | Beiträge des Benutzers
herbivore
myCSharp.de - Experte

Avatar #avatar-2627.gif


Dabei seit:
Beiträge: 52329
Herkunft: Berlin

beantworten | zitieren | melden

Hallo matze72,

die Eigenschaft TreeView.Nodes liefert ein Objekt vom Typ TreeNodeCollection. In der Klasse TreeNodeCollection sind verschiedene Methoden definiert, nämlich Add u.ä., aber auch der Array-Zugriff per []. Du musst also auch so eine Klasse definieren (wenn du nicht eine Standard-Collection wie ArrayList verwenden willst).


class TreeNodeCollection
{
   public void Add(TreeNode child)
   {
         this.AddAt(this.Count, child);
   }

   public TreeNode this[int index]
   {
         get
         {
               return this._list.get_Item(index);
         }
   }
}

herbivore
private Nachricht | Beiträge des Benutzers
Rainbird
myCSharp.de - Experte

Avatar #avatar-2834.jpg


Dabei seit:
Beiträge: 3953
Herkunft: Mauer

Schlüssel

beantworten | zitieren | melden

Du solltest Dir Funktionen bauen, die den Tree füllen. TreeViews sollte man niemals auf einen Rutsch füllen, denn man weiss nie wie groß sie werden, wenn sie Daten aus einer Datenbank anzeigen. Beim laden des Formulars zeichnest Du die/den Wurzelknoten (die TreeNodes die kein Parent haben). Sobald der Benutzer einen Knoten expandiert (aufklappt) feuert das TreeView ein "BeforeExpand" Ereignis. Diese Ereignis behandelst Du und rufst eine Funktion auf, die alle Kindknoten des expandierten Knotens einfügt. Da immer nur eine Ebene gezeichnet wird, baut der Tree immer schnell auf und der Benutzer hat nie Wartezeiten.

Die Funktionen, die Deine TreeNodes zeichnen (Bei mir heißen die immer DrawRootNodes, DrawChildNodes und DrawNode) müssen aber irgendwoher ihre Daten kriegen. Das übernimmt Deine "Mittelschicht" (DAL). Angenommen Du baust ein Programm, welches Produktgruppen und Produkte in einem TreeView darstellen soll. Deine DAL könnte eine Klasse sein mit Methoden wie etwa GetProductGroups(), GetProducts(Guid productGroupID). Diese Methoden verwenden eine Datenzugriffsschicht, die Logik von der Datenbank trennt. Dein TreeView dient nur zu Anzeige und zur Navigation; nicht zur Datenhaltung! Damit die Schihten sauber getrennt sind, kannst Du ID´s etc. in der Tag-Eigenschaft der einzelnen TreeNode-Objekte ablegen. Wenn der Benutzer z.B. ein neues Produkt in eine Produktgruppe im Tree einfügen will, muss der Anzeigeprozess komplett unabhängig vom Tree sein. Es wird z.B. eine Methode AddProduct der DAL-Klasse aufgerufen. Diese erzeugt ein neues Produkt und gibt z.B. ein Product-Objekt oder ein DataRow-Objekt zurück, welches den neu erzeugten Produktdatensatz enthält. Dieser kann der DrawNode-Methode übergeben werden, um das neue Produkt im TreeView anzuzeigen.

Es ist etwas knifflig zu erklären. Wenn Dich meine Methode, TreeViews zu verwenden interessiert, kann ich auch gerne ein kleines Beispiel machen und hier posten.
private Nachricht | Beiträge des Benutzers
matze72
myCSharp.de - Member



Dabei seit:
Beiträge: 3

Themenstarter:

beantworten | zitieren | melden

Hallo herbivore & Schlüssel,
erst einmal vielen Dank für eure Hilfe.

@Schüssel: Dein Angebot nehme ich gerne an, würde mir sehr weiterhelfen.

@herbivore: Ich probiere immer noch rum, wie ich deinen Code in meine NodesCollection einbauen kann X( . Im Moment schmeisst mir VS noch den Fehler:

'System.Collections.ArrayList.this[int].get': Operator oder Accessor kann nicht explizit aufgerufen werden

Ich muss was c# und Programmieren anbelangt noch ne ganze Menge lernern. Habe in meiner Ausbildung (Fachinf. Anwendungsentwicklung 1. Jahr) erst ein paar kleinere Sachen gemacht.

Gruß und schönes Wochenende
Matze
private Nachricht | Beiträge des Benutzers
Rainbird
myCSharp.de - Experte

Avatar #avatar-2834.jpg


Dabei seit:
Beiträge: 3953
Herkunft: Mauer

Beispielcode

beantworten | zitieren | melden

Ich habe das Beispiel fertig. Am besten Du erstellst zum testen ein Windows-Forms-C#-Projekt. Mein Code besteht aus einem Formular und drei Klassen.

Das Beispiel verwendet die Northwind-Datenbank und geht deshalb davon aus, dass ein lokaler SQL Server 7.0 oder höher bzw. MSDE läuft.

Quelltext der Klasse DataAccess (Datenzugriffsschicht):


using System;
using System.Collections.Generic;
using System.Text;
using System.Data;
using System.Data.SqlClient;

namespace TreeViewDemo
{
	/// <summary>
	/// Implementiert eine wiederverwendbare Datenzugriffsschicht für SQL Server.
	/// </summary>
	public class DataAccess
	{
		/// <summary>
		/// Führt eine SQL-Anweisung ais und gibt das Ergbnis in einer DataTable zurück.
		/// </summary>
		/// <param name="connectionString">Verbindungszeichenfolge zur Datenbank</param>
		/// <param name="sqlStatement">SQL-Anweisung</param>
		/// <param name="parameters">Parameterliste</param>
		/// <returns>Abfrageergebnisse in Tabellenform</returns>
		public static DataTable RequestData(string connectionString, string sqlStatement, SqlParameter[] parameters)
		{
			// Verbindung zur Datenbank vorbereiten
			SqlConnection connection = new SqlConnection(connectionString);

			// Befehl zur Ausführung der SQL-Anweisung erzeugen
			SqlCommand command = new SqlCommand(sqlStatement, connection);

			// Wenn Parameter übergeben wurden ...
			if (parameters != null && parameters.Length > 0)
				// Parameter zufügen
				command.Parameters.AddRange(parameters);

			// Datenadapter erzeugen
			SqlDataAdapter adapter = new SqlDataAdapter();
			adapter.SelectCommand = command;

			// DataTable erzeugen
			DataTable result = new DataTable();

			try
			{
				// Verbindung öffnen
				connection.Open();
			}
			catch (SqlException ex)
			{
				// Anwendungsausnahme werfen
				throw new ApplicationException("Verbindung zur Datenbank konnte nicht aufgebaut werden.", ex);
			}

			try
			{
				// Befehl ausführen und Tabelle mit den Ergebnissen füllen
				adapter.Fill(result);
			}
			catch (SqlException ex)
			{
				// Anwendungsausnahme werfen
				throw new ApplicationException("Fehler beim ausführen von folgender SQL-Anweisung: '" + sqlStatement + "'", ex);
			}
			finally
			{
				// Verbindung schließen
				connection.Close();
			}
			// Ergebnistabelle zurückgeben
			return result;
		}

		/// <summary>
		/// Führt Änderungen auf der Datenbank aus.
		/// </summary>
		/// <param name="transaction">Transaktionsobjekt</param>
		/// <param name="table">DataTable mit geänderten Daten</param>
		/// <param name="dbTableName">Name der Original-Datenbanktabelle</param>
		public static void UpdateData(SqlTransaction transaction, DataTable table, string dbTableName)
		{
			// Verbindung ermitteln
			SqlConnection connection = transaction.Connection;

			// Wenn die Transaktion über keine gültige Verbindung verfügt ...
			if (connection == null)
				// Anwendungsausnahme werfen
				throw new ApplicationException("Die angegebene Transaktion ist nicht mehr gültig.");

			// SELECT-Statement zum abrufen von Tabellen-Metadaten erzeugen
			string selectStatement = "SELECT TOP 0 * FROM " + dbTableName;

			// SELECT-Befehl für den Metadatenabruf erzeugen
			SqlCommand metaCommand = new SqlCommand(selectStatement, connection);

			// Datenadapter erzeugen
			SqlDataAdapter adapter = new SqlDataAdapter(metaCommand);

			try
			{
				// Passende UPDATE, INSERT und DELETE Befehle automatisch erzeugen
				SqlCommandBuilder builder = new SqlCommandBuilder(adapter);
			}
			catch (Exception ex)
			{
				// Anwendungsausnahme werfen
				throw new ApplicationException("UPDATE, INSERT und DELETE-Befehle zum aktualisieren von Tabelle '" + dbTableName + "' konnten nicht erstellt werden.", ex);
			}
			// Befehle in die laufende Transaktion einbinden
			adapter.InsertCommand.Transaction = transaction;
			adapter.UpdateCommand.Transaction = transaction;
			adapter.DeleteCommand.Transaction = transaction;

			try
			{
				// Änderungen auf der Datenbank durchführen
				adapter.Update(table);
			}
			catch (SqlException ex)
			{
				// Anwendungsausnahme werfen
				throw new ApplicationException("Fehler beim aktualisieren von Datenbanktabelle '" + dbTableName + "'", ex);
			}
		}

		/// <summary>
		/// Erzeugt eine neue Transaktion für sichere Änderungen in der Datenbank.
		/// </summary>
		/// <param name="connectionString">Verbindungszeichenfolge</param>
		/// <param name="transactionName">Transaktionsname</param>
		/// <param name="isolation">Isolationsstufe</param>
		/// <returns>Transaktionsobjekt</returns>
		public static SqlTransaction BeginTransaction(string connectionString, string transactionName, IsolationLevel isolation)
		{
			// Verbindung zur Datenbank herstellen
			SqlConnection connection = new SqlConnection(connectionString);

			try
			{
				// Verbindung öffnen
				connection.Open();
			}
			catch (SqlException ex)
			{
				// Anwendungsausnahme werfen
				throw new ApplicationException("Verbindung zur Datenbank konnte nicht aufgebaut werden.", ex);
			}
			// Neue Transaktion erzeugen und zurückgeben
			return connection.BeginTransaction(isolation, transactionName);
		}

		/// <summary>
		/// Bestätigt eine bestimmte Transaktion.
		/// </summary>
		/// <param name="transaction">Transaktionsobjekt</param>		
		public static void CommitTransaction(SqlTransaction transaction)
		{
			// Verbindung ermitteln
			SqlConnection connection = transaction.Connection;

			// Wenn die Transaktion über keine gültige Verbindung verfügt ...
			if (connection == null)
				// Anwendungsausnahme werfen
				throw new ApplicationException("Die angegebene Transaktion ist nicht mehr gültig.");

			try
			{
				// Transaktion bestätigen
				transaction.Commit();

				// Verbindung schließen
				connection.Close();
			}
			catch (SqlException ex)
			{
				// Anwendungsausnahme werfen
				throw new ApplicationException("Die Datenbank-Transaktion konnte nicht abgeschlossen werden.", ex);
			}
		}

		/// <summary>
		/// Verwirft eine bestimmte Transaktion.
		/// </summary>
		/// <param name="transaction">Transaktionsobjekt</param>		
		public static void RollbackTransaction(SqlTransaction transaction)
		{
			// Verbindung ermitteln
			SqlConnection connection = transaction.Connection;

			// Wenn die Transaktion über keine gültige Verbindung verfügt ...
			if (connection == null)
				// Anwendungsausnahme werfen
				throw new ApplicationException("Die angegebene Transaktion ist nicht mehr gültig.");

			try
			{
				// Transaktion verwerfen
				transaction.Rollback();

				// Verbindung schließen
				connection.Close();
			}
			catch (SqlException ex)
			{
				// Anwendungsausnahme werfen
				throw new ApplicationException("Die Datenbank-Transaktion konnte nicht verworfen werden.", ex);
			}
		}
	}
}



Quelltext der Klasse DomainLogic (DAL):


using System;
using System.Collections.Generic;
using System.Text;
using System.Data;
using System.Data.SqlClient;

namespace TreeViewDemo
{
	/// <summary>
	/// Beispiel Domänenlogik.
	/// </summary>
	public class DomainLogic
	{
		#region Deklarationen

		//TODO: Verbindungszeichenfolge aus Registry lesen, anstatt direkt im Code zu hinterlegen!
		// Verbindungszeichenfolge
		private const string DBCONNECTION = @"Data Source=localhost;Initial Catalog=Northwind;Integrated Security=True";

		#endregion

		#region Zugriff auf Lieferanten

		/// <summary>
		/// Gibt alle Lieferanten in einer DataTable zurück.
		/// </summary>
		/// <returns>Tabelle mit Lieferantendaten</returns>
		public DataTable GetAllSuppliers()
		{			
			// Lieferanten abrufen und zurückgeben
			// Durch die Datenzugriffsschicht genügt eine Zeile Quellcode, um die Lieferantenliste zu bekommen.
			return DataAccess.RequestData(DBCONNECTION, "SELECT * FROM Suppliers", null);
		}

		/// <summary>
		/// Gibt einen bestimmten Lieferanten anhand seines Schlüssels zurück.
		/// </summary>
		/// <param name="supplierID">Lieferantenschlüssel</param>
		/// <returns>Tabelle mit einem bestimmten Lieferantendatensatz</returns>
		public DataTable GetSupplierByID(Int32 supplierID)
		{
			// Parameterliste erzeugen
			SqlParameter[] inputParams = new SqlParameter[1];
			
			// Parameter zur Übergabe des Lieferantenschlüssels erzeugen
			inputParams[0]=new SqlParameter("@supplierID", SqlDbType.Int, 0, "SupplierID");
			inputParams[0].Value = supplierID;

			// Lieferantendatensatz abrufen und zurückgeben
			return DataAccess.RequestData(DBCONNECTION, "SELECT * FROM Suppliers WHERE [email protected]", inputParams);
		}

		#endregion

		#region Zugriff auf Produkte

		/// <summary>
		/// Gibt alle Produkte einens bestimmten Lieferanten zurück.
		/// </summary>
		/// <param name="supplierID">Lieferantenschlüssel</param>
		/// <returns>Tabelle mit Produktdatensätzen</returns>
		public DataTable GetProductsBySupplierID(Int32 supplierID)
		{
			// Parameterliste erzeugen
			SqlParameter[] inputParams = new SqlParameter[1];

			// Parameter zur Übergabe des Lieferantenschlüssels erzeugen
			inputParams[0] = new SqlParameter("@supplierID", SqlDbType.Int, 0, "SupplierID");
			inputParams[0].Value = supplierID;

			// Produktdatensätze abrufen und zurückgeben
			return DataAccess.RequestData(DBCONNECTION, "SELECT * FROM Products WHERE [email protected]", inputParams);
		}

		/// <summary>
		/// Speichert Änderungen an Produkten dauerhaft in der Datenbank.
		/// </summary>
		/// <param name="productTable">Tabelle mit Änderungen</param>
		public void SaveProducts(DataTable productTable)
		{
			// Neue Datenbanktransaktion erzeugen
			SqlTransaction transaction = DataAccess.BeginTransaction(DBCONNECTION, "saveproducts", IsolationLevel.Serializable);

			try
			{
				// Änderungen speichern
				DataAccess.UpdateData(transaction, productTable, "Products");

				// Transaktion abschließen
				DataAccess.CommitTransaction(transaction);
			}
			catch (Exception ex)
			{ 
				// Transaktion verwerfen
				DataAccess.RollbackTransaction(transaction);

				// Anwendungsausnahme werfen
				throw new ApplicationException("Änderungen an Produkten konnten nicht gespeichert werden.", ex);
			}
		}

		#endregion
	}
}


Quelltext der Klasse DisplayFacade (Vorgelagerte Schicht, um anzeigespezifische Datenaufbereitung von der DAL fernzuhalten):


using System;
using System.Collections.Generic;
using System.Text;
using System.Data;

namespace TreeViewDemo
{
	/// <summary>
	/// Methoden zur Aufbereitung der Daten zur Anzeige werden in dieser Klasse implementiert.
	/// </summary>
	class DisplayFacade : DomainLogic
	{
		/// <summary>
		/// Gibt eine nach Firmenname sortierte Datensicht auf alle Lieferanten zurück.
		/// </summary>
		/// <returns>Datensicht</returns>
		public DataView GetSortedSupplierList()
		{ 
			// Lieferanten abrufen
			DataTable table = base.GetAllSuppliers();

			// Neue Datensicht erzeugen
			DataView view = new DataView(table);

			// Nach Firmenname sortieren
			view.Sort = "CompanyName";

			// Datensicht zurückgeben
			return view;
		}

		/// <summary>
		/// Gibt alle Produkte eines bestimmten Lieferanten als nach Produktname sortierte Datenansicht zurück.
		/// </summary>
		/// <param name="supplierID">Lieferantenschlüssel</param>
		/// <returns>Datensicht</returns>
		public DataView GetSortedSupplierProductList(Int32 supplierID)
		{
			// Produkte abrufen
			DataTable table = base.GetProductsBySupplierID(supplierID);

			// Neue Datensicht erzeugen
			DataView view = new DataView(table);

			// Nach Produktname sortieren
			view.Sort = "ProductName";

			// Datensicht zurückgeben
			return view;
		}

	}
}

Quelltext des Formulars GUI:


using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;

namespace TreeViewDemo
{
	/// <summary>
	/// Testoberfläche zur Demonstration eines TreeViews.
	/// </summary>
	public class GUI : Form
	{
		#region Deklarationen

		// Anzeigefassade
		DisplayFacade _facade;

		/// <summary>
		/// Required designer variable.
		/// </summary>
		private System.ComponentModel.IContainer components = null;

		// Steuerelemente
		private System.Windows.Forms.TreeView tree;
		private System.Windows.Forms.Button button1;

		#endregion

		#region Konstruktoren

		/// <summary>
		/// Standardkonstruktor.
		/// </summary>
		public GUI()
		{
			// Steuerelemente erzeugen und plazieren
			InitializeComponent();

			// Anzeigefassade erzeugen
			_facade = new DisplayFacade();
		}

		#endregion

		#region Einstiegspunkt

		/// <summary>
		/// The main entry point for the application.
		/// </summary>
		[STAThread]
		public static void Main()
		{			
			Application.Run(new GUI());
		}

		#endregion

		#region Windows Form Designer Code

		/// <summary>
		/// Required method for Designer support - do not modify
		/// the contents of this method with the code editor.
		/// </summary>
		private void InitializeComponent()
		{
			this.tree = new System.Windows.Forms.TreeView();
			this.button1 = new System.Windows.Forms.Button();
			this.SuspendLayout();
			// 
			// tree
			// 
			this.tree.Dock = System.Windows.Forms.DockStyle.Left;
			this.tree.Location = new System.Drawing.Point(0, 0);
			this.tree.Name = "tree";
			this.tree.Size = new System.Drawing.Size(369, 402);
			this.tree.TabIndex = 0;
			this.tree.BeforeExpand += new System.Windows.Forms.TreeViewCancelEventHandler(this.tree_BeforeExpand);
			// 
			// button1
			// 
			this.button1.Location = new System.Drawing.Point(399, 12);
			this.button1.Name = "button1";
			this.button1.Size = new System.Drawing.Size(97, 36);
			this.button1.TabIndex = 1;
			this.button1.Text = "button1";
			this.button1.Click += new System.EventHandler(this.button1_Click);
			// 
			// GUI
			// 
			this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
			this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
			this.ClientSize = new System.Drawing.Size(508, 402);
			this.Controls.Add(this.button1);
			this.Controls.Add(this.tree);
			this.Name = "GUI";
			this.Text = "TreeView Beispiel mit n-Schichten";
			this.ResumeLayout(false);

		}

		/// <summary>
		/// Clean up any resources being used.
		/// </summary>
		/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
		protected override void Dispose(bool disposing)
		{
			if (disposing && (components != null))
			{
				components.Dispose();
			}
			base.Dispose(disposing);
		}

		#endregion

		#region Baumansicht

		/// <summary>
		/// Fügt einen Dummy-Baumknoten ein (Damit ein [+] Symbol im TreeView angezeigt wird.
		/// </summary>
		/// <param name="node">Baumknoten, unter dem der Dummy eingefügt werden soll</param>
		private void InsertDummyNode(TreeNode node)
		{ 
			// Dummy einfügen
			node.Nodes.Add("");
		}

		/// <summary>
		/// Wurzelknoten zeichnen.
		/// </summary>
		private void DrawRootNodes()
		{ 
			// Baumansicht leeren
			tree.Nodes.Clear();

			// Sanduhr anzeigen
			Cursor = Cursors.WaitCursor;

			// Füllen starten
			tree.BeginUpdate();

			// Lieferanten sortiert abrufen
			DataView suppliers = _facade.GetSortedSupplierList();

			// Alle Lieferanten durchlaufen
			foreach (DataRowView row in suppliers)
			{ 
				// Lieferanten-Baumknoten zeichnen
				DrawSupplierNode(tree.Nodes, row);
			}

			// Füllen abschließen
			tree.EndUpdate();

			// Mauspfeil anzeigen
			Cursor = Cursors.Default;
		}

		/// <summary>
		/// Kindknoten zeichnen.
		/// </summary>
		/// <param name="node">Baumknoten, dessen Kindknoten gezeichnet werden sollen</param>
		private void DrawChildNodes(TreeNode node)
		{
			// Knoteninfos auslesen
			NodeInfo info=(NodeInfo)node.Tag;

			// Knotentyp auswerten
			switch (info.Type)
			{ 
				case NodeType.Supplier: // Lieferant

					// Produkte dieses Lieferanten zeichnen
					DrawSupplierProducts(node, Int32.Parse(info.Key));

					break;
				case NodeType.Product: // Produkt

					// Für Produkte gibt es momentan keine möglichen Kindknoten geben ;-)
					// Vorhandene Kindknoten löschen (Dummy entfernen)
					node.Nodes.Clear();

					break;
			}			
		}

		/// <summary>
		/// Zeichnet baumknoten für alle Produkte eines bestimmten Lieferanten.
		/// </summary>
		/// <param name="node">Elternbaumknoten</param>
		/// <param name="supplierID">Lieferantenschlüssel</param>
		private void DrawSupplierProducts(TreeNode node, Int32 supplierID)
		{
			// Vorhandene Kindknoten löschen (Dummy entfernen)
			node.Nodes.Clear();

			// Sanduhr anzeigen
			Cursor = Cursors.WaitCursor;

			// Füllen starten
			tree.BeginUpdate();

			// Produkte sortiert abrufen
			DataView products = _facade.GetSortedSupplierProductList(supplierID);

			// Alle Lieferanten durchlaufen
			foreach (DataRowView row in products)
			{
				// Produkt-Baumknoten zeichnen
				DrawProductNode(node.Nodes, row);
			}

			// Füllen abschließen
			tree.EndUpdate();

			// Mauspfeil anzeigen
			Cursor = Cursors.Default;
		}


		/// <summary>
		/// Zeichent einen Lieferanten im TreeView.
		/// </summary>
		/// <param name="parent">Knotenebene in die gezeichnet werden soll</param>
		/// <param name="supplier">Lieferantendatensatz</param>
		/// <returns>Verweis auf neuen Lieferanten-Baumknoten</returns>
		private TreeNode DrawSupplierNode(TreeNodeCollection parent,DataRowView supplier)
		{
			// Baumknoteninformationen zum Lieferanten zusammenstellen
			NodeInfo info = new NodeInfo(NodeType.Supplier, supplier["SupplierID"].ToString());

			// Neuen Baumknoten erzeugen
			TreeNode node = new TreeNode(supplier["CompanyName"].ToString());

			// Zusatzinfos im Tag des Knotens ablegen
			node.Tag = info;

			// Knoten in die angegebene Ebene einfügen
			parent.Add(node);

			// Dummy einfügen
			InsertDummyNode(node);

			// Neuen Baumknoten zurückgeben
			return node;			
		}

		/// <summary>
		/// Zeichent eine Produkt im TreeView.
		/// </summary>
		/// <param name="parent">Knotenebene in die gezeichnet werden soll</param>
		/// <param name="product">Produktdatensatz</param>
		/// <returns>Verweis auf neuen Produkt-Baumknoten</returns>
		private TreeNode DrawProductNode(TreeNodeCollection parent, DataRowView product)
		{
			// Baumknoteninformationen zum Produkt zusammenstellen
			NodeInfo info = new NodeInfo(NodeType.Product, product["ProductID"].ToString());

			// Neuen Baumknoten erzeugen
			TreeNode node = new TreeNode(product["ProductName"].ToString());

			// Zusatzinfos im Tag des Knotens ablegen
			node.Tag = info;

			// Knoten in die angegebene Ebene einfügen
			parent.Add(node);

			// Dummy einfügen
			InsertDummyNode(node);

			// Neuen Baumknoten zurückgeben
			return node;
		}

		#endregion

		#region Ereignisprozeduren

		/// <summary>
		/// Wird ausgeführt, wenn auf den Knopf geklickt wird.
		/// </summary>
		/// <param name="sender">Herkunftsobjekt</param>
		/// <param name="e">Ereignisargumente</param>
		private void button1_Click(object sender, EventArgs e)
		{
			// Lieferanten zeichnen
			DrawRootNodes();
		}

		/// <summary>
		/// Wird ausgeführt, bevor ein Baumknoten aufgeklappt wird.
		/// </summary>
		/// <param name="sender">Herkunftsobjekt</param>
		/// <param name="e">Ereignisargumente</param>
		private void tree_BeforeExpand(object sender, TreeViewCancelEventArgs e)
		{
			// Ggf. Kindknoten zeichnen
			DrawChildNodes(e.Node);
		}

		#endregion
	}

	/// <summary>
	/// Aufzählung von möglichen Baumknotenarten. 
	/// </summary>
	public enum NodeType {Supplier,Product};

	/// <summary>
	/// Speichert Zusatzinformationen für Baumknoten.
	/// </summary>
	[Serializable]
	public class NodeInfo
	{
		// Datenfelder
		private NodeType _type;
		private string _key;

		/// <summary>
		/// Konstruktor, der die Angabe der Datenfelder verlagt.
		/// </summary>
		/// <param name="type">Knotentyp (Produkt/Lieferant)</param>
		/// <param name="key">Schlüsseldaten</param>
		public NodeInfo(NodeType type, string key)
		{
			// Werte übernehmen
			_type = type;
			_key = key;
		}


		/// <summary>
		/// Gibt den Knoten zurück oder legt ihn fest.
		/// </summary>
		public NodeType Type
		{
			get { return _type; }
			set { _type = value; }
		}		

		/// <summary>
		/// Gibt Schlüsseldaten zurück oder legt ihn fest.
		/// </summary>
		public string Key
		{
			get { return _key; }
			set { _key = value; }
		}				
	}
}
private Nachricht | Beiträge des Benutzers
schrotty
myCSharp.de - Member



Dabei seit:
Beiträge: 34

beantworten | zitieren | melden

Hallo rainbird,

dein Beispiel hat auch bei mir einiges erhellt. Mir ist klar, dass du in erster Linie das Zusammenspiel der GUI- und Geschäftsschicht aufzeigen wolltest. Bei der Durchsicht des Codes viel mir allerdings die Abhängigkeit der Geschäftsschicht von der Datenzugriffsschicht auf. Beide binden System.Data.SqlClient ein und setzen damit eine MS SQL-DB voraus. Würde ich z.B. irgenwann zu der Firebird DB wechseln, müssten in beiden Schichten Änderungen vorgenommen werden.

Würde man eigentlich in realen Projekten diese Abhängigkeit noch ausmerzen oder doch in Kauf nehmen? Die .Net ADO Dataprovider müsssen ja auf definierten Interfaces basieren, kann man diese Schnittstellen nicht irgendwie in der Geschäftsschicht verwenden, so das bei Wechsel der DB tatsächlich auch nur die Datenbankzugriffsschicht angepasst werden muss? Oder gibt es andere Möglichkeiten?
private Nachricht | Beiträge des Benutzers
spidermike
myCSharp.de - Member



Dabei seit:
Beiträge: 245

beantworten | zitieren | melden

mich würde mal interessieren, was da alles so an extras eingebaut wurde? ich meine zB UPDATE, INSERT und DELETE Befehle sind in dem form ja nicht enthalten, in der classe DataAccess aber angeführt ... oder check ich da was net?
private Nachricht | Beiträge des Benutzers
Rainbird
myCSharp.de - Experte

Avatar #avatar-2834.jpg


Dabei seit:
Beiträge: 3953
Herkunft: Mauer

Nur ein Beispiel

beantworten | zitieren | melden

Sowohl die Datenzugriffsschicht als auch das Formular sind nur Beispiele und haben keinen Anspruch auf vollständigkeit (Ich verwende wirklich keine schreibenden Funktionen im Formular, wollte aber zum Verständnis eine Datenbankzugriffsschicht posten, die auch Änderungen durchführen kann). Die Bindung zu System.Data.SqlClient kann man ganz einfach ausmerzen. Alle Provider (also SqlClient, OleDb etc.) implementieren nur die Schnittstellen aus System.Data (z.B. IDbParameter etc.). System.Data ist unabhängig von irgendeiner Datenbank. Wenn man in der Datenzugriffsschicht nur diese Schnittstellen für Parameter und Transaktionen veröffentlicht, braucht man System.Data.SqlClient im Client nicht mehr, sondern nur System.Data. Um wirklich beliebige Datenbank zu unterstützen, müsste man für jede Datenbank-Engine so eine Datenzugriffsschicht schreiben. Dann müsste man das Factory-pattern einsetzen, um für die angeforderte Datenbank automatisch die passende Zugriffsschicht zu verwenden.

Damit hat man aber noch nicht die unterschiedlichen SQL-Dialekte berücksichtigt. Deshalb muss man die Geschäftsschiht trotzdem umbauen. Beispiel:

SQL-SELECT in SQL Server:

SELECT TOP 10 * FROM Tabelle

SQL-SELECT in MySQL:

SELECT * FROM Table LIMIT 10

Um alles wirklich unabhängig zu halten, muss man also eine Abstraktionsschicht für die SQL-Anweisungen bauen. Im Prinzip einen SQL-interpreter, der einen festgelegten eigenen Dialekt auf den Dialekt der Zieldatenbank umsetzt. Viele fertige Persistenztools (z.B. Hibernate) haben das getan. Allerdings lassen sich viele Leistungsmerkmale von relationalen Datenbank dann gar nicht mehr nutzen. Spätestens bei Reports (z.B. Umsatzstatistiken etc.) sind solche Tools sehr hinderlich.

Datenbankunabhängige Datenzugriffsschichten sind also sehr aufwendig und damit sehr teuer. Das ist nur in sehr großen Projekten lohnenswert (Es sei denn, man ist mit dem Funktionsangebot und der Leistung von OR-Mappern zufrieden).

Es ist natürlich Ansichtssache, aber ich lege mich lieber auf eine Plattform fest und nutze diese voll aus.

@spidermike: INSERTS, UPDATES und DELETES erzeugt die Geschäftslogik über die Datenzugriffsschicht automatisch (Siehe z.B. Methode SaveProducts). Der Client nutzt die Geschäftslogik in folgender weise:
  • Gewünschte Daten als DataTable abrufen (z.B. GetProduct)
  • DataTable in einem Form bearbeiten (Neue Zeilen hinzufügen, bearbeiten, löschen)
  • DataTable mit Änderungen an die entsprechende Savexxx-Methode übergeben (Die Datenzugriffsschicht erzeugt für jede geänderte, hinzugefügte oder gelöschte Zeile automatisch einen UPDATE, INSERT oder DELETE Befehl und führt in auf der Datenbank aus)
private Nachricht | Beiträge des Benutzers
spidermike
myCSharp.de - Member



Dabei seit:
Beiträge: 245

beantworten | zitieren | melden

ales klar, aber in deiner beispiel-GUI haben diese dinge keine funktion ... hat mich nur etwas verwirrt, aber ansonsten hat es mir sehr geholfen

bin ziemlich neu in C# und habe mich gefragt, in welcher der dateien man zB den baum erzeugt? also treeview.cs[Design] ist klar, aber deine baumansicht würde man zB in treeview.cs implementieren, und steuerelemente wie den button dann in treeview.designer.cs? Ich frage nur, weil ich eine eigene komponente dafür entwickeln möchte und mir nicht ganz klar ist, was in welches der cs-files (außer dem designer) reinkommt.
private Nachricht | Beiträge des Benutzers
Rainbird
myCSharp.de - Experte

Avatar #avatar-2834.jpg


Dabei seit:
Beiträge: 3953
Herkunft: Mauer

Partielle Klassen

beantworten | zitieren | melden

Das xxx.designer.cs enthält einen Teil der Implementierung. Visual Studio 2005 macht das automatisch. Der Teil, der früher in der Region "Vom Windows-Forms-Designer erzeugter Code" stand, hat man einfach ein eine separate Datei verfrachtet. Das ist sinnvoll, da man den sperrigen Generatorcodeblock meistens nicht sehen möchte. Trotzdem fasst der Compiler alles zu einer Klasse zusammen. Damit man Klassencode auf mehrere Dateien verteilen kann, muss man der Klasse das Schlüsselwort partial voranstellen. Das geht erst ab .NET 2.0. Ich habe leider nichts älteres auf meinem Rechner. Die Aufteilung war nicht beabsichtigt. Es ist einfach Standard bei VS.NET 2005.

Wenn Du den Baum öfters baruchst, solltest Du in in einem UserControl implementieren (benutzerdefiniertes Steuerelement). Dieses UserControll kann entweder direkt in Deinem GUI-Projekt liegen, oder einem separaten Projekt (DLL).

Letzteres ist dann sinnvoll, wenn der selbe Baum auch in anderen Projekten von Dir genutzt werden soll. In 80% der Fälle macht es keinen Sinn, ein spezielles Baum-Control als wiederverwendbare, projektübergreifende Komponente zu bauen. Ich kann Dir aber auch einen Fall sagen, wo es bei mir Sinn gemacht hat.

Wir haben in der Firma einen Exchange Server. Den verwendet auch Outlook. Wenn man nun in eigenen Programmen die Ordnerstruktur des Exchange Servers darstellen will, macht man das natürlich in einem TreeView. Da der Exchange Server sämtliche E-Mails verwaltet und archiviert, wird er in allen Projekten benötigt, die mit E-Mails zu tun haben. Also habe ich eine Komponente gebaut, die diese Ordnerstruktur darstellt, eine Auswahl ermöglicht und diverse ExchangeIDs zurückliefert. Dieses Control ist in einer separaten Assembly, die in jeder beliebigen Anwendung eingebunden werden kann.
private Nachricht | Beiträge des Benutzers
spidermike
myCSharp.de - Member



Dabei seit:
Beiträge: 245

beantworten | zitieren | melden

ich habe auch eine eigene komponente (user control in eigenem projekt der solution) gemacht, einfach um eben bei änderungen zB des inhalts oder des designs einfach den datenzugriff oder auch mal die komponente (also den treeview + combobox) ändern zu können.

mir war nur nicht wirklich klar, welcher teil deines "quelltext des Formulars GUI" in welchem der files steht, da es ja ein ganzer block ist und eben nicht .designer.cs und .cs unterteilt war. also quasi wo implementiert man zB die funktion drawrootnodes oder addcomboboxitem und wo werden diese funktionen dann aufgerufen? was kommt also in .designer.cs und was in .cs? ist .designer.cs VS vorbehalten, und eigenen code wie dein drawrootnodes kommt in .cs und wird auch dort aufgerufen?
private Nachricht | Beiträge des Benutzers
Rainbird
myCSharp.de - Experte

Avatar #avatar-2834.jpg


Dabei seit:
Beiträge: 3953
Herkunft: Mauer

Designer

beantworten | zitieren | melden

Die Datei xxx.Designer.cs wird automatisch vom Visual Studio Forms Designer erstellt. Sämtliche Funktionen (DrawNode etc.) kommen NICHt in diese xxx.Designer.cs! Am besten diese xxx.Designer.cs gar nicht anfassen. Visual Studio ändert den Inhalt dieser Datei automatisch, wenn Du Controls im Forms Designer verschiebst, hinzufügst oder löschst.
private Nachricht | Beiträge des Benutzers
spidermike
myCSharp.de - Member



Dabei seit:
Beiträge: 245

beantworten | zitieren | melden

habs so gemacht ... gefällt mir mitlerweile ganz gut ...

kann man hier eigentlich schon von einer 3-tier-architecture sprechen, oder ist die trennung präsentationsschicht - businessschicht hier doch noch zu wenig?
private Nachricht | Beiträge des Benutzers
herbivore
myCSharp.de - Experte

Avatar #avatar-2627.gif


Dabei seit:
Beiträge: 52329
Herkunft: Berlin

beantworten | zitieren | melden

Hallo spidermike,

wie die 3 in 3-tier-architecture andeutet, hat man eine solche Architektur erst bei Trennung in drei Schichten. Die dritte Schicht ist hierbei meist für Datenspeicherung zuständig.

herbivore
private Nachricht | Beiträge des Benutzers
spidermike
myCSharp.de - Member



Dabei seit:
Beiträge: 245

beantworten | zitieren | melden

hi,

schon klar, aber mein sql server als data tier ist eigentlich dann meine 3. schicht oder?
private Nachricht | Beiträge des Benutzers
spidermike
myCSharp.de - Member



Dabei seit:
Beiträge: 245

beantworten | zitieren | melden

rainbird, kannst du mir noch erklären wie du das mit dem zugriff auf lieferanten und prudukten gelöst hast? was macht deine parameterliste bei getproductbysupplier genau?

thx
private Nachricht | Beiträge des Benutzers
Rainbird
myCSharp.de - Experte

Avatar #avatar-2834.jpg


Dabei seit:
Beiträge: 3953
Herkunft: Mauer

Parameter

beantworten | zitieren | melden

Man sollte es vermeiden, SQL-Statements mit Stringverkennung aufzubauen. Also NICHT so:


string sql="SELECT * FROM Address WHERE LastName='" + lastname + "'";

Das hat zwei gewaltige Nachteile:
  • Solche Lösungen sind sehr anfällig für SQL-Injection-Angiffe! Angenommen die Variable lastname wird über die Benutzereingabe einer TextBox befüllt. Wenn der Benutzer folgendes eingibt, wird z.B. die Datenbank komplett gelöscht (Angenommen die Datenbank heißt test):
    '; DROP DATABASE test; --
    Zusammengebaut sieht das ganze dann so aus:
    SELECT * FROM Address WHERE LastName=''; DROP DATABASE test; --'
    Bei Verwendung von Parametern würde nichts passieren, da der Inhalt des Parameters nur als Übergabewert und nicht als SQL-Code behandelt wird.

  • SQL Server cached Auführungspläne für SQL-Abfragen. Wenn innerhalb eines bestimmten Zeitraums die gleiche oder eine sehr ähnliche Abfrage gemacht wird, erkennt SQL Server dies und verwendet den vorhandenen Ausführungsplan. Das spart Zeit und Ressourcen auf dem Server. Wenn keine Parameter verwendet werden, wird die Abfrage sehr viel schlechter mit einer voherigen übereinstimmen.


Deshalb wenn möglich Parameter verwenden. Das Beispiel sieht mit Parameter so aus:


string sql="SELECT * FROM Address WHERE [email protected]";

Das @ signalisiert dem SQL-Parser, dass es sich um einen Paramter handelt. Dieser muss natürlich auch mit der Abfrage zusammen an der Server übergeben werden, Damit er den Wert auch in die Abfrage einsetzen und diese ausführen kann. Dazu ist die Parameterliste notwendig. Die Funktioniert eigentlich genau wie Standard-ADO.NET.

Für komplexe Abfragen und Berechnungen auf der Datenbank sollte man gespeicherte Prozeduren bzw. gespeicherte Funktionen verwenden. Die sind noch schneller als parametrisierte Abfragen, da sie sozusagen "vorkompilliert" werden.
private Nachricht | Beiträge des Benutzers
spidermike
myCSharp.de - Member



Dabei seit:
Beiträge: 245

beantworten | zitieren | melden

verstehe, danke

welche vor- bzw. nachteile glaubst du hat deine methode gegenüber zB datengebundenen mit datasets udgl.?

und noch eine wichtige frage:
in meiner DB habe ich zB eine tabelle lieferung (verschiedene dates). in der tabelle produkte ist nun bei jedem produkt (ich sollte erwähnen, es gibt jedes produkt nur EIN mal) eine dateID dabei wann es gekauft wurde, und ich möchte anstatt der produkte ein oder mehrere lieferungen (also datum) anzeigen, dass haut bei mir aber irgendwie nicht hin

also wie bekomm ich vom lieferanten als unterknoten die lieferdaten wenn ich über die produkttabelle gehe? (lieferant - produkt (enthält lieferantID u. dateID) - lieferung)
private Nachricht | Beiträge des Benutzers
spidermike
myCSharp.de - Member



Dabei seit:
Beiträge: 245

beantworten | zitieren | melden

ich höre?
private Nachricht | Beiträge des Benutzers
Rainbird
myCSharp.de - Experte

Avatar #avatar-2834.jpg


Dabei seit:
Beiträge: 3953
Herkunft: Mauer

Inner Join

beantworten | zitieren | melden

Unterknoten? Das versteh ich nicht. Ein Bestellwesen baut z.B. so auf:

Tabellen:

Lieferanten (1:n) Bestellungen (1:n) Bestellpositionen (n:1) Produkte

Um z.B. alle Lieferdaten eines bestimmten Produkts von einem bestimmten Lieferanten abzufragen, bräuchte man dann folgende SQL-Anweisung:

SELECT pos.BestellNr,pos.Lieferdatum,pos.Menge
FROM Bestellpositionen pos 
INNER JOIN Bestellungen b ON pos.BelegID=b.BelegID
INNER JOIN Produkte p ON pos.ProduktID=p.ProduktID
WHERE [email protected]
private Nachricht | Beiträge des Benutzers
spidermike
myCSharp.de - Member



Dabei seit:
Beiträge: 245

beantworten | zitieren | melden

mit unterknoten meinte ich die child-nodes

sql ist nicht so mein problem, hat wo anders gehakt aber ich habs jetz ... feinfein ...

kannst mir noch die frage bez. vor- u. nachteile deiner methode gegenüber zB datengebundenen mit datasets udgl. beantworten?

und wie kann ich die bestehenden abfragen für den treeview auch ganz einfach in einer kleinen formularkomponente einbauen? also wenn ich einen lieferanten auswähle, dass er daneben in der GUI in einem formular mit name, adresse usw. angezeigt wird. oder muss ich da wieder eigens für die textboxen was coden?

dann noch was wichtiges: ich möchte über die komponente treeview verschiedene andere komponenten anzeigen lassen, also eine art menü, bei dem bei auswahl von "lieferanten" im treeview dann rechts daneben (splitcontainer wo links treeview und rechts freiraum für komponenten ist) die adresskomponente (wie oben erwähnt) angezeigt wird, und das durch setzen von visible = true. wo und wie kann ich in der GUI abfragen was im treeview ausgewählt wurde und daraufhin visible = true oder false setzen lassen?

ich weis es sind jede menge fragen, aber dafür denk ich is das forum ja da

danke schonmal!
private Nachricht | Beiträge des Benutzers
Rainbird
myCSharp.de - Experte

Avatar #avatar-2834.jpg


Dabei seit:
Beiträge: 3953
Herkunft: Mauer

Unterschiede

beantworten | zitieren | melden

Mit datengebunden meinst Du bestimmt die Datenbindung über den Forms Designer (Tabellen vom Server Explorer mit Drag&Drop auf der Form ziehen und sowas). Das ist Müll! Vergiss es. Es geht zwar bequem und schnell, produziert aber Spaghetti-Anwendungen. Datenzugriff, Geschäftslogik und Anzeigelogik sind mit dieser Methode nicht voneinander getrennt, sondern untrennbar zusammengemixt.

Die Datenbindung kann man auch dann sehr gut nutzen, wenn man Connections, DataAdapter und Co. nicht direkt im Formular hat. Du kannst DataTables mit wenigen Zeilen Code an Steuerelemente binden.

Bau Deine Anwendung aus einzelnen unabhängigen Komponenten. Eine GUI-Komponente sollte nicht direkt auf die Datenbank zugreifen und eine Geschäftskomponente sollte nichts anzeigen (auch keine Messageboxen). Halte GUI und Geschäftslogik immer in unterschiedlichen Assemblies. Dadurch bleibt Deine Anwendung stets gut strukturiert. Je größer die Anwendung wird, desto wichtiger ist eine saubere Architektur.

Für ein einfache Lieferantenverwaltung und Bestellabwicklung würde ich z.B. folgende Komponenten bauen:

Geschäftskomponenten:

- SupplierManager - Lesen, erstellen, ändern und löschen von Lieferantenstammdaten
- OrderManager - Lesen,erstellen,ändern und löschen von Bestellungen/Lieferungen und deren Positionen
- ProductManager - Lesen,erstellen,ändern und löschen von Produkten
- ProductGroupManager - Lesen,erstellen,ändern und löschen von Produktgruppen
- StockManager - Verwalten von Lagerbeständen für die Produkte

GUI-Komponenten:

- SupplierEditor - Steuerelement zum bearbeiten eines Lieferanten
- SupplierList - Steuerelement zum auflisten und suchen von Lieferanten
- ProductEditor - Steuerelement zum bearbeiten eines Produkts
- ProductGroupEditor - Steuerelement zum bearbeiten einer Produktgruppe
- ProductCatalogTree - Steuerelement, um Produktgruppen und Produkte in einer Baumansicjt anzuzeigen (Der Produktkatalog eben)
- ProductCatalogList - Steuerelement, um Produkte aufzulisten und zu suchen
- StockList - Steuerelement zum anzeigen von Lagerbeständen
- OrderList - Steuerelement zum anzeigen und suchen von Bestellungen/Lieferungen
- OrderEditor - Steuerelement zum bearbeiten einer Bestelklung/Lieferung

Hauptprogramm:

- MainForm - Formular mit Menüleiste, Symbolleiste, Navigationstreeview und freiem Raum für MDI-Childs oder Register-childs
+ diverse Formular, die mit den einzelnen Controls (GUI-Komponenten) bestückt werden und den Workflow definieren


Damit Du verstehst, warum ich Dir vorschlage, viele einzelne UserControls zu schreiben, möchte ich ein Beispiel herausgreifen:

Die Anwendung muss über eine Listenansicht verfügen, die dem Benutzer ermöglicht , laufende oder auch abgeschlossene Bestellungen aufzulisten und nach Belegnummern, Datum etc. zu suchen. Ein Doppelklick auf einen Listeneintrag könnte z.B. das Bestllungsdetails-Formular öffnen und die angeklickte Bestellung gleich laden.
Im Lieferanten-Detailformular sollen auch die laufenden Bestellungen eines Lieferanten angezeigt werden. Auch hier soll ein Doppelklick die Bestellung zur bearbeitung öffnen.

Beide Anforderungen lassen sich ohne doppelten Programmieraufwand erledigen, da die GUI-Komponente OrderList das auflisten von Bestellungen wiederverwendbar kapselt. Eingrenzen der Liste im Fall des Lieferanten-Detailformulars könnte z.B. über eine Filter-Eigenschaft von OrderList erfolgen. OrderList führt dabei keine direkten Aktionen auf der Datenbank aus, sondern ruft dafür entsprechende Methoden der Geschäftskomponente OrderManager auf.

Was die Kommunikation zwischen verschiedenen Forms und Controls angeht, solltest Du hier im Forum nach "MVC" (Model View Controller) suchen. Dieses Entwurfmuster ist dabei sehr nützlich.

Noch was: Lieferanten wählt man nicht über ein TreeView aus. Angenommen Deine Anwendung soll von einer Handelsfirma eingesetzt werden, die 500 Lieferanten und 20.000 Produkte in 230 Produktgruppen hat. Es würde zu lage dauern, bis sich der Anwender durch den ganzen Tree gehangelt hat. Außerdem werden manche Produkte von mehreren Lieferanten zu unterschiedlichen Preisen angeboten. Da ist ein Tree zu unübersichtlich. Für eine Produktkatalogansicht (Hierarchie von Produktgruppen und Produkten) ist ein Tree ideal. Allerdings muss immer eine schnelle Suche über ein Textfeld möglich sein.

Ganz wichtig bei solchen Anwendungen: Erst die Geschäftsprozesse kennen, verstehen und dokumentieren, dann Datenbank modellieren, dann Komponenten entwerfen und zuletzt Code schreiben. Alles andere ist das Geld zum Fenster raus geworfen!
private Nachricht | Beiträge des Benutzers
spidermike
myCSharp.de - Member



Dabei seit:
Beiträge: 245

beantworten | zitieren | melden

puh danke für die sehr ausführliche auskunft rainbird.

ich werde nicht nur einige deiner tips gut gebrauchen können, sondern sehe auch das ich auf dem richtigen weg bin. hab also doch was gelernt im studium *g* ... ist beruhigend eine kleine bestätigung zu haben, ich bin in meinem praktikumssemester so gut wie ganz auf mich allein gestellt.

So ziemlich alles, was du an konzeption und vorgehensweisen vorschlägst habe ich auch so eingehalten, use-cases definiert, DB modelliert, GUI designed und nun sitze ich eben an der implementierung. auch ich habe so gut wie für alles eigene geschäfts- u. gui-komponenten gemacht, so wie du es tun würdest. das mit den lieferanten ist auf jedenfall schon im treeview mit einem suchfeld ausgestattet um diese einzugrenzen, da ich aber den treeview in den anderen modulen, wie du schon schreibst eben produktkataloge u.ä. fix im mainForm eingebaut habe, wollte ich auch hier das look-and-feel der gesamten GUI beibehalten. natürlich liegt das hauptaugenmerk beim lieferanten-modul auf der rechten seite mit den details, wofür ich eben die kommunikation zw. den komponenten brauche (wie vorher angesprochen).

ich werd mir auf jeden fall MVC noch ansehen und werd wieder posten falls es fragen gibt

danke!


Update:
ich hab jetzt mal einiges nachgelesen und verstehe das prinzip von MVC. Das würde heißen es fehtl ein controler der auf die benutzereingaben reagiert und daraufhin die datenlogik und die komponente (also ein form) aktiviert?
private Nachricht | Beiträge des Benutzers
Rainbird
myCSharp.de - Experte

Avatar #avatar-2834.jpg


Dabei seit:
Beiträge: 3953
Herkunft: Mauer

Verweise

beantworten | zitieren | melden

Der Controller fehlt nicht, aber er ist nicht mehr von so zentraler Bedeutung. Der Controller regelt den "Workflow" (Welches Form wird wann geöffnet). Er verbindet die Views mit dem Model. Irgendwie müssen die verschiedenen Views (es können ja beliebig viele sein) mit dem EINEN Model-Objekt verbunden werden (Es soll ja nur ein Model-Objekt geben anstatt dass jede View ihr eigenes erzeugt). Diese Aufgabe hat der Controller. In einer Schöpfungsgeschichte würde man schreiben:

Am Anfang war der Controller. Dieser erzeugte das Model in seiner ganzen Pracht und sorgte dafür, dass es lebte bis die Lebenszeit des Applikationsprozesses abgelaufen war. Als der Controller das Model erzeugt hatte, sah er dass es gut war. Er freute sich und erzeugte viele bute Views, die alle die heilige Schnittstelle IView implementierten. Einer jeden View zeigte er das Model und sparch: "Dies ist dein Model! Alle Daten sollst Du nur von ihm abfragen. All deine Wünsche richte an dein Model. Du sollst kein anderes Model neben ihm haben. Höre auf die Events, die dein Model feuert und handle danach. Feuere auch du Events, damit ich dein Controller deine Brüder und Schwestern erwecken und schlafen legen kann, wenn es Zeit dafür ist.

;-)

Die Schöpfungsgeschiche im Paralleluniversum der Web-Anwendung würde etwas anders aussehen, da dort die Views HTML + Javascript sind und nicht direkt mit dem Model auf dem Webserver sprechen können. Sie wenden sich mit ihren Postbacks an den Controller und dieser spricht mit dem Model. Der Controller wäre dabei die aspx/ascx/asmx Datei.

Dieser feine Unterschied wird auch als passives und aktives MVC-Modell bezeichnet.

Vielleicht hilft Dir auch das folgende Diagramm:

Trennung von Awendungslogik und Optik
private Nachricht | Beiträge des Benutzers
spidermike
myCSharp.de - Member



Dabei seit:
Beiträge: 245

beantworten | zitieren | melden

ok also wenn wir vom eigentlichen anfangsbeispiel, dem treeview, ausgehen, ist ein controler nicht von bedeutung, aber wenn wir weitergehn in der (immerhin schon 3-seiten langen) geschichte bis hin zu meiner GUI, dann wäre ein controler hier sehr nützlich.

hab mir den user interface process application block angesehen, aber ordentlich erklärt ist da auch nichts meiner meinung nach.
ich werd nicht unbeding schlauer aus den ganzen infos auf msdn. irgendwie weis ich jetzt nicht wirklich was ich wie an meiner bestehenden applikation ändern soll.
private Nachricht | Beiträge des Benutzers
Rainbird
myCSharp.de - Experte

Avatar #avatar-2834.jpg


Dabei seit:
Beiträge: 3953
Herkunft: Mauer

Selber bauen

beantworten | zitieren | melden

Implementier das MVC-Pattern selbst und verwende nicht den Application Block! Nicht dass der nicht gut wäre. Man sollte immer Herr seines Codes sein und wissen, warum was wie implementiert ist.

Wenn Du MVC einmal vollständig implementierts, verstehst Du das Konzept auch.
private Nachricht | Beiträge des Benutzers
herbivore
myCSharp.de - Experte

Avatar #avatar-2627.gif


Dabei seit:
Beiträge: 52329
Herkunft: Berlin

beantworten | zitieren | melden

Hallo spidermike,

vielleicht hilft dir Trennung von Awendungslogik und Optik

herbivore
private Nachricht | Beiträge des Benutzers
spidermike
myCSharp.de - Member



Dabei seit:
Beiträge: 245

beantworten | zitieren | melden

danke für den link, es gibt wirklich jede menge im net zu dem thema ... puh, da werd ich mich wohl reinhängen müssen, und fang gleich mal mit dem thread an

update:
so ich hab jetzt mal ne frage zu MVC. wenn man das ausgansbeispiel von dir nimmt rainbird, dann wäre
- ein Model zB DisplayFacade, da diese klasse auf die Geschäftslogik GetAllSuppliers zugreift, und GetSortedSupplierList an eine View zurückgibt
- die View wäre deine GUI
- und als Controler würde noch eine eigene klasse fehlen, die deinen teil der "#region Baumansicht" übernimmt

lieg ich da nun richtig? wie gesagt, ich hab ja schon eine applikation und überlege, wie ich diese nun als MVC anpassen kann um ev. später mal ein webinterface anzubinden, dazu wäre ja passives MVC geeignet.
private Nachricht | Beiträge des Benutzers