8
votes

I have a DataGridView with "Type" and "Value" columns. The user selects a data type, and then enters a value compatible with that type, some types (e.g. "Text") accept any string. Other types (e.g. "Yes/No") are restricted to a list of possible values. For each row, I set the cell in the "Value" column to be either a Textbox for freeform types, or a combobox for list types. The DataGridView is bound to a DataTable.

The problem comes in if the user enters a value for one type, but then switches the row to a different type for which the current value is not allowed. No matter what I try, I cannot clear the Value cell. Then when I assign a combobox to the cell, I get a DataError, because the current value of the cell is incompatible. How can I clear the value before changing the cell from a textbox to a combobox?

public enum DataType 
{
    Text,
    YesNo
}

public class IndexedItem 
{
    public string Name { get; set; }
    public int ID {get; set; }

    public string strID 
    {
        get { return ID.ToString(); }
    }       

    //constructors & other methods;
}

public static DataTable ParameterTable;
public List<IndexedItem> YesNoList;

In the form constructor (dgvInputs is the DataGridView):

ParameterTable = new DataTable("ParameterTable");
ParameterTable.Columns.Add("Type", typeof(DataType));
ParameterTable.Columns.Add("Value", typeof(string));

YesNoList = new List<IndexedItem>();
YesNoList.Add(new IndexedItem("Yes", 1));
YesNoList.Add(new IndexedItem("No", 0));

var D = (DataGridViewComboBoxColumn)dgvInputs.Columns[0];
D.ValueMember = "Value";
D.DisplayMember = "Display";
D.DataSource = new DataType[] { 
        DataType.Text,
        DataType.YesNo
}.Select(x => new { Display = x.ToString(), Value = (int)x }).ToList();

BindingSource ParamSource = new BindingSource();
ParamSource.DataSource = ParameterTable;
dgvInputs.AutoGenerateColumns = false;
dgvInputs.DataSource = ParamSource;
dgvInputs.Columns[0].DataPropertyName = "Type";
dgvInputs.Columns[1].DataPropertyName = "Value";

And Events:

private void dgvInputs_CurrentCellDirtyStateChanged(object sender, EventArgs e) {
    if (dgvInputs.IsCurrentCellDirty) {
        dgvInputs.CommitEdit(DataGridViewDataErrorContexts.Commit);
    }
}

private void dgvInputs_CellValueChanged(object sender, DataGridViewCellEventArgs e) {
   if (e.RowIndex >= 0 && e.ColumnIndex == 0) {
      var cb = (DataGridViewComboBoxCell)dgvInputs[0, e.RowIndex];
      if (cb.Value != null && cb.Value != DBNull.Value) {
         DataType Type = (DataType)cb.Value;
         dgvInputs[1, e.RowIndex].Value = string.Empty;
         dgvInputs.CommitEdit(DataGridViewDataErrorContexts.Commit);
         switch (Type) {
            case DataType.YesNo:
               dgvInputs[1, e.RowIndex].Dispose();
               var newBox = new DataGridViewComboBoxCell();
               newBox.DisplayMember = "Name";
               newBox.ValueMember = "strID";
               newBox.DataSource = YesNoList;
               dgvInputs[1, e.RowIndex] = newBox;
               break;
            default:
               dgvInputs[1, e.RowIndex] = new DataGridViewTextBoxCell();
               break;
         }
      }
   }
}

If you have it set to "text" and enter something arbitrary, then switch to "YesNo", it gives an error "System.ArgumentException: DataGridViewComboBoxCell value is not valid.", that will reappear any time the cursor is over the cell. Changing it back to a text row causes the original value to reappear.

I am assuming that the problem is that the value is saved in ParameterTable, but I can't get it to propagate my clearing of the original value to ParameterTable. I've tried null and DBNull.Value instead of string.Empty, but neither one made any difference. I added the "CommitEdit" line in hopes of getting it to make the change, but that made no difference either.

Edit: As it turns out, the problem was this code that I had in the cell change event:

string Default = dgvInputs[4, e.RowIndex].Value as string;
// code switching out text box and combo box above
try 
{
    dgvInputs[4, e.RowIndex].Value = Default;
} catch (Exception e2) {
    MessageBox.Show(e2.GetType().ToString());
}

The idea had been to preserve the value if possible, and I had the messagebox to show me the specific exception I needed to catch, as I was not sure. But apparently this assignment does not immediately induce the exception. That only occurs later, apparently during some other event I am not handling.

It is obvious in hindsight that I should have included this code in the sample. I have no idea now how I overlooked it. My apologies to everybody I led on a wild goose chase by leaving out the critical information. I do appreciate all of your assistance.

2
DBType Type = (DataType)cb.Value; doesn't compile. - Lei Yang
I copied the code, fixed some compile errors, run, select both types and input or select values, did not pop up any error message. - Lei Yang
@LeiYang - I severelly truncated the code for the example, and tried to change "DBType" to "DataType" because there is a "DbType" in some stock library (which I only discovered after I had chosen my name) and didn't want to confuse on it. I'll have to try the truncated example here myself. Though I tried not to do so, I may have removed something relevant. - Paul Sinclair
@LeiYang - I've tried it, and you are right: the code here works for me too. So the problem must be in something that I didn't include. I'll have to hunt it down. - Paul Sinclair
The solution you're trying to implement is usually done via PropertyGrid. The reason this hasn't got much attention is probably due to the unusual implementation of DataGridView, you might want to consider changing the approach. - bokibeg

