【问题标题】:WPF user controls and name scopingWPF 用户控件和名称范围
【发布时间】:2016-07-27 17:47:59
【问题描述】:

我一直在使用 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 错误:40:BindingExpression 路径错误: 在“对象”“字符串”上找不到“DeleteEmployee”属性 (哈希码=-843597893)'。 BindingExpression:Path=DataContext.DeleteEmployee; DataItem='EmployeeControl' (Name='root');目标元素是 'EmployeeControl'(名称='root');目标属性是“DeleteEmployee” (输入“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. 这是否与数据模板定义自己的名称对应的事实有关?如果我只是重命名它,它不会阻止我从模板中引用主窗口,这样名称就不会与控件发生冲突。

【问题讨论】:

  • 太宽泛了? “请添加细节以缩小答案范围或隔离可以在几段中回答的问题”?我认为有足够的细节,答案可以用几句话来回答,就像“这是因为 foo,做 bar 和 frobz,但永远不要做 bazz,这不是问题”。
  • 我正要写一个答案,但here你发现不止一个。另外,我无法重现您的显示问题:(
  • 你可以(我认为)通过替换 RelativeSource={RelativeSource AncestorType={x:Type local:EmployeeControl}} 来避免 ElementName 绑定的事情。
  • 这五个问题对某些人来说触发了“太宽泛”。
  • @lok​​usking,这个答案是关于 如何 访问父数据上下文,我的问题是关于 为什么 它不能以这种特定方式工作。

标签: c# wpf xaml mvvm scope


【解决方案1】:

在这种情况下,您对wpf namescopes 工作方式的困惑是可以理解的。

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

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

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

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

1.这是设计的吗?

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

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

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

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

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

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

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

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

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

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

【讨论】:

  • “您的假设是父级的名称范围具有优先权”——不,我的假设是当我使用自定义控件时,它在父级的范围内。就像传递给方法调用的实际参数的名称不会与该方法内的正式参数或局部变量的名称冲突。
  • “当您在名称范围的根目录上调用 FindName 时,只会检查在此范围内定义的名称。不搜索父范围。”——你能澄清这一点吗?如果我的用户控件有不同的名称,那么 ElementName=root 在这种情况下工作得很好。所以父作用域 is 被搜索,但前提是在当前作用域内没有找到该名称,对吧?就像一个字段只有在没有被局部变量或其他东西遮蔽时才能访问。
  • 已编辑。如果您点击链接,您可以看到实际的算法。我错过了它沿着视觉树向上走直到找到候选人的部分。无论如何,它从放置绑定的元素开始,因此如果它具有名称范围,则首先检查该名称范围,然后检查其父级的名称范围,等等。
猜你喜欢
  • 2013-07-11
  • 2013-07-04
  • 2016-10-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-05-21
  • 1970-01-01
相关资源
最近更新 更多