WPF 用户控件和名称范围 [英] WPF user controls and name scoping

查看:37
本文介绍了WPF 用户控件和名称范围的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我一直在玩 WPF 和 MVVM,并注意到一件奇怪的事情.在自定义用户控件上使用 {Binding ElementName=...} 时,用户控件中根元素的名称似乎在使用该控件的窗口中可见.说,这是一个示例用户控件:

I've been playing around with WPF and MVVM and noticed a strange thing. When using {Binding ElementName=...} on a custom user control, the name of the root element within the user control seems to be visible in the window using the control. Say, here is an example user control:

<UserControl x:Class="TryWPF.EmployeeControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:TryWPF"
             Name="root">
  <Grid>
    <Grid.ColumnDefinitions>
      <ColumnDefinition Width="*" />
      <ColumnDefinition Width="Auto" />
    </Grid.ColumnDefinitions>
    <TextBlock Grid.Column="0" Text="{Binding}"/>
    <Button Grid.Column="1" Content="Delete"
                Command="{Binding DeleteEmployee, ElementName=root}"
                CommandParameter="{Binding}"/>
  </Grid>
</UserControl>

对我来说看起来很合法.现在,依赖属性 DeleteEmployee 在代码隐藏中定义,如下所示:

Looks pretty legit to me. Now, the dependency property DeleteEmployee is defined in the code-behind, like this:

public partial class EmployeeControl : UserControl
{
    public static DependencyProperty DeleteEmployeeProperty
        = DependencyProperty.Register("DeleteEmployee",
                                      typeof(ICommand),
                                      typeof(EmployeeControl));

    public EmployeeControl()
    {
        InitializeComponent();
    }

    public ICommand DeleteEmployee
    {
        get
        {
            return (ICommand)GetValue(DeleteEmployeeProperty);
        }
        set
        {
            SetValue(DeleteEmployeeProperty, value);
        }
    }
}

这里没有什么神秘的.然后,使用该控件的窗口如下所示:

Nothing mysterious here. Then, the window using the control looks like this:

<Window x:Class="TryWPF.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:TryWPF"
        Name="root"
        Title="Try WPF!" Height="350" Width="525">
  <StackPanel>
    <ListBox ItemsSource="{Binding Employees}" HorizontalContentAlignment="Stretch">
      <ListBox.ItemTemplate>
        <DataTemplate>
          <local:EmployeeControl
            HorizontalAlignment="Stretch"
            DeleteEmployee="{Binding DataContext.DeleteEmployee, ElementName=root}"/>
        </DataTemplate>
      </ListBox.ItemTemplate>
    </ListBox>
  </StackPanel>
</Window>

再说一次,没什么特别的……除了窗口和用户控件具有相同的名称!但我希望 root 在整个窗口 XAML 文件中的含义相同,因此指的是窗口,而不是用户控件.唉,运行的时候会打印如下信息:

Again, nothing fancy... except the fact that both the window and the user control have the same name! But I'd expect root to mean the same thing throughout the whole window XAML file, and therefore refer to the window, not to the user control. Alas, the following message is printed when I run it:

System.Windows.Data 错误:40:BindingExpression 路径错误:在对象"字符串"上找不到DeleteEmployee"属性(HashCode=-843597893)'.BindingExpression:Path=DataContext.DeleteEmployee;DataItem='EmployeeControl' (Name='root');目标元素是'EmployeeControl' (Name='root');目标属性是DeleteEmployee"(输入'ICommand')

System.Windows.Data Error: 40 : BindingExpression path error: 'DeleteEmployee' property not found on 'object' ''String' (HashCode=-843597893)'. BindingExpression:Path=DataContext.DeleteEmployee; DataItem='EmployeeControl' (Name='root'); target element is 'EmployeeControl' (Name='root'); target property is 'DeleteEmployee' (type 'ICommand')

DataItem='EmployeeControl' (Name='root') 让我觉得它把 ElementName=root 视为指代控件本身.它在 string 上查找 DeleteEmployee 的事实证实了这种怀疑,因为 string 正是我设计的 VM 中的数据上下文.为了完整起见,这里是:

DataItem='EmployeeControl' (Name='root') makes me think that it treats ElementName=root as referring to the control itself. The fact that it looks for DeleteEmployee on string confirms that suspicion because string is exactly what the data context is in my contrived VM. Here it is, for the sake of completeness:

class ViewModel
{
    public ObservableCollection<string> Employees { get; private set; }
    public ICommand DeleteEmployee { get; private set; }

    public ViewModel()
    {
        Employees = new ObservableCollection<string>();
        Employees.Add("e1");
        Employees.Add("e2");
        Employees.Add("e3");
        DeleteEmployee = new DelegateCommand<string>(OnDeleteEmployee);
    }

    private void OnDeleteEmployee(string employee)
    {
        Employees.Remove(employee);
    }
}

它在构造函数中被实例化并分配给窗口,这是窗口代码隐藏的唯一内容:

It is instantiated and assigned to the window in the constructor, which is the only thing in code-behind for the window:

    public MainWindow()
    {
        InitializeComponent();
        DataContext = new ViewModel();
    }

这种现象提示以下问题:

