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

WPF 无外观控件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.98/5 (42投票s)

2015 年 11 月 15 日

CPOL

14分钟阅读

viewsIcon

69676

downloadIcon

837

无外观控件与用户控件。无外观控件的使用模式

引言

我在一家最近才开始使用 WPF 的公司工作。因此,许多软件工程师正在从其他平台和语言转向 C# WPF 编程。在 WPF 中创建可重用视觉控件最简单、最直观的方法是利用 VS2015 内置的 UserControl 模板。

本文的目的是说明,无外观控件实际上比 UserControls 是更好的选择。如下文所示,无外观控件提供了更高的灵活性和更好的关注点分离。

我还会讨论无外观控件的最佳实践和重用模式。

读者 'webmaster442' 指出,大多数 WPF 文献将无外观控件称为“自定义控件”。我想避免使用这个名称,原因有两个:

  1. 一些读者可能会被这个名称混淆,认为用户控件也是自定义控件,因为它们是可以自定义创建的。
  2. Visual Studio 提供了一个“自定义控件”模板,它几乎无用且令人困惑,从我的角度来看没有任何实际意义。所以,我不想鼓励读者使用它。

本文适合刚开始接触 WPF 并对如何改进 WPF 编码实践感到好奇的人。

本文涵盖以下主题:

用户控件示例

在 WPF(和其他框架)中,控件的创建是为了封装一些可重用的 UI 功能部分。

让我们先通过一个创建和(重用)WPF UserControl 的例子开始。

本示例的代码可以在 UserControlSample 项目下找到。

运行此项目,您将看到以下窗口:

窗口中有两行,一行用于输入姓名,另一行用于输入密码。

每行都包含一个标签(不可编辑文本)、一个可编辑文本框和一个“X”按钮,点击该按钮可清除相应的文本框。

用于输入姓名和密码的两行代表了同一个 EditableTextAndLabelUserControl 类的两个非常简单的实例。

以下是创建窗口的 MainWindow.xaml 代码:

<Window x:Class="UserControlSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:UserControlSample"
        Title="MainWindow"
        Height="350"
        Width="525">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="30"/>
            <RowDefinition Height="30"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <!-- user control for entering the name -->
        <local:EditableTextAndLabelUserControl Label="Enter your name:" 
                                               HorizontalAlignment="Left"
                                               VerticalAlignment="Center"
                                               Margin="10,0,0,0"/>
        
        <!-- user control for entering the passcode -->
        <local:EditableTextAndLabelUserControl Label="Enter your passcode:"
                                               HorizontalAlignment="Left"
                                               VerticalAlignment="Center" 
                                               Margin="10,0,0,0"
                                               Grid.Row="1"/>
    </Grid>
</Window>

EditableTextAndLabelUserControl 的代码也包含在同一个项目中,位于 XAML 文件 EditableTextAndLabelUserControl.xaml 和 C# 文件 EditableTextAndLabelUserControl.xaml.cs 中。(有关如何在 Visual Studio 中创建 UserControl 的说明,请参阅 附录 A:创建用户控件。)

EditableTextAndLabelUserControl.xaml 文件包含以下简单的 XAML:

<UserControl x:Class="UserControlSample.EditableTextAndLabelUserControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:UserControlSample">
    <StackPanel Orientation="Horizontal"
                VerticalAlignment="Stretch">
        <TextBlock x:Name="TheLabel"
                   Margin="0,0,2,0"/>
        <TextBox x:Name="TheTextBox"
                 Width="100"
                 Margin="2,0"/>
        <Button x:Name="TheClearButton"
                Content="X"
                Width="30"/>
    </StackPanel>
</UserControl>

标签由名为 TheLabelTextBlock 表示;可编辑文本由名为 TheTextBoxTextBox 表示;按钮由名为 TheClearButtonButton 控件表示(我喜欢在 XAML 名称前加上“The”前缀,以区分它们与类型名称)。

