WPF DataGrid绑定ViewModel中DataTable时SelectedItem绑定异常及绑定机制咨询
Hey there! Coming from WinForms to WPF, it's totally normal to hit these binding gotchas with DataGrid and DataTable—they work quite differently under the hood. Let's break down your questions and fix this properly, while keeping MVVM principles intact.
First, Let's Clarify the Binding Mechanism
When you bind a DataTable to WPF's DataGrid, the control doesn't treat your Booking objects as direct row/column data. Instead:
- Each row in the DataGrid maps to a
DataRowView(a wrapper around a DataRow from your DataTable). - When you assign a
Bookingobject to a DataTable cell, the cell stores the object, but the DataGrid defaults to callingToString()on it for display. Since you haven't overriddenToString()(or the DisplayMemberPath setting isn't targeting the right object), you see the full type namemyNamespace.Models.Booking.
Answers to Your Questions
Q1: What's actually stored in the DataGrid cells? Why can't I cast the selected item to Booking?
- Cells do store your Booking object, but the
SelectedItemof the DataGrid is the entireDataRowView(the row wrapper), not the individual cell's Booking. That's why you get a casting error when trying to convert it toBooking—you're trying to turn a row wrapper into a single cell object. - The confusion comes from WinForms' behavior, where you might directly access cell values more intuitively. In WPF, you need to dig into the
DataRowViewto get your Booking from the correct column.
Why isn't DisplayMemberPath="Id" working?
DisplayMemberPath tells the DataGrid which property of the row object to display. Your row objects are DataRowView, not Booking—so the DataGrid looks for an Id property on DataRowView (which doesn't exist), hence it ignores the setting and falls back to ToString().
Fixes: Two Approaches
Approach 1: Keep Using DataTable (Quick Adjustments)
If you want to stick with your current DataTable setup, here's how to fix the display and selection:
1. Fix Display to Show Booking.Id
Instead of relying on DisplayMemberPath, define explicit columns in XAML and use a value converter to extract the Id from the Booking object:
<!-- First, declare the converter in your resources --> <Window.Resources> <local:BookingToIdConverter x:Key="BookingToIdConverter" /> </Window.Resources> <DataGrid x:Name="grdInstructors" ItemsSource="{Binding DtBookings}" SelectedCell="{Binding SelectedCell}" AutoGenerateColumns="False"> <!-- Add columns for each time slot (adjust headers and indices to match your DataTable) --> <DataGrid.Columns> <DataGridTextColumn Header="Instructor" Binding="{Binding [0]}" /> <!-- Assuming column 0 is instructor name --> <DataGridTextColumn Header="Slot 1" Binding="{Binding [1], Converter={StaticResource BookingToIdConverter}}" /> <DataGridTextColumn Header="Slot 2" Binding="{Binding [2], Converter={StaticResource BookingToIdConverter}}" /> <!-- Add more columns for other time slots --> </DataGrid.Columns> </DataGrid>
Create the converter class:
public class BookingToIdConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { if (value is Booking booking) return booking.Id.ToString(); // Or just booking.Id if it's a string return string.Empty; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { // Not needed for display-only, throw or return null throw new NotImplementedException(); } }
2. Fix Selection to Get the Booking Object
Bind to SelectedCell (instead of SelectedItem) to get the specific cell, then extract the Booking from the DataRowView:
private DataGridCellInfo _selectedCell; public DataGridCellInfo SelectedCell { get => _selectedCell; set { _selectedCell = value; OnPropertyChanged(); // Make sure your ViewModel implements INotifyPropertyChanged if (_selectedCell.IsValid) { // Get the row wrapper var rowView = _selectedCell.Item as DataRowView; if (rowView != null) { // Get the column index from the selected cell var columnIndex = _selectedCell.Column.DisplayIndex; // Extract the Booking from the cell var selectedBooking = rowView[columnIndex] as Booking; // Now you can work with selectedBooking! } } } }
Approach 2: Switch to a ViewModel-Friendly Structure (Recommended for MVVM)
DataTable is great for ADO.NET scenarios, but it's not the most MVVM-friendly choice for WPF. Instead, create a custom row model to represent each instructor's schedule, then use an ObservableCollection:
1. Create a Row ViewModel
public class InstructorScheduleRow : INotifyPropertyChanged { // Instructor info for the row header private Instructor _instructor; public Instructor Instructor { get => _instructor; set { _instructor = value; OnPropertyChanged(); } } // Properties for each time slot private Booking _slot1; public Booking Slot1 { get => _slot1; set { _slot1 = value; OnPropertyChanged(); } } private Booking _slot2; public Booking Slot2 { get => _slot2; set { _slot2 = value; OnPropertyChanged(); } } // Add more Slot properties for other time slots... public event PropertyChangedEventHandler PropertyChanged; protected void OnPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } }
2. Update Your ViewModel
public ObservableCollection<InstructorScheduleRow> InstructorSchedules { get; set; } public YourViewModel() { InstructorSchedules = new ObservableCollection<InstructorScheduleRow>(); } void DisplayBookings() { // First, initialize rows for each instructor foreach (var instructor in YourInstructorList) { InstructorSchedules.Add(new InstructorScheduleRow { Instructor = instructor }); } // Assign bookings to the correct row and slot foreach (Booking booking in bookings) { var row = InstructorSchedules.First(r => r.Instructor.Id == booking.InstructorId); // Map SlotTimeId to the corresponding property switch (booking.SlotTimeId) { case 0: row.Slot1 = booking; break; case 1: row.Slot2 = booking; break; // Add cases for other SlotTimeIds } } }
3. Bind in XAML
<DataGrid x:Name="grdInstructors" ItemsSource="{Binding InstructorSchedules}" SelectedCell="{Binding SelectedCell}" AutoGenerateColumns="False"> <DataGrid.Columns> <DataGridTextColumn Header="Instructor" Binding="{Binding Instructor.Name}" /> <DataGridTextColumn Header="Slot 1" Binding="{Binding Slot1.Id}" /> <DataGridTextColumn Header="Slot 2" Binding="{Binding Slot2.Id}" /> <!-- Add more columns for other slots --> </DataGrid.Columns> </DataGrid>
Now, when you select a cell, you can get the Booking directly from the InstructorScheduleRow:
private DataGridCellInfo _selectedCell; public DataGridCellInfo SelectedCell { get => _selectedCell; set { _selectedCell = value; OnPropertyChanged(); if (_selectedCell.IsValid) { var row = _selectedCell.Item as InstructorScheduleRow; if (row != null) { // Get the Booking based on the selected column Booking selectedBooking = null; switch (_selectedCell.Column.Header.ToString()) { case "Slot 1": selectedBooking = row.Slot1; break; case "Slot 2": selectedBooking = row.Slot2; break; // Add cases for other columns } // Use selectedBooking as needed } } } }
Key Takeaways
- DataTable + DataGrid in WPF uses
DataRowViewas row objects, not your domain models directly. DisplayMemberPathtargets row-level properties, not cell-level objects.- For proper MVVM compliance, using custom view models and
ObservableCollectionis far more maintainable than DataTable.
内容的提问来源于stack exchange,提问作者Ray

