如何将任意内容嵌入 WPF 控件






4.97/5 (19投票s)
许多 WPF 控件都可以在内部显示任意 XAML。我如何才能为我的自定义控件实现这一点?
引言
许多 WPF 控件(例如 Button、Border 等)都可以在内部显示任意 XAML
<Button>any XAML here...</Button>
如何创建具有此功能的自定义控件?也就是说,如果我这样写:
<MyControl><TextBox Text="edit me" /></MyControl>
然后我的控件中就会出现一个文本框?本文将演示如何正确地做到这一点,以及几种错误的方法及其原因。
粗略尝试:用户控件
我的第一次尝试是创建一个用户控件并尝试向其中添加一个 ContentPresenter
。不幸的是,这不起作用,原因如下。
我向项目中添加了一个新的用户控件,并在其 XAML 文件中指定了内容
<UserControl x:Class="MyNamespace.MyControl" ... >
<StackPanel Orientation="Vertical">
<Label>Foo</Label>
<Label>Bar</Label>
</StackPanel>
</UserControl>
然后我尝试像这样在我的窗口中使用该控件:
<Window xmlns:local="clr-namespace:MyNamespace" ...>
<local:MyControl />
</Window>
到目前为止一切看起来都很好。现在有人告诉我 ContentPresenter
类可以在自定义控件中显示用户内容,所以我将其放入我的用户控件的 XAML 定义中:
<!-- don't do this at home -->
<!-- MyControl.xaml -->
<UserControl x:Class="MyNamespace.MyControl" ... >
<StackPanel Orientation="Vertical">
<Label>Foo</Label>
<ContentPresenter />
<Label>Bar</Label>
</StackPanel>
</UserControl>
...
<!-- MyWindow.xaml -->
<Window xmlns:local="clr-namespace:MyNamespace" ...>
<local:MyControl>User supplied content here</local:MyControl>
</Window>
这样做不起作用,因为我们最终会在用户控件 XAML 内部和 Window XAML 内部两次定义用户控件的 Content
属性。Window XAML 在这场争夺中获胜,你唯一能看到的是文本User supplied content here。Foo 和 Bar 都不会显示,ContentPresenter
也没有起到任何作用。
粗制滥造的尝试
事实证明 ContentPresenter
只能在 <ControlTemplate>
中使用。所以,我创建了一个名为 QuickAndDirtyAttempt
的项目,其中包含一个 Decorator
控件(是的,我知道,WPF 已经有一个 Decorator
类型,但我实在想不出更好的名字)。为了以一种强硬的方式达到我们的目标,我只是为用户控件分配了一个控件模板。与此 MSDN 文章相反,将控件模板应用于用户控件是可能的。
<UserControl x:Class="QuickAndDirtyAttempt.Decorator" ...=""
<UserControl.Template>
<ControlTemplate TargetType="{x:Type local:Decorator}">
<StackPanel Orientation="Vertical">
<Label>Foo</Label>
<ContentPresenter />
<Label>Bar</Label>
</StackPanel>
</ControlTemplate>
</UserControl.Template>
</UserControl>
注意模板上的 TargetType 属性:没有它,项目将愉快地编译,但 ContentPresenter 将不起作用。<Window ... >
<StackPanel Orientation="Vertical">
<local:Decorator>
<Label Background="Wheat">User supplied content here</Label>
</local:Decorator>
</StackPanel>
</Window>

成功了!但我们可以做得更好。
为什么粗制滥造的解决方案是粗制的
首先,我们在这里并不真正需要自定义控件。我们可以直接使用一个现有控件,比如 ContentControl,并为其提供一个自定义控件模板。在 80% 的情况下,这是正确的解决方案。
WPF 的核心在于将“外观和感觉”与呈现逻辑分离。WPF 控件被称为“无外观”。例如,复选框控件只处理复选框的状态(开、关、未定义)以及这些状态之间的转换。它不定义复选框的外观。系统为您提供了一个默认的标准外观,但如果您想要一个带有圆形按钮和其下方文本的复选框,您只需要提供一个备用控件模板。
如果您需要处理额外的逻辑,例如用户输入、可绑定属性等,那么您将需要一个自定义控件。
下面我将演示两种解决方案:一种是使用 ContentControl 的控件模板,另一种是使用功能齐全的自定义控件。
使用 ContentControl
我们在应用程序资源中定义我们的自定义模板:
<Application ...>
<Application.Resources>
<ControlTemplate x:Key="Decorator" TargetType="ContentControl">
<StackPanel Orientation="Vertical" >
<Label>Foo</Label>
<ContentPresenter />
<Label>Bar</Label>
</StackPanel>
</ControlTemplate>
</Application.Resources>
</Application>
然后我们在窗口中使用 ContentControl,如下所示指定模板:
<Window ... >
<StackPanel Orientation="Vertical">
<ContentControl Template="{StaticResource Decorator}">
<Label Background="Yellow">User supplied content here</Label>
</ContentControl>
</StackPanel>
</Window>
我确实希望模板名称的语法不那么冗长,但我认为这里无法简化。如果我们想同时指定控件的其他属性以及模板,我们可以定义一个自定义控件样式,而不是直接设置模板。然后该样式会有一个模板属性的 setter。但是,我认为在这种特定情况下,这将是不必要的复杂化。

