Laden...

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

Erstellt von matze72 vor 18 Jahren Letzter Beitrag vor 14 Jahren 72.714 Views
M
matze72 Themenstarter:in
3 Beiträge seit 2005
vor 18 Jahren
Tree(Node) Collection als Zwischenschicht bauen [und grundsätzliches zur 3-Schichten-Architektur]

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).

49.485 Beiträge seit 2005
vor 18 Jahren

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

M
matze72 Themenstarter:in
3 Beiträge seit 2005
vor 18 Jahren

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

49.485 Beiträge seit 2005
vor 18 Jahren

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

3.728 Beiträge seit 2005
vor 18 Jahren
Schlüssel

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.

M
matze72 Themenstarter:in
3 Beiträge seit 2005
vor 18 Jahren

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

3.728 Beiträge seit 2005
vor 18 Jahren
Beispielcode

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 SupplierID=@supplierID", 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 SupplierID=@supplierID", 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; }
		}				
	}
}

S
34 Beiträge seit 2005
vor 18 Jahren

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?

S
243 Beiträge seit 2005
vor 18 Jahren

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?

3.728 Beiträge seit 2005
vor 18 Jahren
Nur ein Beispiel

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)

S
243 Beiträge seit 2005
vor 18 Jahren

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.

3.728 Beiträge seit 2005
vor 18 Jahren
Partielle Klassen

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.

S
243 Beiträge seit 2005
vor 18 Jahren

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?

3.728 Beiträge seit 2005
vor 18 Jahren
Designer

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.

S
243 Beiträge seit 2005
vor 18 Jahren

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?

49.485 Beiträge seit 2005
vor 18 Jahren

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

S
243 Beiträge seit 2005
vor 18 Jahren

hi,

schon klar, aber mein sql server als data tier ist eigentlich dann meine 3. schicht oder?

S
243 Beiträge seit 2005
vor 18 Jahren

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

3.728 Beiträge seit 2005
vor 18 Jahren
Parameter

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 LastName=@lastName";

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.

S
243 Beiträge seit 2005
vor 18 Jahren

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)

S
243 Beiträge seit 2005
vor 18 Jahren

ich höre? 😉

3.728 Beiträge seit 2005
vor 18 Jahren
Inner Join

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 p.ProduktID=@ProduktID

S
243 Beiträge seit 2005
vor 18 Jahren

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!

3.728 Beiträge seit 2005
vor 18 Jahren
Unterschiede

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!

S
243 Beiträge seit 2005
vor 18 Jahren

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?

3.728 Beiträge seit 2005
vor 18 Jahren
Verweise

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

S
243 Beiträge seit 2005
vor 18 Jahren

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.

3.728 Beiträge seit 2005
vor 18 Jahren
Selber bauen

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.

49.485 Beiträge seit 2005
vor 18 Jahren

Hallo spidermike,

vielleicht hilft dir Trennung von Awendungslogik und Optik

herbivore

S
243 Beiträge seit 2005
vor 18 Jahren

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.

3.728 Beiträge seit 2005
vor 18 Jahren
Nein

Nachtrag: ich gehe bei diesem Modell davon aus, dass eine mehrschichtige verteilte Anwendung entworfen werden soll, deren Kern-Geschäftslogik in Diensten/Komponenten gekapselt ist und ggf. über entfernte Aufrufe konsumiert werden kann/soll.

Nein! Model, View und Controller gehören alle drei zur GUI. Das Model implementiert NICHT die Geschäftsregeln (Wenn man triviale Dinge nicht als Geschäftsregeln betrachtet), sondern verwaltet nur den temporären Clientstatus (also die Daten die der Benutzer der GUI momentan bearbeitet). Das Model wird über eine Fassade an die benötigten Geschäftskomponenten angebunden (Das Model greift also nur über die Fassade zu). Die Fassade stellt dem Model die Funktionen der Geschäftslogik zur Verfügung, die für diesen Anwendungsfall (use case) benötigt werden. Fassaden erleichtern den GUI-Programmierern im Team die Arbeit und sorgen für weniger Beziehungen zwischen den Komponenten.

