简体   繁体   English

如何使用 NAMED 内容创建 WPF UserControl

[英]How to create a WPF UserControl with NAMED content

I have a set of controls with attached commands and logic that are constantly reused in the same way.我有一组带有附加命令和逻辑的控件,它们以相同的方式不断重复使用。 I decided to create a user control that holds all the common controls and logic.我决定创建一个包含所有常用控件和逻辑的用户控件。

However I also need the control to be able to hold content that can be named.但是,我还需要控件能够保存可以命名的内容。 I tried the following:我尝试了以下方法:

<UserControl.ContentTemplate>
    <DataTemplate>
        <Button>a reused button</Button>
        <ContentPresenter Content="{TemplateBinding Content}"/>
        <Button>a reused button</Button>
    </DataTemplate>
</UserControl.ContentTemplate>

However it seems any content placed inside the user control cannot be named.但是,似乎无法命名放置在用户控件内的任何内容。 For example if I use the control in the following way:例如,如果我按以下方式使用控件:

<lib:UserControl1>
     <Button Name="buttonName">content</Button>
</lib:UserControl1>

I receive the following error:我收到以下错误:

Cannot set Name attribute value 'buttonName' on element 'Button'.无法在元素“Button”上设置 Name 属性值“buttonName”。 'Button' is under the scope of element 'UserControl1', which already had a name registered when it was defined in another scope. 'Button' 位于元素 'UserControl1' 的范围内,该元素在另一个范围中定义时已经注册了一个名称。

If I remove the buttonName, then it compiles, however I need to be able to name the content.如果我删除 buttonName,则它会编译,但是我需要能够命名内容。 How can I achieve this?我怎样才能做到这一点?

The answer is to not use a UserControl to do it.答案是不要使用 UserControl 来做到这一点。

Create a class that extends ContentControl创建一个扩展ContentControl的类

public class MyFunkyControl : ContentControl
{
    public static readonly DependencyProperty HeadingProperty =
        DependencyProperty.Register("Heading", typeof(string),
        typeof(MyFunkyControl), new PropertyMetadata(HeadingChanged));

    private static void HeadingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        ((MyFunkyControl) d).Heading = e.NewValue as string;
    }

    public string Heading { get; set; }
}

then use a style to specify the contents然后使用样式来指定内容

<Style TargetType="control:MyFunkyControl">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="control:MyFunkyControl">
                <Grid>
                    <ContentControl Content="{TemplateBinding Content}"/>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

and finally - use it最后 - 使用它

<control:MyFunkyControl Heading="Some heading!">            
    <Label Name="WithAName">Some cool content</Label>
</control:MyFunkyControl>

It seems this is not possible when XAML is used.当使用 XAML 时,这似乎是不可能的。 Custom controls seem to be a overkill when I actually have all the controls I need, but just need to group them together with a small bit of logic and allow named content.当我实际上拥有我需要的所有控件时,自定义控件似乎是一种矫枉过正,但只需要用一点逻辑将它们组合在一起并允许命名内容。

The solution on JD's blog as mackenir suggests, seems to have the best compromise.正如 mackenir 所建议的,京东博客上的解决方案似乎是最好的妥协。 A way to extend JD's solution to allow controls to still be defined in XAML could be as follows:一种扩展 JD 解决方案以允许仍然在 XAML 中定义控件的方法可能如下:

    protected override void OnInitialized(EventArgs e)
    {
        base.OnInitialized(e);

        var grid = new Grid();
        var content = new ContentPresenter
                          {
                              Content = Content
                          };

        var userControl = new UserControlDefinedInXAML();
        userControl.aStackPanel.Children.Add(content);

        grid.Children.Add(userControl);
        Content = grid;           
    }

In my example above I have created a user control called UserControlDefinedInXAML which is define like any normal user controls using XAML.在我上面的示例中,我创建了一个名为 UserControlDefinedInXAML 的用户控件,它的定义与使用 XAML 的任何普通用户控件一样。 In my UserControlDefinedInXAML I have a StackPanel called aStackPanel within which I want my named content to appear.在我的 UserControlDefinedInXAML 中,我有一个名为 aStackPanel 的 StackPanel,我希望在其中显示我的命名内容。