WPF XAML 解析引擎会创建与控件名称对应的变量。这些变量可以从代码隐藏(在本例中位于 EditableTextAndLabelUserControl.xaml.cs 文件中)访问。这段代码也非常简单:

public partial class EditableTextAndLabelUserControl : UserControl
{
    public EditableTextAndLabelUserControl()
    {
        InitializeComponent();

        TheClearButton.Click += TheClearButton_Click;
    }

    private void TheClearButton_Click(object sender, RoutedEventArgs e)
    {
        Clear();
    }

    public void Clear()
    {
        EditableText = null;
    }

    public string Label
    {
        get
        {
            return TheLabel.Text;
        }

        set
        {
            TheLabel.Text = value;
        }
    }

    public string EditableText
    {
        get
        {
            return TheTextBox.Text;
        }

        set
        {
            TheTextBox.Text = value;
        }
    }
}  

它提供了 public string 属性 LabelEditableText,作为与应用程序其余部分通信的手段,以及一个在点击“X”按钮时调用的 Clear() 方法(此调用是通过按钮的单击事件处理程序连接的)。

用户控件的缺点

尽管 UserControls 可以像它们那样简单而强大,但它们有一个固有的缺点——视觉表示永久地与 C# 代码绑定。上面讨论的 EditableTextAndLabelUserControl 控件的视觉表示无法更改——它将永远保持一个 TextBlock,后面跟着一个 TextBox,再后面跟着一个 Button,它们水平排列。公平地说——通过参数化(就像下面将为无外观控件讨论的那样),可以为 UserControls 实现一些额外的自由度,但它们的**核心视觉表示是固定的,永远与 C# 代码隐藏绑定**。

无外观控件完全克服了这些限制。它们通常由一组非视觉方法和属性组成,并且不对视觉实现施加任何约束。无外观控件的视觉实现由 XAML 模板和样式提供,并且可以非常轻松地更改,而**无需对控件代码本身或无外观控件与应用程序其余部分的交互方式进行任何更改**。

无外观控件示例

用于简单无外观控件示例的代码位于 LooklessControlSample 项目下。运行时,它会显示与上面用户控件示例完全相同的窗口。

查看 EditableTextAndLabelControl.cs 文件中的 EditableTextAndLabelControl 类。(注意,它是一个简单的 C# 文件——创建它不需要特殊的 VS 模板。)

该类派生自 System.Windows.Controls.Control 类,并包含两个字符串依赖项属性 LabelEditableText,以及一个 Clear 方法(该方法仅将 EditableText 属性设置为 null)。

public class EditableTextAndLabelControl : Control
{
    #region Label Dependency Property
    public string Label
    {
        get { return (string)GetValue(LabelProperty); }
        set { SetValue(LabelProperty, value); }
    }

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

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

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

    public void Clear()
    {
        this.EditableText = null;
    }
}  

对于不熟悉依赖项属性的人来说,依赖项属性的代码可能看起来很糟糕,但使用 Visual Studio 内置的 propdp 代码片段可以非常轻松地创建它们,只需键入 propdp,然后提供依赖项属性的名称、类型和默认值。您可以按 Tab 键在依赖项属性的各个部分之间切换,完成后按“Enter”。

我的 propdp 代码片段(位于 CODE/Snippets 文件夹下)是 Visual Studio 自带代码片段的一个稍作改进的版本。它以更垂直的方式排列代码,自动提供容器类名,并为每个依赖项属性创建一个区域。可以将此代码片段复制到您的 NetFX30 代码片段文件夹中,以替换默认的 propdp 代码片段。有关安装此代码片段的详细说明,请参阅 附录 B:安装自定义 propdp 代码片段

定义了依赖项属性后,就可以像使用普通属性一样使用它,但具有一些额外功能,特别是依赖项属性

  1. 提供 WPF 绑定能够理解的更改通知,因此可以作为 WPF 绑定的源属性
  2. 可以用作 WPF 绑定目标属性(普通属性不能用于此目的)
  3. 可以被 WPF 样式修改
  4. 可以被动画化

我们最关心的是依赖项属性可以作为 WPF 绑定的源属性和目标属性。这对于控件能够与应用程序的其余部分进行通信非常重要。

为简单起见,我们将无外观控件的 ControlTemplate 放在 MainWindow.xaml 文件中——位于 Window.Resources 部分。

<ControlTemplate x:Key="PlainHorizontalEditableTextTemplate"
                 TargetType="local:EditableTextAndLabelControl">
    <StackPanel Orientation="Horizontal"
                VerticalAlignment="Stretch">
        <!-- bound to the Label dependency property of the control -->
        <TextBlock x:Name="TheLabel"
                   Text="{Binding Path=Label,
                                  RelativeSource={RelativeSource TemplatedParent}}"
                   Margin="0,0,2,0" />
        <!-- bound to the EditableText dependency property of the control -->
        <TextBox x:Name="TheTextBox"
                 Text="{Binding Path=EditableText,
                                Mode=TwoWay,
                                RelativeSource={RelativeSource TemplatedParent}}"
                 Width="100"
                 Margin="2,0" />

        <!-- We use Expression Blend's SDK's EventTrigger and CallMethodAction
             objects to wire Clear() method of the control 
             to the Click event of the button -->
        <Button x:Name="TheClearButton"
                Content="X"
                Width="30">
            <i:Interaction.Triggers>
                <i:EventTrigger EventName="Click">
                    <ei:CallMethodAction MethodName="Clear"
                     TargetObject="{Binding RelativeSource={RelativeSource TemplatedParent}}"/>
                </i:EventTrigger>
            </i:Interaction.Triggers>
        </Button>
    </StackPanel>
