tags:

views:

12569

answers:

8

This is frustrating, as I am sure I have done this before without issue.

I have a combo box on a WinForms app in which an item may be selected, but it is not mandatory. I therefore need an 'Empty' first item to indicate that no value has been set.

The combo box is bound to a DataTable being returned from a stored procedure (I offer no apologies for Hungarian notation on my UI controls :p ):

 DataTable hierarchies = _database.GetAvailableHierarchies(cmbDataDefinition.SelectedValue.ToString()).Copy();//Calls SP
 cmbHierarchies.DataSource = hierarchies;
 cmbHierarchies.ValueMember = "guid";
 cmbHierarchies.DisplayMember = "ObjectLogicalName";

How can I insert such an empty item?

I do have access to change the SP, but I would really prefer not to 'pollute' it with UI logic.

Update: It was the DataTable.NewRow() that I had blanked on, thanks. I have upmodded you all (all 3 answers so far anyway). I am trying to get the Iterator pattern working before I decide on an 'answer'

Update: I think this edit puts me in Community Wiki land, I have decided not to specify a single answer, as they all have merit in context of their domains. Thanks for your collective input.

+1  A: 

Cant you add a new DataRow to the DataTable before you bind it to your DataSource?

You can use the NewRow function of the DataTable to achieve this:

http://msdn.microsoft.com/en-us/library/system.data.datatable.newrow.aspx

Mark
+2  A: 

insert a blank row in your datatable, and check for it in validation/update/create

Steven A. Lowe
Better would be to "select" a blank row and UNION that with your real query. That would leave your database intact.
BoltBait
@[BoltBait]: except that he already said he did not want to change his stored procedure
Steven A. Lowe
I prefer to keep UI requirements outside of the DB.
Coov
@[Coov]: so do i, but the OP's mileage may vary; obviously this is a hack!
Steven A. Lowe
+5  A: 

I usually create an iterator for this type of thing. It avoids polluting your data, and works well with data-binding:

DataTable hierarchies = _database.GetAvailableHierarchies(cmbDataDefinition.SelectedValue.ToString()).Copy();//Calls SP
cmbHierarchies.DataSource = GetDisplayTable(hierarchies);
cmbHierarchies.ValueMember = "guid";
cmbHierarchies.DisplayMember = "ObjectLogicalName";

...

private IEnumerable GetDisplayTable(DataTable tbl)
{
    yield return new { ObjectLogicalName = string.Empty, guid = Guid.Empty };

    foreach (DataRow row in tbl.Rows)
        yield return new { ObjectLogicalName = row["ObjectLogicalName"].ToString(), guid = (Guid)row["guid"] };
}

Disclaimer: I have not compiled this code, but have used this pattern many times.

Note: I have been in WPF and ASP.Net land for the last couple of years. Apparently the Winforms combo box wants an IList, not an IEnumerable. A more costly operation would be to create a list. This code is really stream-of-conciseness and I really, really have not compiled it:

DataTable hierarchies = _database.GetAvailableHierarchies(cmbDataDefinition.SelectedValue.ToString()).Copy();
List<KeyValuePair<string, Guid>> list = new List<KeyValuePair<string, Guid>>(hierarchies.Rows.Cast<DataRow>().Select(row => new KeyValuePair<string, Guid>(row["Name"].ToString(), (Guid)row["Guid"])));
list.Insert(0, new KeyValuePair<string,Guid>(string.Empty, Guid.Empty));
cmbHierarchies.DataSource = list;
cmbHierarchies.ValueMember = "Value";
cmbHierarchies.DisplayMember = "Key";
Jason Jackson
I am getting a "Complex DataBinding accepts as a data source either an IList or an IListSource" error, and am unable to find a conversion between IEnumerable and IList (I am in WinForms if you are used to doing this in WPF)
johnc
I have been in ASP.Net land for a while, but recall using this pattern in WPF. I have not worked with WinForms for about 3 years. If this wants a list then I would suggest a similar strategy, just create a list instead of using an iterator.
Jason Jackson
ASP.NET, makes sense. I do like the pattern though
johnc
how about GetDisplayTable(theDataTable).ToList() ?
geofftnz
Coov
+4  A: 

