Communication UserControls imbriquée

Sep 09 2020

En bref: j'ai un ListView et lorsque je sélectionne un élément de ListView, cet élément doit apparaître et édité dans un UserControl détaillé.

J'ai une fenêtre (ViewMain) avec un UserControl (UserControlEmployees) qui a un ListView et un autre UserControl (UserControlEmployeeDetails). Les éléments de ListView sont affichés par un troisième UserControl (UserControlEmployee). UserControlEmployees a deux propriétés de dépendance: une ObservableCollection (employés) et un seul employé (SelectedEmployee). Le ViewModel transmet un ObservableCollection à UserControlEmployees. UserControlEmployees transmet ensuite les employés à ListView. Le SelectedItem de ListView est lié à SelectedEmployee.

Quelque chose comme ça:

SelectedEmployee est censé être également lié à UserControlEmployeeDetails. J'ai donc essayé de lier ViewModelEmployeeDetail et le SelectedItem de ListView à la même propriété de dépendance.

Je suppose que le problème est dans UserControlEmployees: Mon idée était que control.ControlEmployeesListView.SelectedItem = e.NewValue en tant qu'employé; lierait SelectedItem à SelectedEmployee. Mais cela ne fonctionne pas et je ne sais pas comment je peux le lier autrement. Habituellement, je ferais quelque chose comme en XAML, mais je n'y ai pas accès dans ce cas.

EDIT J'ai remarqué que j'ai oublié de définir mon ListView SelectedItem sur Binding.

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

J'ai corrigé cela mais maintenant j'obtiens cette exception:

System.Windows.Markup.XamlParseException: '' Fournir une valeur sur 'System.Windows.Data.Binding' a levé une exception. ' Numéro de ligne «26» et position de ligne «17». »

Exception interne InvalidOperationException: la liaison bidirectionnelle nécessite Path ou XPath.

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

C'est le code dans son 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 ⬆️
}
}

Réponses

2 BionicCode Sep 09 2020 at 14:40

Pour corriger l'erreur de liaison: l'explication de l'erreur et la solution pour la résoudre sont fournies par le message d'erreur. Réglez simplement Binding.Path.

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

Notez que Selector.SelectedItemse lie TwoWaypar défaut. Il suffit donc d'écrire:

<ListView SelectedItem="{Binding}">

D'après les fixations, il semble que vous vous DataContexttrompez. Étant donné que tous les contrôles utilisateur fonctionnent avec les mêmes données, par exemple une collection d'employés et un employé sélectionné, tous les contrôles utilisateur doivent partager le même DataContextmodèle de vue qui contient la collection source.

Ce modèle de vue doit également définir une SelectedEmployeepropriété à laquelle ControlEmployeesListView(le ListView) et UserControlEmployeeDetailspeut à la fois se lier.

Étant donné que le UserControlEmployeesne fonctionne pas sur la collecte des employés à l' interne, il n'a pas besoin d' un dédié Employeeet des SelectedEmployeebiens. Uniquement si le contrôle utilisateur est censé être réutilisable, il peut ou doit avoir ces propriétés. Mais lorsqu'il n'est utilisé que dans ce contexte spécifique, où vous connaissez DataContextà l'avance, vous pouvez les éviter et vous lier directement au UserControl.DataContext.

Control, UserControlou DependencyObjecten général ne doivent pas implémenter INotifyPropertyChangedmais implémenter leurs propriétés en tant que DependecyProperty. Les méthodes setet getdu ne DependencyPropertysont que des enveloppes autour DependencyObject.SetValueet DependencyObject.GetValue. Ces wrappers ne sont appelés que par votre code personnalisé, mais jamais par le framework.

Étant donné que DependencyPropertyfournit son propre mécanisme de notification et que les wrappers ne font que définir leur associé DependencyProperty, une notification de modification sera générée automatiquement. Par conséquent, appeler NotifyPropertyChanged()chaque passeur est redondant.

Un autre point concerne les SetNew...rappels modifiés par votre propriété. Ils ne font que déléguer la nouvelle valeur aux contrôles. Cela devrait être fait à l'aide de la liaison de données à la place.

Je me demande aussi de quoi il <ListView><ListView /></ListView>s'agit. Supprimez-le également (est-ce que cela compile même?).

Le DependencyPropertychamp doit avoir le même nom que la propriété enregistrée: SelectedEmployeePropertyau lieu de EmployeeProperty.

L'exemple suivant montre comment câbler correctement les données. Il est basé sur votre code et utilise des propriétés dédiées pour Emloyeeset SelectedEmployee. Il semble tout à fait raisonnable dans votre scénario de supprimer ces propriétés et de se lier directement au DataContext(qui est le modèle de vue). Mais cela dépend de l'objectif du contrôle utilisateur. Mais cela simplifierait également le code.

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>