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

WPF 控件模式。(WPF 和 XAML 代码重用模式的简易示例。第一部分)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (55投票s)

2014 年 6 月 25 日

CPOL

25分钟阅读

viewsIcon

62388

downloadIcon

1120

本文描述了用于代码和功能重用的 WPF 和 XAML 模式。

引言

有一种流行的误解,认为 XAML 不允许良好地分离关注点和代码重用。实际上,情况恰恰相反。WPF 实现了独特的编程技术,一旦你理解了它,就会将代码重用和定制提升到一个新的水平。

WPF 和 XAML 引入了许多革命性的编程概念,这些概念可以应用于 WPF 之外的纯非可视化编程,并且可以用于其他(非 .NET)平台。

在我看来,WPF 风格的概念比通常的面向对象编程更进一步,其重要性类似于 OOP 与过程式编程的比较。

不幸的是,微软领导层本身并没有完全理解他们的工程师所提出的东西,并改变了他们的优先级。

如上所述,WPF 概念可以被泛化并应用于 WPF 领域之外,例如构建非可视化软件。事实上,我发表了一系列关于在 WPF 领域之外应用 WPF 范式的博客文章。然而,本文不涉及这一点。它只是通过使用 WPF 示例来呈现和分类这些模式。

使用下面描述的 XAML/WPF 范式,我能够以极快的速度进行编程,同时生成高质量、高密度的代码(具有大量重用和单位代码功能丰富的代码)。

这不是 WPF 初学者教程——我假设读者熟悉 WPF 基本概念——依赖项属性和附加属性、绑定、样式、模板、数据模板、触发器、路由事件。

我计划将本文作为 2 或 3 篇文章系列的第一部分。这一部分致力于基于 `Control` 和 `ControlTemplate` 的重用模式。后续文章将更多地讨论 MVVM 模式以及如何将非可视化对象映射到可视化对象。

控件和控件模板

控件

WPF 控件是 WPF 中基本的视觉重用单元之一。控件可以放置在 XAML 代码中,包括其他控件,并使用各种方法协同工作。这里我们将通过非常简单的例子来介绍 WPF 控件编程的注意事项。

让我们首先考虑一个非常简单的控件——它将允许用户在可编辑字段中输入一些文本(例如使用 `TextBox`),它将为该可编辑字段提供一个标签。(该标签将给出字段的名称,解释用户正在输入什么)。此外,该控件还将有一个“保存” `Button`,作为对输入的文本执行某些操作的示例。我们将把这个控件命名为 `EditableTextAndLabelControl`。

反模式 - 带有代码隐藏的用户控件

创建此类控件最简单也是最糟糕的方法是使用 WPF 的 `UserControl` 功能。此反模式的代码位于 `UserControlWithCodeBehind` 项目下。不幸的是,Visual Studio 提供了一种现成的创建用户控件的方法——您只需在解决方案资源管理器中右键单击项目,选择“添加”菜单下的“新建项”,然后选择“用户控件 (WPF)”(不要只选择“用户控件”——这会创建一个 Win 窗体用户控件)。将控件命名为“EditableTextAndLabelControl”。

点击确定按钮后,你会看到创建了两个文件:XAML 文件“EditableTextAndLabelControl.xaml”和代码隐藏文件“EditableTextAndLabelControl.xaml.cs”。XAML 文件将用于指定控件的布局,而代码隐藏可以用于定义控件的属性和行为。代码隐藏可以通过名称访问 XAML 中定义的控件,这非常方便(但这是使用代码隐藏编程的一个欺骗性优势)。

在 `MainWindow` 中,我们显示了两个这样的控件,标签分别为“MyText”和“MyOtherText”。当按下相应控件的“保存”按钮时,控件的 `SaveEvent` 会触发。它在 `MainWindow` 级别处理,我们会在控件下方打印以下行,例如:“已保存标签 'MyText' 的字符串 'Hello'”。

