Home > Mobile >  Error with wpf datagrid combobox "Two-way binding requires Path or XPath"
Error with wpf datagrid combobox "Two-way binding requires Path or XPath"

Time:05-19

I am receiving the dreaded "Two-way binding requires Path or XPath" error when trying to use a ComboBox inside a DataGrid. Yes I have looked at several existing answers already, but none seem to match the flavor of my example. Sorry in advanced for the lengthy code, but wanted to be thorough with my example.

SQL Server Data Model:

-- list of dropdown values lives in here, specifically the CarrierName
create table Carrier
(
    CarrierId int identity(1,1) primary key,
    CarrierName nvarchar(500) not null,
    CarrierType nvarchar(500) not null
);

-- this is what my datagrid will be looking at
create table Contract
(
    ContractId int identity(1,1) primary key,
    CarrierId int foreign key references Carrier(CarrierId)
    -- other attributes removed for example
);

Models that were generated via Scaffold-DbContext command in EF Core:

public partial class Carrier
{
    public Carrier()
    {
        Contracts = new HashSet<Contract>();
    }
    public int CarrierId { get; set; }
    public string CarrierName { get; set; }
    public string CarrierType { get; set; }
    public virtual ICollection<Contract> Contracts { get; set; }
}

public partial class Contract
{
    public Contract() { }
    public int ContractId { get; set; }
    public int CarrierId { get; set; }
    public virtual Carrier Carrier { get; set; }
}

MainViewModel.cs

public class MainViewModel : ViewModelBase
{
    public IContractViewModel ContractViewModel { get; set; }
    public MainViewModel(IContractViewModel contractViewModel)
    {
        ContractViewModel = contractViewModel;
    }
    public async Task LoadAsync()
    {
        await ContractViewModel.LoadAsync();
    }
}

ContractViewModel.cs

public class ContractViewModel : ViewModelBase, IContractViewModel
{
    private readonly IRepository _repository;
    private CoreContract _selectedContract;

    public ObservableCollection<Contract> Contracts { get; set; }
    public ObservableCollection<Carrier> Carriers { get; set; }

    public Contract SelectedContract
    {
        get { return _selectedContract; }
        set { _selectedContract = value; }
    }

    public ContractViewModel(IRepository repository)
    {
        Contracts = new ObservableCollection<Contract>();
        Carriers = new ObservableCollection<Carrier>();
        _repository = repository ?? throw new ArgumentNullException(nameof(repository));
    }

    public async Task LoadAsync()
    {
        Contracts.Clear();
        foreach (var contract in await _repository.GetAllContractsAsync())
        {
            Contracts.Add(contract);
        }

        Carriers.Clear();
        foreach(var carrier in await _repository.GetAllCarriersAsync())
        {
            Carriers.Add(carrier);
        }
    }
}

MainWindow.cs

public partial class MainWindow : Window
{
    private readonly MainViewModel _viewModel;
    public MainWindow(MainViewModel mainViewModel)
    {
        InitializeComponent();
        this.Loaded  = async (s, e) => await _viewModel.LoadAsync();
        _viewModel = mainViewModel;
        DataContext = _viewModel;
    }
}

MainWindow.xaml

<Window xmlns:Views="clr-namespace:COREContracts.Views"  
        x:Class="COREContracts.Views.MainWindow"
        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:COREContracts"
        mc:Ignorable="d">

        <Views:ContractsView DataContext="{Binding ContractViewModel}"/>
</Window>

ContractsView.xaml

Because the list of carriers lives outside the list of contracts, I used this answer to link things up correctly:

<UserControl x:Class="COREContracts.Views.ContractsView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
             Name="root">
    <DataGrid Name="Contracts" 
            ItemsSource="{Binding Contracts}"
            SelectedItem="{Binding SelectedContract}"
            AutoGenerateColumns="False">
        <DataGrid.Columns>
            <DataGridTextColumn Header="Contract Id" Binding="{Binding Path=ContractId}"/>
            <DataGridTemplateColumn Header="Carrier Name">
                <DataGridTemplateColumn.CellTemplate>
                    <DataTemplate>
                        <TextBlock Text="{Binding Path=Carrier.CarrierName}" />
                    </DataTemplate>
                </DataGridTemplateColumn.CellTemplate>
                <DataGridTemplateColumn.CellEditingTemplate>
                    <DataTemplate>
                        <ComboBox ItemsSource="{Binding Path=DataContext.Carriers, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type UserControl}}}" 
                                    SelectedItem="{Binding Path=SelectedContract.CarrierName}">
                            <ComboBox.ItemTemplate>
                                <DataTemplate>
                                    <TextBlock Text="{Binding Path=CarrierName}" />
                                </DataTemplate>
                            </ComboBox.ItemTemplate>
                        </ComboBox>
                    </DataTemplate>
                </DataGridTemplateColumn.CellEditingTemplate>
            </DataGridTemplateColumn>
        </DataGrid.Columns>
    </DataGrid>
</UserControl>

The exception "Two-way binding requires Path or XPath" occurs when I update the dropdown value to another value and then click off the data row.

What am I doing wrong here?

CodePudding user response:

The problem is with your ComboBox's SelectedItems binding:

SelectedItem="{Binding Path=SelectedContract.CarrierName}">

DataContext is already set to SelectedContract, so no need to specify it here. And you want to bind to the Carrier object itself, not the CarrierName property:

SelectedItem="{Binding Path=Carrier}">

As a side note, keep in mind that binding directly to your data layer like this isn't always the best idea. Your code above will modify the Carrier properties as you change them, but it won't propegate those changes through to the Contracts lists in your Carrier instances, so there's a good chance your ORM will simply ignore any changes when it comes time to serialize back to your database. Generally speaking, your data layer should be mirrored with a higher layer in your view model that takes care of this stuff and is a better fit for the view. It's an implementation issue I'll leave for you to sort out though, as it depends a lot on your architecture.

  • Related