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

4

我想引用另一个元素,该元素作为自定义用户控件的嵌套元素存在:

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

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

基本上我正在使用命令,并尝试引用具有名称的另一个元素,但我遇到了这个著名的错误:
无法在元素“...”上设置名称属性值“...”。当在另一个范围中定义它时,“...”处于元素“...”的范围内,该元素已经注册了一个名称。
我发现不可能给一个元素命名:如何创建带有命名内容的WPF UserControl 那么,在嵌套的自定义用户控件内容中,是否有其他引用该元素的方法可以工作呢?

在XAML中,你不应该使用x:Name而是直接使用Name吗?我知道这不是答案... - mlemay
1
你能展示一些代码吗,包括它所操作的XAML的实际精简版本吗? - Jon
上面的代码对我使用UserControl没有问题。你能提供更多细节吗?你是指自定义控件还是用户控件?此外,我假设在你的示例中应该有一个闭合的}。 - Phil
5个回答

3

虽然不是对你问题的直接回答,但有一个合理的解决方法。在你的视图中将文本绑定到viewmodel上的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)); 
    } 
} 

1
为什么不为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>

然后使用一个关键字来命名模板,并指定哪个UserControl使用哪个模板。

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

UIElement GetUserControlPart(UserControl control, String name)
{
   return control.Template.FindName(name, control) as UIElement;
}

如果我使用自定义属性,例如<userControls:Panel.Header>...</userControls:Panel.Header>,这个方法会如何工作?因为目前采用这种方法会呈现文本“System.Windows.Controls.ControlTemplate”。 - Tower
在移除了 ControlTemplate 后,它似乎可以在 VS 预览模式下工作,但是会抛出无法将 TextBox 强制转换为 DockPanel 的异常(将其添加到一组停靠面板中)。 - Tower

1

我最终是这样做的:

<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://dev59.com/snRB5IYBdhLWcg3wZmdz#1759923。按钮的名称也与文本框以相同的方式给出。

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


0
如果您使用.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吗? - Lee Louviere
在这种情况下不能指定ElementName,那是最初的问题。 - Stipo
有没有其他方法可以“标记”文本框以及任何可供使用的标识符?在这种情况下,兄弟节点是可行的,但如果结构更复杂,则问题仍然存在。 - Tower
也许可以使用标记(Tag)属性(http://msdn.microsoft.com/en-us/library/system.windows.frameworkelement.tag.aspx)。然后需要通过逻辑(或者可能是可视)树进行搜索,以找到具有特定标记值的 FrameworkElement。我假设该元素必须在文本框之前在 XAML 中定义,否则当调用 MarkupExtension.ProvideValue 时它不会成为逻辑树的一部分(当前解决方案也存在这个问题)。 - Stipo

0

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

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

然后您使用以下函数(该函数是此答案的解决方案在WPF中按其Uid获取对象):
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...
}

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