Wenn die Geschäftslogik nicht sehr komplex ist, kann man Fassaden auch einfach weglassen (dann redet das Model direkt mit den Geschäftskomponeten).

Das Model ist statuswahrend und liegt immer auf der Maschine, die die GUI bereitstellt. Bei einer Web-Anwendung ist das der Webserver und bei einer Forms-Anwendung der Client-PC. Geschäftskomponenten sind statuslos (d.h. sie speichern keine Daten über mehrere Funktionsaufrufe hinweg).

Die Geschäftskomponenten liegen auf einem Applikationsserver (z.B. Enterprise Services oder ein .NET Remoting Host oder SOAP-Webservices auf einem IIS). Nur die Geschäftskomponenten sprechen mit dem Datenbank-Server. Die GUI spricht mit den Geschäftskomponenten (wahlweise direkt oder über Anwendungsfall-Fassaden).

Meistens wird für jeden Anwendungsfall ein separates Model benötigt. Alle Views die zu diesem Anwendungsfall gehören verwenden das selbe Model. Angenommen Du schreibt eine Lagerbestandsverwaltung. Das Model hält ein DataSet welches die Datesätze der Bestandstabelle des geladenen Artikels enthält. Das UserControl für die Bestandsauskunft in den einzelnen Lagern ist automatisch mit dem Dialog für Bestandskorrekturen synchron. Die Bestandsauskunft aktualisiert sich sogar automatisch, sobald man im Dialog eine Bestandskorrektur vornimmt (Weil sie vom Model über Ereignisse über die Änderungen an den Daten informiert wird). Der GUI-Entwickler kann sich wirklich auf das entwickeln guter Oberflächenelement konzentrieren. Das ist MVC.

S
243 Beiträge seit 2005
vor 18 Jahren

ok also dann wäre einfach sozusagen die GUI in deinem beispiel folgendermaßen zu gestalten:

  • Model wäre also eine klasse, die mit der klasse DisplayFacade kommuniziert und für die beschaffung der daten zuständig ist, sobald sie einen "auftrag" vom controler bekommt
  • View wäre dann die GUI also das Form selbst, das events wirft welche vom controler bearbeitet werden
  • Controler wäre die klasse die die Baumansicht erstellt, wenn dies die gui so will, und dazu vom model die datenbeschaffung anfordert
3.728 Beiträge seit 2005
vor 18 Jahren
Controller

Genau, bis auf den Controller. Die Baumansicht ist reine Anzeige und wird deshalb im entsprechenden View (Ich würde ein UserControl schreiben und es auf einer zweiten View, nämlich dem Form plazieren) implementiert.

Der Controller ist das Fugenkit, der die Instanz des Models erzeugt und die Views erzeugt. Er weist jeder View nach dem erzeugen das Model zu (Über die Model-Eigenschaft der IView-Schnittstelle). Jede View implementiert diese Eigenschaft. Beim zuweisen (set) kann die View auch gleich die jenigen Ereignisse des Models abonnieren, die für sie von Interesse sind (Nicht jede View reagiert auf alle Model-Ereignisse).

Auch der Controller kann Ereignisse des Models abonnieren. Man kann über den Controller den Workflow kapseln (z.B. öffnen von Dialogen etc.). Auf diese Art und weise kann man den Workflow des Anwendungsfalls anpassen, ohne alle Views anfassen zu müssen.

S
243 Beiträge seit 2005
vor 18 Jahren

ok langsam dämmerts immer mehr ...

ich habe verschiedene usercontrols die ich in einer mainform einbaue.

gibts wo eine beispielapplikation die ich mir ansehen könnte, um das zusammenspiel von model-view-controler veranschaulichen zu können?

3.728 Beiträge seit 2005
vor 18 Jahren
Beispiele

