嵌套的用户控件通信
Nested UserControls communication
简而言之:我有一个 ListView,当我 select ListView 的一个项目时,这个项目应该在详细的 UserControl 中显示和编辑。
我有一个 Window (ViewMain),它有一个 UserControl (UserControlEmployees),它有一个 ListView 和另一个 UserControl (UserControlEmployeeDetails)。 ListView 的项目由第三个 UserControl (UserControlEmployee) 显示。 UserControlEmployees 有两个依赖属性:一个 ObservableCollection (Employees) 和一个 Employee (SelectedEmployee)。 ViewModel 将 ObservableCollection 传递给 UserControlEmployees。 UserControlEmployees 然后将 Employees 传递给 ListView。 ListView 的 SelectedItem 绑定到 SelectedEmployee。
像这样:
SelectedEmployee 也应该绑定到 UserControlEmployeeDetails。所以我尝试将 ViewModelEmployeeDetail 和 ListView 的 SelectedItem 绑定到相同的依赖项 属性.
我猜问题出在 UserControlEmployees 中:
我的想法是 control.ControlEmployeesListView.SelectedItem = e.NewValue 作为员工;会将 SelectedItem 绑定到 SelectedEmployee。但这不起作用,我不知道我还能如何绑定它。通常我会做类似 XAML 的事情,但在这种情况下我无法访问它。
编辑
我注意到我忘记将 ListView SelectedItem 设置为 Binding。
<ListView
x:Name="ControlEmployeesListView"
Grid.Row="0"
SelectedItem="{Binding Mode=TwoWay}">
我修复了这个问题,但现在我得到了这个异常:
System.Windows.Markup.XamlParseException:“在 'System.Windows.Data.Binding' 上提供值引发了异常。”行号“26”和行位置“17”。'
内部异常
InvalidOperationException: 双向绑定需要 Path 或 XPath。
/编辑
UserControlEmployees.xaml
<UserControl
x:Class="TestNestedUserControls.View.UserControls.UserControlEmployees"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:uc="clr-namespace:TestNestedUserControls.View.UserControls"
d:DesignHeight="25"
d:DesignWidth="300"
mc:Ignorable="d">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- ListView -->
<ListView Grid.Row="0">
<ListView x:Name="ControlEmployeesListView" Grid.Row="0">
<ListView.ItemTemplate>
<DataTemplate>
<uc:UserControlEmployeeListItem EmployeeListItem="{Binding}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</ListView>
<!-- Details -->
<uc:UserControlEmployeeDetails x:Name="ControlUserControlEmployeeDetails" Grid.Row="1" />
<!-- SelectedEmployee="{Binding}" -->
</Grid>
</UserControl>
这就是 UserControlEmployees.xaml.cs
中的代码
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using TestNestedUserControls.Model;
namespace TestNestedUserControls.View.UserControls
{
/// <summary>
/// Interaction logic for UserControlEmployees.xaml
/// </summary>
public partial class UserControlEmployees : UserControl, INotifyPropertyChanged
{
public UserControlEmployees()
{
InitializeComponent();
}
// List Items
public ObservableCollection<Employee> Employees
{
get { return (ObservableCollection<Employee>)GetValue(EmployeesProperty); }
set
{
SetValue(EmployeesProperty, value);
NotifyPropertyChanged();
}
}
public static readonly DependencyProperty EmployeesProperty =
DependencyProperty.Register(nameof(Employees), typeof(ObservableCollection<Employee>), typeof(UserControlEmployees), new PropertyMetadata(default, SetNew));
private static void SetNew(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var control = d as UserControlEmployees;
if (control != null)
{
control.ControlEmployeesListView.ItemsSource = e.NewValue as ObservableCollection<Employee>;
}
}
//Selected Item
public Employee SelectedEmployee
{
get { return (Employee)GetValue(EmployeeProperty); }
set
{
SetValue(EmployeeProperty, value);
NotifyPropertyChanged();
}
}
public static readonly DependencyProperty EmployeeProperty =
DependencyProperty.Register(nameof(SelectedEmployee), typeof(Employee), typeof(UserControlEmployees), new PropertyMetadata(default, SetNewSelected));
private static void SetNewSelected(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var control = d as UserControlEmployees;
if (control != null)
{
control.ControlUserControlEmployeeDetails.EmployeeDetail = e.NewValue as Employee;
control.ControlEmployeesListView.SelectedItem = e.NewValue as Employee;
}
}
#region INotifyPropertyChanged ⬇️
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion ⬆️
}
}
修复绑定错误:错误信息中提供了错误的解释和解决方法。只需设置 Binding.Path
.
<ListView SelectedItem="{Binding Path=., Mode=TwoWay}">
请注意,Selector.SelectedItem
默认绑定 TwoWay
。所以写就足够了:
<ListView SelectedItem="{Binding}">
从绑定来看,您的 DataContext
似乎是错误的。由于所有用户控件都使用相同的数据进行操作,例如员工集合和选定员工,所有用户控件应共享相同的 DataContext
,即保存源集合的视图模型。
此视图模型还应定义 SelectedEmployee
属性,ControlEmployeesListView
(ListView
)和 UserControlEmployeeDetails
都可以绑定。
由于 UserControlEmployees
不在内部对员工集合进行操作,因此不需要专用的 Employee
和 SelectedEmployee
属性。仅当用户控件旨在可重用时,它才可以或应该具有这些属性。但是当它只在这个特定的上下文中使用时,如果你提前知道 DataContext
你可以避免它们并直接绑定到 UserControl.DataContext
.
Control
、UserControl
或 DependencyObject
通常不应实现 INotifyPropertyChanged
,而应将其属性实现为 DependecyProperty
。 DependencyProperty
的 set
和 get
方法只是 DependencyObject.SetValue
和 DependencyObject.GetValue
的包装器。这些包装器仅由您的自定义代码调用,而不会由框架调用。
由于 DependencyProperty
提供了自己的通知机制,包装器只是设置它们的关联 DependencyProperty
,因此会自动发出更改通知。因此在每个 setter 中调用 NotifyPropertyChanged()
是多余的。
另一点是您的 SetNew...
属性 更改了回调。他们只是将新值委托给控件。这应该在数据绑定的帮助下完成。
我也想知道这个嵌套的 <ListView><ListView /></ListView>
是什么意思。也删除它(这甚至可以编译吗?)。
DependencyProperty
字段应与注册的属性同名:SelectedEmployeeProperty
而不是EmployeeProperty
。
以下示例显示了如何正确连接数据。它基于您的代码并使用 Emloyees
和 SelectedEmployee
的专用属性。在您的场景中,删除这些属性并直接绑定到 DataContext
(即视图模型)似乎很合理。但这取决于用户控件的目的。但这也会简化代码。
MainViewModel.cs
class MainViewModel : INotifyPropertyChanged
{
public ObservableCollection<Employee> Employees { get; set; }
private Employee selectedEmployee;
public Employee SelectedEmployee
{
get => this.selectedEmployee;
set
{
this.selectedEmployee = value;
OnPropertyChanged();
}
}
}
UserControlEmployees.xaml.cs
public partial class UserControlEmployees : UserControl
{
public UserControlEmployees()
{
InitializeComponent();
}
public IEnumerable<Employee> Employees
{
get => (IEnumerable<Employee>) GetValue(EmployeesProperty);
set => SetValue(EmployeesProperty, value);
}
public static readonly DependencyProperty EmployeesProperty = DependencyProperty.Register(
nameof(Employees),
typeof(IEnumerable<Employee>),
typeof(UserControlEmployees),
new PropertyMetadata(default));
}
public Employee SelectedEmployee
{
get => (Employee) GetValue(SelectedEmployeeProperty);
set => SetValue(SelectedEmployeeProperty, value);
}
// Configure to bind TwoWay by default
public static readonly DependencyProperty SelectedEmployeeProperty = DependencyProperty.Register(
nameof(SelectedEmployee),
typeof(Employee),
typeof(UserControlEmployees),
new FrameworkPropertyMetadata(
default,
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
}
UserControlEmployees.xaml
<UserControl>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- ListView -->
<ListView Grid.Row="0"
ItemsSource="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=Employees}"
SelectedItem="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=SelectedEmployee}">
<ListView.ItemTemplate>
<DataTemplate DataType="{x:Type local:Employee}">
<uc:UserControlEmployeeListItem EmployeeListItem="{Binding}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<!-- Details -->
<uc:UserControlEmployeeDetails Grid.Row="1"
SelectedEmployee="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=SelectedEmployee}" />
</Grid>
</UserControl>
MainWndow.xaml
<Window>
<Window.DataContext>
<MainViewModel />
</Window.DataContext>
<UserControlEmployees Employees="{Binding Employees}"
SelectedEmployee="{Binding SelectedEmployee}" />
</Window>
简而言之:我有一个 ListView,当我 select ListView 的一个项目时,这个项目应该在详细的 UserControl 中显示和编辑。
我有一个 Window (ViewMain),它有一个 UserControl (UserControlEmployees),它有一个 ListView 和另一个 UserControl (UserControlEmployeeDetails)。 ListView 的项目由第三个 UserControl (UserControlEmployee) 显示。 UserControlEmployees 有两个依赖属性:一个 ObservableCollection (Employees) 和一个 Employee (SelectedEmployee)。 ViewModel 将 ObservableCollection 传递给 UserControlEmployees。 UserControlEmployees 然后将 Employees 传递给 ListView。 ListView 的 SelectedItem 绑定到 SelectedEmployee。
像这样:
SelectedEmployee 也应该绑定到 UserControlEmployeeDetails。所以我尝试将 ViewModelEmployeeDetail 和 ListView 的 SelectedItem 绑定到相同的依赖项 属性.
我猜问题出在 UserControlEmployees 中: 我的想法是 control.ControlEmployeesListView.SelectedItem = e.NewValue 作为员工;会将 SelectedItem 绑定到 SelectedEmployee。但这不起作用,我不知道我还能如何绑定它。通常我会做类似 XAML 的事情,但在这种情况下我无法访问它。
编辑 我注意到我忘记将 ListView SelectedItem 设置为 Binding。
<ListView
x:Name="ControlEmployeesListView"
Grid.Row="0"
SelectedItem="{Binding Mode=TwoWay}">
我修复了这个问题,但现在我得到了这个异常:
System.Windows.Markup.XamlParseException:“在 'System.Windows.Data.Binding' 上提供值引发了异常。”行号“26”和行位置“17”。'
内部异常 InvalidOperationException: 双向绑定需要 Path 或 XPath。
/编辑 UserControlEmployees.xaml
<UserControl
x:Class="TestNestedUserControls.View.UserControls.UserControlEmployees"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:uc="clr-namespace:TestNestedUserControls.View.UserControls"
d:DesignHeight="25"
d:DesignWidth="300"
mc:Ignorable="d">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- ListView -->
<ListView Grid.Row="0">
<ListView x:Name="ControlEmployeesListView" Grid.Row="0">
<ListView.ItemTemplate>
<DataTemplate>
<uc:UserControlEmployeeListItem EmployeeListItem="{Binding}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</ListView>
<!-- Details -->
<uc:UserControlEmployeeDetails x:Name="ControlUserControlEmployeeDetails" Grid.Row="1" />
<!-- SelectedEmployee="{Binding}" -->
</Grid>
</UserControl>
这就是 UserControlEmployees.xaml.cs
中的代码using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using TestNestedUserControls.Model;
namespace TestNestedUserControls.View.UserControls
{
/// <summary>
/// Interaction logic for UserControlEmployees.xaml
/// </summary>
public partial class UserControlEmployees : UserControl, INotifyPropertyChanged
{
public UserControlEmployees()
{
InitializeComponent();
}
// List Items
public ObservableCollection<Employee> Employees
{
get { return (ObservableCollection<Employee>)GetValue(EmployeesProperty); }
set
{
SetValue(EmployeesProperty, value);
NotifyPropertyChanged();
}
}
public static readonly DependencyProperty EmployeesProperty =
DependencyProperty.Register(nameof(Employees), typeof(ObservableCollection<Employee>), typeof(UserControlEmployees), new PropertyMetadata(default, SetNew));
private static void SetNew(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var control = d as UserControlEmployees;
if (control != null)
{
control.ControlEmployeesListView.ItemsSource = e.NewValue as ObservableCollection<Employee>;
}
}
//Selected Item
public Employee SelectedEmployee
{
get { return (Employee)GetValue(EmployeeProperty); }
set
{
SetValue(EmployeeProperty, value);
NotifyPropertyChanged();
}
}
public static readonly DependencyProperty EmployeeProperty =
DependencyProperty.Register(nameof(SelectedEmployee), typeof(Employee), typeof(UserControlEmployees), new PropertyMetadata(default, SetNewSelected));
private static void SetNewSelected(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var control = d as UserControlEmployees;
if (control != null)
{
control.ControlUserControlEmployeeDetails.EmployeeDetail = e.NewValue as Employee;
control.ControlEmployeesListView.SelectedItem = e.NewValue as Employee;
}
}
#region INotifyPropertyChanged ⬇️
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion ⬆️
}
}
修复绑定错误:错误信息中提供了错误的解释和解决方法。只需设置 Binding.Path
.
<ListView SelectedItem="{Binding Path=., Mode=TwoWay}">
请注意,Selector.SelectedItem
默认绑定 TwoWay
。所以写就足够了:
<ListView SelectedItem="{Binding}">
从绑定来看,您的 DataContext
似乎是错误的。由于所有用户控件都使用相同的数据进行操作,例如员工集合和选定员工,所有用户控件应共享相同的 DataContext
,即保存源集合的视图模型。
此视图模型还应定义 SelectedEmployee
属性,ControlEmployeesListView
(ListView
)和 UserControlEmployeeDetails
都可以绑定。
由于 UserControlEmployees
不在内部对员工集合进行操作,因此不需要专用的 Employee
和 SelectedEmployee
属性。仅当用户控件旨在可重用时,它才可以或应该具有这些属性。但是当它只在这个特定的上下文中使用时,如果你提前知道 DataContext
你可以避免它们并直接绑定到 UserControl.DataContext
.
Control
、UserControl
或 DependencyObject
通常不应实现 INotifyPropertyChanged
,而应将其属性实现为 DependecyProperty
。 DependencyProperty
的 set
和 get
方法只是 DependencyObject.SetValue
和 DependencyObject.GetValue
的包装器。这些包装器仅由您的自定义代码调用,而不会由框架调用。
由于 DependencyProperty
提供了自己的通知机制,包装器只是设置它们的关联 DependencyProperty
,因此会自动发出更改通知。因此在每个 setter 中调用 NotifyPropertyChanged()
是多余的。
另一点是您的 SetNew...
属性 更改了回调。他们只是将新值委托给控件。这应该在数据绑定的帮助下完成。
我也想知道这个嵌套的 <ListView><ListView /></ListView>
是什么意思。也删除它(这甚至可以编译吗?)。
DependencyProperty
字段应与注册的属性同名:SelectedEmployeeProperty
而不是EmployeeProperty
。
以下示例显示了如何正确连接数据。它基于您的代码并使用 Emloyees
和 SelectedEmployee
的专用属性。在您的场景中,删除这些属性并直接绑定到 DataContext
(即视图模型)似乎很合理。但这取决于用户控件的目的。但这也会简化代码。
MainViewModel.cs
class MainViewModel : INotifyPropertyChanged
{
public ObservableCollection<Employee> Employees { get; set; }
private Employee selectedEmployee;
public Employee SelectedEmployee
{
get => this.selectedEmployee;
set
{
this.selectedEmployee = value;
OnPropertyChanged();
}
}
}
UserControlEmployees.xaml.cs
public partial class UserControlEmployees : UserControl
{
public UserControlEmployees()
{
InitializeComponent();
}
public IEnumerable<Employee> Employees
{
get => (IEnumerable<Employee>) GetValue(EmployeesProperty);
set => SetValue(EmployeesProperty, value);
}
public static readonly DependencyProperty EmployeesProperty = DependencyProperty.Register(
nameof(Employees),
typeof(IEnumerable<Employee>),
typeof(UserControlEmployees),
new PropertyMetadata(default));
}
public Employee SelectedEmployee
{
get => (Employee) GetValue(SelectedEmployeeProperty);
set => SetValue(SelectedEmployeeProperty, value);
}
// Configure to bind TwoWay by default
public static readonly DependencyProperty SelectedEmployeeProperty = DependencyProperty.Register(
nameof(SelectedEmployee),
typeof(Employee),
typeof(UserControlEmployees),
new FrameworkPropertyMetadata(
default,
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
}
UserControlEmployees.xaml
<UserControl>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- ListView -->
<ListView Grid.Row="0"
ItemsSource="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=Employees}"
SelectedItem="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=SelectedEmployee}">
<ListView.ItemTemplate>
<DataTemplate DataType="{x:Type local:Employee}">
<uc:UserControlEmployeeListItem EmployeeListItem="{Binding}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<!-- Details -->
<uc:UserControlEmployeeDetails Grid.Row="1"
SelectedEmployee="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=SelectedEmployee}" />
</Grid>
</UserControl>
MainWndow.xaml
<Window>
<Window.DataContext>
<MainViewModel />
</Window.DataContext>
<UserControlEmployees Employees="{Binding Employees}"
SelectedEmployee="{Binding SelectedEmployee}" />
</Window>