如何在自定义嵌套用户控件中引用另一个 XAML 元素?

Posted

技术标签:

【中文标题】如何在自定义嵌套用户控件中引用另一个 XAML 元素?【英文标题】:How to refer to another XAML element inside a custom nested user control? 【发布时间】:2012-07-05 02:06:57 【问题描述】:

我想引用另一个元素,它嵌套在自定义用户控件下:

<userControls:Test>
     <TextBox Name="Foo" />

     <Button CommandParameter="Binding Text, ElementName=Foo" Command="Binding anything" />
</userControls:Test>

基本上我正在使用命令,我试图用一个名称来引用另一个元素,但我得到了这个著名的:

无法在元素“...”上设置名称属性值“...”。 '...' 在下面 元素“...”的范围,它已经注册了一个名称 它是在另一个范围内定义的。

我认为不可能命名一个元素:How to create a WPF UserControl with NAMED content

那么是否有任何其他方法来引用可以在嵌套的自定义用户控件内容中工作的元素?

【问题讨论】:

在 xaml 中你不应该使用 x:Name 而不仅仅是 Name?我知道这不是答案... 您能否展示一些代码,包括它所运行的 XAML 的实际精简版本? 上面的代码对我来说可以使用 UserControl。您能否提供更多详细信息,您是指自定义控件还是用户控件?另外我假设您的示例中应该有一个结束 。 【参考方案1】:

不是直接回答您的问题,而是一种合理的解决方法。在您的视图中,将文本绑定到视图模型上的 FooText 属性,直接在 ICommand 的声明旁边。命令执行时,使用最新的FooText

<!-- View -->
<userControls:Test>
     <TextBox Text=Binding FooText, Mode=TwoWay />

     <Button Command="Binding FooCommand" />
</userControls:Test>


// ViewModel
public string FooText  get; set; 

public ICommand FooCommand 
 
    get 
     
        return new DelegateCommand(() => ExecuteFoo(FooText)); 
     
 

【讨论】:

【参考方案2】:

为什么不为 UserControl 制作一个模板。

<....Resources>
    <Style TargetType="x:Type UserControl" x:Key="MyUserControl">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate>
                    <StackPanel>
                        <TextBox x:Name="PART_Foo"/>
                        <TextBlock Text="Binding ElementName=PART_Foo, Path=Text"/>
                        <ContentPresenter  Content="Binding RelativeSource=RelativeSource Mode=TemplatedParent,Path=Content"/>
                    </StackPanel>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</....Resources>
<UserControl Style="StaticResource MyUserControl">
</UserControl>

然后用key命名模板,并指定哪个UserControl使用哪个模板。

此外,在您的代码中,您可以使用以下方法获取模板化部分的 PART_Name:

UIElement GetUserControlPart(UserControl control, String name)

   return control.Template.FindName(name, control) as UIElement;

【讨论】:

如果我使用像 &lt;userControls:Panel.Header&gt;...&lt;/userControls:Panel.Header&gt; 这样的自定义属性,这将如何工作?因为现在使用这种方法,它会呈现文本"System.Windows.Controls.ControlTemplate" 删除 ControlTemplate 后,它似乎在 VS 预览模式下工作,但抛出无法将 TextBox 转换为 DockPanel (将其添加到停靠面板集合)。【参考方案3】:

我最终这样做了:

<UserControl x:Class="MyProject.FooBar"
         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:userControls="clr-namespace:MyProject.UserControls"
         Loaded="Loaded"> <!-- notice -->

    <userControls:Test>
         <TextBox Initialized="FooInitialized" />

         <Button Initialized="AnotherInitialized" />
    </userControls:Test>

和代码:

// This for every initialization, all we do is set names after the elements are initialized.
private void FooInitialized(object sender, EventArgs e)

    ((TextBox) sender).Name = "Foo";


