c# - WPF 用户控件和名称范围

标签 c# wpf xaml mvvm scope

我一直在研究 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 视为指代控件本身。它在 string 上查找 DeleteEmployee 的事实证实了这种怀疑,因为 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 根本不应该在自定义控件中使用?
  4. 如果是这样,那么有哪些替代方案?我切换到在 FindAncestor 模式下使用 {RelativeSource},这工作正常,但有更好的方法吗?
  5. 这与数据模板定义它们自己的名称 copes 有什么关系吗?如果我只是重命名它以便名称不与控件冲突,它不会阻止我从模板中引用主窗口。

最佳答案

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

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

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

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

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

1. Is this by design?

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

2. If so, how is someone using a custom control supposed to know what name it uses internally?

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

3. If Name is not supposed to be used in custom control at all? If so, then what are the alternatives? I switched to using {RelativeSource} in FindAncestor mode, which is working fine, but are there better ways?

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

如果您要共享您的 UserControl,您应该将其重命名为不会与其他人的名称冲突的名称。称它为 MuhUserControlTypeName_MuhRoot_Durr 或其他名称。

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?

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

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.

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

关于c# - WPF 用户控件和名称范围,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/38619921/

相关文章:

c# - 从图像中获取点的坐标

wpf - 如何改变文本 block 的高度

c# - WPF 在触发器上更改 Grid.ColumnSpan

c# - 如何将对 DataTable 所做的更改提交到我从中获取它的表?

c# - 如何在 C# 中读取 MKV 电影文件的元数据?

c# - 通过Mysql数据库验证用户身份

c# - 将嵌套字段绑定(bind)到 devexpress Grid 列

c# - 从后台线程启动时,UI 线程上的任务继续

C# - 带有 aspx 页面的 Web 服务

c# - 如何获取被点击的 ListView 项目