C# DataGrid中的WPF组合框
这是一个显示我有问题的行为的例子。我有一个datagrid,它绑定到viewmodel中可观察到的记录集合。在datagrid中,我有一个DataGridTemplateColumn,其中包含一个组合框,该组合框是从viewmodel中的列表填充的。datagrid还包含文本列。窗口底部有一些文本框显示记录内容C# DataGrid中的WPF组合框,c#,wpf,mvvm,combobox,datagrid,C#,Wpf,Mvvm,Combobox,Datagrid,这是一个显示我有问题的行为的例子。我有一个datagrid,它绑定到viewmodel中可观察到的记录集合。在datagrid中,我有一个DataGridTemplateColumn,其中包含一个组合框,该组合框是从viewmodel中的列表填充的。datagrid还包含文本列。窗口底部有一些文本框显示记录内容 <Window x:Class="Customer.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/x
<Window x:Class="Customer.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Customer"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<local:SelectedRowConverter x:Key="selectedRowConverter"/>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="8*"/>
<RowDefinition Height="3*"/>
</Grid.RowDefinitions>
<DataGrid x:Name="dgCustomers" AutoGenerateColumns="False"
ItemsSource="{Binding customers}" SelectedItem="{Binding SelectedRow,
Converter={StaticResource selectedRowConverter}, Mode=TwoWay}"
CanUserAddRows="True" Grid.Row="0" >
<DataGrid.Columns>
<DataGridTemplateColumn Width="Auto" Header="Country">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox x:Name="cmbCountry" ItemsSource="{Binding DataContext.countries,
RelativeSource={RelativeSource AncestorType={x:Type Window}}}"
DisplayMemberPath="name" SelectedValuePath="name" Margin="5"
SelectedItem="{Binding DataContext.SelectedCountry,
RelativeSource={RelativeSource AncestorType={x:Type Window}}, Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged}" SelectionChanged="cmbCountry_SelectionChanged" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="Name" Binding="{Binding name}" Width="1*"/>
<DataGridTextColumn Header="Phone" Binding="{Binding phone}" Width="1*"/>
</DataGrid.Columns>
</DataGrid>
<Grid x:Name="grdDisplay" DataContext="{Binding ElementName=dgCustomers}" Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="1*"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="2" Content="Country:" VerticalAlignment="Center" HorizontalAlignment="Right"/>
<Label Grid.Column="4" Content="Code:" VerticalAlignment="Center" HorizontalAlignment="Right"/>
<BulletDecorator Grid.Column="0">
<BulletDecorator.Bullet>
<Label Content="Name:" VerticalAlignment="Center" HorizontalAlignment="Right"/>
</BulletDecorator.Bullet>
<TextBox x:Name="txtId" Text="{Binding ElementName=dgCustomers, Path=SelectedItem.name}" Margin="5,5,5,5"/>
</BulletDecorator>
<BulletDecorator Grid.Column="1">
<BulletDecorator.Bullet>
<Label Content="Code:" VerticalAlignment="Center" HorizontalAlignment="Right"/>
</BulletDecorator.Bullet>
<TextBox x:Name="txtCode" Text="{Binding ElementName=dgCustomers, Path=SelectedItem.countryCode}" Margin="5,5,5,5"/>
</BulletDecorator>
<BulletDecorator Grid.Column="2">
<BulletDecorator.Bullet>
<Label Content="Phone:" VerticalAlignment="Center" HorizontalAlignment="Right"/>
</BulletDecorator.Bullet>
<TextBox x:Name="txtPhone" Text="{Binding ElementName=dgCustomers, Path=SelectedItem.phone}" Margin="5,5,5,5"/>
</BulletDecorator>
</Grid>
</Grid>
</Window>
记录类:
public class Record : ViewModelBase
{
private string _name;
public string name
{
get { return _name; }
set
{
_name = value;
OnPropertyChanged("name");
}
}
private string _phone;
public string phone
{
get { return _phone; }
set
{
_phone = value;
OnPropertyChanged("phone");
}
}
private int _countryCode;
public int countryCode
{
get { return _countryCode; }
set
{
_countryCode = value;
OnPropertyChanged("countryCode");
}
}
}
国家级:
public class Country : ViewModelBase
{
private string _name;
public string name
{
get { return _name; }
set
{
_name = value;
OnPropertyChanged("name");
}
}
private int _id;
public int id
{
get { return _id; }
set
{
_id = value;
OnPropertyChanged("id");
}
}
private int _code;
public int code
{
get { return _code; }
set
{
_code = value;
OnPropertyChanged("code");
}
}
public override string ToString()
{
return _name;
}
}
网格模型:
public class GridModel : ViewModelBase
{
public ObservableCollection<Record> customers { get; set; }
public List<Country> countries { get; set; }
public GridModel()
{
customers = new ObservableCollection<Record>();
countries = new List<Country> { new Country { id = 1, name = "England", code = 44 }, new Country { id = 2, name = "Germany", code = 49 },
new Country { id = 3, name = "US", code = 1}, new Country { id = 4, name = "Canada", code = 11 }};
}
private Country _selectedCountry;
public Country SelectedCountry
{
get
{
return _selectedCountry;
}
set
{
_selectedCountry = value;
_selectedRow.countryCode = _selectedCountry.code;
OnPropertyChanged("SelectedRow");
}
}
private Record _selectedRow;
public Record SelectedRow
{
get
{
return _selectedRow;
}
set
{
_selectedRow = value;
Debug.Print("Datagrid selection changed");
OnPropertyChanged("SelectedRow");
}
}
}
ViewModelBase:
public class ViewModelBase : INotifyPropertyChanged
{
public ViewModelBase()
{
}
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string name)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
}
谢谢你的帮助
编辑谢谢你的帮助标记,我正在运行你在下面的答案中提供的代码,但是我仍然没有在窗口底部的文本框中获得国家代码。我发现以下错误:
System.Windows.Data错误:23:无法使用默认转换将“{NewItemPlaceholder}”从类型“NamedObject”转换为类型“CustomerFreezable.RecordViewModel”,用于“en US”区域性;考虑使用转换器的绑定属性。NotSupportedException:'System.NotSupportedException:TypeConverter无法从MS.Internal.NamedObject转换。
在System.ComponentModel.TypeConverter.GetConvertFromExceptionObject值处
位于System.ComponentModel.TypeConverter.ConvertFromitTypeDescriptor上下文、文化信息文化、对象值
在MS.Internal.Data.DefaultValueConverter.ConvertHelperObject o中,类型destinationType,DependencyObject targetElement,CultureInfo区域性,布尔值isForward'
System.Windows.Data错误:7:ConvertBack无法转换值“{NewItemPlaceholder}”类型“NamedObject”。BindingExpression:Path=SelectedRow;DataItem='GridModel'HashCode=62992796;目标元素为“DataGrid”Name='dgCustomers';目标属性为“SelectedItem”类型“Object”NotSupportedException:“System.NotSupportedException:TypeConverter无法从MS.Internal.NamedObject转换。
在MS.Internal.Data.DefaultValueConverter.ConvertHelperObject o中,键入destinationType、DependencyObject targetElement、CultureInfo区域性、Boolean isForward
在MS.Internal.Data.ObjectTargetConverter.ConvertBackObject o中,类型类型、对象参数、文化信息文化
位于System.Windows.Data.BindingExpression.ConvertBackHelperIValueConverter,对象值,类型sourceType,对象参数,CultureInfo区域性'
数据网格选择已更改
数据网格选择已更改
System.Windows.Data错误:40:BindingExpression路径错误:在“object”RecordViewModel“HashCode=47081572”上找不到“countryCode”属性。BindingExpression:Path=SelectedItem.countryCode;DataItem='DataGrid'名称='dgCustomers';目标元素是“TextBox”Name='txtCode';目标属性为“文本”类型“字符串”
System.Windows.Data错误:23:无法使用默认转换将“{NewItemPlaceholder}”从类型“NamedObject”转换为类型“CustomerFreezable.RecordViewModel”,用于“en US”区域性;考虑使用转换器的绑定属性。NotSupportedException:'System.NotSupportedException:TypeConverter无法从MS.Internal.NamedObject转换。
在System.ComponentModel.TypeConverter.GetConvertFromExceptionObject值处
位于System.ComponentModel.TypeConverter.ConvertFromitTypeDescriptor上下文、文化信息文化、对象值
在MS.Internal.Data.DefaultValueConverter.ConvertHelperObject o中,类型destinationType,DependencyObject targetElement,CultureInfo区域性,布尔值isForward'
System.Windows.Data错误:7:ConvertBack无法转换值“{NewItemPlaceholder}”类型“NamedObject”。BindingExpression:Path=SelectedRow;DataItem='GridModel'HashCode=62992796;目标元素为“DataGrid”Name='dgCustomers';目标属性为“SelectedItem”类型“Object”NotSupportedException:“System.NotSupportedException:TypeConverter无法从MS.Internal.NamedObject转换。
在MS.Internal.Data.DefaultValueConverter.ConvertHelperObject o中,键入destinationType、DependencyObject targetElement、CultureInfo区域性、Boolean isForward
在MS.Internal.Data.ObjectTargetConverter.ConvertBackObject o中,类型类型、对象参数、文化信息文化
位于System.Windows.Data.BindingExpression.ConvertBackHelperIValueConverter,对象值,类型sourceType,对象参数,CultureInfo区域性'
数据网格选择已更改
System.Windows.Data错误:40:BindingExpression路径错误:在“object”RecordViewModel“HashCode=47081572”上找不到“countryCode”属性。BindingExpression:Path=SelectedItem.countryCode;DataItem='DataGrid'名称='dgCustomers';目标元素是“TextBox”Name='txtCode';目标属性为“文本”类型“字符串”
我试图通过更改静态资源来解决BindingExpression路径错误:
<local:BindingProxy x:Key="CountryProxy" Data="{Binding}" />
以及文本框的绑定:
<TextBox x:Name="txtCode" Text="{Binding Path=record.countryCode}" Margin="5,5,5,5"/>
这消除了错误40,但我仍然没有在文本框中看到任何东西。你能告诉我出了什么问题吗?请原谅我的诚实,但这段代码有很多问题 首先,有一些严重的偏差 来自MVVM。MVVM是一个分层架构…首先是模型,然后是视图模型,然后是视图模型。从技术上讲,转换器是视图的一部分,但如果有的话,它们位于视图的另一侧,而不是视图模型。您所做的是使用转换器在您的模型中生成新记录:
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is Record)
return value;
return new Customer.Record(); <<<<<<<< this here
}
任何时候,当转换器直接与非视图类一起工作时,都很好地表明您的视图模型没有正常工作,并且几乎总是导致绑定中断和错误行为
另一个问题是,您的Record类看起来像是要作为模型的,也就是说,因为它有一个用于country的整数代码,而不是对country类实例的引用。然而,该类是从ViewModelBase派生的,并且不执行属性更改通知。此外,GridModel中类型为Country(即SelectedCountry)的一个字段将被所有记录绑定,因此更改一个字段的国家代码将更改所有记录
但要回答您的特定问题,问题是DataGrid在检测到其中一个字段已被编辑之前不会创建新记录。在这种情况下,您与SelectedRow的绑定不在记录本身中,因此没有创建记录,也没有传播值
下面是一个更好地坚持MVVM的固定版本,并修复了绑定问题:
// record model
public class Record
{
public string name {get; set;}
public string phone { get; set; }
public int countryCode {get; set;}
}
// record view model
public class RecordViewModel : ViewModelBase
{
private Record record = new Record();
public string name
{
get { return record.name; }
set
{
record.name = value;
RaisePropertyChanged("name");
}
}
public string phone
{
get { return record.phone; }
set
{
record.phone = value;
RaisePropertyChanged("phone");
}
}
private Country _country;
public Country country
{
get { return _country; }
set
{
_country = value;
record.countryCode = value.code;
RaisePropertyChanged("country");
}
}
}
public class Country : ViewModelBase
{
private string _name;
public string name
{
get { return _name; }
set
{
_name = value;
RaisePropertyChanged("name");
}
}
private int _id;
public int id
{
get { return _id; }
set
{
_id = value;
RaisePropertyChanged("id");
}
}
private int _code;
public int code
{
get { return _code; }
set
{
_code = value;
RaisePropertyChanged("code");
}
}
public override string ToString()
{
return _name;
}
}
public class GridModel : ViewModelBase
{
public ObservableCollection<RecordViewModel> customers { get; set; }
public List<Country> countries { get; set; }
public GridModel()
{
customers = new ObservableCollection<RecordViewModel>();
countries = new List<Country> { new Country { id = 1, name = "England", code = 44 }, new Country { id = 2, name = "Germany", code = 49 },
new Country { id = 3, name = "US", code = 1}, new Country { id = 4, name = "Canada", code = 11 }};
}
private RecordViewModel _selectedRow;
public RecordViewModel SelectedRow
{
get
{
return _selectedRow;
}
set
{
_selectedRow = value;
Debug.Print("Datagrid selection changed");
RaisePropertyChanged("SelectedRow");
}
}
}
// this is needed for when you need to bind something that isn't part of the visual tree (i.e. your combobox dropdowns)
// see http://www.thomaslevesque.com/2011/03/21/wpf-how-to-bind-to-data-when-the-datacontext-is-not-inherited/ for details
public class BindingProxy : Freezable
{
#region Overrides of Freezable
protected override Freezable CreateInstanceCore()
{
return new BindingProxy();
}
#endregion
public object Data
{
get { return (object)GetValue(DataProperty); }
set { SetValue(DataProperty, value); }
}
// Using a DependencyProperty as the backing store for Data. This enables animation, styling, binding, etc...
public static readonly DependencyProperty DataProperty =
DependencyProperty.Register("Data", typeof(object), typeof(BindingProxy), new UIPropertyMetadata(null));
}
以及XAML:
<Window.Resources>
<local:BindingProxy x:Key="CountryProxy" Data="{Binding Path=countries}" />
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="8*"/>
<RowDefinition Height="3*"/>
</Grid.RowDefinitions>
<DataGrid x:Name="dgCustomers" AutoGenerateColumns="False"
ItemsSource="{Binding customers}" SelectedItem="{Binding SelectedRow, Mode=TwoWay}"
CanUserAddRows="True" Grid.Row="0" >
<DataGrid.Columns>
<DataGridComboBoxColumn Header="Country"
ItemsSource="{Binding Source={StaticResource ResourceKey=CountryProxy}, Path=Data}" DisplayMemberPath="name"
SelectedItemBinding="{Binding country, UpdateSourceTrigger=PropertyChanged}" />
<DataGridTextColumn Header="Name" Binding="{Binding name, UpdateSourceTrigger=PropertyChanged}" Width="1*" />
<DataGridTextColumn Header="Phone" Binding="{Binding phone, UpdateSourceTrigger=PropertyChanged}" Width="1*"/>
</DataGrid.Columns>
</DataGrid>
<Grid x:Name="grdDisplay" Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="1*"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="2" Content="Country:" VerticalAlignment="Center" HorizontalAlignment="Right"/>
<Label Grid.Column="4" Content="Code:" VerticalAlignment="Center" HorizontalAlignment="Right"/>
<BulletDecorator Grid.Column="0">
<BulletDecorator.Bullet>
<Label Content="Name:" VerticalAlignment="Center" HorizontalAlignment="Right"/>
</BulletDecorator.Bullet>
<TextBox x:Name="txtId" Text="{Binding Path=SelectedRow.name}" Margin="5,5,5,5"/>
</BulletDecorator>
<BulletDecorator Grid.Column="1">
<BulletDecorator.Bullet>
<Label Content="Code:" VerticalAlignment="Center" HorizontalAlignment="Right"/>
</BulletDecorator.Bullet>
<TextBox x:Name="txtCode" Text="{Binding Path=SelectedRow.country.code}" Margin="5,5,5,5"/>
</BulletDecorator>
<BulletDecorator Grid.Column="2">
<BulletDecorator.Bullet>
<Label Content="Phone:" VerticalAlignment="Center" HorizontalAlignment="Right"/>
</BulletDecorator.Bullet>
<TextBox x:Name="txtPhone" Text="{Binding Path=SelectedRow.phone}" Margin="5,5,5,5"/>
</BulletDecorator>
</Grid>
</Grid>
忘记转换器吧,你不需要它。这段代码引入的一个问题是,您现在需要单击组合框两次:首先选择行,然后再次编辑它。但是网络上有很多地方展示了如何解决这个问题,所以我将把它留给你。任何批评都是非常有建设性的:我正在运行你发送的代码,但我仍然没有在窗口底部的文本框中看到countryCode。输出窗口中有几个错误;我已经编辑了问题以添加它们。抱歉,国家代码的绑定不正确,我在上面的XAML中修复了它。我注意到的另一件事是,底部的字段直接绑定到元素。虽然这通常是可行的,但是如果您绑定到视图模型字段,您会发现以后的问题会少一些,我现在将代码改为这样做,特别是当您开始移动XAML或需要在代码中放置断点以确保控件正常工作时。最后一件事…底部的国家代码编辑字段当前正确显示国家代码,但编辑它不会传播回记录,这基本上是在datagrid中使用组合框的副作用,但下面是国家代码。如果确实需要此功能,则需要将countryCode字段添加到视图模型中。这就提出了一个问题,视图模型首先是如何获得国家的数组的……这可以通过让GridModel监视新的RecordViewModels被添加到ObservableCollection并在创建时初始化它们来实现。doh……RecordViewModel的国家设置器中也有一个小错误:我在打电话RaisePropertyChanged在countryCode上而不是在country上。它可以工作!太棒了,谢谢你,马克。底部的那些框只是为了让我能够看到发生了什么,它们没有出现在我的实际应用程序中。还感谢您为我设置了正确的MVVM实现路径:
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is Record)
return value;
return new Customer.Record(); <<<<<<<< this here
}
// record model
public class Record
{
public string name {get; set;}
public string phone { get; set; }
public int countryCode {get; set;}
}
// record view model
public class RecordViewModel : ViewModelBase
{
private Record record = new Record();
public string name
{
get { return record.name; }
set
{
record.name = value;
RaisePropertyChanged("name");
}
}
public string phone
{
get { return record.phone; }
set
{
record.phone = value;
RaisePropertyChanged("phone");
}
}
private Country _country;
public Country country
{
get { return _country; }
set
{
_country = value;
record.countryCode = value.code;
RaisePropertyChanged("country");
}
}
}
public class Country : ViewModelBase
{
private string _name;
public string name
{
get { return _name; }
set
{
_name = value;
RaisePropertyChanged("name");
}
}
private int _id;
public int id
{
get { return _id; }
set
{
_id = value;
RaisePropertyChanged("id");
}
}
private int _code;
public int code
{
get { return _code; }
set
{
_code = value;
RaisePropertyChanged("code");
}
}
public override string ToString()
{
return _name;
}
}
public class GridModel : ViewModelBase
{
public ObservableCollection<RecordViewModel> customers { get; set; }
public List<Country> countries { get; set; }
public GridModel()
{
customers = new ObservableCollection<RecordViewModel>();
countries = new List<Country> { new Country { id = 1, name = "England", code = 44 }, new Country { id = 2, name = "Germany", code = 49 },
new Country { id = 3, name = "US", code = 1}, new Country { id = 4, name = "Canada", code = 11 }};
}
private RecordViewModel _selectedRow;
public RecordViewModel SelectedRow
{
get
{
return _selectedRow;
}
set
{
_selectedRow = value;
Debug.Print("Datagrid selection changed");
RaisePropertyChanged("SelectedRow");
}
}
}
// this is needed for when you need to bind something that isn't part of the visual tree (i.e. your combobox dropdowns)
// see http://www.thomaslevesque.com/2011/03/21/wpf-how-to-bind-to-data-when-the-datacontext-is-not-inherited/ for details
public class BindingProxy : Freezable
{
#region Overrides of Freezable
protected override Freezable CreateInstanceCore()
{
return new BindingProxy();
}
#endregion
public object Data
{
get { return (object)GetValue(DataProperty); }
set { SetValue(DataProperty, value); }
}
// Using a DependencyProperty as the backing store for Data. This enables animation, styling, binding, etc...
public static readonly DependencyProperty DataProperty =
DependencyProperty.Register("Data", typeof(object), typeof(BindingProxy), new UIPropertyMetadata(null));
}
<Window.Resources>
<local:BindingProxy x:Key="CountryProxy" Data="{Binding Path=countries}" />
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="8*"/>
<RowDefinition Height="3*"/>
</Grid.RowDefinitions>
<DataGrid x:Name="dgCustomers" AutoGenerateColumns="False"
ItemsSource="{Binding customers}" SelectedItem="{Binding SelectedRow, Mode=TwoWay}"
CanUserAddRows="True" Grid.Row="0" >
<DataGrid.Columns>
<DataGridComboBoxColumn Header="Country"
ItemsSource="{Binding Source={StaticResource ResourceKey=CountryProxy}, Path=Data}" DisplayMemberPath="name"
SelectedItemBinding="{Binding country, UpdateSourceTrigger=PropertyChanged}" />
<DataGridTextColumn Header="Name" Binding="{Binding name, UpdateSourceTrigger=PropertyChanged}" Width="1*" />
<DataGridTextColumn Header="Phone" Binding="{Binding phone, UpdateSourceTrigger=PropertyChanged}" Width="1*"/>
</DataGrid.Columns>
</DataGrid>
<Grid x:Name="grdDisplay" Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="1*"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="2" Content="Country:" VerticalAlignment="Center" HorizontalAlignment="Right"/>
<Label Grid.Column="4" Content="Code:" VerticalAlignment="Center" HorizontalAlignment="Right"/>
<BulletDecorator Grid.Column="0">
<BulletDecorator.Bullet>
<Label Content="Name:" VerticalAlignment="Center" HorizontalAlignment="Right"/>
</BulletDecorator.Bullet>
<TextBox x:Name="txtId" Text="{Binding Path=SelectedRow.name}" Margin="5,5,5,5"/>
</BulletDecorator>
<BulletDecorator Grid.Column="1">
<BulletDecorator.Bullet>
<Label Content="Code:" VerticalAlignment="Center" HorizontalAlignment="Right"/>
</BulletDecorator.Bullet>
<TextBox x:Name="txtCode" Text="{Binding Path=SelectedRow.country.code}" Margin="5,5,5,5"/>
</BulletDecorator>
<BulletDecorator Grid.Column="2">
<BulletDecorator.Bullet>
<Label Content="Phone:" VerticalAlignment="Center" HorizontalAlignment="Right"/>
</BulletDecorator.Bullet>
<TextBox x:Name="txtPhone" Text="{Binding Path=SelectedRow.phone}" Margin="5,5,5,5"/>
</BulletDecorator>
</Grid>
</Grid>