-1

I have a simple 2 table setup, 'Teams' and 'Players'. Each player has a (nullable) int TeamID (preferrable AllowDBNull, but -1 as default might work too). I insert/update/delete records to both tables, and these are sound (mathich IDs in a small test-DB). The setup looks like this:

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" ,
        DataType = typeof( int ) ,
        //AllowDBNull = true ,
        //DefaultValue = DBNull.Value ,
    } );

    dataSet.Tables.Add( dtPlayers );

    var dtTeams = new DataTable( "Teams" );

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

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

Now I want to have a dropdown to display the teams in the players-table with a default empty selection (meaning no team selected - value eithe r-1 or null, which becomes DBNull later on). For this, I setup the DataGridView and also an additional DataTable as .DataSource for the DataGridViewComboBoxColumn. Each time the Dropdown is selected/edited, I update the dtTeamIds table, to reflect changes. I also setup the proper .DisplayMember & .ValueMember fields.

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

Now, when I start the application, initially the dropdown-list is empty (no selection, not the "---"). When editing the combobox-cell the teams and the default "---" are shown and the selected item on top becomes the "---" item. When clicking in any other cell after doing any change to the combobox-cell to a team, I get a FormatException (CBox Cell Value is not valid).

I tried to get information on where or why the value isn't valid, checking CellBegin/EndEdit, Formattin, Changed etc Events, however I haven't done any progress on where this might come from.

Did I do something wrong in the setup? Is there an easier way?

nonsensation
  • 151
  • 5

1 Answers1

1

From the comments of your previously deleted question and from the current posted code I hope I have this correct….

You have two (2) DataTables you get from a DB: “Teams” and “Players.”

The “Teams” data table has two fields: a unique int “ID” and a string “FullName.”

The “Players” data table has two fields: a unique int “ID” and an int “TeamID.”

Each “TeamID” field in the “players” table “should” match one of the “Team” “IDs” in the “Teams” table.

The goal is to use the “Players” table as a DataSource to a DataGridView such that the “TeamID” column in the grid is a combo box column. And instead of the “TeamID” being displayed in the combo boxes, you want the “Teams” “FullName” field to display in the combo box, but you want the underlying data to remain an int “TeamID.”

Next there is a requirement that the combo box has an item in the combo box to indicate that NO particular team is selected. Meaning the player is not on any team. This would be translated as having the “TeamID” field for that player set to a DBNull value.

The last requirement is that you DO NOT want to alter either the “Teams” or “Players” tables in any way.

The DataGridViewComboBoxCell isn’t very forgiving…

It should be noted that unlike a regular ComboBox, the DataGridViewComboBoxCell is a different monster and is far less forgiving about text being input into the combo box that is NOT in its “items” collection. A regular combo box is very forgiving and will simply “ignore” any invalid text that is input. The DataGridViewComboBoxCell on the other hand will throw the dreaded DataError exception when this happens. Obviously, this is something you are aware of.

The point here, is that extra care and a little extra code is needed to minimize the possibility that the grid will throw up the DataError when dealing with a combo box column. Some of the extra steps may appear unnecessary and would definitely fall under the CYA (Cover Your Ass) category. In this case, making sure that the underlying data and the combo box column are set up properly.

The data from my DB is NEVER bad…

One situation that IMHO you MUST address may appear superfluous, however anytime you are getting data from an external source, like a DB, there is always the possibility that some data may be bad. In this case we MUST assume that one or more “TeamID” values in the “Player” table may NOT match one of the “Team” “IDs.”

Example, if there are ten (10) teams with ids 1-10, and one of the players has a “TeamID” of 22, then, when the code attempts to set the grids data source, the data error is going to start complaining. You may say… “Well I know this will never happen” and you may well be right, however, I would rather be prepared and have the code DO SOMETHING about this as opposed to having the code crash.

