Hallo,
in meiner Datenbank stehen viele Kategorien und Unterkategorien (Elemente werden gespeichert mit "Titel", "ID" und "ParentID"), die ich in einer Treeview darstellen will.
Die Unterkategorien können variabel von 1 bis 10 Ebenen tief sein. Da ich also in den Unterkategorien immer unterschiedlich viele Nodes und so wieder Parents habe, habe ich mir gedacht, die Treeview rekursiv zu befüllen. Ich habe dazu aber keine guten Beispiele gefunden.
Bin ich auf den richtigen Weg? Habt ihr ein paar Beispiele / Anregungen für mich?
danke flo
Jap, du kannste den TreeView sehr einfach rekursiv befüllen.
Die Frage die du dir stellen muss, ist nur, ob du bei jeder Rekursion wieder eine Datenbankabfrage starten willst.
Wenn du mit WPF arbeites, weißt du deinem Treeview ein HieraricatDataTemplate zu, indem du wiederrum über die Property ItemsSource bestimmst, wo dieser jeweils seine SubElemente herbeziehen soll.
Auf C# - Seite sieht es dann so aus, dass jedes Object ein Collectin-Property hat, welches z.B. Children heißt.
Dieses Collectin-Property steuert über die get-Methode, was geschen soll, wenn die Sub-Elemente angefragt werden.
Hier kannst du beliebigen Code reinschreiben (z.B. auch eine Datenbankabfrage, welche alle Elemente abfragt, welche als ParentId die Id des aktuellen Elementes haben).
hallo,
das heißt, wenn einer auf den parent node klickt, mache ich ein event, dass alle childrens aus der datenbank holt und ausließt? wie kann ich dann die childrens genau diesem node zuweißen? bzw. wenn ein parent auch children hat, ist in der treeview ein pfeil vor dem text, um anzuzeigen, dass es eben ein parent ist. wenn ich aber keine childs beim ersten mal zuweise, dann ist der pfeil nicht da und keiner weiß, dass es noch eine ebene hinunter geht...
lg flo
Hallo flflflf,
am einfachsten ist wenn du einfach eine ObservableCollection<myTreeViewItem> machst und diese dann als ItemSource verwendest.
Die Klasse MyTreeViewItem würde ungefähr so aussehen
public class MyTreeViewItem : INotifyPopertyChanged
{
public ObservableCollection<MyTreeViewItem> Children{get;set;}
public string Name {get;set;}
//Noch weitere Properties
}
Somit hast du eine schöne Struktur.
Natürlich musst du dann noch das hierachichalDataTemplate definieren
Gruss
Michael
Also mit Events machst du gar nix.
Das Childrens-Property steuert dir quasi durch seinen Getter, wann es die Elemente brauch.
Wpf ist dabei so inteligen und fordert nur dann das Childrens-Property dazu auf, wenn die Elemente gebraucht werden, sprich spätestens dann, wenn die GUI eine Ebene drüber wissen muss, ob sie einen Pfeil (SubElements vorhanden) zeichnen muss oder nicht.
Hier ein verwendbarer Quellcode, der dir sicherlich besser helfen wird als meine Text 😃
Du siehst über die Debug-Ausgabe, wann Childrens im Getter angefordert werden:
GUI
<Window x:Class="WpfApplication1.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:toolkit="http://schemas.microsoft.com/wpf/2008/toolkit"
Title="Window1" xmlns:sd="http://schemas.divelements.co.uk/wpf/sanddock">
<Window.Resources>
<HierarchicalDataTemplate x:Key="DT" ItemsSource="{Binding Childrens}">
<TextBox Text="{Binding Text}" />
</HierarchicalDataTemplate>
</Window.Resources>
<TreeView ItemTemplate="{StaticResource DT}" ItemsSource="{Binding Item.Childrens}">
</TreeView>
</Window>
C#
namespace WpfApplication1
{
/// <summary>
/// Interaktionslogik für Window1.xaml
/// </summary>
public partial class Window1 : Window
{
public ItemVM Item { get; set; }
public Window1()
{
Item = new ItemVM("root");
this.DataContext = this;
}
}
public class ItemVM
{
public string Text { get; set; }
public ObservableCollection<ItemVM> Childrens
{
get
{
ObservableCollection<ItemVM> returnment = new ObservableCollection<ItemVM>();
for(int i=0; i<10; i++) returnment.Add(new ItemVM("Item Nr. " + i));
System.Diagnostics.Debug.WriteLine("Getting Subitems");
return returnment;
}
}
public ItemVM(string text)
{
this.Text = text;
}
}
}
Hallo flflfl,
die Richtung von michlG wäre schon der Richtige Weg
und auch wenn der Weg von meisteralex noch nicht ganz richtig ist, bringt Dich auch dieser Beitrag deinem Ziel riesen Schritt näher
Herzliche Grüße
BlackCoin
Ich bitte um Erklärung, was du unter "nicht ganz richtig" verstehst g
Also die Datenbankabfrage hab ich natürlich weggelassen.
Halllo meisteralex,
probiere es auch und du wirst es sehen
Herzliche Grüße
BlackCoin
Ja bei mir klappt alles einwandfrei, auch wenn ich mich bis ebenentiefe 20 durchklicke.
Wo sind denn deine Bedenken ?
sorry, ich meinte die erste ebene (root)
das kommt davon wenn ich mich ablenken lasse, die tiefsten ebene sollten bis zu einer bestimmten Tiefe ohne Probleme Funktionieren. (bis auf das nach einer anzahl von 255 Items ende ist aber dafür kannst du nichts)
Herzliche Grüße
BlackCoin
Also die erste Ebene klappt bei mir auch einwandfrei.
Nunja villeicht hätte man es eleganter lösen können und statt eines ItemVM Objectes eine Collection von ItemVM Objecten anlegen können und die ItemsSource des TreeViews direkt an die Collection binden können.
Willst du darauf hinaus? Bzw. sag doch einfach was du bemägeln willst, ich will schließlich auch noch was lernen.
genau das ist es du benutzt an der Untersten Stelle [Root] die klasse nur als Speicher für die Items und die Root ebene Verschwindet dadurch.
Dies ist aber eher nur ein kleinere Unschönheit, die man aufgrund dessen das es als Beispiel gedacht war auch so stehen lassen kann.
ich würde es zwar ein kleines wenig anders machen aber das ist ja Geschmackssache
<HierarchicalDataTemplate DataType="{x:Type local:ItemVM}" ItemsSource="{Binding Childrens}">
<TextBox Text="{Binding Text}" />
</HierarchicalDataTemplate>
Herzliche Grüße
BlackCoin
Jap, du kannste den TreeView sehr einfach rekursiv befüllen.
Die Frage die du dir stellen muss, ist nur, ob du bei jeder Rekursion wieder eine Datenbankabfrage starten willst.
Wenn du mit WPF arbeites, weißt du deinem Treeview ein HieraricatDataTemplate zu, indem du wiederrum über die Property ItemsSource bestimmst, wo dieser jeweils seine SubElemente herbeziehen soll.
Auf C# - Seite sieht es dann so aus, dass jedes Object ein Collectin-Property hat, welches z.B. Children heißt.
Dieses Collectin-Property steuert über die get-Methode, was geschen soll, wenn die Sub-Elemente angefragt werden.
Hier kannst du beliebigen Code reinschreiben (z.B. auch eine Datenbankabfrage, welche alle Elemente abfragt, welche als ParentId die Id des aktuellen Elementes haben).
OT: ich bin zwar kein Mod, aber bitte achte trotzdem mal darauf, dass du zumindest die Schlüsselbegriffe (Collection, HierarchicalDataTemplate) korrekt schreibst! Das vereinfacht die Suche und führt sonst zu Verwirrung.
hallo,
habe gerade das beispiel von meisteralex ausprobiert und bin erstmal beeindruckt 😃
meine frage ist nur noch: wie bekomme ich die daten aus der db (id, parentid, titel) in die ObservableCollection?
danke schon mal für eure super hilfe!
flo
p.s. bitte entschuldigt, aber es ist mein erstes wpf-projekt!
Wo genau ist dein Knapppunkt ?
Also als erstes solltest du über eine Methode deiner Wahl eine Datenbankabfrage machen.
Dann verpackst du die Daten, welche du aus der Datenbank bekommen hasta in Objekte von Typ aus denen die ObserverableCollection besteht (in diesem Fall heißt der Datentyp bei mir ItemVM).
Gruß MA
hallo,
also wenn ich alles richtig verstehe muss ich das so machen:
ich such mir alle parents
passt das?
danke für deine hilfe!
flo
Ja g, dass ist so ungefähr die Definition von Rekursion.
Also ein Objekt der Klasse ItemVM weiß eigentlich immer nur, welche Kinder es hat.
Ganz oben steht sozusagen der root node, welche ich in meinen Datenstrukturen immer dadurch definiere, dass die ParentID des Root-Nodes -1 oder 0 ist.
Diesen Root-Node nimmst du nun und verpackst ihn in ein Objekt der Klasse ItemVM. Hierbei bildest du natürlich alle wichtigen Attribute aus der Datenbank in Attribute der klasse ab.
Der Rest erfolgt durch rekursion undzwar wie folgt:
Sobald von einem Item die Childrens angefordert werden, (der getter der Childrens-Property wird angefordert) stellt das Item wieder eine Datenbankverbindung her um alle Elemente zu suchen, welche gerade dieses Item als Vater haben). Diese angeforderten Elemente werden nun von dem Objekt der Klasse ItemVM wiederrum in Objekte vom Typ ItemVM verpackt und in die Childrens-Collection geschubst.
Das wars, alles andere läuft automatisch.
Wie schon angedeutet, ist diese vorgehensweise nicht immer die optimale Lösung, da für jeden Rutsch von Unterlementen ein neuer Datenbankquery ausgeführt werden muss.
Wenn du keine enge Synchronität zur Datenbank-Basis brauchst kannst du auch so vorgehen, dass du vorab alle Elemente der planen Datenbanktabelle in eine Collection läds, welche außen vor stehst und bei der Betankung deiner Children-Propertys nicht direkt auf die Elemente der DB, sondern auf diese Collection abfragst (z.B. mit linq oder altmodisch itterativ).
Aber dazu weiß ich zu wenig von deiner App um dir da Rat zu geben.
Gruß MA
yeah 🙂 ich habs geschafft.
eine frage hab ich noch:
Ich hab das Label so an die TreeView gebunden:
<Label Content="{Binding sTitle}" Tag="{Binding iID}" />
wie kann ich nun den Tag vom Label beim Auswählen des Childs auslesen?
danke flo
Warum willst du das tun ?
Wozu benutzt du die Property Tag überhaupt, wusste bis jetzt noch nichtmal das es die gibt g
Nein jetzt mal im Ernst, wozu brauchst du die Information, die du da an das Property Tag gebunden hast ?
hi,
naja, einmal der Content des Labels, damit was gescheites angezeigt wird und einmal wird die Id im Tag gespeichert, damit ich den richtigen Datensatz in der Datenbank wieder finde, wenn wer auf das Label klickt...
oder wie macht man das "normal"? wie gesagt, ist meiner erste wpf anwendung...
lg flo
Also Identifier in der GUI irgendwie zu speichern und damit Rückschlüsse auf das Element zu ziehen ist sehr sehr unsaubere programmierung.
Das mit dem Labelbinding, wo du die Property an die Content-Eigenschaft bindest ist vollkommen ok und der richtige Weg - soweit so gut.
Wenn du nun mit den Objekten in der Listview arbeites, dann arbeitest du ja mit den Objekten, welche Datensätze aus deiner Datenbank repräsentieren. Du hast so zu sagen deine Datenbankeinträge auf Objekte in einem Objektorientierten Kontext gemappt.
Ab diesem Zeitpunkt solltest du allerdings auch objektorientiert und nicht mehr relational denken, daher brauchst du die Id höchstens erst wieder, wenn du das Objekt zurück in die Datenbank speichern (oder löschen) willst.
Um dir jetzt genauer zu erklären, wie du vorgehen muss, wenn "jemand auf das label" klickt, müsste ich wissen was du vorhast. Generell feuert das Listview ja ein Event, wenn jemand ein Item (in deinme Fall templetisiert durch ein Label) klickt. Du könntest z.B. auf dieses Event, welches das ListView feuert, reagieren, nachsehen, welches Item aktuell selektiert ist und dann mit diesem Objekt weiterarbeiten, um es z.B. in einem DetailView anzuzeigen.
Hierfür bietet dir WPF jedoch auch wesentlich elegantere Methoden.
Kann es sein, dass du aus der prozeduralorientierten Skriptecke kommst ? Ich hab früher selbst viele PHP-Skripte nach der Vorgehensweise geschreiben, wie du sie geschildert hast und in diesem Kontext ist das gar nicht mal so verkehrt.
Hi,
erwischt, ich komme aus dem php-bereich 😃
also, das ganze gehört zu ein paar tests für eine backend shopverwaltung. man erstellt den artikel und wählt aus der treeview dann die passende kategorie aus. die kategorie wird dann beim artikel in der datenbank gespeichert (deswegen brauch ich die id wieder).
lg flo
Da musst du jetzt Objektorientiert denken.
Der Artikel wird ja auch als Objekt einer Klasse erstellt. Dieses Objekt ist dann über eine Referenz mit einem KategorieObject (des Typs ItemVM, wenn wir beim Beispiel bleiben wollen) assoziert und stellt damit die Verbindung von ArtikelObjekt zu KategorieObjekt her.
Erst beim Zurückspeichern wird wieder von der Objektorientierten Denkweise abgelassen. Hierzu geht man am einfachsten so vor, dass dein Artikel-Objekt eine SaveToDatabase() - Methode hat, welche sich über die Referenz auf das KategorieObject beim Speichern in die Datenbank erst wieder die entsprechenden KategorieID holt.
Wenn du eine sehr prozedurale, relationale Denkweise gewöhnt bist, wird das sicherlich nicht so ganz einfach da umzudenken, in einer Sprache wie C# wirst du jedoch nicht drumrum kommen, dir dieses Denken aneignen zu müssen.
Vor allem musst du , wenn du aus der PHP-Webentwicklung kommst von dem Gedanken ablassen, dass ein Programm eine statischen Zustand hat, wie ein Webseiten-View, sondern eher dazu übergehen ein Programm als ein stetig fließendes Konstrukt zu sehen.
aber spätestens beim SaveToDatabase() brauche ich wieder die ID. Wie greife ich denn dann darauf zu?
lg flo
die speicherst du dir am anfang in ein private Attribut deines Objektes weg. Beim speichern des Artikel-Objektes wird dann über die Referenz auf das Kategorie-Objekt die KategorieId aus dem KategorieObject ausgelesen
die speicherst du dir am anfang in ein private Attribut deines Objektes weg ...also zubeispiel in label.tag?
Beim speichern des Artikel-Objektes wird dann über die Referenz auf das Kategorie-Objekt die KategorieId aus dem KategorieObject ausgelesen
...kannst du mir dazu noch ein kleines beispiel geben?
danke, du hast mir bisher sehr viel geholfen.
lg flo
Nein nicht im Label. Dein Label ist nur eine Möglichkeit zur Abbildung eines Attributes eines Objektes.
Sozusagen Mittel zum Zweck um (einen Teil von) ItemVM zu visualisieren.
Beispiel:
Nehemen wir an, du hast eine Klasse Kategorie
public class Kategorie
{
private int databaseId;
List<Kategorie> Children;
public int DatabaseId { get { return databaseId; } }
...
...
...
}
und eine Klasse Artikel, welche die Referenz zu einem Kategorie-Objekt beinhaltet, zu dessen Kategorie es gehört:
public class Artikel
{
private Kategorie isInKategorie;
private string artikelDescription;
private string artikelFullDescription;
public SaveToDatabase()
{
string updateQuery = "Update Artikel Set kategorieid = '" + this.isInKategorie.DatabaseId + "' ....... ";
....
}
....
}
Die Methode SaveToDatabase zeigt dir , wie du an die ID des referenzierten Artikel-Objektes gelangst.
Dies ist aber nur der absolut einfachste weg um eine persistente Datenspeicherung zu realisieren.
Sobald dein Projekt umfangreicher wird, oder Ansprüche an Sicherheit etc. stellt, solltest du dir andere Pattern ansehen.
ok, also ich brauch aber doch noch in public class Kategorie ein set, oder wie soll ich die datenbank id sonst setzen?
und die klasse (kategorie) ruf ich dann im public ItemVM(string title) auf, oder?
die Datenbank-ID kannst du über verschiedene Wege setzten. Meist macht man es Aber wohl im Konstruktor.
und die klasse (kategorie) ruf ich dann im public ItemVM(string title) auf, oder?
Diese Aussage verstehe ich nicht.
Aber solltest du dir nicht erstmal ein Buch holen und dir die Grundlagen der objektorientierten Programmierung aneignen, bevor du ein Projekt oder ähnliches startest ?
hallo,
zuerst mal: sorry für die verspätete antwort - ich war im urlaub
ich habe mir schon ein buch zugelegt, aber ich bräuchte noch ein beispiel zum zugriff auf die treeview.
danke, flo
Am besten machst du nen neuen Thread auf oder sprichst mich im ICQ an, sonst wird das hier zu unübersichtlich.
Gruß
MA
Ist nicht mehr nötig - ich habs jetzt selber zusammen gebracht! freu
Danke für deine Hilfe!
Eine Frage hätte ich noch: wie kann ich die TreeView so öffnen, dass schon in Child vorausgewählt ist?
Danke, flo
"das schon in Child vorausgewählt ist" ... ?
Meinst du die Vorselektion eines Items ? Hierbei müsste dir die Property IsSeleted von TreeViewItem behilflich sein.
Um genaueres zu sagen müsste ich deinen Code sehen.
Gruß MA
ja genau. sollte natürlich "das schon ein Child vorausgewählt ist" heißen!
der code:
xaml
<Window.Resources>
<HierarchicalDataTemplate x:Key="DT" ItemsSource="{Binding Childrens}">
<Label Content="{Binding sTitle}" Height="24" Margin="0" />
</HierarchicalDataTemplate>
</Window.Resources>
<TreeView Grid.Row="0" Grid.RowSpan="3" Height="300" ItemTemplate="{StaticResource DT}" ItemsSource="{Binding Item.Childrens}" Name="treKategorien" MouseDoubleClick="treKategorien_MouseDoubleClick" SelectedItemChanged="treKategorien_SelectedItemChanged" />
c#
public class ItemVM
{
public string sTitle { get; set; }
public int iId { get; set; }
public ObservableCollection<ItemVM> Childrens
{
get
{
ObservableCollection<ItemVM> returnment = new ObservableCollection<ItemVM>();
dbKategories dbKat = new dbKategories();
dbKat.dbConnect();
DataTable dtKat = new DataTable();
if (iId == -1) //Top Parents holen
{
dtKat = dbKat.dbSelect("SELECT Name, ID FROM Kategorien WHERE ID = ParentID");
}
else //Childs holen
{
dtKat = dbKat.dbSelect("SELECT Name, ID FROM Kategorien WHERE ID <> ParentID AND ParentID = " + iId);
}
for (int i = 0; i < dtKat.Rows.Count; i++)
{
returnment.Add(new ItemVM(dtKat.Rows[i]["Name"].ToString(),
Convert.ToInt32(dtKat.Rows[i]["ID"])));
}
dbKat.dbDisConnect();
return returnment;
}
}
public ItemVM(string sTitle, int iId)
{
this.sTitle = sTitle;
this.iId = iId;
}
}
lg flo
Jap, also wie gesagt:
Verpasse deinem ItemVM eine Property IsSelected und Binde diese Property über einen Trigger an dein TreeView.
Setzt du die Property IsSelected dann im ViewModel auf true, so ist sie auch im TreeView selektiert.
Gruß MA
ok, habe jetzt den code so verändert:
xaml
<TreeView Grid.Row="0" Grid.RowSpan="4" Height="300" ItemTemplate="{StaticResource DT}" ItemsSource="{Binding Item.Childrens}" Name="treKategorien" MouseDoubleClick="treKategorien_MouseDoubleClick" SelectedItemChanged="treKategorien_SelectedItemChanged">
<DataTrigger Binding="{Binding IsSelected}">
<Setter Property="TreeViewItem.IsSelected" Value="True"></Setter>
</DataTrigger>
</TreeView>
c#
public class ItemVM
{
public string sTitle { get; set; }
public int iId { get; set; }
public int iPreSelected { get; set; }
public bool IsSelected { get; set; }
public ObservableCollection<ItemVM> Childrens
{
get
{
ObservableCollection<ItemVM> returnment = new ObservableCollection<ItemVM>();
dbKategories dbKat = new dbKategories();
dbKat.dbConnect();
DataTable dtKat = new DataTable();
if (iId == -1) //Top Parents holen
{
dtKat = dbKat.dbSelect("SELECT CategoryName, CategoryID FROM Kategorien WHERE CategoryID = CategoryParentID");
}
else //Childs holen
{
dtKat = dbKat.dbSelect("SELECT CategoryName, CategoryID FROM Kategorien WHERE CategoryID <> CategoryParentID AND CategoryParentID = " + iId);
}
for (int i = 0; i < dtKat.Rows.Count; i++)
{
returnment.Add(new ItemVM(dtKat.Rows[i]["CategoryName"].ToString(),
Convert.ToInt32(dtKat.Rows[i]["CategoryID"]),iPreSelected));
}
dbKat.dbDisConnect();
return returnment;
}
}
public ItemVM(string sTitle, int iId, int iPreSelected)
{
this.sTitle = sTitle;
this.iId = iId;
this.iPreSelected = iPreSelected;
if (iId == iPreSelected)
this.IsSelected = true;
}
}
iPreSelected ist die Id die vorselektiert werden soll.
ist der Ansatz richtig?
so wie ich das sehe, muss ich aber noch zwischen IsSelected und dem DataTrigger eine Verbindung herstellen, oder? wie mache ich das?
lg flo