I have posted a new build with some enhancements and bug fixes. New features have been added to the "TreeNode Factory" AddOn (be sure to reimport the new version of this add-on into your repository).
Note that you should be familiar with this AddOn before reading this. If this is not the case, you can take a look at the documentation:
<OlyMars directory>\AddOns\TreeNodeFactory\Documentation.mht

Let's say that we have the following scenario:
- We would like to add a parent node named "Categories"
- We would like to add under this parent node all the categories of the database. Each time the user selects one of these nodes, he should see in the status bar the number of products of that category
- We would like to add under each category node all the related products. We would to see the name of the product, the price of that product and the current stock number. Furthermore, each time the product current stock is less than 50, we would like the node to be red; for stock between 50 and 300, we would like it to be orange. For stock above 300, the node should be green. Each time the user selects a product, he should see in the status bar the name of the main supplier of this product. Finally, we do not want to add all the products which price is between 1000 and 1100
To be able to handle this scenario, we will use the new features provided in the "TreeNode Factory" AddOn:
- A new StaticCustomTreeNode class
- Two new events : BeforeInsert and AfterInsert
- The ability to build dynamically both the Id and/or the Display to be used for each node by using the FormatIdDisplay event
- The ContextOfUse has been expanded from the string type to the more general object type allowing you to provide an enumeration instead of a string for example
- A Columns array is now available for each StoredProcedureTreeNode. You can retrieve back all the columns for each row returned by the stored procedure call
Here is an example on how to reuse those new features (you can download this code):
[Database]-----------------------------------------------------------------------------------------
CREATE TABLE [Category] (
[CategoryID] uniqueidentifier,
[CategoryName] [varchar] (255),
CONSTRAINT [PK_Category] PRIMARY KEY ([CategoryID])
)
CREATE TABLE [Supplier] (
[SupplierID] uniqueidentifier,
[SupplierName] [varchar] (255),
CONSTRAINT [PK_Supplier] PRIMARY KEY ([SupplierID])
)
CREATE TABLE [Product] (
[ProductID] uniqueidentifier,
[ProductName] [varchar] (255),
[ProductCategoryID] [uniqueidentifier],
[ProductPrice] [money],
[ProductCurrentStock] [int],
[ProductMainSupplierID] [uniqueidentifier],
CONSTRAINT [PK_Product] PRIMARY KEY ([ProductID]),
CONSTRAINT [FK_Product_Category] FOREIGN KEY ([ProductCategoryID]) REFERENCES [Category] ([CategoryID]),
CONSTRAINT [FK_Product_Supplier] FOREIGN KEY ([ProductMainSupplierID]) REFERENCES [Supplier] ([SupplierID])
)
CREATE Procedure [spS_xTNF_Categories] As
Select
CategoryID
, CategoryName
, (Select IsNull(Count(*), 0) From Product Where ProductCategoryID = CategoryID) As ProductsCount
From Category Order By CategoryName Asc
Return(@@RowCount)
CREATE Procedure [spS_xTNF_Products] (@CategoryID uniqueidentifier) As
Select
ProductID
, ProductName
, ProductPrice
, ProductCurrentStock
, SupplierName
From Product Left Outer Join Supplier On ProductMainSupplierID = SupplierID
Where ProductCategoryID = @CategoryID Order By ProductName Asc
[C#.NET]-----------------------------------------------------------------------------------------
using System.Data.SqlTypes;
using SPs = DemoTNF.DataClasses.StoredProcedures;
using DemoTNF.Windows.TreeNodeFactory;
using SP_TNF = DemoTNF.Windows.TreeNodeFactory.StoredProcedureTreeNodeFactory;
public FormStart() {
//
// Required for Windows Form Designer support
//
InitializeComponent();
//
// TODO: Add any constructor code after InitializeComponent call
//
Init();
}
private void Init() {
// Let's decide once for good how to connect to the back end database
SP_TNF.SetUpConnection(string.Empty);
// We want to handle the following three events:
// FormatIdDisplay, BeforeInsert and AfterInsert
SP_TNF.FormatIdDisplay += new FormatIdDisplayHandler(SP_TNF_FormatIdDisplay);
SP_TNF.BeforeInsert += new BeforeInsertHandler(SP_TNF_BeforeInsert);
SP_TNF.AfterInsert += new AfterInsertHandler(SP_TNF_AfterInsert);
// Let's create a Parent TreeNode to hold all the categories
// Let's use a StaticCustomTreeNode for this
StaticCustomTreeNode categories = new StaticCustomTreeNode(
MyContextOfUse.Categories
, null
, "Categories"
, true);
treeView1.Nodes.Add(categories);
}
private enum MyContextOfUse {
Categories, Category, Product
}
private void treeView1_BeforeExpand(object sender, System.Windows.Forms.TreeViewCancelEventArgs e) {
// Let's get the node being expanded
ITreeNodeFactoryCustomTreeNode currentNode =
e.Node as ITreeNodeFactoryCustomTreeNode;
// If this node is not a node coming from the TreeNodeFactory
// or if this node is already UpToDate, do nothing
if (currentNode == null || currentNode.IsUpToDate) return;
// Depending on the context of use of the current node,
// let's do the right thing
switch ((MyContextOfUse)currentNode.ContextOfUse) {
// If we are expanding the categories node, let's
// fill this node with all the categories. We are going
// to call spS_xTNF_Categories stored procedure for this
case MyContextOfUse.Categories:
SP_TNF.Fill_spS_xTNF_Categories(
currentNode.Nodes // this is the Nodes collection to fill
, MyContextOfUse.Category // this is the context of use of the nodes being added
, true // yes I want to purge the Nodes collection
, true // yes I want to add a sub node to all the added nodes
, SPs.spS_xTNF_Categories.Resultset1.Fields.Column_CategoryID.ColumnIndex // this is the column index of the primary key
, SPs.spS_xTNF_Categories.Resultset1.Fields.Column_CategoryName.ColumnIndex // this is the index of the column to be use for display
, true // I want to get track of all columns for each row
);
break;
// If we are expanding a category node, let's
// fill this node with all the related products. We are going
// to call spS_xTNF_Products stored procedure for this
case MyContextOfUse.Category:
SP_TNF.Fill_spS_xTNF_Products(
currentNode.Nodes // this is the Nodes collection to fill
, MyContextOfUse.Product // this is the context of use of the nodes being added
, true // yes I want to purge the Nodes collection
, false // no I do not want to add a sub node to the added nodes
, (Guid)currentNode.Id // This is the value to use for the @CategoryID stored procedure parameter
, SPs.spS_xTNF_Products.Resultset1.Fields.Column_ProductID.ColumnIndex // this is the column index of the primary key
, -1 // We do not want to use one specific column for the display but rather be called in the FormatdDisplay event to format this display
, true // I want to get track of all columns for each row
);
break;
}
// Let's specify that this node is UpToDate now
currentNode.IsUpToDate = true;
}
private void SP_TNF_FormatIdDisplay(object sender, StoredProcedureFactoryFormatIdDisplayEventArgs e) {
switch ((MyContextOfUse)e.Node.ContextOfUse) {
// Let's decide how to format the display for our products
// All the row columns are available via e.Node.Columns[]
case MyContextOfUse.Product:
SqlString productName = (SqlString)e.Node.Columns[SPs.spS_xTNF_Products.Resultset1.Fields.Column_ProductName.ColumnIndex];
SqlMoney productPrice = (SqlMoney)e.Node.Columns[SPs.spS_xTNF_Products.Resultset1.Fields.Column_ProductPrice.ColumnIndex];
SqlInt32 productCurrentStock = (SqlInt32)e.Node.Columns[SPs.spS_xTNF_Products.Resultset1.Fields.Column_ProductCurrentStock.ColumnIndex];
e.Display = string.Format("{0} ({1:c}, {2:### ##0} in stock)", productName.Value, productPrice.Value, productCurrentStock.Value);
break;
}
}
private void SP_TNF_BeforeInsert(object sender, StoredProcedureFactoryBeforeInsertEventArgs e) {
switch ((MyContextOfUse)e.Node.ContextOfUse) {
// We don't want to see all products which prices are greater
// than 1 000 and less than 1 100
case MyContextOfUse.Product:
SqlMoney productPrice = (SqlMoney)e.Node.Columns[SPs.spS_xTNF_Products.Resultset1.Fields.Column_ProductPrice.ColumnIndex];
e.Cancel = (productPrice.Value >= 1000m && productPrice.Value < 1100m);
break;
}
}
private void SP_TNF_AfterInsert(object sender, StoredProcedureFactoryAfterInsertEventArgs e) {
switch ((MyContextOfUse)e.Node.ContextOfUse) {
// We would like to set some color to the products nodes
// Red for the products which current stock is less than 50
// Orange for the products which current stock is greater than 50 and less than 300
// Green for the products which current stock is greater than 300
case MyContextOfUse.Product:
SqlInt32 productCurrentStock = (SqlInt32)e.Node.Columns[SPs.spS_xTNF_Products.Resultset1.Fields.Column_ProductCurrentStock.ColumnIndex];
if (productCurrentStock.Value < 50) {
e.Node.ForeColor = Color.Red;
}
else if (
productCurrentStock.Value >= 50
&& productCurrentStock.Value < 300
) {
e.Node.ForeColor = Color.Orange;
}
else {
e.Node.ForeColor = Color.Green;
}
break;
}
}
private void treeView1_AfterSelect(object sender, System.Windows.Forms.TreeViewEventArgs e) {
StoredProcedureCustomTreeNode currentNode = e.Node as StoredProcedureCustomTreeNode;
if (currentNode == null) return;
switch ((MyContextOfUse)currentNode.ContextOfUse) {
case MyContextOfUse.Category:
SqlInt32 productsCount = (SqlInt32)currentNode.Columns[SPs.spS_xTNF_Categories.Resultset1.Fields.Column_ProductsCount.ColumnIndex];
statusBar1.Text = string.Format("This category has {0} products", productsCount.Value);
break;
case MyContextOfUse.Product:
SqlString supplierName = (SqlString)currentNode.Columns[SPs.spS_xTNF_Products.Resultset1.Fields.Column_SupplierName.ColumnIndex];
statusBar1.Text = supplierName.IsNull ? "No supplier" : string.Format("Main supplier for this product is: {0}", supplierName.Value);
break;
}
}
[VB.NET]-----------------------------------------------------------------------------------------
Imports System.Data.SqlTypes
Imports SPs = DemoTNF.DataClasses.StoredProcedures
Imports DemoTNF.Windows.TreeNodeFactory
Imports SP_TNF = DemoTNF.Windows.TreeNodeFactory.StoredProcedureTreeNodeFactory
Public Sub New()
MyBase.New()
'This call is required by the Windows Form Designer.
InitializeComponent()
'Add any initialization after the InitializeComponent() call
Init()
End Sub
Private Enum MyContextOfUse
Categories
Category
Product
End Enum
Private Sub Init()
' Let's decide once for good how to connect to the back end database
SP_TNF.SetUpConnection(String.Empty)
' We want to handle the following three events:
' FormatIdDisplay, BeforeInsert and AfterInsert
AddHandler SP_TNF.FormatIdDisplay, New FormatIdDisplayHandler(AddressOf SP_TNF_FormatIdDisplay)
AddHandler SP_TNF.BeforeInsert, New BeforeInsertHandler(AddressOf SP_TNF_BeforeInsert)
AddHandler SP_TNF.AfterInsert, New AfterInsertHandler(AddressOf SP_TNF_AfterInsert)
' Let's create a Parent TreeNode to hold all the categories
' Let's use a StaticCustomTreeNode for this
Dim categories As New StaticCustomTreeNode( _
MyContextOfUse.Categories _
, Nothing _
, "Categories" _
, True)
TreeView1.Nodes.Add(categories)
End Sub
Private Sub SP_TNF_FormatIdDisplay(ByVal sender As Object, ByVal e As StoredProcedureFactoryFormatIdDisplayEventArgs)
' Let's decide how to format the display for our products
' All the row columns are available via e.Node.Columns[]
Select Case CType(e.Node.ContextOfUse, MyContextOfUse)
Case MyContextOfUse.Product
Dim productName As SqlString = CType(e.Node.Columns(SPs.spS_xTNF_Products.Resultset1.Fields.Column_ProductName.ColumnIndex), SqlString)
Dim productPrice As SqlMoney = CType(e.Node.Columns(SPs.spS_xTNF_Products.Resultset1.Fields.Column_ProductPrice.ColumnIndex), SqlMoney)
Dim productCurrentStock As SqlInt32 = CType(e.Node.Columns(SPs.spS_xTNF_Products.Resultset1.Fields.Column_ProductCurrentStock.ColumnIndex), SqlInt32)
e.Display = String.Format("{0} ({1:c}, {2:### ##0} in stock)", productName.Value, productPrice.Value, productCurrentStock.Value)
End Select
End Sub
Private Sub SP_TNF_BeforeInsert(ByVal sender As Object, ByVal e As StoredProcedureFactoryBeforeInsertEventArgs)
' We don't want to see all products which prices are greater
' than 1 000 and less than 1 100
Select Case CType(e.Node.ContextOfUse, MyContextOfUse)
Case MyContextOfUse.Product
Dim productPrice As SqlMoney = CType(e.Node.Columns(SPs.spS_xTNF_Products.Resultset1.Fields.Column_ProductPrice.ColumnIndex), SqlMoney)
e.Cancel = productPrice.Value >= 1000 And productPrice.Value < 1100
End Select
End Sub
Private Sub SP_TNF_AfterInsert(ByVal sender As Object, ByVal e As StoredProcedureFactoryAfterInsertEventArgs)
' We would like to set some color to the products nodes
' Red for the products which current stock is less than 50
' Orange for the products which current stock is greater than 50 and less than 300
' Green for the products which current stock is greater than 300
Select Case CType(e.Node.ContextOfUse, MyContextOfUse)
Case MyContextOfUse.Product
Dim productCurrentStock As SqlInt32 = CType(e.Node.Columns(SPs.spS_xTNF_Products.Resultset1.Fields.Column_ProductCurrentStock.ColumnIndex), SqlInt32)
If productCurrentStock.Value < 50 Then
e.Node.ForeColor = Color.Red
ElseIf productCurrentStock.Value >= 50 And productCurrentStock.Value < 300 Then
e.Node.ForeColor = Color.Orange
Else
e.Node.ForeColor = Color.Green
End If
End Select
End Sub
Private Sub TreeView1_BeforeExpand(ByVal sender As Object, ByVal e As System.Windows.Forms.TreeViewCancelEventArgs) Handles TreeView1.BeforeExpand
' Let's get the node being expanded
Dim currentNode As ITreeNodeFactoryCustomTreeNode = _
CType(e.Node, ITreeNodeFactoryCustomTreeNode)
' If this node is not a node coming from the TreeNodeFactory
' or if this node is already UpToDate, do nothing
If currentNode Is Nothing Or currentNode.IsUpToDate Then Return
' Depending on the context of use of the current node,
' let's do the right thing
Select Case CType(currentNode.ContextOfUse, MyContextOfUse)
' If we are expanding the categories node, let's
' fill this node with all the categories. We are going
' to call spS_xTNF_Categories stored procedure for this
Case MyContextOfUse.Categories
SP_TNF.Fill_spS_xTNF_Categories( _
currentNode.Nodes _
, MyContextOfUse.Category _
, True _
, True _
, SPs.spS_xTNF_Categories.Resultset1.Fields.Column_CategoryID.ColumnIndex _
, SPs.spS_xTNF_Categories.Resultset1.Fields.Column_CategoryName.ColumnIndex _
, True _
)
' currentNode.Nodes: this is the Nodes collection to fill
' MyContextOfUse.Category: this is the context of use of the nodes being added
' true: yes I want to purge the Nodes collection
' true: yes I want to add a sub node to all the added nodes
' SPs.spS_xTNF_Categories.Resultset1.Fields.Column_CategoryID.ColumnIndex: this is the column index of the primary key
' SPs.spS_xTNF_Categories.Resultset1.Fields.Column_CategoryName.ColumnIndex: this is the index of the column to be use for display
' true: I want to get track of all columns for each row
' If we are expanding a category node, let's
' fill this node with all the related products. We are going
' to call spS_xTNF_Products stored procedure for this
Case MyContextOfUse.Category
SP_TNF.Fill_spS_xTNF_Products( _
currentNode.Nodes _
, MyContextOfUse.Product _
, True _
, False _
, New SqlGuid(CType(currentNode.Id, Guid)) _
, SPs.spS_xTNF_Products.Resultset1.Fields.Column_ProductID.ColumnIndex _
, -1 _
, True _
)
' currentNode.Nodes: this is the Nodes collection to fill
' MyContextOfUse.Product: this is the context of use of the nodes being added
' true: yes I want to purge the Nodes collection
' false: no I do not want to add a sub node to the added nodes
' (Guid)currentNode.Id: This is the value to use for the @CategoryID stored procedure parameter
' SPs.spS_xTNF_Products.Resultset1.Fields.Column_ProductID.ColumnIndex: this is the column index of the primary key
' -1: We do not want to use one specific column for the display but rather be called in the FormatdDisplay event to format this display
' true: I want to get track of all columns for each row
End Select
' Let's specify that this node is UpToDate now
currentNode.IsUpToDate = True
End Sub
Private Sub TreeView1_AfterSelect(ByVal sender As Object, ByVal e As System.Windows.Forms.TreeViewEventArgs) Handles TreeView1.AfterSelect
Dim currentNode As StoredProcedureCustomTreeNode
Try
currentNode = CType(e.Node, StoredProcedureCustomTreeNode)
Catch
End Try
If currentNode Is Nothing Then Return
Select Case CType(currentNode.ContextOfUse, MyContextOfUse)
Case MyContextOfUse.Category
Dim productsCount As SqlInt32 = CType(currentNode.Columns(SPs.spS_xTNF_Categories.Resultset1.Fields.Column_ProductsCount.ColumnIndex), SqlInt32)
StatusBar1.Text = String.Format("This category has {0} products", productsCount.Value)
Case MyContextOfUse.Product
Dim supplierName As SqlString = CType(currentNode.Columns(SPs.spS_xTNF_Products.Resultset1.Fields.Column_SupplierName.ColumnIndex), SqlString)
StatusBar1.Text = IIf(supplierName.IsNull, "No supplier", String.Format("Main supplier for this product is: {0}", supplierName.Value))
End Select
End Sub