Silverlight 2b1 的简单 Treeview






4.90/5 (27投票s)
在 Silverlight 中创建可模板化的树状视图
引言
首先,请接受我蹩脚英语的歉意。:)
本文讨论了在 Silverlight 中创建一个简单的、可模板化的 treeview
。我的目标是向读者解释如何通过继承 Control
和 ContentControl
类来从头开始创建一个真正的控件(而不是一个简单的 UserControl
)。我想展示 INotifyCollectionChanged
的强大功能,以简单 CRUD 的方式管理项,从而控制内部数据。
模板?
什么是模板?根据 Microsoft Silverlight 团队的说法,这是一个非常简单的事情。模板控件是一个包含两个部分的小部件:一个逻辑部分(.NET 代码)和一个视觉部分(XAML 代码)。例如,你们都知道 Button
小部件;无论你们在 Linux、Windows、网站等地方看到它,按钮都有很多可能的皮肤。但按钮只有一个行为集。每个行为都称为状态。按钮有
- 按下状态
- 鼠标悬停状态
- 禁用状态
- 正常状态
这个状态是如何改变的?控件的逻辑部分通过监听外部事件(如鼠标事件)来管理这组状态。例如,当鼠标点击我们的 Button 时,逻辑部分会尝试在视觉部分中找到一个特定的 storyboard,该 storyboard 会使 Button 呈现按下按钮的外观,并调用它。
逻辑部分(你的 C# 代码)和视觉部分(XAML 代码)之间的关系由类上的 TemplatePartAttribute
集合指定。如果你在反射器中打开 Button
类(Silverlight Framework),你会看到类似这样的内容
[TemplatePart(Name="Normal State", Type=typeof(Storyboard)),
TemplatePart(Name="MouseOver State", Type=typeof(Storyboard)),
TemplatePart(Name="RootElement", Type=typeof(FrameworkElement)),
TemplatePart(Name="Pressed State", Type=typeof(Storyboard)),
TemplatePart(Name="FocusVisualElement", Type=typeof(UIElement)),
TemplatePart(Name="Disabled State", Type=typeof(Storyboard))]
public class Button : ButtonBase
{
// ...
protected override void OnApplyTemplate();
}
TemplatePart 将一个键(这里是 Name)与一个类型关联起来。这个键必须在逻辑部分与 UIElement
或 Storyboard
关联。我们在这里可以看到两个重要部分:“Pressed State”是一个 Storyboard
,而 RootElement
是一个 FrameworkElement
。当逻辑部分拦截到对 RootElement
的鼠标点击时,它会调用 Pressed State storyboard。这个 storyboard 会改变视觉部分中指定的 UI 元素的外观。如果你查看该类中的 OnApplyTemplate
方法,你会发现代码会处理这里定义的每个部分。
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
object templateChild = base.GetTemplateChild("RootElement");
this._elementRoot = templateChild as FrameworkElement;
this._elementFocusVisual = base.GetTemplateChild("FocusVisualElement")as UIElement;
if (this._elementRoot != null)
{
this._stateNormal = this._elementRoot.Resources["Normal State"]as Storyboard;
this._stateMouseOver =
this._elementRoot.Resources["MouseOver State"]as Storyboard;
this._stateMouseOver = obj5 as Storyboard;
this._statePressed = this._elementRoot.Resources["Pressed State"]as Storyboard;
this._stateDisabled = this._elementRoot.Resources["Disabled State"]as Storyboard;
}
base.UpdateVisualState();
}
这个方法建立了视觉和逻辑部分之间的链接。通过这些成员(如 _elementRoot
、_stateNormal
等),逻辑部分可以控制视觉部分,而无需担心其内容。你可以创建自己的视觉部分,而无需任何 .NET 代码来改变控件的行为。你只需要在模板中为你自己的 UIElement
和 Storyboard
指定与模板部分中指示的相同的名称。
视觉部分的模板定义在你的资源中。所以,在 generic.xaml(System.Windows.Control
使用 generic.xaml)中,你可以在 app.xaml 或你的用户控件资源中指定它,以制作自己的皮肤并保存默认皮肤。
抱歉,这非常理论化,但对于接下来的内容是必要的。
INotifyCollectionChanged
像 ListBox
控件这样的控件是如何知道在其绑定的集合更改时更新其 UI 的?
Microsoft Silverlight 团队使用一个名为 ObservableCollection
的类型,它继承自 INotifyCollectionChanged
。控件可以订阅它的事件,以便在集合更改时知道,通过
- 添加
- 删除
- 清空
- ...
只需用反射器查看 listbox
控件。我们可以看到,当 ItemsSource
属性更改时,它会调用一个名为 ItemsSourceChanged
的方法。
ItemsSourceProperty = DependencyProperty.Register
("ItemsSource", typeof(IEnumerable), typeof(ItemsControl),
new PropertyChangedCallback(ItemsControl.ItemsSourceChanged));
此方法订阅 ItemsSource
背后的集合,以便调用一个名为 OnCollectionChange
的方法。在此方法中,控件可以管理对其集合所做的修改,并更新其视觉外观。
private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
// ...
this.UpdateContainerForItem(e.NewStartingIndex);
return;
case NotifyCollectionChangedAction.Remove:
//...
this.ClearContainerForItemOverride
(elements[e.OldStartingIndex], e.OldItems[0] as UIElement);
return;
case NotifyCollectionChangedAction.Replace:
//...
this.ClearContainerForItemOverride
(elements[e.NewStartingIndex], e.OldItems[0] as UIElement);
elements.RemoveAt(e.NewStartingIndex);
this.UpdateContainerForItem(e.NewStartingIndex);
return;
case NotifyCollectionChangedAction.Reset:
this.ClearVisualChildren(this.GetItems());
break;
default:
return;
}
}
此方法的代码已截断以保持“可读性”。
理论结束,我们开始实践部分。
我的 DOM
我的 TreeView
控件基于我们刚才看到的两个理论。
我有三个重要的类
TreeNodeCollection
,由 Node 为其子项使用。此类继承自 Control。TreeView
,继承自TreeNodeCollection
。TreeNode
,继承自ContentControl
。
treenode
期望其视觉部分中有大约 10 个部分。
[TemplatePart(Name = "Normal Expand State", Type = typeof(Storyboard)),
TemplatePart(Name = "Normal Collapse State", Type = typeof(Storyboard)),
TemplatePart(Name = "Selected Collapse State", Type = typeof(Storyboard)),
TemplatePart(Name = "Selected Expand State", Type = typeof(Storyboard)),
TemplatePart(Name = "NodeIcon Expand State", Type = typeof(Storyboard)),
TemplatePart(Name = "NodeIcon Collapse State", Type = typeof(Storyboard)),
TemplatePart(Name = "MouseOver Collapse State", Type = typeof(Storyboard)),
TemplatePart(Name = "MouseOver Expand State", Type = typeof(Storyboard)),
TemplatePart(Name = "RootElement", Type = typeof(FrameworkElement)),
TemplatePart(Name = "ExpandedNodeIconZone", Type = typeof(FrameworkElement)),
TemplatePart(Name = "ContentZone", Type = typeof(FrameworkElement)),
TemplatePart(Name = "NodesPresenter", Type = typeof(FrameworkElement)),
TemplatePart(Name = "SelectionZone", Type = typeof(FrameworkElement))]
这将为你提供自由更改皮肤和行为的机会。
我的 treeview
与 Windows Forms 的 treeview
类类似,具有相同的事件和方法。
我将在未来几天添加一些其他内容。
public interface ITreeView
{
event Arcane.Silverlight.Controls.TreeViewEventHandler AfterCollapse;
event Arcane.Silverlight.Controls.TreeViewEventHandler AfterExpand;
event Arcane.Silverlight.Controls.TreeViewEventHandler AfterSelect;
event Arcane.Silverlight.Controls.TreeViewCancelEventHandler BeforeCollapse;
event Arcane.Silverlight.Controls.TreeViewCancelEventHandler BeforeExpand;
event Arcane.Silverlight.Controls.TreeViewCancelEventHandler BeforeSelect;
event Arcane.Silverlight.Controls.TreeNodeMouseClickEventHandler NodeMouseClick;
event Arcane.Silverlight.Controls.TreeNodeMouseClickEventHandler
NodeMouseDoubleClick;
event Arcane.Silverlight.Controls.TreeNodeMouseHoverEventHandler NodeMouseHover;
System.Windows.DataTemplate NodeTemplate { get; set; }
Arcane.Silverlight.Controls.TreeNode SelectedNode { get; set; }
}
示例
示例显示了两种皮肤(基础皮肤和 WinForm 皮肤)以及两种插入数据的方式。

