WPF用户控件和名称范围

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

        

看起来非常合法。 现在,依赖属性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); } } } 

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

            

再一次,没有什么花哨……除了窗口和用户控件都具有相同名称的事实! 但我希望root在整个窗口XAML文件中意味着相同的事情,因此请参考窗口,而不是用户控件。 唉,运行时会打印以下消息:

System.Windows.Data错误:40:BindingExpression路径错误:’object”’String’(HashCode = -843597893)’上找不到’DeleteEmployee’属性。 BindingExpression:路径= DataContext.DeleteEmployee; DataItem =’EmployeeControl’(Name =’root’); target元素是’EmployeeControl’(Name =’root’); target属性是’DeleteEmployee’(类型’ICommand’)

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

 class ViewModel { public ObservableCollection Employees { get; private set; } public ICommand DeleteEmployee { get; private set; } public ViewModel() { Employees = new ObservableCollection(); Employees.Add("e1"); Employees.Add("e2"); Employees.Add("e3"); DeleteEmployee = new DelegateCommand(OnDeleteEmployee); } private void OnDeleteEmployee(string employee) { Employees.Remove(employee); } } 

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

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

这种现象提示以下问题:

  1. 这是设计的吗?
  2. 如果是这样,那么使用自定义控件的人如何知道它在内部使用的名称?
  3. 如果Name不应该在自定义控件中使用?
  4. 如果是这样,那么有哪些替代方案呢? 我在FindAncestor模式中切换到使用{RelativeSource} ,这种方法运行正常,但是有更好的方法吗?
  5. 这是否与数据模板定义自己的名称相关的事实有关? 如果我只是重命名它,那么它不会阻止我从模板中引用主窗口,因此名称不会与控件冲突。

在这种情况下,你对wpf名称范围如何工作的困惑是不可思议的 。

你的问题只是你在UserControl上应用绑定,这是它自己的名称范围的“根”(可以这么说)。 UserControls和几乎所有容器对象都有自己的名称范围。 这些范围不仅包含子元素,还包含包含名称范围的对象。 这就是为什么你可以将x:Name="root"到你的窗口并且(在这种情况下除外)从子控件中找到它。 如果你做不到 ,名望望远镜将毫无用处。

当你在一个包围范围的范围内对一个名望镜的根进行动作时,会出现混乱。 你的假设是父母的名称范围优先,但事实并非如此。 Binding在目标对象上调用FindName,在您的情况下是您的用户控件(旁注,Binding没有做插孔,实际调用可以在ElementObjectRef.GetObject找到,但这是Binding委托调用的地方)

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

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

这是设计的吗?

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

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

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

3.如果根本不应该在自定义控件中使用Name? 如果是这样,那么有哪些替代方案呢? 我在FindAncestor模式中切换到使用{RelativeSource},这种方法运行正常,但是有更好的方法吗?

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

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

如果是这样,那么有哪些替代方案呢? 我在FindAncestor模式中切换到使用{RelativeSource},这种方法运行正常,但是有更好的方法吗?

罗。 只需更改用户控件的x:Name ,然后继续。

5.这是否与数据模板定义自己的名称相关的事实有关? 如果我只是重命名它,那么它不会阻止我从模板中引用主窗口,因此名称不会与控件冲突。

不,我不相信。 无论如何,我认为没有任何理由。