</ControlTemplate>

请注意,我们使用的是 RelativeSource 设置为 TemplatedParent 模式的绑定,例如:

<TextBox x:Name="TheTextBox"
     Text="{Binding Path=EditableText,
                    Mode=TwoWay,
                    RelativeSource={RelativeSource TemplatedParent}}"  
.../>

RelativeSource TemplatedParent 指示绑定在此 XAML 代码所属的控件的控制模板上搜索源属性——在本例中,它是 EditableTextAndLabelControl

对于 TemplatedParent 绑定有一个简写方式——无需编写所有上述绑定 XAML,只需写:

<TextBox x:Name="TheTextBox"
     Text="{TemplateBinding EditableText}"  
.../>  

我再说一遍,上面的绑定之所以能工作,仅仅是因为我们正在为控件编程一个控件模板。更通用的方法是使用 RelativeSourceAncestorType 属性而不是 TemplatedParent 来按类型查找绑定的源对象。

<TextBox x:Name="TheTextBox"
     Text="{Binding Path=EditableText,
            Mode=TwoWay,
            RelativeSource={RelativeSource AncestorType=local:EditableTextAndLabelControl}}"  
.../>

上面的代码更通用,因为它可以在我们的 EditableTextAndLabelControl 的视觉树下的任何位置工作,而不仅仅是直接模板。这可能很有用,如果有人试图通过使用具有子模板的子控件来最大化 XAML 重用,或者如果使用 DataTemplates 而不是 ControlTemplate(这两个主题都超出了本文的范围)。

ButtonClick 事件通过 Expression Blend SDK 功能连接到 EditableTextAndLabelControlClear() 方法。

<Button x:Name="TheClearButton"
        Content="X"
        Width="30">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="Click">
            <ei:CallMethodAction MethodName="Clear"
                TargetObject="{Binding RelativeSource={RelativeSource TemplatedParent}}" />
        </i:EventTrigger>
    </i:Interaction.Triggers>
</Button>  

您无需安装 MS Expression Blend 即可获得此功能。您只需要引用两个 DLL 文件,它们可以在 CODE/ExpressionBlenSDK 文件夹下找到:Microsoft.Expression.Interactions.dllSystem.Windows.Interactivity.dll