Visual Studio.NET zum Beispiel. Das View "Eigenschaften" zeigt immer die Eigenschaften des ausgewählten Objekts an. Die Dynamische Hilfe zeigt immer Hilfe zu dem Element unter dem Cursor an. Wie können diese Views immer alles genau wissen? Bestimmt weil sie ein gemeinsames Model haben. Deshalb lassen sich auch ganz einfach PlugIns entwickeln.

Wenn sich Zeit habe, schreibe ich mal eine kleine Beispielapplikation.

S
243 Beiträge seit 2005
vor 18 Jahren

das wäre kuhl, danke ... am besten gleich anhand deines beispiels hier zu beginn =)

irgendwo hab ich gelesen, MVC wäre auch eine architektur ... meine applikation ist unterteilt in forms (also view), geschäftslogik und datenschicht (ms sql db).
dann wird auch MVC pattern im zusammenhang mit model als business logic und view als presentation logic genannt, irgendwie schon verwirrend.

3.728 Beiträge seit 2005
vor 18 Jahren
Ursprung

Bei einer monolithischen 2 schichtigen Anwendung ist das auch völlig in Ordnung. Das MVC-Pattern stammt aus den 80er Jahren, nämlich von der Sprache Smalltalk-80. Damals waren verteilte Anwendungen in der heute üblichen Form nicht verbreitet. Geschäftslogik und GUI waren nicht in Komponenten aufgeteilt, die verschiedenen Prozessen oder sogar auf verschiedenen Maschinen laufen mussen. Die Geschwindigkeit der damaligen Rechner und der Netzwerke waren auch entsprechend langsam.

Bei Anwendungen die keine Datenbank und zentrale Geschäftsregeln benötigen (z.B. Textverarbeitung) kann diese Aussage (Model = Geschäftslogik) auch heute noch zutreffend sein.

Außerdem sollte man Pattern eher als Richtlinie bzw. allgemeine Konzepte verstehen. Meine Implementierung ist nur eine Möglichkeit. Bis jetzt ist mir nichts besseres unter die Augen gekommen. Wenn man im Web nach MVC sucht, findet man meistens nur allgemeines Blah.

S
243 Beiträge seit 2005
vor 18 Jahren

Original von Rainbird
Meine Implementierung ist nur eine Möglichkeit. Bis jetzt ist mir nichts besseres unter die Augen gekommen.

welche implementierung? das beispiel hier im thread?

und in der grafik dieses beispiels auf
http://www.c-sharpcorner.com/Code/2003/Feb/MVCDesign.asp
wird eine applikation dargestellt, die auch alle komponenten enthält die ich in meinem programm habe ... und sorry, aber genau das ist das verwirrende, hier ist das model wieder business- und data-tier ... irgendwie steh ich auf der leitung glaub ich

3.728 Beiträge seit 2005
vor 18 Jahren
Statusbehaftete Geschäftskomponenten

Das Beispiel von c-sharpcorner ist mit Fokus auf Webanwendungen ausgelegt. Für Forms-Anwendungen stimmt es nicht, da der Benutzer nicht über den Controller Befehle eingibt, sondern über die Views (z.B. Eingabe in Textbox, klicken auf einen Button etc.).

.NET and MVC:

Lets try to find out how closely ASP.NET allows architecting application to meet MVC concept.
Model: Dataset/Business Entities, DataAccess components
View: .ASPX pages

Controller: Code-behind .vb/.cs files

Typed dataset is one of the greatest features in .NET, which holds the application data in memory and represents the application business entity model, which bridges the UI components with Service Interface and Data Access layer.

So, service interface and Data Access components populate these typed Datasets. Now, these filled datasets can be bound to any view in UI layer.

.ASPX and ASCX pages server as view and mean to interface with application, while the code behind classes for these files server as the controller functions.

ASP.NET doesnt have central controller function, instead the code-behind file can directly make request to the Model and can update the dataset. But lot of controller function can be implemented in code behind class events in .aspx or .ascx pages, like Page_Onload(), OnItem_Created() etcevents.

