嵌套的用户控件通信

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 属性,ControlEmployeesListViewListView)和 UserControlEmployeeDetails 都可以绑定。

由于 UserControlEmployees 不在内部对员工集合进行操作,因此不需要专用的 EmployeeSelectedEmployee 属性。仅当用户控件旨在可重用时,它才可以或应该具有这些属性。但是当它只在这个特定的上下文中使用时,如果你提前知道 DataContext 你可以避免它们并直接绑定到 UserControl.DataContext.

ControlUserControlDependencyObject 通常不应实现 INotifyPropertyChanged,而应将其属性实现为 DependecyPropertyDependencyPropertysetget 方法只是 DependencyObject.SetValueDependencyObject.GetValue 的包装器。这些包装器仅由您的自定义代码调用,而不会由框架调用。

由于 DependencyProperty 提供了自己的通知机制,包装器只是设置它们的关联 DependencyProperty,因此会自动发出更改通知。因此在每个 setter 中调用 NotifyPropertyChanged() 是多余的。

另一点是您的 SetNew... 属性 更改了回调。他们只是将新值委托给控件。这应该在数据绑定的帮助下完成。

我也想知道这个嵌套的 <ListView><ListView /></ListView> 是什么意思。也删除它(这甚至可以编译吗?)。

DependencyProperty字段应与注册的属性同名:SelectedEmployeeProperty而不是EmployeeProperty

以下示例显示了如何正确连接数据。它基于您的代码并使用 EmloyeesSelectedEmployee 的专用属性。在您的场景中,删除这些属性并直接绑定到 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>