DataBound TreeView控件






4.60/5 (23投票s)
2005年1月7日
6分钟阅读

234077

4883
一种绑定简单 TreeView 控件的方法。
引言
我找了很长时间,试图找到一个不错的可数据绑定的 TreeView 控件。我一次又一次地听到同样的说法,由于围绕 TreeView
控件固有的设计考虑,Microsoft 无法将该控件与 DataBinding 框架连接起来。虽然我相信这是真的,但我也认为在一些简单的控件实现中,数据绑定确实是可能的。我第一次看到 Microsoft 的 Duncan McKenzie 的一篇文章。
Duncan 的文章是用 VB.NET 编写的,后来由 LZF 用 C# 重写。我建议对原始想法感兴趣的任何人阅读这两篇文章。我从这两篇文章中汲取了灵感,开发了一个解决方案,我可以使用该控件和 DataSet
轻松使用。当然,最终会有一些情况会破坏我的解决方案,但目前它运行良好。我期待您的批评。
背景
我想要的是……我想展示一个 TreeView
控件,其中包含一个以 DataSource
属性形式呈现的基本分层 DataSet
,并让该 DataSet
根据 DataSet
中的 DataRelation
表示为 TreeView
。我需要能够控制树中每个节点级别的 DisplayMember
和 ValueMember
。此外,不可避免地需要向设计器提供 ImageList
、ImageIndex
、SelectedImageIndex
属性。
在导航方面,我必须确保当选择一个节点时,所有相应的绑定控件都会通过其货币管理器接收消息。也就是说,如果我在树中移动,绑定控件也应该移动。我使用 TreeView
的 AfterSelect
事件来连接此活动。另一方面,如果选定的 DataRow
从另一个控件(即 DataGrid
)移动,我需要移动树的选定节点。换句话说,如果用户在 DataGrid
中移动了位置,它也应该移动树的选定节点。
数据同步要求,如果在 UI 中的任何地方更新了 DataRow
对象,则 TreeView
的显示成员也应更新。这显然只影响 TreeNode
的 Text
属性中正在使用的 DataRow
列。
使用代码
我提供了一个简单的解决方案,展示了所有功能如何协同工作。祝您玩得开心。
示例代码提供了一个可与 Northwind 数据库一起使用的类型化数据集。演示将使用 Customer、Order、OrderDetail 的经典层次结构。确保您的 SqlConnection
对象连接到 Northwind 数据库。SqlDataAdapter
应该已经设置好以加载类型化数据集,并且关系已经存在于 DataSet
中。
这里的大部分代码都很简单,但请注意我们如何手动创建 TableBinding
对象数组,这些对象描述了 DataSet
中每个表的用法和表示。如果我们不指定表名、显示成员和值成员,控件将使用每个表中的第一列进行显示和值。您还可以在加载树之前指定 ImageList
、ImageIndex
和 SelectedImageIndex
属性。
然后唯一要做的就是使用 DataSet
和 TableBinding
数组调用 LoadTree
。加载树后,我扔了一些 DataGrid
来允许导航和编辑 DataSet
。
private NorthwindDataSet ds;
private void Form1_Load(object sender, System.EventArgs e)
{
ds = new NorthwindDataSet();
}
private void button1_Click(object sender, System.EventArgs e)
{
// Fill up the DataSet
this.sqlDataAdapter3.Fill(ds, "Customers");
this.sqlDataAdapter4.Fill(ds, "Orders");
this.sqlDataAdapter5.Fill(ds, "OrderDetails");
// Create an array of TableBindings that
// define Table Name, Value Member and Display Member
TableBinding[] tableBindings = new TableBinding[] {
new TableBinding("Customers", "CustomerID", "CompanyName"),
new TableBinding("Orders", "OrderID", "OrderID"),
new TableBinding("OrderDetails", "ProductID", "ProductID")};
// Setup the initial TreeView defaults
treeResource.TreeView.HideSelection = false;
treeResource.TreeView.ImageList = this.imageList1;
treeResource.TreeView.ImageIndex = 0;
treeResource.TreeView.SelectedImageIndex = 1;
// Load up the Tree
treeResource.LoadTree(ds, tableBindings);
// Load up the DataGrids
this.dataGrid1.DataSource = ds;
this.dataGrid1.DataMember = "Customers";
this.dataGrid2.DataSource = ds;
this.dataGrid2.DataMember = "Customers.CustomersOrders";
this.dataGrid3.DataSource = ds;
this.dataGrid3.DataMember = "Customers.CustomersOrders.OrdersOrderDetails";
}
运行示例并单击左侧的加载树按钮。尝试使用导航并查看您是否得到了预期的结果。尝试单击第二个加载树按钮...
请注意,在加载第二棵树时,我在 LoadTree
之前调用了 SetEvents(ds, false)
。这是为了让第一棵树不导航到每个节点。不要忘记使用 SetEvents(ds, true)
重新打开导航。您会注意到 DataGrid
正在更新。
private void button2_Click(object sender, System.EventArgs e)
{
TableBinding[] tableBindings = new TableBinding[] {
new TableBinding("Customers", "CustomerID", "CompanyName"),
new TableBinding("Orders", "OrderID", "OrderID"),
new TableBinding("OrderDetails", "ProductID", "ProductID")};
treeResource2.TreeView.HideSelection = false;
treeResource2.TreeView.ImageList = this.imageList1;
treeResource2.TreeView.ImageIndex = 0;
treeResource2.TreeView.SelectedImageIndex = 1;
// I turn off the events for the first tree so that loading the second
// tree doesn't navigate to every node in the first tree.
// You'll notice that the DataGrids do move... You could remove the DataSource
// from them temporarily to eliminate that as well.
treeResource.SetEvents(ds, false);
treeResource2.LoadTree(ds, tableBindings);
treeResource.SetEvents(ds, true);
}
我在这里截取了控件逻辑的要点,以便向您展示。可能需要更多解释,但如果您理解了这一点,其余的就会迎刃而解。我在“背景”中提到,我需要树中节点的选择能够反映受影响的货币管理器中位置的相应变化。这就是我通过连接到 TreeView
的 tv_AfterSelect
事件来实现这一点的。
因为在树中选择任何节点(叶节点除外)都将在 CurrencyManager
上创建一个不同的 IBindingList
,所以我们将从最高父节点开始重新定位每个 CurrencyManager
。考虑到选择节点的方式,很明显,例如,您可以通过展开另一个节点(而不首先选择该节点)然后选择一个新子节点来选择一个不同父节点的子节点。发生这种情况时,您必须首先选择该新节点的祖先,因此我们创建一个 nodeList
数组并将选定的节点及其祖先添加到其中。一旦我们有了这些,我们只需从最旧(最高)的节点开始循环。
我们还使用 DisablePositionChanged
布尔字段禁用 PositionChanged
事件,否则您将陷入 cm_PositionChanged
移动 TreeView
(AfterSelect
)和 tv_AfterSelect
移动 CurrencyManager
(PositionChanged
)之间的往复循环。我们移除的另一个处理程序是 ListChanged
,因为正如我所说,每次选择新节点时,CurrencyManager
的列表都会重新创建。
// When the BoundTreeView's nodes are selected,
// we must synchronize the CurrencyManagers...
private void tv_AfterSelect(object sender, TreeViewEventArgs e)
{
// We have to move the currency manager positions for every node in the
// selected heirarchy because the parent node selection determines the
// currency manager "list" contents for the children
ArrayList nodeList = new ArrayList();
// Start with the node that has been selected
BoundTreeNode node = (BoundTreeNode)((TreeView)sender).SelectedNode;
nodeList.Add(node);
// Recursively add all the parent nodes
node = (BoundTreeNode)node.Parent;
while (node != null)
{
nodeList.Add(node);
node = (BoundTreeNode)node.Parent;
}
// Don't fire the our own position
// change event other controls bound to the
// currency managers will move accordingly
// because we are setting the position
// explicitly
DisablePositionChanged = true;
// Start at the highest parent node
for (int i = nodeList.Count; i > 0; i--)
{
node = (BoundTreeNode)nodeList[i-1];
((IBindingList)node.CurrencyManager.List).ListChanged
-= handlerListChanged;
node.CurrencyManager.Position = node.Position;
((IBindingList)node.CurrencyManager.List).ListChanged
+= handlerListChanged;
}
DisablePositionChanged = false;
}
此外,我们声明如果 DataSet
从另一个绑定控件(即 DataGrid
)移动,则应在 TreeView
中选择相应的节点。只要其他控件绑定到相同的 CurrencyManager
(“精确地”具有相同的导航路径),BoundTreeView
控件就会收到位置更改通知。在这里,我们通过确保我们正在导航到现有行(cm.Position
>= 0)并且 DataRow
已附加到 DataTable
来处理它。
从那里,我们构建一个父行层次结构。一旦确定了这些,我们就可以遍历数组并在树中找到相应的节点。这是通过将 DataRow
值列中的值与 SelectNode
方法中 BoundTreeNode
的标签值进行比较来完成的。在树中找到一行后,我们只需搜索其子节点即可找到其余节点。(只搜索一个父系的血统。)这可能看起来有点令人困惑,但如果您按照代码进行操作,它应该会变得更清晰。
最后,我们再次处理事件的附加和分离,并且随着新的 IBindingList
的创建,新的 ListChanged
处理程序被附加。
// When the CurrencyManagers change
// position we must reposition the TreeView...
private void cm_PositionChanged(object sender, EventArgs e)
{
// We manually disable this if we are changing position from tv_AfterSelect
if (!DisablePositionChanged)
{
CurrencyManager cm = (CurrencyManager)sender;
// The position may be -1 if the currency manager list is empty
if (cm.Position >= 0)
{
DataRowView drv = (DataRowView)((DataView)cm.List)[cm.Position];
DataRow dr = drv.Row;
// other controls (DataGrid) may
// allow adding rows that are unaccessible
if (dr.RowState != DataRowState.Detached)
{
// Start with the data row that was selected
ArrayList dataRows = new ArrayList();
dataRows.Add(dr);
// We have to select the parents
// first so that we only search the
// specific lineage when we call SelectNode
while (dr.Table.ParentRelations.Count > 0)
{
dr = dr.GetParentRow(dr.Table.ParentRelations[0]);
dataRows.Add(dr);
}
// Start searching the tree with the base nodes collection
TreeNodeCollection nodes = _treeView.Nodes;
TreeNode node = null;
TableBinding tableBinding;
// Select the highest parent
// and then the subsequent children from
// the returned node's collection of children
// Start with the highest datarow in the heirarchy
for (int i = dataRows.Count; i>0; i--)
{
dr = (DataRow)dataRows[i-1];
// TableBinding tells us what the field
// in the datarow is that will be
// compared to the tag value in the node
tableBinding = GetBinding(dr.Table.TableName);
// Find the node and then search
// it's children for the next datarow
if (tableBinding != null)
node = SelectNode(dr[tableBinding.ValueMember],
nodes);
else
node = SelectNode(dr[0], nodes);
// The next nodes collection to search
nodes = node.Nodes;
}
// We're going to move the tree node
// selection here, but we don't want
// the AfterSelect event to be handled
// because it would fire the
// currency manager PositionChanged event reciprocally
_treeView.AfterSelect -= handlerAfterSelect;
_treeView.SelectedNode = node;
_treeView.AfterSelect += handlerAfterSelect;
// The (IBindingList) has changed,
// so wire up the child lists to the handler
while (node.Nodes.Count > 0)
{
((IBindingList)((BoundTreeNode)
node.Nodes[0]).CurrencyManager.List).ListChanged
-= handlerListChanged;
((IBindingList)((BoundTreeNode)
node.Nodes[0]).CurrencyManager.List).ListChanged
+= handlerListChanged;
node = node.Nodes[0];
}
}
}
}
}
最后一条消息……我们发现 IBindingList
发生了变化;因此,我们必须在树中找到节点并更改 Text
属性,以防显示成员是被更改的列。只需将发送方转换为 DataView
,获取受 NewIndex
影响的 DataRowView
,确定 TableBinding
,然后搜索树。
// Some data in the lists has changed,
// we may need to update the TreeView Display
private void cm_ListChanged(object sender, ListChangedEventArgs e)
{
// Cast the sender to a DataView
DataView dv = (DataView)sender;
// Get the DataRowView of the newly selected row in the "list".
DataRowView drv = (DataRowView)dv[e.NewIndex];
DataRow dr = drv.Row;
// Start searching the tree with the base nodes collection
TreeNodeCollection nodes = _treeView.Nodes;
TreeNode node = null;
TableBinding tableBinding;
// TableBinding tells us what the field in the datarow is that will be
// compared to the tag value in the node and what the display value is
tableBinding = GetBinding(dr.Table.TableName);
// Find the node
if (tableBinding != null)
{
node = SelectNode(dr[tableBinding.ValueMember], nodes);
node.Text = dr[tableBinding.DisplayMember].ToString();
}
else
{
node = SelectNode(dr[0], nodes);
node.Text = dr[0].ToString();
}
}
关注点
我在这里学到的最有趣的事情之一是 CurrencyManager
的 IBindingList
接口。CurrencyManager
的 List
属性包含由父行过滤的项目列表。因此,当您更改选定的 Order 时,列表中只有相应的 OrderDetail 记录。这使得此列表不断变化,因此每次列表更改(即创建)时都必须连接事件。
这个东西不是很快速,所以我不会将它用于大型 DataSet
;但是,对于较小的数据集,它似乎运行良好。
别忘了感谢 Duncan McKenzie 和 LZF!
历史
修订版 1 - 等待反馈。