0

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?

1 Answer 1

2

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.

Sign up to request clarification or add additional context in comments.

5 Comments

That worked! Thank you for that. However I am bit confused on your side note. I am using MVVM, and my ViewModel is loaded via a repository layer which sits on top of EF Core. In my opinion not directly binding to my data layer. Could you elaborate a bit more or provide links to an example?
This is quite a big topic, so not really something I can easily summerize here. In short though, Carrier and Contract are part of your Model, not your ViewModel (technically they're actually part of your data later, but let's keep things simple). You're encapsulating your lists as ObservableCollection in your ViewModel layer, but the objects themselves are still Model objects that you are binding to directly. This is potentially going to cause you problems down the track, because your view bindings are not doing everything an ORM like EF will be expecting.
As an example, your Contract has two properties for the Carrier: i.e. CarrierId (integer) and Carrier (reference). Simultaneously, your Carrier object contains a list of all the contract that reference it (i.e. ICollection<Contract> Contracts). All your view layer is doing is updating the Contract.Carrier property. It's not updating the Contract.CarrierId property, and it's certainly not removing the carrier from whatever Contracts.Carrier list it's in and moving it to a new one when the user changes the contract carrier.
In MVVM, your Contract and Carrier classes would typically have corresponding ContractViewModel and CarrierViewModel classes that would perform this work in the background (or rather, delegate it down to the model layer to do itself) whenever a carrier property changes. It's possible to cheat, and set your ids/rebuild your lists etc when you go to save everything back into the database, but this can have severe performance implications, because your ORM won't understand what you've actually done and will simply interpret it as meaning your entire database has changed every time you go to save.
Again, this is very dependent not only on which ORM you are using, but also how you've configured it. The overall principle though remains the same, and it will propbably rear its ugly head sooner or later. The data in your model layer is in a format that's convenient for your database serialization, whereas your view layer typically needs a format better suited to its own requirements. In MVVM, the ViewModel is the glue that binds these two together.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.