Sometimes you might just need to reference the element from C#.有时您可能只需要从 C# 引用元素。 Depending on the use case, you can then set an x:Uid instead of an x:Name and access the elements by calling a Uid finder method like Get object by its Uid in WPF .根据用例,您可以设置x:Uid而不是x:Name并通过调用 Uid finder 方法(如WPF 中的 Uid 获取对象)来访问元素。

Another alternative I've used is to just set the Name property in the Loaded event.我使用的另一个替代方法是在Loaded事件中设置Name属性。

In my case, I had a rather complex control which I didn't want to create in the code-behind, and it looked for an optional control with a specific name for certain behavior, and since I noticed I could set the name in a DataTemplate I figured I could do it in the Loaded event too.就我而言,我有一个相当复杂的控件,我不想在代码隐藏中创建它,它为某些行为寻找具有特定名称的可选控件,并且因为我注意到我可以在DataTemplate我想我也可以在Loaded事件中做到这一点。

private void Button_Loaded(object sender, RoutedEventArgs e)
{
    Button b = sender as Button;
    b.Name = "buttonName";
}

You can use this helper for set name inside the user control:您可以使用此帮助程序在用户控件中设置名称:

using System;
using System.Reflection;
using System.Windows;
using System.Windows.Media;
namespace UI.Helpers
{
    public class UserControlNameHelper
    {
        public static string GetName(DependencyObject d)
        {
            return (string)d.GetValue(UserControlNameHelper.NameProperty);
        }

        public static void SetName(DependencyObject d, string val)
        {
            d.SetValue(UserControlNameHelper.NameProperty, val);
        }

        public static readonly DependencyProperty NameProperty =
            DependencyProperty.RegisterAttached("Name",
                typeof(string),
                typeof(UserControlNameHelper),
                new FrameworkPropertyMetadata("",
                    FrameworkPropertyMetadataOptions.None,
                    (d, e) =>
                    {
                        if (!string.IsNullOrEmpty((string)e.NewValue))
                        {
                            string[] names = e.NewValue.ToString().Split(new char[] { ',' });

                            if (d is FrameworkElement)
                            {
                                ((FrameworkElement)d).Name = names[0];
                                Type t = Type.GetType(names[1]);
                                if (t == null)
                                    return;
                                var parent = FindVisualParent(d, t);
                                if (parent == null)
                                    return;
                                var p = parent.GetType().GetProperty(names[0], BindingFlags.Instance | BindingFlags.Public | BindingFlags.SetProperty);
                                p.SetValue(parent, d, null);
                            }
                        }
                    }));

        public static DependencyObject FindVisualParent(DependencyObject child, Type t)
        {
            // get parent item
            DependencyObject parentObject = VisualTreeHelper.GetParent(child);

            // we’ve reached the end of the tree
            if (parentObject == null)
            {
                var p = ((FrameworkElement)child).Parent;
                if (p == null)
                    return null;
                parentObject = p;
            }

            // check if the parent matches the type we’re looking for
            DependencyObject parent = parentObject.GetType() == t ? parentObject : null;
            if (parent != null)
            {
                return parent;
            }
            else
            {
                // use recursion to proceed with next level
                return FindVisualParent(parentObject, t);
            }
        }
    }
}

and your Window or Control Code Behind set you control by Property:和你的窗口或控制代码隐藏设置你通过属性控制:

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

    }

    public Button BtnOK { get; set; }
}

your window xaml:你的窗口 xaml:

    <Window x:Class="user_Control_Name.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:test="clr-namespace:user_Control_Name"
            xmlns:helper="clr-namespace:UI.Helpers" x:Name="mainWindow"
            Title="MainWindow" Height="350" Width="525">
        <Grid>
            <test:TestUserControl>
                <Button helper:UserControlNameHelper.Name="BtnOK,user_Control_Name.MainWindow"/>
            </test:TestUserControl>
            <TextBlock Text="{Binding ElementName=mainWindow,Path=BtnOK.Name}"/>
        </Grid>
    </Window>

