Comunicación de UserControls anidada

Sep 09 2020

En resumen: tengo un ListView y cuando selecciono un elemento de ListView, este elemento debería aparecer y editarse en un UserControl detallado.

Tengo una ventana (ViewMain) con un UserControl (UserControlEmployees) que tiene un ListView y otro UserControl (UserControlEmployeeDetails). Los elementos de ListView se muestran mediante un tercer UserControl (UserControlEmployee). UserControlEmployees tiene dos propiedades de dependencia: una ObservableCollection (Empleados) y un solo Empleado (SelectedEmployee). ViewModel pasa una ObservableCollection a UserControlEmployees. UserControlEmployees luego pasa los Empleados a ListView. SelectedItem de ListView está vinculado a SelectedEmployee.

Algo como esto:

Se supone que SelectedEmployee también está vinculado a UserControlEmployeeDetails. Así que intenté unir ViewModelEmployeeDetail y SelectedItem de ListView a la misma propiedad de dependencia.

Supongo que el prtoblem está en UserControlEmployees: Mi idea era que control.ControlEmployeesListView.SelectedItem = e.NewValue as Employee; enlazaría SelectedItem a SelectedEmployee. Pero esto no funciona y no tengo idea de qué otra forma puedo vincularlo. Por lo general, haría algo como en XAML, pero no tengo acceso a eso en este caso.

EDITAR Me di cuenta de que me olvidé de establecer mi ListView SelectedItem en Binding.

        <ListView
            x:Name="ControlEmployeesListView"
            Grid.Row="0"
            SelectedItem="{Binding Mode=TwoWay}">

Lo arreglé pero ahora obtengo esta excepción:

System.Windows.Markup.XamlParseException: '' Proporcionar valor en 'System.Windows.Data.Binding' arrojó una excepción. ' Número de línea '26' y posición de línea '17'.

Inner Exception InvalidOperationException: el enlace bidireccional requiere Path o XPath.

/ EDITAR 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>

Ese es el código en su 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 ⬆️
}
}

Respuestas

2 BionicCode Sep 09 2020 at 14:40

Para corregir el error de encuadernación: la explicación del error y la solución para resolverlo la proporciona el mensaje de error. Simplemente configúrelo Binding.Path.

<ListView SelectedItem="{Binding Path=., Mode=TwoWay}">

Tenga en cuenta que se Selector.SelectedItemune TwoWayde forma predeterminada. Entonces es suficiente escribir:

<ListView SelectedItem="{Binding}">

Por las fijaciones, parece que DataContextestá mal. Dado que todos los controles de usuario operan con los mismos datos, por ejemplo, una colección de empleados y un empleado seleccionado, todos los controles de usuario deben compartir lo mismo, DataContextque es el modelo de vista que contiene la colección de origen.

Este modelo de vista también debe definir una SelectedEmployeepropiedad a la que ControlEmployeesListView(el ListView) y UserControlEmployeeDetailsambos puedan vincularse.

Dado que el UserControlEmployeesno opera en la colección empleado internamente, que no necesita un dedicado Employeey SelectedEmployeepropiedad. Solo si el control de usuario está destinado a ser reutilizable, puede o debe tener esas propiedades. Pero cuando solo se usa en este contexto específico, donde conoce DataContextde antemano, puede evitarlos y vincularse directamente a UserControl.DataContext.

Control, UserControlo DependencyObjecten general no deben implementar INotifyPropertyChangedsino implementar sus propiedades como DependecyProperty. Los métodos sety getde DependencyPropertyson solo envoltorios de DependencyObject.SetValuey DependencyObject.GetValue. Esos envoltorios solo los llama su código personalizado, pero nunca el marco.

Dado que DependencyPropertyproporciona su propio mecanismo de notificación y los contenedores solo están configurando sus asociados DependencyProperty, se generará una notificación de cambio automáticamente. Por lo tanto, llamar NotifyPropertyChanged()a cada armador es redundante.

Otro punto son las SetNew...devoluciones de llamada cambiadas por su propiedad. Simplemente están delegando el nuevo valor a los controles. En su lugar, esto debe hacerse con la ayuda del enlace de datos.

También me pregunto de qué <ListView><ListView /></ListView>se trata este anidado . Elimine esto también (¿esto incluso se compila?).

El DependencyPropertycampo debe tener el mismo nombre que la propiedad registrada: en SelectedEmployeePropertylugar de EmployeeProperty.

El siguiente ejemplo muestra cómo conectar los datos correctamente. Se basa en su código y utiliza propiedades dedicadas para Emloyeesy SelectedEmployee. Parece bastante razonable en su escenario eliminar esas propiedades y enlazar directamente a DataContext(que es el modelo de vista). Pero depende del propósito del control del usuario. Pero esto también simplificaría el código.

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>