65.9K
CodeProject 正在变化。 阅读更多。
Home

Silverlight 2b1 的简单 Treeview

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.90/5 (27投票s)

2008年4月23日

CPOL

4分钟阅读

viewsIcon

150204

downloadIcon

1744

在 Silverlight 中创建可模板化的树状视图

引言

首先,请接受我蹩脚英语的歉意。:)

本文讨论了在 Silverlight 中创建一个简单的、可模板化的 treeview。我的目标是向读者解释如何通过继承 ControlContentControl 类来从头开始创建一个真正的控件(而不是一个简单的 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)与一个类型关联起来。这个键必须在逻辑部分与 UIElementStoryboard 关联。我们在这里可以看到两个重要部分:“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 代码来改变控件的行为。你只需要在模板中为你自己的 UIElementStoryboard 指定与模板部分中指示的相同的名称。

视觉部分的模板定义在你的资源中。所以,在 generic.xamlSystem.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 皮肤)以及两种插入数据的方式。

Arcane

第一种方式,在 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 不会改变,但会有新增内容。

© . All rights reserved.