这是 `MainWindow` 类 XAML 代码的重要部分

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="*"/>
    </Grid.RowDefinitions>
    <this:EditableTextAndLabelControl x:Name="MyUserControl1"
                                      HorizontalAlignment="Center"
                                      VerticalAlignment="Center"
                                      TheLabel="MyText"
                                      Margin="0,10"/>
    <this:EditableTextAndLabelControl x:Name="MyUserControl2"
                                      Grid.Row="1"
                                      HorizontalAlignment="Center"
                                      VerticalAlignment="Center"
                                      TheLabel="MyOtherText"                                         
                                      Margin="0,10"/>
    <TextBlock x:Name="TheSaveEventLog"
               Grid.Row="2"
               HorizontalAlignment="Stretch"
               VerticalAlignment="Stretch"/>
</Grid>

我们简单地定义了两个控件,并在它们下方定义了一个 `TextBlock` 来显示 `SaveEvent` 触发的结果。

`MainWindow.xaml.cs` 代码也非常简单

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        MyUserControl1.SaveEvent += MyUserControl_SaveEvent;
        MyUserControl2.SaveEvent += MyUserControl_SaveEvent;
    }

    void MyUserControl_SaveEvent(string arg1, string arg2)
    {
        TheSaveEventLog.Text += "\nSaved string \"" + arg2 + "\" for label \"" + arg1 + "\"";
    }
}  

我们通过名称访问两个用户控件,并将 `MyUserControl_SaveEvent` 分配为它们 `SaveEvent` 的处理程序。`MyUserControl_SaveEvent` 函数向 `TheSaveEventLog` 控件的 `Text` 属性添加一行新内容。

现在让我们看看 `EditableTextAndLabelControl` 本身的代码

。控件的代码隐藏定义了控件上的两个依赖属性 `TheLabel` 和 `TheEditableText`。它还定义了当按钮被点击时触发的 `SaveEvent`。

public partial class EditableTextAndLabelControl : UserControl
{
public event Action<string, string> SaveEvent = null;

public EditableTextAndLabelControl()
{
    InitializeComponent();

    SaveButton.Click += SaveButton_Click;
}

// fires SaveEvent of the control
void SaveButton_Click(object sender, RoutedEventArgs e)
{
    if (SaveEvent != null)
    {
        SaveEvent(TheLabel, TheEditableText);
    }
}

#region TheLabel Dependency Property
public string TheLabel
{
    get { return (string)GetValue(TheLabelProperty); }
    set { SetValue(TheLabelProperty, value); }
}

public static readonly DependencyProperty TheLabelProperty =
DependencyProperty.Register
(
    "TheLabel",
    typeof(string),
    typeof(EditableTextAndLabelControl),
    new PropertyMetadata(null)
);
#endregion TheLabel Dependency Property


#region TheEditableText Dependency Property
public string TheEditableText
{
    get { return (string)GetValue(TheEditableTextProperty); }
    set { SetValue(TheEditableTextProperty, value); }
}

public static readonly DependencyProperty TheEditableTextProperty =
DependencyProperty.Register
(
    "TheEditableText",
    typeof(string),
    typeof(EditableTextAndLabelControl),
    new PropertyMetadata(null)
);
#endregion TheEditableText Dependency Property
}  

请注意,当“保存”按钮被点击时触发 `SaveEvent` 的机制包含在代码隐藏 `SaveButton.Click += SaveButton_Click;` 中,而控件和依赖属性之间的绑定则在 XAML 中定义。

<StackPanel Orientation="Horizontal">
    <TextBlock x:Name="TheLabelTextBlock"
               Text="{Binding Path=TheLabel, RelativeSource={RelativeSource AncestorType=this:EditableTextAndLabelControl}}"
               VerticalAlignment="Bottom"/>

    <TextBox x:Name="TheEditableTextTextBox"
             Grid.Column="1"
             Width="100"
             Text="{Binding Path=TheEditableText, RelativeSource={RelativeSource AncestorType=this:EditableTextAndLabelControl}}"
             VerticalAlignment="Bottom"
             Margin="10,0,10,0"/>

    <Button x:Name="SaveButton"
            Content="Save"
            Width="70"
            Grid.Column="2"
            VerticalAlignment="Bottom"/>
</StackPanel>  

模式 - 无外观控件