2 Answers

1
votes

You problem is not with clearing the value but with the YesNoList.

The grid compbobox tries to find the value for the current record and there is no empty neither null value in your YesNoList.

You will even get an error if you try to add a new record and first set the DataType without setting the Value.

You can solve this by either adding an empty item to your YesNoList or by setting a default value to the existing record when switching DataType.

0
votes

Edit: I know below does not specifically answer your question as it states, but the example may help you out. Consider having two controllers in one cell.


Original

I am not sure if this will help you or not, but I tried to make a very basic program that you discussed. A dataset is created with 2 entries. The first column is the DataType, the second is of Value. If the DataType Text is chosen, the Value cell turns into a Textbox. If the DataType Yes/No is chosen, it hides the Textbox and shows a DropDownList. The idea is to hide one component when not needed.

Default.aspx.cs

using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;


namespace DataGridViewBounds
{
  public partial class _Default : Page
  {
    public enum DataType
    {
      Text,
      YesNo
    }

    public class IndexedItem
    {
      public string Name
      { get; set; }
      public int ID
      { get; set; }
      public string strID
      { get { return ID.ToString(); } }
    }

    protected void Page_Load (object sender, EventArgs e)
    {

      if (!IsPostBack)
      {
        Bind();
      }

      for (int i = 0; i < dg.Items.Count; ++i)
      {
        bool ShowText = ((DropDownList)dg.Items[i].Cells[0].Controls[1]).SelectedValue.Equals("text");
        ((DropDownList)dg.Items[i].Cells[1].Controls[1]).Visible = !ShowText;
        ((TextBox)dg.Items[i].Cells[1].Controls[3]).Visible = ShowText;
      }

    }

    private void Bind ()
    {
      DataTable ParameterTable = new DataTable("ParameterTable");
      ParameterTable.Columns.Add("", typeof(string));
      ParameterTable.Columns.Add("Type", typeof(DataType));
      ParameterTable.Columns.Add("Value", typeof(string));

      List<ListItem> YesNoList = new List<ListItem>();    // Should be ListItem, not IndexedItem
      YesNoList.Add(new ListItem("Yes", "1"));
      YesNoList.Add(new ListItem("No", "0"));

      DataRow row = ParameterTable.NewRow();
      row["Type"] = DataType.Text;
      row["Value"] = "Some text";

      DataRow row2 = ParameterTable.NewRow();
      ParameterTable.Rows.Add(row);
      row2["Type"] = DataType.YesNo;
      row2["Value"] = "false";
      ParameterTable.Rows.Add(row2);

      dg.DataSource = ParameterTable;
      dg.DataBind();
      dg.ShowHeader = true;
      dg.Visible = true;

      for (int i = 0; i < dg.Items.Count; ++i)
      { // Showing 2 ways to bind the DropDownList items
        ((DropDownList)dg.Items[i].Cells[0].Controls[1]).Items.Add(new ListItem("Text", "text"));
        ((DropDownList)dg.Items[i].Cells[0].Controls[1]).Items.Add(new ListItem("Yes/No", "bool"));

        ((DropDownList)dg.Items[i].Cells[1].Controls[1]).DataSource = YesNoList;
        ((DropDownList)dg.Items[i].Cells[1].Controls[1]).DataBind();
      }
    }
  }
}

And the Default.aspx page

<%@ Page Title="Home Page" Language="C#" MasterPageFile="~/Site.Master" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="DataGridViewBounds._Default" %>

<asp:Content ID="BodyContent" ContentPlaceHolderID="MainContent" runat="server">
<asp:DataGrid ID="dg" runat="server" AutoGenerateColumns="false">
<Columns>
  <asp:TemplateColumn HeaderText="Type">
    <ItemTemplate>
    <asp:DropDownList runat="server" ID="ddListType" AutoPostBack="true"></asp:DropDownList>
    <asp:Label id="TypeLabel" runat="server" Visible="false"></asp:Label>    
    </ItemTemplate>  
  </asp:TemplateColumn>

  <asp:TemplateColumn HeaderText="Value">
    <ItemTemplate>
    <asp:DropDownList runat="server" ID="ddListValue" AutoPostBack="true" Visible="false"></asp:DropDownList>
    <asp:TextBox id="ValueLabel" runat="server" Visible="false"></asp:TextBox>
    </ItemTemplate>
  </asp:TemplateColumn>

</Columns>
</asp:DataGrid>
</asp:Content>

This is the best I could do at the moment without seeing more of the code, but may you can use it. One suggestion, dgvInputs_CurrentCellDirtyStateChanged appears to commit code. I am assuming this is SQL code. You may want to wait until committing until a final 'Submit' button or 'Accept Changes' button has been pressed so that you don't have to call SQL so much, but also if there is an error that occurs between the start of the first SQL call and the last. If an interruption occurs between the two, you may not necessarily want to commit.