wpf 数据网格组合框出错 "Two-way binding requires Path or XPath"

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

我在尝试在 DataGrid 中使用 ComboBox 时收到可怕的“双向绑定需要 Path 或 XPath”错误。是的,我已经查看了几个现有的答案,但 none 似乎符合我的示例的风格。对于冗长的代码,我深表歉意,但我想对我的示例进行全面介绍。

SQL 服务器数据模型:

-- 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
);

在 EF Core 中通过 Scaffold-DbContext 命令生成的模型:

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

因为承运人列表在合同列表之外,所以我使用这个 answer 来 link 正确设置:

<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>

当我将下拉列表值更新为另一个值然后单击数据行时出现异常“双向绑定需要 Path 或 XPath”。

我做错了什么?

问题出在您的 ComboBox 的 SelectedItems 绑定上:

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

DataContext已经设置为SelectedContract,这里不需要指定。并且您想绑定到 Carrier 对象本身,而不是 CarrierName 属性:

SelectedItem="{Binding Path=Carrier}">

附带说明一下,请记住,像这样直接绑定到数据层并不总是最好的主意。您上面的代码将在您更改它们时修改 Carrier 属性,但它不会将这些更改传播到您的 Carrier 实例中的 Contracts 列表,因此您的 ORM 很可能会在序列化时忽略任何更改到你的数据库。一般来说,您的数据层应该与您的视图模型中的更高层进行镜像,该层负责处理这些内容并且更适合视图。这是一个实施问题,我会留给您解决,因为它在很大程度上取决于您的体系结构。