如何使用 NAMED 内容创建 WPF 用户控件

我有一组带有附加命令和逻辑的控件,它们以相同的方式不断地被重用。我决定创建一个包含所有常见控件和逻辑的用户控件。

但是,我还需要控件能够保存可以命名的内容。我尝试了以下方法:

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

然而,似乎放置在用户控件中的任何内容都不能命名。例如,如果我以下列方式使用该控件:

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

我收到以下错误:

无法设置 Name 属性值‘ buttonName’ on element 'Button'. 'Button' is 在元素的范围内 ‘ UserControl1’,它已经有了一个 name registered when it was defined in 另一个瞄准镜。

如果我删除了 buttonName,那么它就会编译,但是我需要能够命名内容。我怎么才能做到呢?

45881 次浏览

在使用 XAML 时,这似乎是不可能的。自定义控件似乎是一个过度杀伤时,我实际上有所有的控件,我需要,但只需要组合他们与一个小的逻辑位,并允许命名内容。

The solution on JD 的博客 as mackenir suggests, seems to have the best compromise. A way to extend JD's solution to allow controls to still be defined in XAML could be as follows:

    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;
}

在上面的示例中,我创建了一个名为 UserControlDefinition edInXAML 的用户控件,它的定义与使用 XAML 的任何普通用户控件类似。在我的 UserControlDefinition edInXAML 中,我有一个名为 aStackPanel 的 StackPanel,我希望我的命名内容出现在其中。

答案是不要使用 UserControl 进行操作。

创建一个扩展 内容控制的类

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; }
}

然后使用样式指定内容

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

最后,使用它

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

Another alternative I've used is to just set the Name property in the Loaded event.

在我的案例中,我有一个相当复杂的控件,我不想在代码隐藏中创建它,它为某些行为寻找一个具有特定名称的可选控件,因为我注意到我可以在 DataTemplate中设置名称,我想我也可以在 Loaded事件中设置。

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

我选择为需要获取的每个元素创建一个额外的属性:

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

这使我能够访问 XAML 中的子元素:

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

有时您可能只需要引用 C # 中的元素。根据用例的不同,您可以设置 x:Uid而不是 x:Name,并通过调用类似 通过 WPF 中的 Uid 获取对象的 Uid 查找器方法来访问元素。

您可以在用户控件中将这个 helper 用于设置名称:

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);
}
}
}
}

以及“窗口或控件代码后面”通过属性设置您的控件:

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


}


public Button BtnOK { get; set; }
}

你的窗户 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 获取将 Control 设置为 Property 的控件名和类名。

<Popup>
<TextBox Loaded="BlahTextBox_Loaded" />
</Popup>

暗号:

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

真正的解决方案将是微软修复这个问题,以及所有其他与破损的视觉树等。假设一下。

还有另一种变通方法: 将元素引用为 RelativeSource

我在使用 TabControl 时遇到了同样的问题,当将一堆命名控件放入。

我的解决办法是使用一个控件模板,其中包含要在选项卡页中显示的所有控件。在模板内部,您可以使用 Name 属性,也可以使用来自同一模板内部的其他控件的数据绑定到已命名控件的属性。

As Content of the TabItem Control, use a simple Control and set the ControlTemplate accordingly:

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

从后面的代码访问模板中的那些命名控件需要使用可视化树。

我遇到了这个问题,并找到了一个解决方案,可以使用 Xaml 设计自定义控件。它仍然有一点黑客,但一个解决了我的所有问题,没有任何明显的妥协。

基本上,您可以按照通常使用 xaml 的方式执行所有操作,但是您还需要在控件模板本身上包含一些头声明,并且 Base64将该模板编码以便在代码构造函数中加载。在这个 Xaml 摘录中没有显示,但是我的完整 Xaml 使用的名称空间实际上是针对 XamlTemplate 而不是 Controls 名称空间的。这是有意为之的,因为“发布”构建将开发调试引用从我的生产控件名称空间中移开。下面详细介绍。

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

我将指出,上面的 XAML 没有命名它派生自的控件,模板中的所有内容都使用相对查找来绑定其属性,甚至是自定义属性。

在 C # 方面,我使用 Xaml 中的 Base64编码版本的控件模板和指令来对控件的开发/发布版本进行洗牌。理论上,我在开发空间中的控件不会遇到这个主题所涉及的问题,但是会给我一种测试/开发它们的方法。发行版 DLL 版本似乎工作得很好,而且构建的控件确实有很好的设计时支持,就像它们在 Debug/Development 方面一样。

#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());
}
}

在创建要在主应用程序中使用的“发布”控件 DLL 之前,需要记住的关键事项是使用其控件模板的最新和最好版本更新 base64编码的字符串。这是因为发布版本与原始 Xaml 完全分离,并且完全依赖于编码的 Xaml。

上述控件和其他类似的控件可以在 GitHub上找到。这是一个库,我的目的是“解锁”许多东西,我想样式的标准控件不公开。还添加了一些不存在的特性。例如,上面的 TabControl 有一个额外的内容属性,用于使用选项卡标题的“未使用”区域。

重要提示:

  • 使用此方法会丢失基本样式,但如果自定义控件的样式使用 BasedOn="{StaticResource {x:Type TabControl}}"机制,则全部返回。
  • 我需要找时间来研究这是否会导致任何值得注意的内存泄漏,以及我是否可以做些什么来对抗它们,如果有人对此有任何想法,请在评论中告诉我。