|
|||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||
|
Announcements
Want a new Job?
Chapters
Services
Feature Zones
|
IntroductionThis tutorial shows how to use the Basically, I would like to talk about:
developer viewBefore I start to describe the data, I would like to note that all developers like to write code that they don't have to change often. They like to write code which they can reuse later to develop straight forward on a tested code base. If we now create a That's where the Normally, you will have to fill data into a tree view which reflects a hierarchical data schema. Typically, your data contains a text and ID field, which should be used as underlying data for your tree nodes. This example uses an Access database which is delivered in the sample application and makes use of this schema and data:
As you can see, we have items with a text field identified by an ID field, which can hold sub items identified by a User viewThe user expects a fast and short tree view handling, that you can't provide if you fill all items at once, we have to fill on demand. This means we fill all the items the user expands during his work. The user don't need all items for his work, only a few ones, which he expand automatically during his work. To load only a few items is a fast operation and gives the users a good experience about working with the software.DesignOverviewAfter this short analysis, we should notice the following requirements:
This UML diagram shows the classes created to design the tutorial base classes.
But, let's take a closer look at the classes now... Base classesTreeNodeBaseFirst, we create a new class which inherits from public class TreeNodeBase : System.Windows.Forms.TreeNode
{
Public Instance Properties /// Gets a value indicating if this node owns a dummy node.
public virtual bool HasDummyNode
{
get
{
return (Nodes.Count>0 && Nodes[0].Text == "@@Dummy@@");
}
}
Public Instance Methods /// Adds a dummy node to the parent node
public virtual void AddDummyNode()
{
Nodes.Add(new TreeNodeBase("@@Dummy@@"));
}
/// Removes the dummy node from the parent node.
public virtual void RemoveDummyNode()
{
if ((Nodes.Count == 1 ) & (Nodes[0].Text == "@@Dummy@@"))
{
Nodes[0].Remove();
}
}
}
Every TreeViewStrategyNow, we create a new class which inherits from public class TreeViewStrategy : System.Windows.Forms.TreeView
{
Public Instance Fields /// <SUMMARY>Fired if the datasource has changed</SUMMARY>
public EventHandler DataSourceChanged;
/// <SUMMARY>The underlying data provider which is
/// used to start requests.</SUMMARY>
private IDataProvider _dataProvider;
ConstructorBecause our /// Initializes a new instance of the TreeViewStrategy class.
public TreeViewStrategy()
{
// create the context menu and assing it to the tree view
ContextMenu = new ContextMenu();
ContextMenu.Popup += new EventHandler(ContextMenu_Popup);
}
Public Instance PropertiesThe /// Gets or sets the IDataProvider which is responsible
/// to manage this <SEE cref="TreeView" /> instance.
public IDataProvider DataSource
{
get
{
return _dataProvider;
}
set
{
_dataProvider = value;
// fire the DataSourceChanged event
OnDataSourceChanged(EventArgs.Empty);
}
}
Public Instance MethodsThe /// Fill's the root level.
public void Fill()
{
System.Diagnostics.Debug.Assert(_dataProvider!=null);
// show wait cursor, maybe there is
// a longer operation on the data provider
Cursor.Current = Cursors.WaitCursor;
try
{
// clear all nodes
Nodes.Clear();
// request the new root nodes
_dataProvider.RequestRootNodes(this);
}
finally
{
// Make sure to reset the current cursor.
// NOTE: There is no catch block, because we don't
// want to handle occured exceptions at this level,
// the user of this code is responsible
// to manage it.
Cursor.Current = Cursors.Default;
}
}
This class provides a /// Raises the DataSourceChanged event.
protected virtual void OnDataSourceChanged(EventArgs e)
{
if(DataSourceChanged!=null) DataSourceChanged(this, e);
}
Because the /// Invokes the <SEE cref="IDataProvider.QueryContextMenuItems" /> method
protected virtual void OnContextMenuPopup(EventArgs e)
{
System.Diagnostics.Debug.Assert(_dataProvider!=null);
// retrieve node which was clicked
TreeNodeBase node =
GetNodeAt(PointToClient(Cursor.Position)) as TreeNodeBase;
if(node==null) return;
// clear previous items
ContextMenu.MenuItems.Clear();
// let the provider do his work
_dataProvider.QueryContextMenuItems(this.ContextMenu, node);
}
This event enables load on demand, because the event is fired every time the user expands a node. Because we're only dealing with /// Invokes the <SEE cref="IDataProvider.RequestNodes" /> method
protected override void OnBeforeExpand(TreeViewCancelEventArgs e)
{
// cast node back to our base type
TreeNodeBase node = (TreeNodeBase)e.Node;
System.Diagnostics.Debug.Assert(node!=null);
// fill expanded node if not filled at this time (on demand)
if(node.HasDummyNode)
{
// remove dummy node before we fill the child nodes
// and let the node expand itself to show
// it's children nodes
node.RemoveDummyNode();
//
Cursor.Current = Cursors.WaitCursor;
try
{
// let the provider do his work
_dataProvider.RequestNodes(this, node, e);
}
finally
{
Cursor.Current = Cursors.Default;
}
}
//
base.OnBeforeExpand (e);
}
We handle the /// Fired by the <SEE cref="ContextMenu" />
/// before the shortcut menu is displayed.
/// The private event handler is used to call the virtual
/// protected method <SEE cref="OnContextMenuPopup" />
private void ContextMenu_Popup(object sender, EventArgs e)
{
OnContextMenuPopup(e);
}
Don't forget to close the bracket, otherwise your compiler will let you know about messy things :). }
IDataProviderNow, we are ready to define the /// DataProvider interface is based on TreeViewStrategy and TreeNodeBase and
/// it is responsible to fill a <SEE cref="TreeViewStrategy" />
/// instance with the requested data.
public interface IDataProvider
{
/// Request to fill the root nodes.
void RequestRootNodes(TreeViewStrategy treeView);
/// Request to fill child nodes.
void RequestNodes(TreeViewStrategy treeView,
TreeNodeBase node, TreeViewCancelEventArgs e);
/// Request to fill a <SEE cref="ContextMenu" />
/// with <SEE cref="MenuItem" />'s for the
/// specified <SEE cref="TreeNode" />.
void QueryContextMenuItems(ContextMenu contextMenu, TreeNodeBase node);
}
Now, we can start to write different Sample ApplicationThe sample application provides a DataBaseNodeInfoThe first step was to create a public struct DataBaseNodeInfo
{
Public Instance Fields /// <SUMMARY>Holds the text</SUMMARY>
public string Text;
/// <SUMMARY>Hodsl the id</SUMMARY>
public readonly int Id;
Constructor /// Initializes a new instance of the DataBaseNodeInfo class.
public DataBaseNodeInfo(string text, int id)
{
Text = text;
Id = id;
}
}
TreeNodeDatabaseAnd a public class TreeNodeDatabase : Raccoom.Windows.Forms.TreeNodeBase
{
Public Instance Fields /// <SUMMARY>holds the underlying data
/// in a <SPAN lang=de-ch>simple struct</SUMMARY>
private DataBaseNodeInfo _dbInfo;
Constructor /// Initializes a new instance of the TreeNodeDatabase class.
internal TreeNodeDatabase(string text) : base(text)
{
_dbInfo = new DataBaseNodeInfo(text, -2);
}
/// Initializes a new instance of the TreeNodeDatabase class.
public TreeNodeDatabase(DataBaseNodeInfo dbInfo) : base(dbInfo.Text)
{
_dbInfo = dbInfo;
}
/// Initializes a new instance of the TreeNodeDatabase class.
public TreeNodeDatabase(string text, int id) : base(text)
{
this._dbInfo = new DataBaseNodeInfo(text, id);
}
Public Instance Properties /// Gets the node Id.
public int Id
{
get
{
return _dbInfo.Id;
}
}
/// Gets or sets the text property
new public string Text
{
get
{
return _dbInfo.Text;
}
set
{
_dbInfo.Text = value;
}
}
}
Now we have completed the basic classes that can deal with the database data in a proper manner. The only thing that is missing is the DataProviderDatabaseSo, let's dive into the code, the code that controls the core process, retrieving data. To give you an example how the data provider can extend the existing /// DataProviderDatabase implements the
/// <SEE cref="IDataProvider" /> interface
/// and it supports hiearchical database schemas.
public class DataProviderDatabase : IDataProvider
{
Public Instance Fields /// <SUMMARY>DataTable which holds the data to fill</SUMMARY>
Raccoom.Sample.DataSet1.treeview_dataDataTable _table = null;
Constructor /// Initializes a new instance of the DataProviderDatabase class.
public DataProviderDatabase(){}
Public Instance PropertiesThe filled /// Gets or sets the DataTable which holds the data to fill.
public Raccoom.Sample.DataSet1.treeview_dataDataTable DataTable
{
get
{
return _table;
}
set
{
_table = value;
}
}
Public Instance MethodsInterface method which calls the private /// Request to fill child nodes.
public void RequestNodes(TreeViewStrategy treeView,
TreeNodeBase node, TreeViewCancelEventArgs e)
{
System.Diagnostics.Debug.Assert(node is TreeNodeDatabase);
// fill the childs from data source (table)
Fill(node.Nodes, ((TreeNodeDatabase)node).Id);
}
Interface method which also calls the private /// Request to fill the root nodes.
public void RequestRootNodes(TreeViewStrategy treeView)
{
System.Diagnostics.Debug.Assert(DataTable!=null);
// Fill the root level
Fill(treeView.Nodes, -1);
}
Simple create some context menu items for the demo: /// Request to fill aContextMenu
/// with MenuItem's for the specified TreeNode.
public void QueryContextMenuItems(ContextMenu contextMenu,
TreeNodeBase node)
{
contextMenu.MenuItems.Add("&Open " + node.Text);
contextMenu.MenuItems.Add("&Edit "+ node.Text);
contextMenu.MenuItems.Add("&Delete "+ node.Text);
}
This method deals with a Maybe, a possible newbie question can be: I can't provide nested /// Does a recursive search looking for the node
/// that represents the specified database id
/// starting by specified root collection.
public TreeNodeDatabase FindNodeById(TreeNodeCollection col, int id)
{
// go through collection and compare
// each node.id which against the db id.
foreach(TreeNodeDatabase node in col)
{
if(object.Equals(node.Id, id)) return node;
// call this method recursive to process
// all nodes in the treeview.
TreeNodeDatabase subNode = FindNodeById(node.Nodes, id);
if(subNode!=null) return subNode;
}
return null;
}
The /// Retrieves the child nodes for the given node
/// and add's them to the node childs collection
private void Fill(TreeNodeCollection col, int parent_id)
{
foreach(Raccoom.Sample.DataSet1.treeview_dataRow
row in _table.Select("parent_id = "+parent_id))
{
TreeNodeDatabase node =
new TreeNodeDatabase(row.node_text, row.id);
col.Add(node);
// count on childs, if node is parent node,
// add a dummy node for + sign
if(_table.Select("parent_id = "+node.Id).Length>0)
{
node.AddDummyNode();
}
}
}
}
Possible improvements
ConclusionsThis is my first tutorial and I hope it is somewhat useful for others. Keep in mind that this doesn't have to be the only or best way to deal with a As always, any feedback or criticism is appreciated. Links
History
Have phun... | ||||||||||||||||||||||||||