This phenomenon prompts the following questions:

  1. 这是故意的吗?
  2. 如果是这样,使用自定义控件的人如何知道它在内部使用的名称?
  3. 如果 Name 根本不应该用于自定义控件?
  4. 如果是这样,那么有哪些替代方案?我切换到在 FindAncestor 模式下使用 {RelativeSource},这工作正常,但有更好的方法吗?
  5. 这是否与数据模板定义自己的名称应对这一事实有关?如果我只是重命名它以便名称不会与控件冲突,它并不会阻止我从模板中引用主窗口.
  1. Is this by design?
  2. If so, how is someone using a custom control supposed to know what name it uses internally?
  3. If Name is not supposed to be used in custom control at all?
  4. If so, then what are the alternatives? I switched to using {RelativeSource} in FindAncestor mode, which is working fine, but are there better ways?
  5. Does this have anything to do with the fact that data templates define their own names copes? It doesn't stop me from referring to the main window from within a template if I just rename it so the name doesn't clash with the control.

推荐答案

您在这里对 wpf namescopes 在这种情况下工作是可以理解的.

Your confusion here about how wpf namescopes work is understanable in this situation.

您的问题只是您正在对 UserControl 应用绑定,这是其自己的名称范围的根"(可以这么说).UserControls 和几乎所有容器对象都有自己的名称范围.这些范围不仅包含子元素,还包含包含名称范围的对象.这就是为什么您可以将 x:Name="root" 应用于您的窗口并(除了这种情况)从子控件中定位它的原因.如果你不能,名称范围就几乎没用了.

Your issue is simply that you are applying a binding upon a UserControl, which is the "root" (so to speak) of its own namescope. UserControls, and pretty much any container objects, have their own namescopes. These scopes encompass not only child elements, but the object that contains the namescope as well. This is why you can apply x:Name="root" to your window and (except in this one case) locate it from a child control. If you couldn't, namescopes would be pretty much useless.

当您在一个包含的名称范围内对名称范围的根执行操作时,就会出现混淆.您的假设是父级的名称范围具有优先权,但事实并非如此.Binding 在目标对象上调用 FindName,在您的情况下是您的用户控件.(旁注,Binding 不做 jack,实际调用可以在 ElementObjectRef.GetObject 中找到,但那是 Binding 将调用委托给的地方)

The confusion comes when you're acting upon a root of a namescope within an encompassing namescope. Your assumption was that the parent's namescope had precedence, but it does not. The Binding is calling FindName on the target object, which in your case is your user control. (Side note, the Binding isn't doing jack, the actual calls can be found in ElementObjectRef.GetObject, but that's where the Binding delegates the call to)

当您在名称范围的根上调用 FindName 时,只会检查在此范围内定义的名称.不搜索父范围. (编辑...更多阅读源http://referencesource.microsoft.com/#PresentationFramework/src/Framework/MS/Internal/Data/ObjectRef.cs,5a01adbbb94284c0从第 46 行开始,我看到算法沿着可视化树向上走,直到找到目标,因此子作用域优先于父作用域)

When you call FindName on the root of a namescope, only names defined within this scope are examined. Parent scopes are not searched. (Edit... a bit more reading of the source http://referencesource.microsoft.com/#PresentationFramework/src/Framework/MS/Internal/Data/ObjectRef.cs,5a01adbbb94284c0 starting at line 46 I see that the algorithm walks up the visual tree until it finds a target, so child scopes have precedence over parent scopes)

所有这一切的结果是您获得了用户控件实例而不是窗口,就像您希望的那样.现在,回答您的个人问题...

The result of all this is that you get the user control instance instead of the window, like you were hoping. Now, to answer your individual questions...

1.这是故意的吗?

是的.否则名称范围将不起作用.

Yep. Otherwise namescopes wouldn't work.

2.如果是这样,使用自定义控件的人如何知道它内部使用的名称?

理想情况下,您不会.就像您永远不想必须知道TextBox 的根名称一样.有趣的是,在尝试修改控件的外观和感觉时,了解控件中定义的模板的名称通常很重要...

Ideally, you wouldn't. Just like you don't ever want to have to know the name of the root of a TextBox. Interestingly, though, knowing the names of templates defined within a control is often important when attempting to modify it's look and feel...

3.如果 Name 根本不应该用于自定义控件?如果是这样,那么有哪些替代方案?我在 FindAncestor 模式下改用 {RelativeSource},效果很好,但有更好的方法吗?

不!没关系.用它.如果您不与其他人共享您的 UserControl,请确保在遇到此特定问题时更改其名称.如果您没有任何问题,请整天重复使用相同的名称,它不会有任何伤害.

No! It's fine. Use it. If you aren't sharing your UserControl with other people, just make sure to change its name if you are experiencing this particular problem. If you aren't having any problem, reuse the same name all day, it isn't hurting anything.

如果您正在共享您的 UserControl,您可能应该将其重命名为不会与其他人的名称冲突的名称.称之为 MuhUserControlTypeName_MuhRoot_Durr 或其他什么.

If you ARE sharing your UserControl, you should probably rename it to something that won't conflict with other people's names. Call it MuhUserControlTypeName_MuhRoot_Durr or something.

4.如果是这样,那么有哪些替代方案?我在 FindAncestor 模式下改用 {RelativeSource},效果很好,但有更好的方法吗?

不.只需更改用户控件的 x:Name 并继续.

Nah. Just change the x:Name of your user control and move on.

5.这与数据模板定义自己的名称应对这一事实有关吗?如果我只是重命名它以便名称不会与控件冲突,它并不会阻止我从模板中引用主窗口.

不,我不这么认为.无论如何,我认为它没有任何充分的理由.

No, I don't believe so. I don't think there is any good reason for it to be, anyhow.

这篇关于WPF 用户控件和名称范围的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