您还需要在这两个命名空间中设置 XAML 文件头部的引用:

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

如上所示,此功能允许在事件触发时挂钩任何控件的任何路由事件,并调用任何可绑定对象上的任何方法。

将事件连接到 C# 功能的更常见方法是使用 WPF 命令。但是,命令由于以下原因不够灵活且更麻烦:

  • 命令只能为少数控件(例如 ButtonsMenus)的 Click 事件触发,而 Expression Blend SDK 功能可用于**任何**控件上的**任何**路由事件。
  • 命令通常需要将它们添加到 C# 代码中,而 Expression Blend SDK 功能可以调用 C# 代码中的任何方法,只要该方法没有参数,或者其参数与路由事件的参数相同。

注意:Expression Blend SDK 功能包含在许多其他流行包中,例如,如果您使用 Prism,您应该已经拥有所需的 Expression Blend SDK DLL。

MainWindow 的代码非常简单,与上一个示例非常相似:

<!-- user control for entering the name -->
<local:EditableTextAndLabelControl Label="Enter your name:"
                                   HorizontalAlignment="Left"
                                   VerticalAlignment="Center"
                                   Margin="10,0,0,0"
                                   Template="{StaticResource 
                                              PlainHorizontalEditableTextTemplate}"/>

<!-- user control for entering the passcode -->
<local:EditableTextAndLabelControl Label="Enter your passcode:"
                                   HorizontalAlignment="Left"
                                   VerticalAlignment="Center"
                                   Margin="10,0,0,0"
                                   Grid.Row="1"
                                   Template="{StaticResource 
                                              PlainHorizontalEditableTextTemplate}"/>  

注意通过 StaticResource 标记扩展引用 ControlTemplate 的地方。

Template="{StaticResource PlainHorizontalEditableTextTemplate}"  

请注意,两个控件的 XAML 代码非常相似:我们对两者都使用了相同的 HorizontalAlignmentVerticalAlignmentMarginTemplate 属性。为了重用更多 XAML 代码,我们可以使用一个设置了这些属性的 WPF Style

<Style x:Key="TheEditableTextAndLabelControlStyle"
       TargetType="local:EditableTextAndLabelControl">
    <Setter Property="HorizontalAlignment"
            Value="Left"/>
    <Setter Property="VerticalAlignment"
            Value="Center"/>
    <Setter Property="Margin"
            Value="10,0,0,0"/>
    <Setter Property="Template"
            Value="{StaticResource PlainHorizontalEditableTextTemplate}"/>
</Style>  

此样式应放在 MainWindow.xaml 文件的 Windows.Resources 部分,位于 Template 定义之下(因为样式引用了模板)。

然后,我们可以从控件中删除 Style 中定义的属性,而是让控件以以下方式引用样式:

<!-- user control for entering the name -->
<local:EditableTextAndLabelControl Label="Enter your name:"
                                   Template="{StaticResource 
                                              PlainHorizontalEditableTextTemplate}"
                                   Style="{StaticResource 
                                           TheEditableTextAndLabelControlStyle}"/>

<!-- user control for entering the passcode -->
<local:EditableTextAndLabelControl Label="Enter your passcode:"
                                   Grid.Row="1"
                                   Style="{StaticResource 
                                           TheEditableTextAndLabelControlStyle}" />  

为了进一步简化 XAML,我们可以通过不设置 x:Key 属性来使 Style 成为所有 EditableTextAndLabelControl 的默认样式。在这种情况下,我们甚至可以从控件中删除对此类 Style 的引用。

<!-- user control for entering the name -->
<local:EditableTextAndLabelControl Label="Enter your name:"
                                   Template="{StaticResource 
                                              PlainHorizontalEditableTextTemplate}"/>

<!-- user control for entering the passcode -->
<local:EditableTextAndLabelControl Label="Enter your passcode:"
                                   Grid.Row="1">   

