Search code examples
c#wpflistviewuser-controlsitemssource

Bind to ItemsSource and SelectedValue of a ListView inside of a UserControl


My goal is to reuse a ListView and a couple of other controls that are inside of a UserControl I designed.

For the sake of brevity, imagine that I have a Person class like this, and a list of its instances.

public class Person
{
    public string Name { get; set; }
    public string City { get; set; }
}

My MainWindow:

<Window x:Class="ReusableListView.MainWindow"
        ...
        WindowStartupLocation="CenterScreen"
        Title="MainWindow" Height="600" Width="600">
    <Grid>        
        <local:UCListView Margin="8"
                          ItemsSource="{Binding PersonList, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
    </Grid>
</Window>

public partial class MainWindow : Window, INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    private void OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    private ObservableCollection<Person> _personList = null;
    public ObservableCollection<Person> PersonList
    {
        get { return _personList; }
        set { _personList = value; OnPropertyChanged("PersonList"); }
    }

    private Person _selectedPerson = null;
    public Person SelectedPerson
    {
        get { return _selectedPerson; }
        set { _selectedPerson = value; OnPropertyChanged("SelectedPerson"); }
    }

    public MainWindow()
    {
        InitializeComponent();
        PersonList = GetPeople();
    }

    private ObservableCollection<Person> GetPeople()
    {
        var list = new ObservableCollection<Person>
        {
            new Person() { Name = "Jane", City = "NYC" },
            new Person() { Name = "John", City = "LA" }
        };
        return list;
    }
}

I want to display the Name property of Person as individual items in my ListView inside the UserControl, and then to the right of it, I want to display the selected person's City property. So my UserControl look like this:

<UserControl x:Class="ReusableListView.UCListView"
             ...
             x:Name="MyListViewUC"
             d:DesignHeight="500" d:DesignWidth="580">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <ListView Grid.Column="0" MinWidth="256" Margin="8"
                  DataContext="{Binding ElementName=MyListViewUC}"
                  ItemsSource="{Binding ItemsSource}">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <TextBlock HorizontalAlignment="Left" VerticalAlignment="Center"
                               Width="Auto" Margin="8" Background="Pink"
                               Text="{Binding Name}"/>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
        <TextBox Grid.Column="1" Margin="8" Background="PaleGreen"/>
    </Grid>
</UserControl>

And the UserControl code behind:

public partial class UCListView : UserControl
{
    public UCListView()
    {
        InitializeComponent();
    }

    public object ItemsSource
    {
        get { return GetValue(ItemsSourceProperty); }
        set { SetValue(ItemsSourceProperty, value); }
    }
    public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register("ItemsSource", typeof(object), typeof(UCListView), new PropertyMetadata(null));
}

The above code is stitched together from most examples I saw online including SO. Here are my problems and questions.

  1. When I run this, nothing is displayed in the UserControl list. What seems to be the problem?
  2. How do I bind the SelectedPerson property to the UserContro. so it knows how to display the correct City based on selection?

Solution

  • Besides that you missed to set the DataContext of the Window like

    DataContext = this;
    

    you should consider to derive your control directly from ListView or the simpler ListBox, because then you would get direct access to all its useful properties.

    The difference to a UserControl is that the XAML is in a default Style in the ResourceDictionary Themes/Generic.xaml, which is automatically generated when you add a custom control to a WPF project.

    The control's code, where you change the base class from Control to ListBox:

    public class MyListBox : ListBox
    {
        static MyListBox()
        {
            DefaultStyleKeyProperty.OverrideMetadata(
                typeof(MyListBox),
                new FrameworkPropertyMetadata(typeof(MyListBox)));
        }
    }
    

    Generic.xaml:

    <ResourceDictionary
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:...">
    
        <Style TargetType="local:MyListBox">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="local:MyListBox">
                        <Border Background="{TemplateBinding Background}"
                                BorderBrush="{TemplateBinding BorderBrush}"
                                BorderThickness="{TemplateBinding BorderThickness}">
                            <Grid>
                                <Grid.ColumnDefinitions>
                                    <ColumnDefinition/>
                                    <ColumnDefinition/>
                                </Grid.ColumnDefinitions>
    
                                <ScrollViewer Grid.Column="0">
                                    <ItemsPresenter/>
                                </ScrollViewer>
    
                                <TextBlock Grid.Column="1"
                                           Text="{TemplateBinding SelectedValue}"/>
                            </Grid>
                        </Border>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </ResourceDictionary>
    

    You would use your MyListBox like any other ListBox:

    <local:MyListBox ItemsSource="{Binding PersonList}"
                     SelectedItem="{Binding SelectedPerson}"
                     DisplayMemberPath="Name"
                     SelectedValuePath="City">
    

    If you're not intending to have additional properties in your derived ListBox, you could as well not derive a control at all, and just assign the ControlTemplate to a ListBox when you declare it:

    <Window.Resources>
        <ControlTemplate x:Key="MyListBoxTemplate">
            ...
        </ControlTemplate>
    </Window.Resources>
    ...
    
    <ListBox Template="{StaticResource MyListBoxTemplate}"
             ItemsSource="{Binding PersonList}"
             SelectedItem="{Binding SelectedPerson}"
             DisplayMemberPath="Name"
             SelectedValuePath="City">