.NET Framework has all Object-oriented features like code reuse, encapsulation etcSo, proper design of your model can make your user interface and view completely separated from model which can server multiple views.

Das deckt sich doch fast genau mit meinem Diagramm unter Trennung von Awendungslogik und Optik
(Man beachte die Seite des Diagramms für Web-Anwendungen)

Ich betrachte das Model als den Teil der Geschäftslogik, der den Clientstatus verwaltet (das entspricht dem Baustein "business entity" im Diagramm von c-harpcorner).

S
243 Beiträge seit 2005
vor 18 Jahren

alles klar, dann werd ich dich mal nicht länger nerven 😉

falls du noch zeit hast ein kleines beispiel zu posten, oder am besten einfach anhand unseres beispiels hier mit den lieferanten usw. erklären würdest, welches file bei dir (also dienem eingangsbeispiel) als Model, View, Controler etc agiert und was es macht (muss aber nicht gecoded sein) dann bin ich schon zufrieden =)

danke und allen einen schönen wochenbeginn!

S
243 Beiträge seit 2005
vor 18 Jahren

hallo nochmals,

falls jemand den thread verfolgt hat und mir meinen letzten post-wunsch noch erfüllen kann, wär mir sehr geholfen, ich bräuchte dringend den letzten kick in die richtige richtung um weitermachen zu können

thx!
Mike

49.485 Beiträge seit 2005
vor 18 Jahren

Hallo spidermike,

Rainbird hat doch schon ein komplettes Beispiel gepostet. Was feht dir denn da noch?

herbivore

S
243 Beiträge seit 2005
vor 18 Jahren

hey!

hat er schon? meinst das eingangsbeispiel? dachte das wäre eben kein MVC beispiel? 8o

3.728 Beiträge seit 2005
vor 18 Jahren
beispiel

Ich habe schon angefangen eine komplette kleine Beispielanwendung mit Access-Db, eigenen Geschäftskomponenten (Zurgiff über .NET Remoting) und einer MVC-GUI zu schreiben. Ist ca. 50% fertig. Ich poste die Solution, wenn alles steht.

Bitte noch etwas Geduld!

3.728 Beiträge seit 2005
vor 18 Jahren
MVC Beispielsolution ist fertig!

Die versprochene MVC Beispielanwendung ist fertig!

3.728 Beiträge seit 2005
vor 18 Jahren
Screenshot

So sollte es aussehen:

S
243 Beiträge seit 2005
vor 18 Jahren

ich kann nur sagen vielen dank für deine mühen und die tolle hilfe rainbird ... werds mir gleich anschaun!

S
243 Beiträge seit 2005
vor 18 Jahren

puh ... gar nicht so leicht hier durchzublicken, zumindest für mich net 😉

ich versuch gerade, meni bisheriges werk zu vergleichen und zu schaun, wo ich was zu ändern hätte, damit aus der GUI daraus ein MVC pattern wird ... hab da manchmal das gefühl, nochmals von vorne anzufangen ginge schneller. hmpf

49.485 Beiträge seit 2005
vor 18 Jahren

Hallo spidermike,

wenn du ein Programm geschrieben hast, in dem Oberfläche und Modell vermischt sind und das du auf MVC umstellen willst, dann ist von vorne anfangen schneller.

herbivore

3.728 Beiträge seit 2005
vor 18 Jahren
Komplexität

Ich musste das Beispiel etwas komplexer machen. Nur in einer Anwendung mit mehreren Views, die auf den selben Datenbestand zugreifen, wird der Vorteil von MVC überhaupt deutlich. Die über .NET Remoting abgetrennten Geschäftskomponenten mussten auch sein. Damit wollte ich aufzeigen, dass Geschäftsregeln bzw. Datenzugriff und Persistenz nicht ins Model gehören.

Was genau hast Du an meinem Beispiel nicht verstanden?