【问题标题】:WPF combobox in DataGridDataGrid 中的 WPF 组合框
【发布时间】:2015-10-06 21:37:30
【问题描述】:

这是一个展示我遇到问题的行为的示例。我有一个数据网格,它绑定到视图模型中的可观察记录集合。在数据网格中,我有一个 DataGridTemplateColumn 包含一个组合框,该组合框是从视图模型中的列表中填充的。数据网格还包含文本列。窗口底部有一些文本框用于显示记录内容​​。

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

最初没有记录,所以数据网格是空的,只显示一行包含组合框。如果用户首先在文本列中输入数据,则将一条记录添加到集合中,并且可以将组合框值添加到记录中。但是,如果用户首先选择ComboBox值,则选择另一列时,该值会消失。如果先选中,如何获取添加到记录中的组合框数据?

代码隐藏:

public partial class MainWindow : Window
{
    public GridModel gridModel { get; set; }

    public MainWindow()
    {
        InitializeComponent();
        gridModel = new GridModel();
        //dgCustomers.DataContext = gridModel;
        this.DataContext = gridModel;
    }

    private void cmbCountry_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        ComboBox c = sender as ComboBox;
        Debug.Print("ComboBox selection changed, index is " + c.SelectedIndex + ", selected item is " + c.SelectedItem);
    }
}

记录类:

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

转换器:

class Converters
{
}

public class SelectedRowConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return value;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value is Record)
            return value;
        return new Customer.Record();
    }
}

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”类型转换为“en-US”类型的“CustomersFreezable.RecordViewModel” ' 具有默认转换的文化;考虑使用 Binding 的 Converter 属性。 NotSupportedException:'System.NotSupportedException: TypeConverter 无法从 MS.Internal.NamedObject 转换。 在 System.ComponentModel.TypeConverter.GetConvertFromException(对象值) 在 System.ComponentModel.TypeConverter.ConvertFrom(ITypeDescriptorContext 上下文,CultureInfo 文化,对象值) 在 MS.Internal.Data.DefaultValueConverter.ConvertHelper(Object o, Type destinationType, DependencyObject targetElement, CultureInfoculture, Boolean isForward)'

System.Windows.Data 错误:7:ConvertBack 无法转换值“{NewItemPlaceholder}”(类型“NamedObject”)。 BindingExpression:Path=SelectedRow; DataItem='GridModel' (HashCode=62992796);目标元素是“DataGrid”(名称=“dgCustomers”);目标属性是'SelectedItem'(类型'Object') NotSupportedException:'System.NotSupportedException:TypeConverter 无法从 MS.Internal.NamedObject 转换。 在 MS.Internal.Data.DefaultValueConverter.ConvertHelper(对象 o,类型 destinationType,DependencyObject targetElement,CultureInfo 文化,布尔 isForward) 在 MS.Internal.Data.ObjectTargetConverter.ConvertBack(对象 o,类型类型,对象参数,CultureInfo 文化) 在 System.Windows.Data.BindingExpression.ConvertBackHelper(IValueConverter 转换器,对象值,类型 sourceType,对象参数,CultureInfo 文化)' 数据网格选择已更改 数据网格选择已更改

System.Windows.Data 错误:40:BindingExpression 路径错误:在“对象”“RecordViewModel”(HashCode=47081572)上找不到“countryCode”属性. BindingExpression:Path=SelectedItem.countryCode; DataItem='DataGrid'(名称='dgCustomers');目标元素是'TextBox'(名称='txtCode');目标属性是“文本”(类型“字符串”)