UserControlNameHelper get your control name and your Class name for set Control to Property. UserControlNameHelper 获取您的控件名称和类名称,以便将控件设置为属性。

I've chosen to create an extra property for each element I need to get:我选择为我需要获取的每个元素创建一个额外的属性:

    public FrameworkElement First
    {
        get
        {
            if (Controls.Count > 0)
            {
                return Controls[0];
            }
            return null;
        }
    }

This enables me to access the child elements in XAML:这使我能够访问 XAML 中的子元素:

<TextBlock Text="{Binding First.SelectedItem, ElementName=Taxcode}"/>
<Popup>
    <TextBox Loaded="BlahTextBox_Loaded" />
</Popup>

Code behind:后面的代码:

public TextBox BlahTextBox { get; set; }
private void BlahTextBox_Loaded(object sender, RoutedEventArgs e)
{
    BlahTextBox = sender as TextBox;
}

The real solution would be for Microsoft to fix this issue, as well as all the others with broken visual trees etc. Hypothetically speaking.真正的解决方案是让微软解决这个问题,以及所有其他视觉树损坏等问题。假设地说。

另一种解决方法:将元素引用为RelativeSource

I had the same problem using a TabControl when placing a bunch of named controls into.在将一堆命名控件放入时,我使用 TabControl 遇到了同样的问题。

My workaround was to use a control template which contains all my controls to be shown in a tab page.我的解决方法是使用一个控件模板,其中包含要显示在选项卡页中的所有控件。 Inside the template you can use the Name property and also data bind to properties of the named control from other controls at least inside the same template.在模板内部,您可以使用 Name 属性,也可以使用数据绑定到至少在同一模板内的其他控件的命名控件的属性。

As Content of the TabItem Control, use a simple Control and set the ControlTemplate accordingly:作为 TabItem 控件的内容,使用一个简单的控件并相应地设置 ControlTemplate:

<Control Template="{StaticResource MyControlTemplate}"/>

Accessing those named control inside the template from code behind you would need to use the visual tree.从您背后的代码访问模板内的那些命名控件将需要使用可视化树。

I ran into this problem and found a workaround that lets you design custom controls using Xaml.我遇到了这个问题,并找到了一种解决方法,可以让您使用 Xaml 设计自定义控件。 Its still has a bit of a hack, but one that solved all of my problems without any obvious compromises.它仍然有一些技巧,但它解决了我所有的问题而没有任何明显的妥协。

Basically, you do everything the way you normally would with the xaml, but you also include some of the header declarations on the control template itself and Base64 encode that template to be loaded in the code constructor.基本上,您可以按照通常使用 xaml 的方式执行所有操作,但您还可以在控件模板本身上包含一些标头声明,并对要加载到代码构造函数中的模板进行 Base64 编码。 Not shown in this Xaml excerpt, but the namespace my full Xaml used is actually targeting a XamlTemplates instead of the Controls namespace.此 Xaml 摘录中未显示,但我的完整 Xaml 使用的命名空间实际上是针对 XamlTemplates 而不是 Controls 命名空间的。 This was on purpose because the "Release" build moves that developmental Debug reference out of the way from my production controls namespace.这是故意的,因为“发布”构建将开发性调试引用从我的生产控件命名空间中移开。 More on that below.更多关于下面的内容。

