【问题标题】:Nested UserControls communication嵌套的 UserControls 通信
【发布时间】:2020-12-27 20:09:08
【问题描述】:

简而言之:我有一个 ListView,当我选择 ListView 的一个项目时,该项目应显示并在详细 UserControl 中进行编辑。

我有一个窗口 (ViewMain) 和一个用户控件 (UserControlEmployees),它有一个 ListView 和另一个用户控件 (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 as Employee;将 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 ⬆️
}
}

【问题讨论】:

  • 请只发布代码的相关部分
  • 当然,我不确定。
  • 您的数据结构看起来很简单:员工列表,以及查看所选员工详细信息的能力。所以:一个包含Employee 列表和当前Employee 的文件的包装器。这使您在视图端拥有一个 ListView,在 VM 端拥有一个 ObservableCollection
  • 抱歉,P.Manthe,我不确定我是否可以跟上。

标签: c# wpf xaml mvvm user-controls


【解决方案1】:

修复绑定错误:错误说明和解决方法由错误消息提供。只需设置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... 属性更改回调。他们只是将新值委托给控件。这应该在数据绑定的帮助下完成。

我也想知道这个嵌套的&lt;ListView&gt;&lt;ListView /&gt;&lt;/ListView&gt; 是关于什么的。也删除它(这甚至可以编译吗?)。

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>

【讨论】:

  • 非常感谢。这工作得很好。之所以添加 INotifyPropertyChanged,是因为我真的很绝望。
  • 我发现学习 c# 真的很容易也很有趣,但我真的很纠结 wpf。大多数时候,就像在这种情况下,我什至不知道如何开始。我想要为员工、部门、公司提供不同的“页面”……但我知道“真实”页面的使用方式不同。所以我把 UC 叠放在一起。非常欢迎提供有关资源的建议。
  • 是的,WPF 是一个非常复杂和强大的框架。了解一些基础知识很重要。如果您想编写单页应用程序(用户在页面之间导航),我推荐所谓的视图模型优先方法。您可以查看this simple example 如何实现这一点。这很容易。它基本上是一个ContentPresenterContentControl,它使用隐式DataTemplate 资源显示页面视图模型。
  • 视图模型优先的方法非常优雅。在过去,我使用了一些“类似”但更粗糙的东西:一个 MainVM 和一个 MainV。 MainV 包含所有页面作为 UC。MainVM 包含所有页面的 VM 和一个“隐藏\可见”字符串来隐藏/显示 UC。但是 UC 没有依赖属性或被分配给类型/VM。很容易弄坏东西。上面的这个员工代码是一种让事情变得更复杂的方法。 V-M-First-Approach 正是我想要的。而且我认为将我当前的项目更改为它很容易。再次感谢。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-02-08
  • 1970-01-01
  • 2014-05-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多