System.Windows.Data 错误:23:无法将“{NewItemPlaceholder}”从“NamedObject”类型转换为“en-US”类型的“CustomersFreezable.RecordViewModel” ' 具有默认转换的文化;考虑使用 Binding 的 Converter 属性。 NotSupportedException:'System.NotSupportedException: TypeConverter 无法从 MS.Internal.NamedObject 转换。 在 System.ComponentModel.TypeConverter.GetConvertFromException(对象值) 在 System.ComponentModel.TypeConverter.ConvertFrom(ITypeDescriptorContext 上下文,CultureInfo 文化,对象值) 在 MS.Internal.Data.DefaultValueConverter.ConvertHelper(Object o, Type destinationType, DependencyObject targetElement, CultureInfoculture, Boolean isForward)'

System.Windows.Data 错误:7:ConvertBack 无法转换值“{NewItemPlaceholder}”(类型“NamedObject”)。 BindingExpression:Path=SelectedRow; DataItem='GridModel' (HashCode=62992796);目标元素是“DataGrid”(名称=“dgCustomers”);目标属性是'SelectedItem'(类型'Object') NotSupportedException:'System.NotSupportedException:TypeConverter 无法从 MS.Internal.NamedObject 转换。 在 MS.Internal.Data.DefaultValueConverter.ConvertHelper(对象 o,类型 destinationType,DependencyObject targetElement,CultureInfo 文化,布尔 isForward) 在 MS.Internal.Data.ObjectTargetConverter.ConvertBack(对象 o,类型类型,对象参数,CultureInfo 文化) 在 System.Windows.Data.BindingExpression.ConvertBackHelper(IValueConverter 转换器,对象值,类型 sourceType,对象参数,CultureInfo 文化)' 数据网格选择已更改

System.Windows.Data 错误:40:BindingExpression 路径错误:在“对象”“RecordViewModel”(HashCode=47081572)上找不到“countryCode”属性. BindingExpression:Path=SelectedItem.countryCode; DataItem='DataGrid'(名称='dgCustomers');目标元素是'TextBox'(名称='txtCode');目标属性是“文本”(类型“字符串”)

我尝试通过更改静态资源来解决 BindingExpression 路径错误:

<local:BindingProxy x:Key="CountryProxy" Data="{Binding}" />

因此是 DataGrid 的 ItemsSource:

ItemsSource="{Binding Source={StaticResource ResourceKey=CountryProxy}, Path=Data.countries}" DisplayMemberPath="name"

以及文本框的绑定:

<TextBox x:Name="txtCode" Text="{Binding Path=record.countryCode}" Margin="5,5,5,5"/>

这消除了错误 40,但我仍然没有在文本框中看到任何内容。你能告诉我有什么问题吗?

【问题讨论】:

    标签: c# wpf mvvm combobox datagrid


    【解决方案1】:

    请原谅我说实话,但这段代码有很多问题。

    首先,与 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 类实例的引用。然而,这个类是从 ViewModelBase 派生的,并且会发出属性更改通知。此外,您的所有记录都绑定了 Country 类型的一个字段(即您的 GridModel 中的 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 或者如果您需要在代码中放置断点以确保控件正常工作。
    • 最后一件事......底部的国家代码编辑字段当前正确显示国家代码,但编辑它不会传播回记录,这基本上是使用组合的副作用数据网格中的框,但下面的国家代码。如果您确实需要此功能,则需要将 countryCode 字段添加到视图模型。这引发了视图模型如何首先获取国家/地区数组的问题……这可以通过让 GridModel 监视新的 RecordViewModel 被添加到 ObservableCollection 并在它们创建时对其进行初始化来完成。
    • doh...RecordViewModel 的 Country setter 中还有一个小错误:我在“countryCode”而不是“country”上调用 RaisePropertyChanged。
    • 它有效!太棒了,谢谢马克。底部的那些框只是为了让我能够看到发生了什么,它们不会出现在我的实际应用程序中。也感谢你让我走上正确的 MVVM 实现的道路:)
    猜你喜欢
    • 2023-03-05
    • 2015-08-22
    • 1970-01-01
    • 2013-09-30
    • 1970-01-01
    • 2011-03-02
    • 2018-01-17
    • 2014-10-14
    • 2013-11-03
    相关资源
    最近更新 更多