使用自定义控件
自定义控件是自定义 WPF 最重量级的方式,并且应该仅在必要时使用。如果您认为需要自定义控件,请先尝试找到一个满足您需求的现有控件。请记住,WPF 控件是纯逻辑加上默认的外观和感觉模板(在 WPF 中称为 <ControlTempalte>)。如Charles Petzold 的这篇文章所示,此模板可以进行大幅度的修改。
.如果您的自定义超出了外观和感觉的范畴,并且涉及一些呈现逻辑,那么自定义控件可能是个不错的选择。但即使那样,派生您的控件自现有的控件(如 ContentControl)也可能是个好主意。通常,您的自定义控件将驻留在它们自己的程序集中,并且该程序集将被用户应用程序引用。
我们将创建一个名为 HeaderFooterControl 的控件,它定义了两个自定义属性:Header 和 Footer。您可以使用 Decorator 如下:
<my:HeaderFooterControl>
<my:HeaderFooterControl.Header>
header content here
</my:HeaderFooterControl.Header>
<my:HeaderFooterControl.Footer>
footer content here
</my:HeaderFooterControl.Footer>
main control content here
</my:HeaderFooterControl>
右键单击您的项目,然后选择“添加”?“添加新项”?WPF?“自定义控件(WPF)” Visual Studio 会为您的控件生成样板代码。这包括骨架 .cs 文件和 generic.xaml 中的默认控件模板。

默认情况下,该控件继承自 WPF Control 类,但我们可以做得更好,继承自 HeaderedContentControl。它拥有我们所需的一切,除了页脚。通常,您应该尝试从最适合的基本 WPF 类派生您的控件(此图来自此处)

下一步是为 Footer 定义一个依赖属性:快捷方式是右键单击您的 .cs 文件,然后选择 Insert snippet ? NetFX3d ? Dependency Property。该属性的类型为 object,因为页脚可以包含任何用户定义的内容。
public static readonly DependencyProperty FooterProperty =
DependencyProperty.Register("Footer", typeof(object),
typeof(HeaderFooterControl), new UIPropertyMetadata(null));
然后我们在 Generic.xaml 中定义我们的控件模板。我们使用 ContentPresenter 来显示来自页眉、页脚和主控件体的任意用户内容。
<Style TargetType="{x:Type local:HeaderFooterControl}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:HeaderFooterControl}">
<StackPanel Orientation="Vertical">
<ContentPresenter ContentSource="Header" />
<ContentPresenter />
<ContentPresenter ContentSource="Footer" />
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
我们还可以通过将以下属性应用于控件程序集来将 C# 命名空间映射到 XML 命名空间:
[assembly:XmlnsDefinition
(http://ikriv.com/xaml/samples/DisplayingContent/CustomControl/MyControls,
"MyControls")]
现在,在用户应用程序中,我们可以引用我们的控件库并在 Window1.xaml 中这样说:
<Window xmlns:my=
"http://ikriv.com/xaml/samples/DisplayingContent/CustomControl/MyControls" ...>
<my:HeaderFooterControl>...&lgt;/my:HeaderFooterControl>
这是我们使用自定义控件的应用程序的屏幕截图:

选择哪个选项
我描述了三种在控件中嵌入用户定义内容的不同选项:
- 带有 UserControl 和 ControlTempalte 的粗制滥造选项
- 使用 ContentControl 和 ControlTemplate 的更好选项
- 带有自定义 WPF 控件的重量级选项
您很少会使用粗制滥造的选项 - 也许只作为临时措施,但请记住,今天的临时措施往往会成为明天的遗留代码。如果所有您需要的是显示一些东西,请使用 ContentControl。如果您有自己的呈现逻辑,请使用自定义控件。例如,您可能希望通过设置 HeaderVisible 和 FooterVisible 属性来显示/隐藏页眉或页脚,并使用这些属性在某些触发器中。
源代码
可以使用此处下载包含所有相关源代码的 Visual Studio 2008 解决方案。
本文最初发布在我的网站上。
历史
- 2010 年 5 月 19 日:初始帖子