1

In short: I have a ListView and when I select an Item of the ListView, this item should show up and edited in a detail UserControl.

I have a Window (ViewMain) with a UserControl (UserControlEmployees) that has a ListView and another UserControl (UserControlEmployeeDetails). The ListView's items are displayed by a third UserControl (UserControlEmployee). UserControlEmployees has two dependency properties: a ObservableCollection (Employees) and a single Employee (SelectedEmployee). The ViewModel passes a ObservableCollection to UserControlEmployees. UserControlEmployees then passes the Employees to the ListView. The ListView's SelectedItem is bound to SelectedEmployee.

Something like this: enter image description here

SelectedEmployee is supposed to also be bound to UserControlEmployeeDetails. So I tried to bind ViewModelEmployeeDetail and the SelectedItem of the ListView to the same dependency property.

I guess the prtoblem is in UserControlEmployees: My idea was that control.ControlEmployeesListView.SelectedItem = e.NewValue as Employee; would bind SelectedItem to SelectedEmployee. But this is not working and I have no idea how else I can bind it. Usually I would do somthing like in XAML, but I don't have access to that in this case.

EDIT I noticed that I have forgotten to set my ListView SelectedItem to Binding.

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

I fixed that but now I get this exception:

System.Windows.Markup.XamlParseException: ''Provide value on 'System.Windows.Data.Binding' threw an exception.' Line number '26' and line position '17'.'

Inner Exception InvalidOperationException: Two-way binding requires Path or 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>

Thats the code in its 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 ⬆️
}
}
herrwolken
  • 389
  • 1
  • 4
  • 12

1 Answers1

2

To fix the binding error: the explanation of the error and the solution to solve it is provided by the error message. Simply set Binding.Path.

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

Note that Selector.SelectedItem binds TwoWay by default. So it's sufficient to write:

<ListView SelectedItem="{Binding}">

From the bindings, it looks like your DataContext is wrong. Since all user controls operate with the same data e.g. a collection of employees and a selected employee, all user controls should share the same DataContext which is the view model that holds the source collection.

This view model should also define a SelectedEmployee property that ControlEmployeesListView (the ListView) and UserControlEmployeeDetails can both bind to.

Since the UserControlEmployees doesn't operate on the employee collection internally, it doesn't need a dedicated Employee and SelectedEmployee property. Only if the user control is meant to be reusable, it can or should have those properties. But when it's only used in this specific context, where you know the DataContext in advance you can avoid them and bind directly to the UserControl.DataContext.

Control, UserControl or DependencyObject in general should not implement INotifyPropertyChanged but implement their properties as DependecyProperty. The set and get methods of the DependencyProperty are just wrappers around DependencyObject.SetValue and DependencyObject.GetValue. Those wrappers are only called by your custom code, but never by the framework.

Since DependencyProperty provides its own notification mechanism and the wrappers are just setting their associated DependencyProperty, there will be a change notification raised automatically. Therefore calling NotifyPropertyChanged() in each setter is redundant.

Another point are your SetNew... property changed callbacks. They are just delegating the new value to the controls. This should be done with the help of data binding instead.

I also wonder what this nested <ListView><ListView /></ListView> is about. Remove this too (does this even compile?).

The DependencyProperty field should have the same name as the registered property: SelectedEmployeeProperty instead of EmployeeProperty.

The following example shows how to wire up the data correctly. It is based on your code and uses dedicated properties for Emloyees and SelectedEmployee. It seems quite reasonable in your scenario to drop those properties and bind directly to the DataContext (which is the view model). But it depends on the purpose of the user control. But this would also simplify the 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>
BionicCode
  • 1
  • 4
  • 28
  • 44
  • Thanks a lot. That works perfectly fine. The INotifyPropertyChanged was only added because I was really desperate. – herrwolken Sep 09 '20 at 16:09
  • I find it really easy and fun to learn c#, but I really struggle with wpf. Most of the time, like in this case, I don't even know how to start. I want different "pages" for employees, departments, companies ... but I know that "real" pages are used differently. So I stacked UCs on each other. Advices on resources would be very welcome. – herrwolken Sep 09 '20 at 16:16
  • 1
    Yes, WPF is a very complex and powerful framework. It's essential to understand some fundamentals. If you want to write a single page application (where the user navigates between pages) I recommend the so called view-model-first approach. You can check [this simple example](https://stackoverflow.com/a/61323201/3141792) how to implement this. It's very easy. It's basically a `ContentPresenter` or a `ContentControl`, which displays page view models using implicit `DataTemplate` resources. – BionicCode Sep 09 '20 at 16:47
  • 1
    I recommend to read through the Microsoft Docs: [Data binding overview in WPF](https://docs.microsoft.com/en-us/dotnet/desktop-wpf/data/data-binding-overview), [Data Templating Overview](https://docs.microsoft.com/en-us/dotnet/desktop/wpf/data/data-templating-overview?view=netframeworkdesktop-4.8), [Patterns - WPF Apps With The Model-View-ViewModel Design Pattern](https://docs.microsoft.com/en-us/archive/msdn-magazine/2009/february/patterns-wpf-apps-with-the-model-view-viewmodel-design-pattern), [Framework Design Guidelines](https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/). – BionicCode Sep 09 '20 at 16:47
  • The view-model-first approach is very elegant. In the past I used something 'similar' but MUCH more crude: A MainVM and a MainV. The MainV contained all the Pages as UCs.The MainVM contained all the page's VM and a 'hidden\visible' string to hide/display the UCs. But the UC had no dependency properties or were assigned to a type/VM. It was easy to break stuff. This employee code above is an approach to get something more sophisticated. The V-M-First-Approach is just what I was looking for. And I think it's easy to change my current projects to it. Thanks again. – herrwolken Sep 10 '20 at 08:15
  • If I find time I will try to create a bigger sample project like that one above and put it on github. So maybe I can help others with the same problems. – herrwolken Sep 10 '20 at 08:17
  • Yes, this is very elegant. It's easy to implement, easy to understand, easy to extend, easy to style and very lightweight. You can navigate to pages directly or cycle through them using next/previous. You can use it to implement a single page application or a master/details control. You can animate a `TranslateTransform` on the `ContentPresenter` and swipe in and out the pages when the user navigates., which looks very good. – BionicCode Sep 10 '20 at 08:52