<ControlTemplate TargetType="{x:Type TabControl}" 
                 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
                 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Grid x:Name="templateRoot" 
          ClipToBounds="True" 
          SnapsToDevicePixels="True" 
          Background="Transparent"
          KeyboardNavigation.TabNavigation="Local">
        <Grid.ColumnDefinitions>
            <ColumnDefinition x:Name="ColumnDefinition0"/>
            <ColumnDefinition x:Name="ColumnDefinition1" Width="0"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition x:Name="RowDefinition0" Height="Auto"/>
            <RowDefinition x:Name="RowDefinition1" Height="*"/>
        </Grid.RowDefinitions>
        <TabPanel x:Name="HeaderPanel"
                  Panel.ZIndex="1"                          
                  Margin="{Binding MarginHeaderPanel, RelativeSource={RelativeSource AncestorType=TabControl}}"
                  Background="{Binding Background, RelativeSource={RelativeSource AncestorType=TabControl}}"
                  IsItemsHost="True"                          
                  KeyboardNavigation.TabIndex="2"/>
        <Border x:Name="blankregion" Panel.ZIndex="1" Margin="0" Padding="0" 
                Background="{Binding Background, RelativeSource={RelativeSource AncestorType=TabControl}}">
            <ContentPresenter x:Name="blankpresenter"                                      
                              KeyboardNavigation.TabIndex="1"    
                              Content="{Binding TabBlankSpaceContent, RelativeSource={RelativeSource AncestorType=TabControl}}"                                          
                              ContentSource="TabBlankSpaceContent" 
                              SnapsToDevicePixels="True"/>
        </Border>

        <Grid x:Name="ContentPanel">
            <Border 
                BorderBrush="{Binding BorderBrush, RelativeSource={RelativeSource AncestorType=TabControl}}"
                BorderThickness="{Binding BorderThickness, RelativeSource={RelativeSource AncestorType=TabControl}}"                       
                Background="{Binding SelectedItem.Background, RelativeSource={RelativeSource AncestorType=TabControl}}"
                KeyboardNavigation.DirectionalNavigation="Contained" 
                KeyboardNavigation.TabNavigation="Local"                                           
                CornerRadius="{Binding BorderRadius, RelativeSource={RelativeSource AncestorType=TabControl}}"
                KeyboardNavigation.TabIndex="3">
                <ContentControl x:Name="PART_SelectedContentHost" 
                                ContentTemplate="{Binding SelectedContentTemplate, RelativeSource={RelativeSource AncestorType=TabControl}}"
                                Content="{Binding SelectedContent, RelativeSource={RelativeSource AncestorType=TabControl}}"
                                ContentStringFormat="{Binding SelectedContentStringFormat, RelativeSource={RelativeSource AncestorType=TabControl}}" 
                                Margin="{Binding Padding, RelativeSource={RelativeSource AncestorType=TabControl}}"
                                SnapsToDevicePixels="{Binding SnapsToDevicePixels, RelativeSource={RelativeSource AncestorType=TabControl}}"/>
            </Border>

        </Grid>
    </Grid>
    <ControlTemplate.Triggers>
        <!--Triggers were removed for clarity-->
    </ControlTemplate.Triggers>
</ControlTemplate>

I'll point out that the above XAML didn't name the control it derived from and everything within the template used relative lookups to bind its properties;我要指出的是,上面的 XAML 没有命名它派生的控件,模板中的所有内容都使用相对查找来绑定其属性; even the custom ones.甚至是定制的。

On the C# side, I used the Base64 encoded version of the control template from my Xaml and directives to shuffle around the development/release versions of the controls.在 C# 方面,我使用 Xaml 中控件模板的 Base64 编码版本和指令来调整控件的开发/发布版本。 The theory being that my controls in the development space wouldn't run into the problem this topic is about, but would give me a way to test/develop them.理论是我在开发空间中的控件不会遇到本主题所涉及的问题,但会给我一种测试/开发它们的方法。 The release DLL versions seem to be working really well and the controls built do have great design time support just like they did on the Debug/Development side.发布的 DLL 版本似乎工作得非常好,并且构建的控件确实具有很好的设计时支持,就像它们在调试/开发方面所做的一样。

#if DEBUG
namespace AgileBIM.Controls
{
    public class AgileTabControl : AgileBIM.XamlTemplates.AgileTabControlDesigner { }
}

