在UserControl中添加子控件

29

我的任务

创建一个UserControl,它应该能够包含在WPF中可用的任何可视子元素,这些子元素显示在UserControl的子容器中。

我的问题

我无法正确地将子元素显示在我的容器中,我尝试了几种方法,但没有找到可以在设计器中正常工作的方法。我也尝试使用ContentControl,但没有显示任何内容。

我的尝试

首先,我找到了此链接并尝试了一些变化。我成功地在正确的容器中显示了内容,但是这在设计器中不起作用,因为内容属性是私有设置的,设计器想要覆盖它。将所有内容放在XAML中可以工作,但是在与设计师合作时不太好。这是我最喜欢的方式。

在此之后,我尝试通过将ContentControlContent属性绑定到UIElementCollection类型的可绑定属性来使用它。这种方法在设计器中不会引发任何错误,但我必须承认我从未在我的容器中看到任何控件(例如Button),它保持为空但已添加了子元素。

结论

经过几个小时的寻找快速简单的解决方案,我决定在这里寻求帮助。我有点失望。如果Microsoft能够在MSDN中提供示例,那将非常有帮助。

我相信一定有一个简单的方法可以实现这一点。

当前情况

由于Andrei Gavrilajberger的帮助,我成功创建了一个显示内容的节点(请参见下面的代码),但仍然存在两个问题: - 没有设计器支持 - 边框(参见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));
        }
    }
}

Node-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>

@deerchao,我真的需要一个自己的“UserControl”。但还是谢谢你的建议。 - Felix K.
@jberger 我这里没有代码,但如果你知道一个有效的答案,请随意添加。 - Felix K.
@FelixK. 应该与Andrei的答案类似,将ItemsControlItemsPresenter替换为ContentControlContentPresenter - Jake Berger
@FelixK。我认为如果我们更确切地知道你想做什么,那会对我们非常有帮助。你想要的最终结果是什么?我理解的是,你想要一个可以与设计支持一起使用的控件。WPF提供了这个功能。你想让你的控件“看起来”某种方式吗?“表现”某种方式吗?如果是这样,具体是怎样的呢? - Jake Berger
为什么必须是 UserControl - Jake Berger
显示剩余7条评论
3个回答

55

通常情况下,您不能绑定类型为 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>

太好了!我用Grid替换了你的StackPanel。在主窗口中放置了MultiChildDemo项,然后在其中放置了一个网格。最终设计视图完美地工作了,我可以使用设计师了。非常感谢。 - Felix K.
3
虽然这个功能可以正常工作,但在设计器中会出现“属性 'Content' 被设置了多次”的提示。尽管如此,它仍能够成功构建和运行。 - trampster
1
完美的答案。仅为完整起见,如果使用C# 6.0,则应使用nameof(Children)而不是"Children" - LoRdPMN
@LoRdPMN 谢谢,已更新。同时修正了 XAML 的语法高亮显示。 - Zenexer
2
感谢提供的绝妙解决方案。虽然我已经加入这次讨论好几年了,但是...如果我采用你的解决方案,并更改一个东西——将标签内容绑定起来,它就不能工作(即 <Label Content = "{Binding myDataContextProperty}" /> )。我已尝试过所有绑定属性的组合,但都没有成功。硬编码将内容设置为“Hello”,则能够完美运行。有什么想法吗? - Walter
1
我该如何只允许一个子元素?我尝试了UIElement,但它不起作用。 - Rans

4

首先要理解“用户控件”和“自定义控件”(User ControlControl/Content Control)之间的区别。

简单来说:

标准的WPF控件提供了大量内置功能。如果预设控件(如进度条或滑块)的功能与您要实现的功能相匹配,则应创建该预先存在控件的新模板以实现所需外观。创建一个新模板是创建自定义元素的最简单解决方案,因此您应首先考虑该选项。
如果您想将功能合并到应用程序中,可以通过预先存在的控件和代码组合来实现,请考虑创建用户控件。用户控件使您能够将多个预先存在的控件绑定在单个界面中,并添加确定它们行为方式的代码。
如果没有预先存在的控件或控件组合可以实现所需的功能,则创建自定义控件。自定义控件使您能够创建完全新的模板,定义控件的视觉表示,并添加确定控件功能的自定义代码。
  1. 创建一个新的自定义控件,继承自ContentControl。
  2. 在主题中找到generic.xaml,将内容呈现器添加到您的控件模板中。如上所述,自定义控件逻辑与其视觉呈现分离。
  3. 像普通的ContentControl一样使用该控件。

如果需要作为内容传递多个项,请查看ItemsControl

以上步骤修改为:

派生 Items Control。

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,它有一个子容器(可以是网格或其他任何类型的 UI 元素),可以包含任何类型的 UI 元素。所以我不确定这会如何帮助我。但还是谢谢。 - Felix K.
好的,谢谢你。但我想等待其他答案,因为我希望有可能在不使用模板的情况下设计节点。 - Felix K.
@FelixK.:不幸的是,根据WPF的工作方式,Andrei是正确的。我会使用从Control、ContentControl或ItemsControl派生的自定义控件,或者使用Templates、DataTemplates等。鉴于您没有提供太多关于为什么需要这个确切解决方案的上下文,很难指导您哪种方法可能最适合。 - Jake Berger
@AndreiGavrila 关于你的编辑:我上面的代码(请看我的编辑)就是我所说的代码。 - Felix K.
@AndreiGavrila我不是特别喜欢那样的考试。我认为他们违背了良好的编程伦理。此外,MSDN也有它自己的错误。无论如何,“你错了”这样的回应都是一个糟糕的方式来回应一个意见。 - Zenexer
显示剩余5条评论

4

只需删除UserControl标记并替换为Grid即可。


1
是的,我已经使用标签作为子元素进行了测试。 - Andreas
令人惊讶的是,即使在WinRT中,这实际上也可以工作。一旦您向原始Grid XAML添加任何其他子元素,VS / Blend设计师将不会显示其中包含的任何子元素,但除此之外,这个解决方案非常简单明了,并且完全可行。 - Livven
我以前做过这个,作为用户控件的快速替代方案。虽然有点hackish,但如果你不做太复杂的事情,它通常可以正常工作。虽然已经过了一段时间,但如果我记得正确,你基本上只是扩展Grid而不是UserControl。 - Zenexer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接