简体   繁体   中英

How to refer to another XAML element inside a custom nested user control?

I want to refer to another element, which resides as nested under a custom user control:

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

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

Basically I'm using commands and I'm trying to refer to another element with a name, but I get this famous:

Cannot set Name attribute value '...' on element '...'. '...' is under the scope of element '...', which already had a name registered when it was defined in another scope.

I figured it's impossible to name an element: How to create a WPF UserControl with NAMED content

So is there any other way to refer to the element that would work inside nested custom user control content?

Not a direct answer to your question, but a reasonable workaround. In your view bind the Text to FooText property on the viewmodel, directly beside the declaration of the ICommand. When the command executes, use the latest 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)); 
    } 
} 

Why not make a template to the 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>

Then name the template with a key, and specify which UserControl uses which template.

Also, in your code, you could get the PART_Name of the templated part using:

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

I ended up doing it like this:

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

And code:

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

The UIHelper is from https://stackoverflow.com/a/1759923/283055 . The button is also given a name the same way as the text box.

Basically all names are set via Initialized event. The only down side is that you must refer to the names in code, and that's why I'm setting commands via code after the entire component has loaded. I found this the best compromise between code and UI markup.

If you use .NET 4.0 and require to set the binding source to the sibling control, then you could use this 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);
   }

and use it like this:

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

You can use Uid in your nested element as in the following:

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

Then you use the following function (the function was a solution for this answer 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;
}

Then in your Loaded function add this:

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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