So, indulge me if you will in assuming this COULD happen and make a game plan to figure out “WHAT” we want to do when it does happen. Another reason to address this is that if it DOES happen, your code will be utterly useless and the user is going to question your coding expertise.

So... What to do?

So, what do we do when this happens? We can not simply ignore this player if the team id is invalid. Technically we could ignore this by simply “removing” that player from the table, however, this seems a little drastic and may compound problems. If there is bad data in the DB, I would want to know this ASAP.

Another option, is to simply change the ID to null as this will indicate a player is not on a team. This sounds promising, however, if we simply “change” the invalid value to null, then the bad data will still remain in the DB. This may work if we throw up a message box and let the user know that the code changed the value. This way the user may get involved and want to fix the data in the DB simply to get rid of the annoying message box every time the app is run.

A third option, the one used below, is not the best approach, however, it does not change the data. In this approach the code simply “adds” the invalid value to the combo boxes items list. Then flags this to the user by giving the team name something like “unknown” or in the example below, I will set the team name to “Team XX ???” … where XX is the invalid team number in the players Team ID field.

When the data is loaded, and the user selects the combo box, they would see these “extra” invalid team names to pick from. In addition, any player with a team name of “Team XX ???” will be a red flag to the user as to “which” player(s) have invalid team numbers. Granted, this approach may compound the problem too if the user decides to “select” one of the invalid teams from the combo for some other player.

You may have other ideas, however whatever solution you pick, it will be a better approach than letting the code crash.

Finally, I can get off the soap box and deal with the problem at hand. Given the requirements and the previous comments. It appears clear we will need to create a “copy” of the “Teams” table since we need to add the “empty/null” team and possibly add the invalid teams from the player’s table.

An extra “altered” Teams data table…

Therefore, we will add one more DataTable to the DataSet and call it TeamsCombo. We will use this table as a data source for the combo box column. In order to create this table, we will need both the “Teams” table and “Players” table filled with data.

First, we will make a copy of the team’s table. Then we need to check and add any “TeamIDs” that are NOT in the Teams table. I am a bit old fashioned and created two loops to check the player team IDs. LINQ would also work.

Initially, an “empty” list of ints is created and the first loop collects all the different “IDs” from the teams table and adds them to the list.

Next, we loop through all the rows in the “Players” table. We ignore null TeamIDs and team ids that are already in the list created in the previous loop. If we come across a TeamID that is NOT null and is also NOT in the int list, then we add this ID to the teams (copy) table, in addition to the list of ints. Lastly, we add the “empty” team item as the first row in the teams (copy) table, then return this new table.

This function may look like…

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

This should return a data table that we can use for the combo box column and we can rest a little easier knowing we have minimized the grid throwing up the data error in relation to the combo box.

The Teams combo box column…

After we add the previous table to the data set, we move on to creating the grid’s DataGridViewComboBoxColumn. This should be straight forward since we already have a good data table to use as a data source. Here we set the header text, name, data property name, value member, display member and finally the data source and return the column. This may look something like…

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

Now to put all this to a test and to make a complete example, drop a DataGridView on to a form and add the code below to test the methods above.

Below is a picture of the test used in the example.

enter image description here

I broke most aspects into small pieces and they could obviously be combined.

Most methods are straight forward, but the two methods that fill the Teams and Players table may need a little explanation. The original “Teams” table has ten (10) different teams with ID’s 1-10. The Players table uses a random number generator to randomly set each player to one of the team ids … 1-10.

The random number generator will generate numbers from 0 to 11. If the generated number is zero (0) then this player will get a null team id. If the generated number is an 11, then this player will get a team id that is NOT in the Teams table. This is to test the invalid “TeamID” scenario described earlier.

Please note, that since this uses a random number generator, it is possible that no null or invalid numbers are generated. In that case, simply run the code again. I am confident both situations will eventually be produced.

The two methods to create some test data may look like….

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

Code to complete the example…

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);
}
JohnG
  • 9,259
  • 2
  • 20
  • 29