Search code examples
wpfbindingselecteditemivalueconverter

WPF Binding SelectedItem to a property of another object


What I'd like to do is lookup a "Category" to bind the SelectedItem to based on CurrentItem.CategoryId where that would match with Category.Id.

<ListBox ItemsSource="{Binding Categories}" SelectedItem="{Binding CurrentItem.CategoryId, Mode=TwoWay>

I thought of using an IValueConverter, but I'm not sure how to pass the Category.Id as a ConverterParameter. Or if that's even the right approach. I have to imagine this is a common use case, but I don't even know what to google for. Any suggestions welcome!

Edit: Ok, more details with full code then. Transactions have a property called "CategoryId", which match an "Id" property on the Category type. My end goal is to get the selectedItem in the list box to be whatever Category the current SelectedTransaction in the DataGrid refers to.

It should be a two way binding, the ListView should be initialized to the Category the Transaction.CategoryId poitns to, and if the ListView is updated, then the Transaction.CategoryId property should be updated. Hopefully that's clear.

TransactionWindow.xaml

<Window x:Class="Budgeter.TransactionsWindow"
        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:local="clr-namespace:Budgeter"
        mc:Ignorable="d"
        Title="TransactionsWindow" Height="450" Width="800">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="10px" />
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="auto" />
            <ColumnDefinition Width="10px" />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="10px" />
            <RowDefinition Height="auto" />
            <RowDefinition Height="*" />
            <RowDefinition Height="*" />
            <RowDefinition Height="auto" />
            <RowDefinition Height="auto" />
            <RowDefinition Height="10px" />
        </Grid.RowDefinitions>
        <DataGrid x:Name="grid" ItemsSource="{Binding Transactions}" AutoGenerateColumns="False" SelectionUnit="FullRow" SelectionMode="Single" Grid.Row="1" Grid.Column="1" Grid.RowSpan="4" SelectedItem="{Binding SelectedTransaction}" SelectionChanged="grid_SelectionChanged">
            <DataGrid.Columns>
                <DataGridTextColumn Header="Date" Binding="{Binding Date}" Width="*" IsReadOnly="True" />
                <DataGridTextColumn Header="Description" Binding="{Binding Description}" Width="*" MinWidth="200" IsReadOnly="True" />
                <DataGridTextColumn Header="Amount" Binding="{Binding Amount}" Width="*" IsReadOnly="True" />
                <DataGridTextColumn Header="Category" Binding="{Binding Category.Description}" Width="*" IsReadOnly="True" />
            </DataGrid.Columns>
        </DataGrid>
        <DockPanel Grid.Row="1" Grid.Column="2" Grid.RowSpan="3" Margin="5">
            <TextBlock Text="Category:" DockPanel.Dock="Top" />
            <ListBox ItemsSource="{Binding Categories}" SelectedItem="{Binding ???" DockPanel.Dock="Bottom">
                <ListBox.ItemTemplate>
                    <DataTemplate>
                        <TextBlock Text="{Binding Description}" />
                    </DataTemplate>
                </ListBox.ItemTemplate>
            </ListBox>
        </DockPanel>
    </Grid>
</Window>

TransactionWindow.xaml.cs

    public partial class TransactionsWindow : Window, INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        
        public IList<CategoryViewModel> Categories { get; }
        public IList<TransactionViewModel> Transactions { get; }
        public TransactionViewModel SelectedTransaction { get; set; }
        public TransactionsWindow(IList<CategoryViewModel> categories, IList<TransactionViewModel> transactions)
        {
            InitializeComponent();
            this.Categories = categories;
            this.Transactions = transactions;

            if (this.Transactions != null && this.Transactions.Count > 0)
            {
                this.SelectedTransaction = transactions[0];
            }
            this.DataContext = this;
            
        }

        private void grid_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            this.OnPropertyChanged(nameof(SelectedTransaction));
        }

        protected void OnPropertyChanged(string name = null)
        {
            this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
        }
    }


Solution

  • This should work:

    <ListBox ItemsSource="{Binding Categories}"
             SelectedValuePath="Id"
             SelectedValue="{Binding SelectedTransaction.CategoryId"}>
    

    Note that SelectedIndex, SelectedItem and SelectedValue bind TwoWay by default.


    The SelectedTransaction property should fire the PropertyChanged event:

    private TransactionViewModel selectedTransaction;
    
    public TransactionViewModel SelectedTransaction
    {
        get { return selectedTransaction; }
        set
        {
            selectedTransaction = value;
            OnPropertyChanged(nameof(SelectedTransaction));
        }
    }
    

    You should also consider to move all those TransactionsWindow properties into another class and call it e.g. MainViewModel:

    DataContext = new MainViewModel();