第一种方式,在 XAML 中
<src:TreeView Margin="10, 10, 10, 10" x:Name="myTreeView" Grid.Column="0" Grid.Row="0">
<src:TreeNode Background="Transparent">
<TextBlock Text="Hello"></TextBlock>
</src:TreeNode>
<src:TreeNode Background="Transparent">
<TextBlock Text="This is a test for a long text node !
Yeah, that's great !"></TextBlock>
</src:TreeNode>
<src:TreeNode Background="Transparent">
<TextBlock Text="Node 2"></TextBlock>
<src:TreeNode.Nodes>
<src:TreeNodeCollection>
<src:TreeNode>
<TextBlock Text="Node 2.1"></TextBlock>
</src:TreeNode>
<src:TreeNode>
<TextBlock Text="Node 2.2"></TextBlock>
</src:TreeNode>
<src:TreeNode>
<TextBlock Text="Node 2.3"></TextBlock>
<src:TreeNode.Nodes>
<src:TreeNodeCollection>
<src:TreeNode>
<TextBlock Text="Node 2.3.1"></TextBlock>
</src:TreeNode>
<src:TreeNode>
<TextBlock Text="Node 2.3.2"></TextBlock>
</src:TreeNode>
<src:TreeNode>
<TextBlock Text="Node 2.3.3"></TextBlock>
</src:TreeNode>
</src:TreeNodeCollection>
</src:TreeNode.Nodes>
</src:TreeNode>
<src:TreeNode>
<TextBlock Text="Node 2.4"></TextBlock>
</src:TreeNode>
<src:TreeNode>
<TextBlock Text="Node 2.5"></TextBlock>
</src:TreeNode>
<src:TreeNode>
<TextBlock Text="Node 2.1"></TextBlock>
</src:TreeNode>
</src:TreeNodeCollection>
</src:TreeNode.Nodes>
</src:TreeNode>
<src:TreeNode Background="Transparent">
<TextBlock Text="Node 3"></TextBlock>
<src:TreeNode.Nodes>
<src:TreeNodeCollection>
<src:TreeNode>
<TextBlock Text="Node 3.1"></TextBlock>
</src:TreeNode>
<src:TreeNode>
<TextBlock Text="Node 3.2"></TextBlock>
</src:TreeNode>
<src:TreeNode>
<TextBlock Text="Node 3.3"></TextBlock>
</src:TreeNode>
</src:TreeNodeCollection>
</src:TreeNode.Nodes>
</src:TreeNode>
<src:TreeNode Background="Transparent">
<TextBlock Text="Node 4"></TextBlock>
</src:TreeNode>
<src:TreeNode Background="Transparent">
<TextBlock Text="Node 5"></TextBlock>
</src:TreeNode>
<src:TreeNode Background="Transparent">
<TextBlock Text="Node 6"></TextBlock>
</src:TreeNode>
<src:TreeNode Background="Transparent">
<TextBlock Text="Node 7"></TextBlock>
</src:TreeNode>
<src:TreeNode Background="Transparent">
<TextBlock Text="Node 8"></TextBlock>
<src:TreeNode.Nodes>
<src:TreeNodeCollection>
<src:TreeNode>
<TextBlock Text="Node 8.1"></TextBlock>
</src:TreeNode>
<src:TreeNode>
<TextBlock Text="Node 8.2"></TextBlock>
<src:TreeNode.Nodes>
<src:TreeNodeCollection>
<src:TreeNode>
<TextBlock Text="Node 8.2.1"></TextBlock>
</src:TreeNode>
<src:TreeNode>
<TextBlock Text="Node 8.2.2"></TextBlock>
</src:TreeNode>
<src:TreeNode>
<TextBlock Text="Node 8.2.3"></TextBlock>
</src:TreeNode>
</src:TreeNodeCollection>
</src:TreeNode.Nodes>
</src:TreeNode>
<src:TreeNode>
<TextBlock Text="Node 8.3"></TextBlock>
<src:TreeNode.Nodes>
<src:TreeNodeCollection>
<src:TreeNode>
<TextBlock Text="Node 8.3.1"></TextBlock>
</src:TreeNode>
<src:TreeNode>
<TextBlock Text="Node 8.3.2"></TextBlock>
</src:TreeNode>
</src:TreeNodeCollection>
</src:TreeNode.Nodes>
</src:TreeNode>
</src:TreeNodeCollection>
</src:TreeNode.Nodes>
</src:TreeNode>
<src:TreeNode Background="Transparent">
<TextBlock Text="Node 9"></TextBlock>
</src:TreeNode>
</src:TreeView>
并在代码内部
this.treeview.BeforeExpand += new TreeViewCancelEventHandler(treeview_BeforeExpand);
//a business data
SampleNodeData data = new SampleNodeData();
data.Name = "Adventures works !";
BitmapImage image = new BitmapImage();
image.UriSource = new Uri(HtmlPage.Document.DocumentUri.AbsoluteUri.Replace(
"Arcane.Silverlight.ControlsTestPage.aspx", "database.png"));
data.NodeImage = image;
image = new BitmapImage();
image.UriSource = new Uri(HtmlPage.Document.DocumentUri.AbsoluteUri.Replace(
"Arcane.Silverlight.ControlsTestPage.aspx", "databaseopen.png"));
data.SelectedNodeImage = image;
TreeNode node = this.treeview.Add(data);
node.Nodes = new TreeNodeCollection();
node.Tag = "Database";
使用 DataTemplate
来显示简单的业务数据。
<src:TreeView x:Name="myTreeViewDataBinded"
Margin="10, 10, 10, 10" Background="White"
ItemContainerStyle="{StaticResource WinFormTreeView}"
Grid.Column="1" Grid.Row="0" Width="300" Height="300">
<src:TreeView.ItemTemplate>
<DataTemplate>
<Grid Background="Transparent">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="{Binding Name}"/>
</Grid>
</DataTemplate>
</src:TreeView.ItemTemplate>
lt;/src:TreeView>
然后,最后,我在我的 treeview
中设置了两个皮肤:第一个在 generic.Xaml 中。这是第一个 treeview
(左侧)使用的默认皮肤。第二个更漂亮的皮肤是我在 app.xaml 文件中制作的自定义皮肤。你可以非常轻松地创建自己的皮肤。
你可以问我任何你想问的问题,我比写英语更擅长阅读英语。:)
历史
对 TreeNode.Nodes
属性所做的更改:set 现在是 public
。
我将尽快更新我的工作。DOM 不会改变,但会有新增内容。