最后,最佳实践是将 Styles 和 Templates 放在一个单独的 ResourceDictionary 文件中,通常位于一个单独的文件夹中,以便从多个位置访问它们。在接下来的项目中,我们将有一个名为“Styles”的文件夹,其中包含各种控件的 ResourceDictionarys。

为同一个无外观控件使用两种不同的模板

在下一个示例中,我将展示如何为同一个无外观控件创建两种完全不同的模板。该示例位于 TwoDifferentTemplatesForTheSameLooklessControlSample 项目下。

运行项目时,您会看到以下内容:

在左侧和右侧,我都显示了同一个 EditableTextAndLabelControl 控件,但在左侧,我使用了 PlainHorizontalEditableTextStyle Style,而在右侧,我使用了 SomeCrazyEditableTextStyle Style。

<Grid>
    ...
    <local:EditableTextAndLabelControl Label="Enter your name:"
                                       Style="{StaticResource PlainHorizontalEditableTextStyle}"
                                       Grid.Row="1"/>

    ...

    <local:EditableTextAndLabelControl Label="Enter your name:"
                                       Style="{StaticResource SomeCrazyEditableTextStyle}"
                                       Grid.Column="2"
                                       Grid.Row="1"/>
</Grid>  

这两个 Style 都定义在“Styles”项目文件夹下的 LooklessControlStyles.xaml 资源字典文件中。

有关创建资源字典的步骤,请参阅 附录 C:创建 WPF 资源字典文件

LooklessControlStyles.xaml 资源字典定义了两个模板:“PlainHorizontalEditableTextTemplate”(与上一个示例相同)和“SomeCrazyEditableTextTemplate”(新的)。

它还有两个样式:“PlainHorizontalEditableTextStyle”和“SomeCrazyEditableTextStyle”,它们分别使用这两个模板。

我们对“Crazy”样式和模板更感兴趣,因为“Plain”样式以前已经描述过。

这是“Crazy”样式的 XAML:

<Style TargetType="local:EditableTextAndLabelControl"
       x:Key="SomeCrazyEditableTextStyle">
    <Setter Property="HorizontalAlignment"
            Value="Left" />
    <Setter Property="VerticalAlignment"
            Value="Center" />
    <Setter Property="Margin"
            Value="10,0,0,0" />
    <Setter Property="Template"
            Value="{StaticResource SomeCrazyEditableTextTemplate}" />
</Style> 

这是“Crazy”模板:

<ControlTemplate x:Key="SomeCrazyEditableTextTemplate"
                 TargetType="local:EditableTextAndLabelControl">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition />
            <ColumnDefinition />
        </Grid.ColumnDefinitions>
        <!-- bound to the Label dependency property of the control -->
        <Label x:Name="TheLabel"
               Content="{Binding Path=Label,
                                    RelativeSource={RelativeSource TemplatedParent}}"
               Margin="0,0,2,0"
               Background="Red"
               Grid.Row="1"
               RenderTransformOrigin="0.5,0.5">
            <Label.RenderTransform>
                <RotateTransform Angle="-45" />
            </Label.RenderTransform>
        </Label>
        <!-- bound to the EditableText dependency property of the control -->
        <TextBox x:Name="TheTextBox"
                 Text="{Binding Path=EditableText,
                                Mode=TwoWay,
                                RelativeSource={RelativeSource TemplatedParent}}"
                 Width="100"
                 Margin="2,0"
                 Grid.Column="1" />

        <!-- We use Expression Blend's SDK's EventTrigger and CallMethodAction
                 objects to wire Clear() method of the control 
                 to the Click event of the button -->
        <ToggleButton x:Name="TheClearButton"
                      Content="X"
                      Width="30"
                      Grid.Column="2"
                      Grid.Row="1">
            <i:Interaction.Triggers>
                <i:EventTrigger EventName="Click">
                    <ei:CallMethodAction MethodName="Clear"
                                         TargetObject="{Binding 
                                         RelativeSource={RelativeSource TemplatedParent}}" />
                </i:EventTrigger>
            </i:Interaction.Triggers>
        </ToggleButton>
    </Grid>
