我一直在研究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
视为指向控件本身。它寻找DeleteEmployee
在string
上的事实证实了这种怀疑,因为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();
}
这种现象引发了以下问题:
- 这是故意设计的吗?
- 如果是这样,那么使用自定义控件的人应该如何知道它内部使用的名称呢?
- 如果在自定义控件中根本不应该使用
Name
,那么有没有替代方案?我转而使用{RelativeSource}
以FindAncestor
模式,这很好用,但是否还有更好的方法? - 这是否与数据模板定义其自己的名称有关?如果我只是将名称重命名以避免与控件冲突,那么这并不妨碍我从模板内部引用主窗口。
RelativeSource={RelativeSource AncestorType={x:Type local:EmployeeControl}}
来避免 ElementName 绑定的问题。 - 15ee8f99-57ff-4f92-890c-b56153