There are two things you can do:

  1. Add an empty row to the DataTable that is returned from the stored procedure.

    DataRow emptyRow = hierarchies.NewRow();
    emptyRow["guid"] = "";
    emptyRow["ObjectLogicalName"] = "";
    hierarchies.Rows.Add(emptyRow);
    

    Create a DataView and sort it using ObjectLogicalName column. This will make the newly added row the first row in DataView.

    DataView newView =           
         new DataView(hierarchies,       // source table
         "",                             // filter
         "ObjectLogicalName",            // sort by column
         DataViewRowState.CurrentRows);  // rows with state to display
    

    Then set the dataview as DataSource of the ComboBox.

  2. If you really don't want to add a new row as mentioned above. You can allow the user to set the ComboBox value to null by simply handling the "Delete" keypress event. When a user presses Delete key, set the SelectedIndex to -1. You should also set ComboBox.DropDownStyle to DropDownList. As this will prevent user to edit the values in the ComboBox.

Vivek
A: 

Er, can't you just add a default item to the ComboBox after data binding?

flipdoubt
Setting a default (via Text property) on the Combobox is overriden when the datasource is bound to the datatable.
John M
Then add the value to the DataTable.
flipdoubt
+1  A: 

I would bind the data then insert an blank item at position 0 using the ComboxBox.Items.Insert method. Similar to what flipdoubt suggested, but it adds the item to the top.

Arry
Once the datasource of the combobox is bound to the datatable an error occurs when trying to insert. Specifically - Items collection cannot be modified when the Datasource property is set.
John M
+2  A: 
cmbHierarchies.SelectedIndex = -1;
That will show an empty item until you make a selection, it will not insert an empty item
johnc
+1  A: 

I wrote this method based on the suggestions here by Jason Jackson:

private IEnumerable<KeyValuePair<object,object>> GetDisplayTable(DataTable dataTable,  DataColumn ValueMember, string sep,params DataColumn[] DisplayMembers)
{
    yield return new KeyValuePair<object,object>("<ALL>",null);

    if (DisplayMembers.Length < 1)
        throw new ArgumentException("At least 1 DisplayMember column is required");

    foreach (DataRow r in dataTable.Rows)
    {
        StringBuilder sbDisplayMember = new StringBuilder();
        foreach(DataColumn col in DisplayMembers)
        {
            if (sbDisplayMember.Length > 0) sbDisplayMember.Append(sep);
            sbDisplayMember.Append(r[col]);
        }
        yield return new KeyValuePair<object, object>(sbDisplayMember.ToString(), r[ValueMember]);
    }
}

Usage:

bindingSource1.DataSource = GetDisplayTable(
            /*DataTable*/typedDataTable, 
            /*ValueMember*/typedDataTable.IDColumn, 
            /*DisplayColumn Seperator*/" - ",
            /*List of Display Columns*/
            typedDataTable.DB_CODEColumn,
            typedDataTable.DB_NAMEColumn);

comboBox1.DataSource = bindingSource1;
comboBox1.DisplayMember = "Key";
comboBox1.ValueMember = "Value";

//another example without multiple display data columns:
bindingSource2.DataSource = GetDisplayTable(
            /*DataTable*/typedDataTable, 
            /*ValueMember*/typedDataTable.IDColumn, 
            /*DisplayColumn Seperator*/null,
            /*List of Display Columns*/
                typedDataTable.DESCColumn );

further down, where the Selected Value is consumed:

if (comboBox1.SelectedValue != null)
     // Do Something with SelectedValue   
else 
     // All was selected (all is my 'empty')

This will allow to display several columns concatenated in the ComboBox, while keeping the Value member to the single identifier + it uses the iterator block with the BindingSource, BindingSource might be overkill for your situation.

Сomments and suggestions are welcome.

Vincent