// Here when the entire junk is loaded, we set the necessary commands.
private void Loaded(object sender, EventArgs e)

    // Find elements.
    var button = UIHelper.FindChild<Button>(this, "Bar");
    var textBox = UIHelper.FindChild<TextBox>(this, "Foo");

    // Set up bindings for button.
    button.Command = ((FooViewModel) DataContext).FooCommand;
    button.CommandParameter = textBox;

UIHelper 来自https://***.com/a/1759923/283055。该按钮的名称也与文本框相同。

基本上所有名称都是通过Initialized 事件设置的。唯一的缺点是您必须在代码中引用名称,这就是为什么我在整个组件加载后通过代码设置命令的原因。我发现这是代码和 UI 标记之间的最佳折衷方案。

【讨论】:

【参考方案4】:

如果您使用 .NET 4.0 并且需要将绑定源设置为同级控件,那么您可以使用此 MarkupExtension:

public class SiblingSourceBinding : MarkupExtension
  
    public Binding Binding
    
        get;
        set;
    

    public int SiblingIndex
    
        get;
        set;
    

    public override object ProvideValue(IServiceProvider serviceProvider)
    
        var provideValueTarget = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;

        if (provideValueTarget != null)
            this.Binding.Source = LogicalTreeHelper.GetChildren(LogicalTreeHelper.GetParent(provideValueTarget.TargetObject as DependencyObject)).Cast<object>().ElementAt(this.SiblingIndex);
        else
            throw new InvalidOperationException("Unable to set sibling binding source.");

        return this.Binding.ProvideValue(serviceProvider);
   

并像这样使用它:

<userControls:Test>
    <TextBox/>
    <Button CommandParameter="local:SiblingSourceBinding Binding=Binding Text, SiblingIndex=0" Command="Binding anything"/>
</userControls:Test>

【讨论】:

您可以通过搜索名称将其更改为 ElementName 吗? ElementName 在这种情况下无法指定,这是原来的问题。 有没有其他方法可以用任何可以使用的标识符来“标记”文本框?在这种情况下,兄弟姐妹可以工作,但如果结构更复杂,问题仍然存在。 或许可以使用 Tag (msdn.microsoft.com/en-us/library/…) 属性。然后必须执行通过逻辑(或者可能是视觉?)树的搜索以找到具有特定标记值的 FrameworkElement。我假设元素必须在文本框之前在 XAML 中定义,否则在调用 MarkupExtension.ProvideValue 时它不会成​​为逻辑树的一部分(当前解决方案也有这个问题)。【参考方案5】:

您可以在嵌套元素中使用 Uid,如下所示:

<userControls:Test Name="usrCtrl">
     <TextBox Uid="Foo" />
     <Button Uid="btn"/>
</userControls:Test>

然后您使用以下函数(该函数是该答案Get object by its Uid in WPF的解决方案):

private static UIElement FindUid(this DependencyObject parent, string uid)

    var count = VisualTreeHelper.GetChildrenCount(parent);
    if (count == 0) return null;

    for (int i = 0; i < count; i++)
    
        var el = VisualTreeHelper.GetChild(parent, i) as UIElement;
        if (el == null) continue;

        if (el.Uid == uid) return el;

        el = el.FindUid(uid);
        if (el != null) return el;
    
    return null;

然后在您的 Loaded 函数中添加:

private void Loaded(object sender, EventArgs e)

    // Find elements.
    var button = FindUid(usrCtrl, "btn") as Button;
    var textBox = FindUid(usrCtrl, "Foo") as TextBox;
     
    // Do binding here...

【讨论】:

以上是关于如何在自定义嵌套用户控件中引用另一个 XAML 元素?的主要内容,如果未能解决你的问题,请参考以下文章

Xamarin Forms 命令在自定义控件中始终为空

XAML: 自定义控件中事件处理的最佳实践

XAML: 自定义控件中事件处理的最佳实践

如何在两个项目共享的 xaml 文件中添加用户控件的引用

如何在一个 xaml 页面中使用多个用户控件?

WPF xaml中列表依赖属性的定义