</ControlTemplate>  

请注意,我们不仅排列控件的方式不同,而且使用了不同的控件——例如,我们使用 Label 控件而不是 TextBlock,使用 ToggleButton 而不是 Button

另请注意,为了使在单独资源字典文件中定义的样式和模板在 MainWindow.xaml 中可见,我们在其 Window.Resources 部分添加了以下代码:

<Window.Resources>
    <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>
            <ResourceDictionary Source="Styles/LooklessControlStyles.xaml"/>
        </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
</Window.Resources>  

通常,引用的资源字典会位于不同的项目中。在这种情况下,合并资源字典的 Source 属性应设置得不同,例如,假设我们引用了另一个名为“Controls”的项目中的“Styles/LooklessControlStyles.xaml”。相应的行会是这样的:

<ResourceDictionary Source="/Controls;Component/Styles/LooklessControlStyles.xaml"/> 

请注意,我们还在项目中的相应路径前添加了“Component”前缀。

参数化无外观控件

通过使 XAML 依赖于无外观控件的一些依赖项属性,我们可以进一步提高 XAML 的重用性。

如果您运行 ParametrizingLooklessControl 项目,然后在 TextBox 中开始输入文本,您将看到标签文本是蓝色的,而可编辑文本是红色的。

以下是 MainWindow.xaml 文件的相关内容:

<local:EditableTextAndLabelControl Label="Enter your name:"
                                   Foreground="Blue"
                                   EditableTextForeground="Red"
                                   Style="{StaticResource PlainHorizontalEditableTextStyle}"/>  

您可以看到控件的 Foreground 属性设置为“Blue”,而 EditableTextForeground 属性设置为“Red”。

每个 WPF Control 都有 Foreground 属性,因此,由于 EditableTextAndLabelControl 类派生自 Control,它会自动获得 Foreground 属性。

但是,依赖项属性 EditableTextForeground 已添加到 EditableTextAndLabelControl 类中:

#region EditableTextForeground Dependency Property
public Brush EditableTextForeground
{
    get { return (Brush)GetValue(EditableTextForegroundProperty); }
    set { SetValue(EditableTextForegroundProperty, value); }
}

public static readonly DependencyProperty EditableTextForegroundProperty =
DependencyProperty.Register
(
    "EditableTextForeground",
    typeof(Brush),
    typeof(EditableTextAndLabelControl),
    new PropertyMetadata(null)
);
#endregion EditableTextForeground Dependency Property  

模板(在 LooklessControlStyles.xaml 文件中定义)将相应 TextBlocks 和 TextBoxes 的 Foreground 属性以如下方式绑定到控件的 ForegroundEditableTextForeground 属性:

<!-- bound to the Label dependency property of the control -->
<TextBlock x:Name="TheLabel"
           Foreground="{Binding Path=Foreground,
                                RelativeSource={RelativeSource TemplatedParent}}"
           Text="{Binding Path=Label,
                          RelativeSource={RelativeSource TemplatedParent}}"
           Margin="0,0,2,0" />
<!-- bound to the EditableText dependency property of the control -->
<TextBox x:Name="TheTextBox"
         Foreground="{Binding Path=EditableTextForeground,
                              RelativeSource={RelativeSource TemplatedParent}}"
         Text="{Binding Path=EditableText,
                            Mode=TwoWay,
                            RelativeSource={RelativeSource TemplatedParent}}"
         Width="100"
         Margin="2,0" />  

从 C# 代码访问无外观控件的视觉元素

有时(尽管不经常),有必要从控件的 C# 代码中访问控件模板中定义的那些部分。顺便说一句,这是UserControls 相对于无外观控件的一个小优势——从 C# 代码中访问 UserControls 中 XAML 定义的视觉元素更容易。

