WPF用户控件和名称作用域

6

我一直在研究WPF和MVVM,发现了一个奇怪的问题。当在自定义用户控件上使用{Binding ElementName=...}时,在使用该控件的窗口中似乎可以看到用户控件内根元素的名称。例如,这里是一个示例用户控件:

<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在代码后台中定义如下:

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

这里没有什么神秘的东西。然后,使用控件的窗口如下图所示:

<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文件中都表示相同的含义,并且指的是窗口,而不是用户控件。然而,当我运行它时,会打印出以下消息:

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视为指向控件本身。它寻找DeleteEmployeestring上的事实证实了这种怀疑,因为string恰好是我的虚构VM中的数据上下文。为了完整起见,这里是它:

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

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

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

这种现象引发了以下问题:
  1. 这是故意设计的吗?
  2. 如果是这样,那么使用自定义控件的人应该如何知道它内部使用的名称呢?
  3. 如果在自定义控件中根本不应该使用 Name ,那么有没有替代方案?我转而使用 {RelativeSource}FindAncestor 模式,这很好用,但是否还有更好的方法?
  4. 这是否与数据模板定义其自己的名称有关?如果我只是将名称重命名以避免与控件冲突,那么这并不妨碍我从模板内部引用主窗口。

2
太宽泛了吗?“请添加细节以缩小答案集或隔离可以在几段落中回答的问题”?我认为已经有足够的细节,答案可以用几句话来概括,例如“这是因为foo,做bar和frobz,但永远不要做bazz,这就成为一个非问题”。 - Sergei Tachenov
我本来要写一个答案的,但是在这里你可以找到不止一个。另外,我无法再现你的显示问题:( - lokusking
1
你可以(我认为)通过替换 RelativeSource={RelativeSource AncestorType={x:Type local:EmployeeControl}} 来避免 ElementName 绑定的问题。 - 15ee8f99-57ff-4f92-890c-b56153
五个问题的事情会在某些人中触发“过于宽泛”的反应。 - user1228
1
@lokusking,那个答案是关于如何访问父级数据上下文的,我的问题是为什么它不能以这种特定的方式工作。 - Sergei Tachenov
@Ed,那正是我所做的。我只是想知道为什么名称会冲突。 - Sergei Tachenov
1个回答

5

在这种情况下,你对wpf namescopes如何工作的困惑是可以理解的。

问题很简单,你正在应用绑定到一个UserControl上,它是它自己命名空间的“根”(可以这么说)。UserControls和几乎所有容器对象都有自己的命名空间。这些范围不仅包括子元素,还包括包含命名空间的对象。这就是为什么你可以将 x:Name="root" 应用于你的窗口并且(除了这一种情况)从子控件中定位它的原因。如果你不能做到这一点,那么命名空间基本上就没有用处。

混淆出现在你在一个包含命名空间的命名空间的根上进行操作时。你的假设是父级的命名空间具有优先权,但实际上并不是这样。绑定调用目标对象上的FindName,在你的情况下是你的用户控件(旁注,绑定并没有做什么,实际的调用可以在ElementObjectRef.GetObject中找到,但这是绑定委托调用的地方)

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

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

1. 这是按设计吗?

是的。否则名称范围将无法工作。

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

理想情况下,你是不需要的。就像你永远不想“必须”知道一个TextBox的根的名称一样。有趣的是,当试图修改其外观和感觉时,了解控件内定义的模板名称通常非常重要...
如果名称根本不应该在自定义控件中使用怎么办?如果是这样,那么有什么替代方案吗?我切换到使用以FindAncestor为模式的{RelativeSource},这很好用,但是否有更好的方法?
不!没问题。可以使用它。如果您不与其他人共享UserControl,则确保在遇到此特定问题时更改其名称。如果没有任何问题,请整天重复使用相同的名称,这不会造成任何问题。
如果您正在共享UserControl,则可能需要将其重命名为不会与其他人名称冲突的名称。称之为MuhUserControlTypeName_MuhRoot_Durr之类的东西。

4. 如果是这样,那么有什么替代方案吗?我改用在FindAncestor模式下使用{RelativeSource},目前工作正常,但是否有更好的方法?

没有。只需更改用户控件的x:Name并继续进行。

5. 这与数据模板定义自己的名称有关吗?如果我只是将其重命名以避免名称冲突,那么它不会阻止我从模板内引用主窗口。

不,我不这么认为。无论如何,我认为没有任何好的理由这样做。


1
“你的假设是父级名称空间具有优先权” - 不,我的假设是当我使用自定义控件时,它在父级范围内。就像传递给方法调用的实际参数的名称不会与该方法内的形式参数或局部变量的名称冲突一样。 - Sergei Tachenov
当您在名称范围的根上调用FindName时,只会检查在此范围内定义的名称。不会搜索父级范围。您能否澄清这一点?如果我的用户控件具有不同的名称,则在此上下文中ElementName=root完全正常。因此,父级范围被搜索的,但仅当当前范围内找不到该名称时才会搜索。就像只有在本地变量或其他东西未遮蔽字段时才能访问该字段一样。 - Sergei Tachenov
编辑过。如果你点击链接,你可以看到实际的算法。我错过了它沿着可视树向上走直到找到候选项的部分。无论如何,它从绑定所放置的元素开始,所以如果该元素有一个名称范围,首先会检查该名称范围,然后是其父级的名称范围,依此类推。 - user1228

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