namespace AgileBIM.XamlTemplates
#else
namespace AgileBIM.Controls
#endif
{
#if DEBUG    
    public partial class AgileTabControlDesigner : TabControl
#else
    public class AgileTabControl : TabControl
#endif
    {

        

#if DEBUG
        private static Type ThisControl = typeof(AgileTabControlDesigner);
#else
        private static Type ThisControl = typeof(AgileTabControl);
        private string Template64 = "Base64 encoded template removed for clarity"
#endif


#if DEBUG
        public AgileTabControlDesigner() { InitializeComponent(); }
#else
        public AgileTabControl()
        {
            string decoded = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(Template64));
            System.IO.StringReader sr = new System.IO.StringReader(decoded);
            System.Xml.XmlReader xr = System.Xml.XmlReader.Create(sr);
            ControlTemplate ct = (ControlTemplate)System.Windows.Markup.XamlReader.Load(xr);

            DefaultStyleKey = ThisControl;
            Template = ct;
        }
#endif

        public Thickness MarginHeaderPanel 
        {
            get { return (Thickness)GetValue(MarginHeaderPanelProperty); } 
            set { SetValue(MarginHeaderPanelProperty, value); } 
        }
        public static readonly DependencyProperty MarginHeaderPanelProperty =
            DependencyProperty.Register("MarginHeaderPanel", typeof(Thickness), ThisControl, new PropertyMetadata(new Thickness(0)));

        public CornerRadius BorderRadius 
        { 
            get { return (CornerRadius)GetValue(BorderRadiusProperty); } 
            set { SetValue(BorderRadiusProperty, value); }
        }
        public static readonly DependencyProperty BorderRadiusProperty =
            DependencyProperty.Register("BorderRadius", typeof(CornerRadius), ThisControl, new PropertyMetadata(new CornerRadius(0)));

        public object TabBlankSpaceContent 
        { 
            get { return (object)GetValue(TabBlankSpaceContentProperty); } 
            set { SetValue(TabBlankSpaceContentProperty, value); } 
        }
        public static readonly DependencyProperty TabBlankSpaceContentProperty =
            DependencyProperty.Register("TabBlankSpaceContent", typeof(object), ThisControl, new PropertyMetadata());
    }
}

The critical thing to remember before creating a "release" control DLL to be used in your primary application is to update your base64 encoded string with your latest and greatest version of its control template.在创建要在主应用程序中使用的“发布”控件 DLL 之前,要记住的关键是使用最新和最好的控件模板版本更新 base64 编码的字符串。 This is because the Release build is completely detached from the original Xaml and entirely dependent on the encoded one.这是因为发布版本与原始 Xaml 完全分离,并且完全依赖于编码版本。

The above control and others like it can be found on GitHub .上述控件和其他类似控件可以在GitHub找到 Which is a library I am making intended to "unlock" many of the things I want to style that standard controls don't expose.这是一个我正在制作的库,旨在“解锁”许多我想要设置样式的标准控件不会公开的东西。 That and adding some features that don't exist.那并添加一些不存在的功能。 For example, the above TabControl has an additional content property for utilizing the "unused" area of the tab headers.例如,上面的 TabControl 有一个额外的内容属性,用于利用选项卡标题的“未使用”区域。

Important Notes:重要笔记:

  • Basic styling gets lost using this method, but you get it all back if your styles for the Custom Control uses the BasedOn="{StaticResource {x:Type TabControl}}" mechanism.使用此方法会丢失基本样式,但如果您的自定义控件样式使用了BasedOn="{StaticResource {x:Type TabControl}}"机制,则您可以恢复所有样式。
  • I need to find time to research if this will cause any noteworthy memory leaks and whether I can do anything to combat them, if anyone has any thoughts on this let me know in the comments.我需要找时间研究这是否会导致任何值得注意的内存泄漏,以及我是否可以采取任何措施来解决它们,如果有人对此有任何想法,请在评论中告诉我。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM