WPF 用户控件和名称范围

Ser*_*nov 5 c# wpf xaml scope mvvm

我一直在玩 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>
Run Code Online (Sandbox Code Playgroud)

对我来说看起来很合法。现在,依赖属性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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

<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>
Run Code Online (Sandbox Code Playgroud)

同样,没什么特别的……除了窗口和用户控件具有相同的名称!但我希望root在整个窗口 XAML 文件中都具有相同的含义,因此指的是窗口,而不是用户控件。唉,当我运行它时会打印以下消息:

System.Windows.Data 错误:40:BindingExpression 路径错误:在“对象”“字符串”(HashCode=-843597893)上找不到“DeleteEmployee”属性。BindingExpression:Path=DataContext.DeleteEmployee; DataItem='EmployeeControl' (Name='root'); 目标元素是 'EmployeeControl' (Name='root'); 目标属性是“DeleteEmployee”(类型“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);
    }
}
Run Code Online (Sandbox Code Playgroud)

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

    public MainWindow()
    {
        InitializeComponent();
        DataContext = new ViewModel();
    }
Run Code Online (Sandbox Code Playgroud)

这种现象提示以下问题:

  1. 这是故意的吗?
  2. 如果是这样,使用自定义控件的人如何知道它内部使用的名称?
  3. 如果Name根本不应该用于自定义控件?
  4. 如果是这样,那么有哪些替代方案?我改用{RelativeSource}inFindAncestor模式,这工作正常,但有更好的方法吗?
  5. 这与数据模板定义自己的名称应对这一事实有关吗?如果我只是重命名它,它不会阻止我从模板中引用主窗口,这样名称就不会与控件发生冲突。

小智 5

在这种情况下,您对wpf 名称范围如何工作的困惑是可以理解的。

您的问题很简单,您正在对 UserControl 应用绑定,这是其自己的名称范围的“根”(可以这么说)。用户控件和几乎所有容器对象都有自己的名称范围。这些范围不仅包含子元素,还包含包含名称范围的对象。这就是为什么您可以应用于x:Name="root"窗口并(除了这种情况)从子控件中找到它。如果不能,名称范围就毫无用处。

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

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

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

1. 这是设计使然吗?

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

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

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

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

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

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

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

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

5. 这与数据模板定义自己的名称对应有什么关系吗?如果我只是重命名它,这样名称就不会与控件冲突,这并不能阻止我从模板中引用主窗口。

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