Comunicação de UserControls aninhados
Resumindo: eu tenho um ListView e quando seleciono um item do ListView, este item deve aparecer e editado em um UserControl de detalhes.
Eu tenho uma janela (ViewMain) com um UserControl (UserControlEmployees) que tem um ListView e outro UserControl (UserControlEmployeeDetails). Os itens do ListView são exibidos por um terceiro UserControl (UserControlEmployee). UserControlEmployees tem duas propriedades de dependência: um ObservableCollection (Employees) e um único Employee (SelectedEmployee). O ViewModel passa um ObservableCollection para UserControlEmployees. UserControlEmployees então passa os funcionários para o ListView. O SelectedItem do ListView está vinculado a SelectedEmployee.
Algo assim:
SelectedEmployee também deve ser vinculado a UserControlEmployeeDetails. Portanto, tentei vincular ViewModelEmployeeDetail e o SelectedItem de ListView à mesma propriedade de dependência.
Acho que o problema está em UserControlEmployees: Minha ideia era que control.ControlEmployeesListView.SelectedItem = e.NewValue as Employee; ligaria SelectedItem a SelectedEmployee. Mas isso não está funcionando e não tenho ideia de como posso vinculá-lo. Normalmente, eu faria algo como em XAML, mas não tenho acesso a isso neste caso.
EDITAR Percebi que esqueci de definir meu ListView SelectedItem como Binding.
<ListView
x:Name="ControlEmployeesListView"
Grid.Row="0"
SelectedItem="{Binding Mode=TwoWay}">
Corrigi isso, mas agora recebo esta exceção:
System.Windows.Markup.XamlParseException: '' Fornecer valor em 'System.Windows.Data.Binding' lançou uma exceção. ' Número da linha '26' e posição da linha '17'. '
Exceção interna InvalidOperationException: a vinculação bidirecional requer 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>
Esse é o código em seu 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 ⬆️
}
}
Respostas
Para corrigir o erro de ligação: a explicação do erro e a solução para resolvê-lo são fornecidas pela mensagem de erro. Basta definir Binding.Path.
<ListView SelectedItem="{Binding Path=., Mode=TwoWay}">
Observe que Selector.SelectedItemvincula TwoWaypor padrão. Portanto, é suficiente escrever:
<ListView SelectedItem="{Binding}">
Pelas ligações, parece que você DataContextestá errado. Uma vez que todos os controles de usuário operam com os mesmos dados, por exemplo, uma coleção de funcionários e um funcionário selecionado, todos os controles de usuário devem compartilhar o mesmo, DataContextque é o modelo de visualização que contém a coleção de origem.
Este modelo de visualização também deve definir uma SelectedEmployeepropriedade que ControlEmployeesListView(o ListView) e UserControlEmployeeDetailspode ser associada.
Por UserControlEmployeesnão atuar internamente na arrecadação de funcionários, não necessita de propriedade Employeee dedicados SelectedEmployee. Somente se o controle de usuário for reutilizável, ele pode ou deve ter essas propriedades. Mas quando ele é usado apenas neste contexto específico, onde você sabe DataContextcom antecedência, pode evitá-los e vincular-se diretamente ao UserControl.DataContext.
Control, UserControlou DependencyObjectem geral não deve implementar, INotifyPropertyChangedmas implementar suas propriedades como DependecyProperty. Os métodos sete getdo DependencyPropertysão apenas empacotadores em torno de DependencyObject.SetValuee DependencyObject.GetValue. Esses wrappers são chamados apenas por seu código customizado, mas nunca pela estrutura.
Como DependencyPropertyfornece seu próprio mecanismo de notificação e os wrappers estão apenas configurando seus associados DependencyProperty, haverá uma notificação de alteração gerada automaticamente. Portanto, chamar NotifyPropertyChanged()em cada setter é redundante.
Outro ponto são SetNew...os retornos de chamada alterados de propriedade. Eles estão apenas delegando o novo valor aos controles. Isso deve ser feito com a ajuda da vinculação de dados.
Eu também me pergunto do que se trata esse aninhado <ListView><ListView /></ListView>. Remova isso também (isso ainda compila?).
O DependencyPropertycampo deve ter o mesmo nome da propriedade registrada: em SelectedEmployeePropertyvez de EmployeeProperty.
O exemplo a seguir mostra como conectar os dados corretamente. Ele é baseado em seu código e usa propriedades dedicadas para Emloyeese SelectedEmployee. Parece bastante razoável em seu cenário descartar essas propriedades e vincular diretamente ao DataContext(que é o modelo de visualização). Mas isso depende da finalidade do controle do usuário. Mas isso também simplificaria o 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>