但是,也有相对简单的方法可以访问无外观控件的视觉部分。

项目 AccessingVisualPartsFromCSharpSample 展示了如何做到这一点。

如果您运行项目并单击“Enter your name:”标签,您将看到一个包含文本“label clicked”的模态对话框弹出。

为了继续使用主窗口,您必须先关闭弹出窗口。

查看 LooklessControlStyle.xaml 文件中的控件模板。与之前的示例相比,唯一的变化是 TextBlock 被重命名为“PART_Label”并且具有透明的背景。

<TextBlock x:Name="PART_TheLabel"
           Background="Transparent"
           Text="{Binding Path=Label,
                          RelativeSource={RelativeSource TemplatedParent}}"
           Margin="0,0,2,0" />

PART_”前缀用于可能从 C# 代码中访问的视觉元素的名称,这是 WPF 有用的常见做法。

TextBlock 的背景透明是为了更好地捕获鼠标按下事件(否则,它只会响应单击文本本身时)。

相关的 C# 代码添加位于 EditableTextAndLabelControl.cs 文件的顶部:

public class EditableTextAndLabelControl : Control
{
    FrameworkElement _labelElement = null;

    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate();

        // find _labelElement from the control template
        _labelElement = 
            this.Template.FindName("PART_TheLabel", this) as FrameworkElement;

        // attach event handler to MouseLeftButtonDown event.
        _labelElement.MouseLeftButtonDown += _labelElement_MouseLeftButtonDown;
    }

    private void _labelElement_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
        // create and show a modal window with text "label clicked!"
        Window dialogWindow = new Window() { Width = 150, Height = 100 };

        dialogWindow.Content =
            new TextBox { Text = "label clicked!" };
        dialogWindow.ShowDialog();
    }
...
}

我们重写 OnApplyTemplate() 方法(以确保我们能够找到控件模板中定义的那些部分)。

然后,我们使用 Template.FindName(...) 方法来实际找到控件。

查找模板部分的更强大方法是使用 Generic (Non-WPF) Tree to LINQ and Event Propagation on Trees 中描述的功能。这种方法将在别处讨论。

摘要

在本文中,我解释了为什么 WPF 无外观控件比用户控件更强大,并描述了无外观控件的几种用法模式。

历史记录

  • 2015 年 11 月 16 日 - 添加了目录;解释了使用“无外观控件”一词而不是“自定义控件”
  • 2015 年 11 月 19 日 - 添加了缺失的源代码链接

附录 A:在 Visual Studio 中创建用户控件

要在 Visual Studio 中创建 WPF UserControl,请在解决方案资源管理器中右键单击项目;选择 **添加**->**新建项**;然后在左侧选择 **WPF**,在右侧选择“**用户控件 (WPF)**”,并输入控件名称。

附录 B:安装自定义 propdp 代码片段

您可以通过以下步骤安装随代码提供的自定义 propdp 代码片段(位于 CODE/Snippets 文件夹中):

  1. 打开代码片段管理器,方法是转到 Visual Studio 中的“工具”菜单,然后单击“代码片段管理器”菜单项:
  2. 在代码片段管理器中,将语言更改为 **C#**(使用顶部的下拉列表),然后单击“NetFX30”文件夹:
  3. 复制代码片段管理器中“**位置**”字段中的代码片段文件夹路径。将此位置粘贴到文件资源管理器中。
  4. 将此代码随附的 propdp 文件复制到代码片段位置。
  5. 重新启动您的 Visual Studio。

附录 C:创建 WPF 资源字典文件

您可以通过以下步骤在 Visual Studio 中创建 ResourceDictionary 文件:

  1. 在 VS 解决方案资源管理器中,右键单击您希望创建 ResourceDictionary 的文件夹或项目。
  2. 在左侧选择 WPF 文件夹,在右侧选择“资源字典 (WPF)”:
  3. 选择资源字典文件的名称,然后按“添加”按钮。
© . All rights reserved.