上面介绍的(反)模式的问题在于,控件与视觉表示高度耦合。假设我们想要控件的不同视觉表示,例如我们想水平显示其中一个控件(如上),而垂直显示另一个控件,即标签位于 `TextBox` 的顶部,底部带有“保存” `Button`。我们将不得不创建两个完全不同的控件,它们具有相同的代码隐藏功能,甚至非常相似但不完全相同的 XAML 代码。

在本节中,我们创建了一个非常相似的无外观控件,并展示它如何拥有不同的视觉表示。本节的代码包含在 `LooklessControlSample` 项目下。

当你构建一个无外观控件时,不要试图首先确定它的视觉表现。相反,考虑它有哪些属性和事件,以便与其他控件或视图模型进行交互。

就像上一节一样,创建一个新项,但不要选择“用户控件 (WPF)”作为项类型,而只需创建一个 C# 类 `EditableTextAndLabelControl`。将该类设为公共,并使其继承自 WPF `Control` 类。

public class EditableTextAndLabelControl : Control
{
}  

就像在之前的示例中一样,向它添加两个 `string` 类型的依赖属性:`TheLabel` 和 `TheEditableText`;另外,添加类型为 `Action` 的 `SaveEvent`。最好使用 propdp 代码段在类中定义依赖属性。结果将如下所示:

public partial class EditableTextAndLabelControl : Control
{
    public event Action<string, string> SaveEvent = null;


    #region TheLabel Dependency Property
    public string TheLabel
    {
        get { return (string)GetValue(TheLabelProperty); }
        set { SetValue(TheLabelProperty, value); }
    }

    public static readonly DependencyProperty TheLabelProperty =
    DependencyProperty.Register
    (
        "TheLabel",
        typeof(string),
        typeof(EditableTextAndLabelControl),
        new PropertyMetadata(null)
    );
    #endregion TheLabel Dependency Property


    #region TheEditableText Dependency Property
    public string TheEditableText
    {
        get { return (string)GetValue(TheEditableTextProperty); }
        set { SetValue(TheEditableTextProperty, value); }
    }

    public static readonly DependencyProperty TheEditableTextProperty =
    DependencyProperty.Register
    (
        "TheEditableText",
        typeof(string),
        typeof(EditableTextAndLabelControl),
        new PropertyMetadata(null)
    );
    #endregion TheEditableText Dependency Property
}

基本上就是这样了——我们已经定义了无外观控件。让我们将控件放置到 MainWindow.xaml 文件中,看看它们的样子。这是 MainWindow.xaml 的 XAML 代码(实际上它与上一个示例的代码相同)。

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="*"/>
    </Grid.RowDefinitions>
    <this:EditableTextAndLabelControl x:Name="MyControl1"
                                      HorizontalAlignment="Center"
                                      VerticalAlignment="Center"
                                      TheLabel="MyText"
                                      Margin="0,10"/>
    <this:EditableTextAndLabelControl x:Name="MyControl2"
                                      Grid.Row="1"
                                      HorizontalAlignment="Center"
                                      VerticalAlignment="Center"
                                      TheLabel="MyOtherText"                                         
                                      Margin="0,10"/>
    <TextBlock x:Name="TheSaveEventLog"
               Grid.Row="2"
               HorizontalAlignment="Stretch"
               VerticalAlignment="Stretch"/>
</Grid>

如果我们就这样尝试运行项目,将会得到一个空窗口。原因是,我们从未为无外观控件定义视觉表示。我们应该将其定义为 `ControlTemplate`。为了清晰起见,XAML 编码人员通常会为模板和样式创建一个特殊的项目文件夹。让我们在项目中创建文件夹“Themes”,并在其中创建一个 WPF 资源字典 SampleControls.xaml。我们可以通过将以下行添加到资源字典的 header 标签来定义 XML 命名空间 `this`:`xmlns:this="clr-namespace:LooklessControlSample"`。然后,在 `ResourceDictionary` 中,我们可以定义我们的控件模板。

<ResourceDictionary  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                     xmlns:this="clr-namespace:LooklessControlSample">
    <ControlTemplate x:Key="TheHorizontalTemplateForEditableTextAndLabelControl">
        <StackPanel Orientation="Horizontal">
            <TextBlock x:Name="TheLabelTextBlock"
                       Text="{Binding Path=TheLabel, RelativeSource={RelativeSource AncestorType=this:EditableTextAndLabelControl}}"
                       VerticalAlignment="Bottom" />

            <TextBox x:Name="TheEditableTextTextBox"
                     Grid.Column="1"
                     Width="100"
                     Text="{Binding Path=TheEditableText, RelativeSource={RelativeSource AncestorType=this:EditableTextAndLabelControl}}"
                     VerticalAlignment="Bottom"
                     Margin="10,0,10,0" />

            <Button x:Name="SaveButton"
                    Content="Save"
                    Width="70"
                    Grid.Column="2"
                    VerticalAlignment="Bottom" />
        </StackPanel>
    </ControlTemplate>
<ResourceDictionary/>  

为了让 MainWindow.xaml 文件注意到 SampleControls.xaml 文件,我们需要在其顶部添加以下 XAML 代码。

<Window.Resources>
    <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>
            <ResourceDictionary Source="Themes/SampleControls.xaml" />
        </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
</Window.Resources>  

请注意,如果 XAML 资源字典文件位于不同的项目中,则上述 `Source` 属性的简短表示法不足,还需要指定程序集名称。

为了让控件识别它们具有该模板,我们将它们的 `Template` 属性设置为 `"{StaticResource TheHorizontalTemplateForEditableTextAndLabelControl}"`。

现在,如果我们运行代码,将看到一个与我们上一个示例几乎相同的应用程序,只是当我们按下控件上的“保存”按钮时,什么也不会发生。

有 3 种主要方法可以让无外观控件感知其视觉效果上发生的事件

  1. 使用内置的 WPF 命令功能
  2. 使用 MS Expression Blend SDK
  3. 通过 C# 代码访问控件的视觉部分。

我们将在下面描述所有这些方法。

使用内置的 WPF 命令功能在无外观控件的视觉和非视觉部分之间进行通信

这是大多数 WPF 教程描述的方式,也是视觉和非视觉功能之间通信最糟糕的方式。它之所以糟糕,原因有二:

  1. 只有少数控件上的少数事件,例如 `Button` 或 `Menu` 控件上的 `Click` 事件,可以使用命令功能进行处理。
  2. 它需要修改无外观控件类,为其添加一个 `DelegateCommand` 对象。

由于我们不认为这是视觉和非视觉功能之间通信的好方法,而且它已经在其他地方广泛讨论过,因此我们在此不提供示例。

 

使用 MS Expression Blend SDK 在无外观控件的视觉和非视觉部分之间进行通信

这可能是将事件从视觉(XAML)传递到非视觉代码最优雅的方式。我已经在 MVVM Pattern Made Simple 一文中详细描述了这种方法,并将其应用于 MVVM 模式。相同的原理也可以应用于无外观控件。

首先,不要被“Expression Blend”这个词吓到——这只是一个 SDK,你不需要安装 MS Expression Blend 就能获取它。事实上,它可以从 MS Expression Blend SDK 免费下载和使用。最重要的是,我们只需要 SDK 中的两个 dll 文件:Microsoft.Expression.Interactions.dll 和 System.Windows.Interactivity.dll,并且我们提供这些文件以及我们的代码在 MSExpressionBlendSDKDlls 文件夹中。

由于这些文件是从互联网下载的,您可能需要解除它们。要解除它们,右键单击每个文件,选择“属性”菜单项,然后单击“解除阻止”按钮。

当然,项目需要引用这些文件,并且为了访问 SDK 功能,应将以下 xml 命名空间添加到“SampleControls.xaml”的顶层标签中。

xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"  

让我们向我们的无外观控件 `EditableTextAndLabelControl` 添加一个方法 `Save`。

public void Save()
{
    if (SaveEvent != null)
        SaveEvent(TheLabel, TheEditableText);
}

这个方法只是简单地触发 `SaveEvent`。

现在我们可以通过在 `

© . All rights reserved.