DataGridView:ComboBox设置

问题描述

我有一个简单的2张桌子设置,即“团队”和“玩家”。每个玩家都有一个(可空)int TeamID (最好是 Allowdbnull ,但认值为-1也可以)。 我将记录插入/更新/删除到两个表中,并且这些记录都是正确的(在小型测试数据库中为大量ID)。 设置如下:

private void initDataSet()
{
    dataSet = new DataSet( "DataBase" );

    var dtPlayers = new DataTable( "Players" );

    dtPlayers.Columns.Add( new DataColumn() {
        ColumnName = "ID",Caption = "ID",DataType = typeof( int ),Unique = true,AutoIncrement = true,} );

    dtPlayers.Columns.Add( new DataColumn() {
        ColumnName = "TeamID",Caption = "Team",//Allowdbnull = true,//DefaultValue = dbnull.Value,} );

    dataSet.Tables.Add( dtPlayers );

    var dtTeams = new DataTable( "Teams" );

    dtTeams.Columns.Add( new DataColumn() {
        ColumnName = "ID",} );

    dtTeams.Columns.Add( new DataColumn() {
        ColumnName = "FullName",Caption = "Full Name",DataType = typeof( string ),} );

    dataSet.Tables.Add( dtTeams );

    dtPlayers.PrimaryKey = new DataColumn[] { dtPlayers.Columns[ "ID" ] };
    dtTeams.PrimaryKey = new DataColumn[] { dtTeams.Columns[ "ID" ] };

    dataSet.Relations.Add( "Team - Player",dtTeams.Columns[ "ID" ],dtPlayers.Columns[ "TeamID" ] );
}

现在,我希望有一个下拉列表,以在球员表中显示具有认空选择的球队(这意味着未选择任何球队-值eithe r-1或null,稍后将变为dbnull)。 为此,我为DataGridViewComboBoxColumn设置了DataGridView,并将另一个DataTable设置为.DataSource。 每次选择/编辑下拉列表时,我都会更新dtTeamIds表以反映更改。 我还设置了正确的.displayMember.ValueMember字段。

private DataTable dtTeamIds = new DataTable( "TeamIds" ); // datasource for dorpdown-list only

private void updateTeamIds()
{
    var teamsTable = dataSet.Tables[ "Teams" ] as DataTable;
    var items = teamsTable.Rows.Cast<DaTarow>()
        .Select( row => new keyvaluePair<string,int>(
            row.Field<string>( "FullName" ),row.Field<int>( "ID" ) ) )
        .Prepend( new keyvaluePair<string,int>( "---",-1 ) )
        .ToArray();

    dtTeamIds.Rows.Clear();

    foreach( var item in items )
    {
        var row = dtTeamIds.NewRow();
        row.SetField( dtTeamIds.Columns[ "MyValue" ],item.Value );
        row.SetField( dtTeamIds.Columns[ "MyText" ],item.Key );
        dtTeamIds.Rows.Add( row );
    }
}

private void initDataGridViews()
{
    // Players
    {
        var dgv = dataGridViewPlayers;

        dgv.AutoGenerateColumns = false;
        dgv.CausesValidation = false;

        var sourceTable = dataSet.Tables[ "Players" ];
        var dataSrc = sourceTable;

        dgv.Columns.Clear();
        dgv.DataSource = new BindingSource() {
            DataSource = dataSrc,};

        dgv.Columns.Add( new DataGridViewTextBoxColumn() {
            DataPropertyName = "ID",Visible = false,} );

        // dowpdown-list
        {
            dtTeamIds.Columns.Add( "MyText" );
            dtTeamIds.Columns.Add( "MyValue" );

            updateTeamIds();

            dgv.Columns.Add( new DataGridViewComboBoxColumn() {
                DataPropertyName = "TeamID",HeaderText = dataSrc.Columns[ "TeamID" ].Caption,ToolTipText = "Press 'CTRL + 0' do delete the team!",ValueType = typeof( int ),displayMember = "MyText",ValueMember = "MyValue",DataSource = new BindingSource( dtTeamIds,null ),} );
        }

        SetupDataGridViewDefaults( dgv );

        // HACK: see: https://social.msdn.microsoft.com/Forums/en-US/243da031-760d-4e7f-b09f-be4cfa5a6a4b
        dataSrc.ColumnChanging += ( s,e ) => {
            if( e.Column == dataSrc.Columns[ "TeamID" ] )
            {
                var t2 = e.ProposedValue.GetType();

                if( e.ProposedValue == null || ( e.ProposedValue is int val && val == -1 ) )
                {
                    //e.ProposedValue = dbnull.Value; // -1 or NULL
                }                       
            }
        };

        //dgv.CellEnter += ( s,e ) => {
        //  if( e.ColumnIndex == dgv.Columns[ "TeamID" ].Index )
        //  {
        //      updateTeamIds();
        //  }
        //};
        //dgv.CellBeginEdit += ( s,e ) => {
        //  if( e.ColumnIndex == dgv.Columns[ "TeamID" ].Index )
        //  {
        //      updateTeamIds();
        //  }
        //};
        dgv.Click += ( s,e ) => {
            updateTeamIds();
            //if( e.ColumnIndex == dgv.Columns[ "TeamID" ].Index )
            //{
            //  var cell = dgv[ e.ColumnIndex,e.RowIndex ] as DataGridViewComboBoxCell;
            //}
        };

        dgv.CellFormatting += ( s,e ) => {
            if( e.ColumnIndex == dgv.Columns[ "TeamID" ].Index )
            {
                var cell = dgv[ e.ColumnIndex,e.RowIndex ] as DataGridViewComboBoxCell;
                //var _1 = cell.EditedFormattedValue;
                //var _2 = cell.FormattedValue;
                var _3 = cell.Value;    // null
                var _4 = e.Value;       // null
                var _5 = e.DesiredType; // string
            }
        };
    }

    // setup teams-table etc..
}

private void SetupDataGridViewDefaults( DataGridView dgv )
{
    // Todo: figure out how to properly resize columns

    foreach( var colIdx in Enumerable.Range( 0,dgv.Columns.Count ) )
    {
        var col = dgv.Columns[ colIdx ];

        col.Name = col.DataPropertyName;
        col.MinimumWidth = 50;

        col.AutoSizeMode = DataGridViewAutoSizeColumnMode.AllCells;

        if( colIdx == dgv.Columns.Count - 1 )
            col.AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;

        col.Width = col.Width; //This is important,otherwise the following line will nullify your prevIoUs command
        col.AutoSizeMode = DataGridViewAutoSizeColumnMode.NotSet;
    }

    dgv.AutoResizeColumns( DataGridViewAutoSizeColumnsMode.AllCells );
    dgv.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.None;
    dgv.Columns[ dgv.Columns.Count - 1 ].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;

    dgv.AllowUserToResizeColumns = true;
    dgv.AllowUserToOrderColumns = false;
    dgv.AllowUserToResizeRows = true;

    dgv.Enabled = true;
    dgv.Refresh();
}

现在,当我启动应用程序时,最初的下拉列表为空(没有选择,不是“ ---”)。 编辑组合框单元时,将显示团队和认的“ ---”,并且顶部的所选项目将成为“ ---”项目。 在对团队的comboBox-cell进行任何更改后单击任何其他单元格时,我将收到FormatException(CBox单元格值无效)。

我试图获取有关该值无效或为何无效的信息,请检查CellBegin / EndEdit,Formattin,Changed等事件,但是我对该值可能来自何处尚未取得任何进展。

我在设置中做错了吗? 有没有更简单的方法

解决方法

从您先前删除的问题的注释以及从当前发布的代码中,我希望我是正确的……。

您从数据库中获得了两(2)个DataTables:“团队”和“玩家”。

“团队”数据表具有两个字段:唯一的int“ ID”和 string“全名”。

“玩家”数据表具有两个字段:唯一的int“ ID”和 int“ TeamID”。

“玩家”表中的每个“ TeamID”字段都应与“团队”表中的“团队”“ ID”之一匹配。

目标是将“玩家”表用作DataSourceDataGridView,以使网格中的“ TeamID”列为组合框列。而不是要在组合框中显示“ TeamID”,而是要在组合框中显示“ Teams”“ FullName”字段,但希望基础数据保留为int“ TeamID”。

接下来,要求组合框在组合框中具有一个项目,以指示未选择任何特定的团队。表示该球员不在任何团队中。可以将其转换为该玩家的“ TeamID”字段设置为DBNull值。

最后一个要求是您不希望以任何方式更改“ Teams”或“ Players”表。

DataGridViewComboBoxCell不太宽容……

应该注意的是,ComboBox与普通的DataGridViewComboBoxCell不同,DataGridViewComboBoxCell是不同的怪物,并且对于不在其“项目”集合中的文本输入到组合框中的容忍度要低得多。常规组合框是非常宽容的,只会“忽略”输入的任何无效文本。另一方面,DataError会在发生这种情况时引发可怕的DataError异常。显然,这是您所知道的。

这里的要点是,在处理组合框列时,需要格外小心和一些额外的代码,以最小化网格抛出null的可能性。一些额外的步骤可能显得不必要,并且肯定属于CYA(盖住屁股)类别。在这种情况下,请确保正确设置了基础数据和组合框列。

我数据库中的数据从不差……

IMHO您必须解决的一种情况可能看起来是多余的,但是无论何时您从外部资源(例如DB)获取数据时,总有一些数据可能是错误的。在这种情况下,我们必须假设“玩家”表中的一个或多个“ TeamID”值可能与“ Team”“ ID”之一不匹配。

例如,如果有十(10)个ID为1-10的球队,并且其中一个球员的“ TeamID”为22,那么,当代码尝试设置网格数据源时,数据错误就会发生开始抱怨。您可能会说……“我知道这永远不会发生” ,您可能是对的,但是,我宁愿有所准备,并且让代码对此有所帮助,而不是使代码崩溃。

所以,如果您会假设这种情况会发生,请纵容我,并制定一个游戏计划,弄清楚发生这种情况时我们想做什么。解决此问题的另一个原因是,如果确实发生了,您的代码将完全无用,并且用户会质疑您的编码专业知识。

所以...该怎么办?

那么,发生这种情况时我们该怎么办?如果球队ID无效,我们不能简单地忽略该球员。从技术上讲,我们可以通过简单地从桌面上“移走”该玩家的方式来忽略这一点,但是,这似乎有点过分激烈,并且可能使问题复杂化。如果数据库中有坏数据,我想尽快知道。

另一种选择是简单地将ID更改为null,因为这将指示玩家不在团队中。这听起来很有希望,但是,如果我们仅将无效值“更改”为DataTable,则不良数据仍将保留在数据库中。如果我们抛出一个消息框并让用户知道代码更改了该值,则此方法可能起作用。这样,用户可能会参与其中,并希望在数据库中修复数据,只是为了在每次运行该应用程序时摆脱烦人的消息框。

第三个选项(下面使用的一个)不是最佳方法,但是,它不会更改数据。在这种方法中,代码仅将无效值“添加”到组合框项目列表中。然后通过为团队名称提供类似“未知”的名称来标记用户,在下面的示例中,我将团队名称设置为“ Team XX ???”。 …其中XX是玩家“团队ID”字段中的无效团队编号。

在加载数据后,用户选择了组合框,他们将看到这些“多余的”无效团队名称可供选择。此外,任何团队名称为“ XX队???”的球员对于“哪个”球员的队号无效,这将对用户是一个危险信号。的确,如果用户决定从组合中“选择”一个无效的球队来为其他玩家使用,这种方法也可能使问题变得更加复杂。

您可能还有其他想法,但是,无论选择哪种解决方案,它都会比使代码崩溃更好的方法。

最后,我可以离开肥皂盒,解决眼前的问题。给出要求和先前的评论。显然,我们需要创建“团队”表的“副本”,因为我们需要添加“空/空”团队,并可能从玩家的表中添加无效的团队。

额外的“更改后的”团队数据表…

因此,我们将在DataSet上再添加一个TeamsCombo并将其命名为ints。我们将使用此表作为组合框列的数据源。为了创建此表,我们将需要同时填充数据的“ Teams”表和“ Players”表。

首先,我们将复制团队表格。然后,我们需要检查并添加Teams表中没有的任何“ TeamID”。我有点老套,创建了两个循环来检查玩家团队ID。 LINQ也可以。

最初,创建了一个null的“空”列表,第一个循环从teams表中收集了所有不同的“ ID”,并将它们添加到列表中。

接下来,我们循环浏览“玩家”表中的所有行。我们忽略上一个循环中创建的列表中已经存在的null TeamID和团队ID。如果我们遇到的TeamID并非int,而且不在ints列表中,那么除了{{1}的列表之外,我们还会将此ID添加到teams(副本)表中}。最后,我们在团队(副本)表的第一行中添加“空”团队项目,然后返回此新表。

此功能可能看起来...

private DataTable GetTeamsComboDT() {
  DataTable teams = dataSet.Tables["Teams"].Copy();
  teams.TableName = "TeamsComboDT";
  DataTable players = dataSet.Tables["Players"];
  List<int> teamIDs = new List<int>();
  foreach (DataRow row in teams.Rows) {
    if (row["ID"] != null && !teamIDs.Contains((int)row["ID"])) {
      teamIDs.Add((int)row["ID"]);
    }
  }
  foreach (DataRow row in players.Rows) {
    if (row["TeamID"] != DBNull.Value && !teamIDs.Contains((int)row["TeamID"])) {
      teams.Rows.Add((int)row["TeamID"],"Team " + (int)row["TeamID"] + " ???");
      teamIDs.Add((int)row["TeamID"]);
    }
  }
  DataRow emptyRow = teams.NewRow();
  emptyRow["ID"] = DBNull.Value;
  emptyRow["FullName"] = "---";
  teams.Rows.InsertAt(emptyRow,0);
  return teams;
}

这应该返回一个数据表,我们可以将其用于组合框列,并且知道我们已经最大限度地减少了与组合框相关的引发数据错误的网格,因此我们可以轻松一点。

“团队”组合框列…

将上一张表格添加到数据集中后,我们继续创建表格的DataGridViewComboBoxColumn。这应该很简单,因为我们已经有一个很好的数据表可以用作数据源。在这里,我们设置标题文本,名称,数据属性名称,值成员,显示成员,最后是数据源,然后返回列。可能看起来像...

private DataGridViewComboBoxColumn GetTeamsComboCol() {
  DataGridViewComboBoxColumn col = new DataGridViewComboBoxColumn();
  col.HeaderText = "Team";
  col.Name = "Teams";
  col.DataPropertyName = "TeamID";
  col.ValueMember = "ID";
  col.DisplayMember = "FullName";
  col.DataSource = dataSet.Tables["TeamsComboDT"];
  return col;
}

现在要对所有这些进行测试并制作一个完整的示例,将DataGridView放在表单上并添加下面的代码以测试上面的方法。

下面是示例中使用的测试的图片。

enter image description here

我将大多数方面分解为小块,显然可以将它们组合在一起。

大多数方法都是简单明了的,但是填充Teams和Players表的两种方法可能需要一些解释。原始的“团队”表中有十(10)个不同的团队,其ID为1-10。玩家表使用随机数生成器将每个玩家随机设置为团队ID…1-10。

随机数生成器将生成0到11之间的数字。如果生成的数字为零(0),则该玩家将获得一个null团队ID。如果生成的数字为11,则该玩家将获得不在“团队”表中的团队ID。这是为了测试前面所述的无效“ TeamID”方案。

请注意,由于这使用了随机数生成器,因此可能不会生成null或无效数字。在这种情况下,只需再次运行代码。我相信两种情况最终都会产生。

创建一些测试数据的两种方法可能看起来像……。

private void FillTeamsTable() {
  DataTable dt = dataSet.Tables["Teams"];
  for (int i = 0; i < 10; i++) {
    dt.Rows.Add(i + 1,"Team " + (i + 1).ToString());
  }
}


private void FillPlayersTable() {
  DataTable dt = dataSet.Tables["Players"];
  Random rand = new Random();
  int randTeam;
  for (int i = 0; i < 25; i++) {
    randTeam = rand.Next(0,12);
    switch (randTeam) {
      case 0:
        dt.Rows.Add(i + 1,DBNull.Value);
        break;
      case 11:
        dt.Rows.Add(i + 1,22);
        break;
      default:
        dt.Rows.Add(i + 1,randTeam);
        break;
    }
  }
}

代码以完成示例……

DataSet dataSet;

public Form2() {
  InitializeComponent();
}

private void Form2_Load(object sender,EventArgs e) {
  dataSet = new DataSet();
  AddTeamsDT_ToDS();
  AddPlayerDT_ToDS();
  FillTeamsTable();
  FillPlayersTable();
  dataSet.Tables.Add(GetTeamsComboDT());
  dataGridView1.Columns.Add(GetTeamsComboCol());
  dataGridView1.DataSource = dataSet.Tables["Players"];
  dataGridView1.Columns["Teams"].DisplayIndex = 1;
}


private void AddPlayerDT_ToDS() {
  DataTable dt = new DataTable("Players");
  dt.Columns.Add("ID",typeof(int));
  dt.Columns.Add("TeamID",typeof(int));
  dataSet.Tables.Add(dt);
}

private void AddTeamsDT_ToDS() {
  DataTable dt = new DataTable("Teams");
  dt.Columns.Add("ID",typeof(int));
  dt.Columns.Add("FullName",typeof(string));
  dataSet.Tables.Add(dt);
}