将子级添加到 UserControl
Posted
技术标签:
【中文标题】将子级添加到 UserControl【英文标题】:Adding children to UserControl 【发布时间】:2012-02-24 01:18:06 【问题描述】:我的任务
创建一个UserControl
,它应该能够包含 WPF 中可用的任何可视子项,子项显示在一个容器中,该容器是UserControl
的子项。
我的问题
我无法让孩子在我的容器中正确显示,我尝试了多种方式,但没有找到适合设计器的方式。我也尝试使用ContentControl
,但没有显示任何内容。
我的方法
首先我找到了this 链接,并尝试了一些变化。我设法在正确的容器中显示内容,但它在设计器中不起作用,因为内容属性是设置私有的并且设计器想要覆盖它。将所有内容都放在 XAML 中是可行的,但这在与设计人员合作时并不好。这可能是最喜欢的方式。
在此之后,我尝试通过将 Content
-property 绑定到 UIElementCollection
-type 的可绑定属性来使用 ContentControl
。这种方法不会在设计器中引发任何错误,但我必须承认我在容器中从未看到任何控件(例如 Button
)。它保持为空,但添加了孩子。
结论
经过几个小时寻找简单快捷的解决方案后,我决定在这里寻求解决方案。我有点失望。如果 Microsoft 可以将示例放入 MSDN,那将非常有帮助。
我确信一定有一个简单的方法来归档它。
现状
感谢 Andrei Gavrila 和 jberger 我存档创建了一个显示内容的节点(参见下面的代码),但仍然存在两个问题: - 没有设计师支持 - 边框(见 xaml)不显示在设计器中,并且在应用程序运行时不显示,甚至没有边距
public class NodeContent : ContentControl
static NodeContent()
DefaultStyleKeyProperty.OverrideMetadata(typeof(NodeContent), new FrameworkPropertyMetadata(typeof(NodeContent)));
public partial class Node : UserControl, INotifyPropertyChanged
UIElementCollection _Elements;
public event PropertyChangedEventHandler PropertyChanged;
public UIElementCollection NodeContent
get return _Elements;
set
_Elements = value;
OnPropertyChanged("NodeContent");
public Node()
InitializeComponent();
NodeContent = new UIElementCollection(NodeContentContainer, this);
protected void OnPropertyChanged(string name)
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(name));
节点 Xaml:
<UserControl x:Class="Pipedream.Nodes.Node"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="216" d:DesignWidth="174" Background="Transparent" Name="NodeControl" xmlns:my="clr-namespace:Pipedream.Nodes">
<Border BorderThickness="1" CornerRadius="20" BorderBrush="Black" Background="White">
<Grid>
<my:NodeContent x:Name="NodeContentContainer" Margin="20" Content="Binding Source=NodeControl, Path=NodeContent" />
</Grid>
</Border>
</UserControl>
通用 Xaml:
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Pipedream.Nodes">
<Style TargetType="x:Type local:NodeContent">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="x:Type local:Node">
<Border Background="TemplateBinding Background"
BorderBrush="TemplateBinding BorderBrush"
BorderThickness="TemplateBinding BorderThickness">
<ContentPresenter />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
【问题讨论】:
或者,也许,也许,您可以尝试使用 ControlTemplate 来转换面板,而不是创建新的控件? @deerchao 我真的需要一个自己的用户控件。不过谢谢你的建议。 在“我的方法”下,第 2 段似乎自相矛盾:“它保持空白,但添加了孩子。”..? @jberger 哦,这仅仅意味着没有孩子可见。 :-) 您定义了什么模板/主题来显示自定义ContentControl
?
【参考方案1】:
一般情况下,您不能绑定 UIElementCollection
类型的依赖属性。试试这样的:
MultiChildDemo.xaml
这里没什么可看的。 StackPanel 将保存我们的子元素。你显然可以做得更多。
代码:
<UserControl x:Class="Demo.MultiChildDemo"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:demo="clr-namespace:Demo"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<StackPanel x:Name="PART_Host" />
</UserControl>
MultiChildDemo.xaml.cs
重要提示:
ContentPropertyAttribute
属性设置将由该类型的父元素所包含的任何元素设置的属性。因此,<MultiChildDemo></MultiChildDemo>
中的任何元素都将添加到 Children
属性中。
我们正在扩展UserControl
。这不需要完全自定义的控件。
在 WPF 中使用DependencyProperty.Register()
和变体创建属性是一种很好的做法。您会注意到Children
属性没有支持变量:DependencyProperty
负责为我们存储数据。如果我们不创建只读属性,这将启用绑定和其他很酷的 WPF 功能。因此,养成使用依赖属性的习惯很重要,而不是像您在 Internet 上的示例中经常看到的普通属性。
这是一个相对简单的依赖属性示例。我们所做的只是复制对子依赖属性的引用,从而将调用转发到UIElementCollection.Add
。有很多更复杂的例子,尤其是在 MSDN 上。
代码:
using System.Windows;
using System.Windows.Controls;
using System.Windows.Markup;
namespace Demo
[ContentProperty(nameof(Children))] // Prior to C# 6.0, replace nameof(Children) with "Children"
public partial class MultiChildDemo : UserControl
public static readonly DependencyPropertyKey ChildrenProperty = DependencyProperty.RegisterReadOnly(
nameof(Children), // Prior to C# 6.0, replace nameof(Children) with "Children"
typeof(UIElementCollection),
typeof(MultiChildDemo),
new PropertyMetadata());
public UIElementCollection Children
get return (UIElementCollection)GetValue(ChildrenProperty.DependencyProperty);
private set SetValue(ChildrenProperty, value);
public MultiChildDemo()
InitializeComponent();
Children = PART_Host.Children;
MultiChildDemoWindow.xaml
注意标签是<demo:MultiChildDemo>
元素的直接后代。您也可以将它们包含在 <demo:MultiChildDemo.Children>
元素中。不过,我们添加到 MultiChild 类的 ContentPropertyAttribute
属性允许我们省略这一步。
代码:
<Window x:Class="Demo.MultiChildDemoWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:demo="clr-namespace:Demo"
Title="MultiChildDemoWindow" Height="300" Width="300">
<demo:MultiChildDemo>
<Label>Test 1</Label>
<Label>Test 2</Label>
<Label>Test 3</Label>
</demo:MultiChildDemo>
</Window>
【讨论】:
太棒了!!我用网格替换了你的StackPanel
。将MultiChildDemo
-item 放在主窗口中后,我在其中放置了一个网格。最后,设计视图完美运行,我可以使用设计器。非常感谢。
这行得通,但是当在设计器中你得到“属性'内容'设置不止一次”。它仍然可以正常构建和运行。
完美答案。仅出于完整性考虑,如果使用 C# 6.0,则应使用 nameof(Children)
,而不是 "Children"
。
感谢出色的解决方案。我知道我已经进行了几年的讨论,但是......如果我采用您的解决方案并更改 1 件事,绑定它不起作用的标签内容(即 )。我已经尝试了所有绑定属性的组合,但都没有成功。将内容硬编码为“Hello”,就像一个魅力。有什么想法吗?
我怎样才能只允许一个孩子?我尝试了 UIElement,但它不起作用【参考方案2】:
只需删除 UserControl 标记并替换为 Grid
【讨论】:
是的,我用儿童标签对其进行了测试 令人惊讶的是,这确实有效,即使在 WinRT 中也是如此。 VS/Blend 设计器将不会显示包含在原始 Grid XAML 中的任何子项,一旦您将任何其他子项添加到它,但除此之外这工作得很好,并且是迄今为止最直接的解决方案。 我以前做过这个,作为用户控件的快速和肮脏的替代品。这有点骇人听闻,但如果你没有做任何太花哨的事情,它通常可以正常工作。已经有一段时间了,但如果我没记错的话,你基本上只是在扩展 Grid 而不是 UserControl。【参考方案3】:首先尝试了解User Control 和自定义控件(Control/Content Control)之间的区别
为了简单起见:
"标准的 WPF 控件提供了大量的内置 功能。如果预设控件之一的功能, 例如进度条或滑块,与您的功能相匹配 想要合并,那么您应该为此创建一个新模板 预先存在的控件来实现您想要的外观。创建一个新的 模板是创建自定义元素最简单的解决方案,因此您 应该首先考虑该选项。
如果您想要整合到您的应用程序中的功能可以 通过预先存在的控件和代码的组合来实现, 考虑创建一个用户控件。用户控件使您能够绑定 将多个预先存在的控件放在一个界面中并添加 决定它们行为方式的代码。
如果没有预先存在的控件或控件组合可以实现 您想要的功能,创建一个自定义控件。自定义控件 使您能够创建一个全新的模板来定义视觉 控件的表示并添加确定的自定义代码 控件的功能。”
Adam Nathan - WPF Unleashed 4
现在,如果你想要的只是一个 ContentControl:
-
创建一个派生 ContentControl 的新 CustomControl
在主题中找到 generic.xaml 并将 Content Presenter 添加到您的控件模板中。如上所述,自定义控制逻辑与其视觉呈现是分开的
将该控件用作常规 ContentControl。
对于作为内容的多个项目,请查看ItemsControl
以上步骤修改为:
派生项目控制
public class MyCtrl : ItemsControl
static MyCtrl()
DefaultStyleKeyProperty.OverrideMetadata(typeof(MyCtrl), new FrameworkPropertyMetadata(typeof(MyCtrl)));
修改 Generic.xaml 以包含 ItemsPresenter
<Style TargetType="x:Type local:MyCtrl">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="x:Type local:MyCtrl">
<Border Background="TemplateBinding Background"
BorderBrush="TemplateBinding BorderBrush"
BorderThickness="TemplateBinding BorderThickness">
<ItemsPresenter />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
使用控件
<StackPanel>
<ctrl:MyCtrl>
<Button Width="100" Height="50">Click</Button>
<Button Width="100" Height="50">Click</Button>
<Button Width="100" Height="50">Click</Button>
</ctrl:MyCtrl>
</StackPanel>
如上所述,对于这种简单的情况,不需要派生 ItemsControl,只需使用 ItemsControl 并为其定义一个模板。计划通过添加功能进行扩展时派生 ItemsControl
编辑:
边框(见 xaml)不显示在设计器中,并且在应用程序运行时不显示,甚至没有边距
您应该在控件本身上设置 Border 属性:
<ctrl:MyCtrl BorderBrush="Red" BorderThickness="3" Background="Green" >
【讨论】:
你测试你的代码了吗?它对我没有任何表现。无论如何,您的MyCtrl
似乎只是其他控件的容器。但这对我没有帮助。我想创建一个预先存在的控件的组合,一个UserControl
,它有一个可以包含任何类型的 UIElements 的子容器(可以是网格或其他东西)。所以我不确定这对我有什么帮助。不过还是谢谢。
我确实做到了。这是与来源skydrive.live.com/… 的链接。它应该会有所帮助。您的控件也可以包含其他控件。只需将它们添加到您的类中,将它们的内部属性作为 DP 在父控件本身上公开,并改进模板以显示它们
好的,谢谢,但我会等待其他答案,因为我希望能够在不使用模板的情况下设计节点。
@FelixK.:不幸的是,WPF 的工作方式,Andrei 是正确的。我将使用从 Control、ContentControl 或 ItemsControl 派生的自定义控件,或者使用模板、DataTemplates 等。鉴于您没有提供太多上下文来说明为什么需要这个确切的解决方案,因此很难将您引导到哪个可能实际上效果最好。
@jberger + Andrei Gavrila:现在可以使用,但不能使用设计器将内容放置在节点中。以上是关于将子级添加到 UserControl的主要内容,如果未能解决你的问题,请参考以下文章