0

I have a Winforms application. There are two classes. One class stores the people that contribute to a book. The other class is a list of possible contribution types (like editor, reviewer, and so on).

The BookContributors class is the datasource for my datagridview and that works just fine. Now, I want to have the BookContributorTypes class be a combobox that feeds the BookContributors class.

So far in my reading, it looks like I have a number of options, including forcing a combobox into the datagridview, using class attributes, creating a 1-to-many relationship between classes.

Since my code will have many of these types of situations, I want to do this right. I think this means showing the relationship between the two classes somehow so the datagridview knows to just display the combobox, but I am not sure how to go about doing this.

BookContributors is populated by a person filling out a contributor. Example: BookContributor = "John Smith"; BookContributorFileAs="Smith, John"; BookContributorType = "rev".

public class BookContributors : INotifyPropertyChanged
{
    private string _bookContributor;
    [DescriptionLocalized(typeof(ResourcesClassBooks), "BookContributorComment")]
    [DisplayNameLocalized(typeof(ResourcesClassBooks), "BookContributorDisplayName")]
    public string BookContributor
    {
        get { return _bookContributor; }
        set
        {
            if (SetField(ref _bookContributor, value, "BookContributor"))
            {
                // When the user types an author name, add the sorted name to the sorted field.
                //  ex Name = William E Raymond
                //  ex File As = Raymond, William E
                var name = _bookContributor.Split(' ');
                if (name.Length >= 2)
                {
                    string fileAsName = (name[name.Length - 1] + ",");
                    for (int i = 0; i <= (name.Length - 2); i++)
                    {
                        fileAsName = fileAsName + " " + name[i];
                    }
                    BookContributorFileAs = fileAsName;
                }
            }
        }
    }

    private string _bookContributorFileAs;
    [DescriptionLocalized(typeof(ResourcesClassBooks), "BookContributorFileAsComment")]
    [DisplayNameLocalized(typeof(ResourcesClassBooks), "BookContributorFileAsDisplayName")]
    public string BookContributorFileAs { get { return _bookContributorFileAs; } set { SetField(ref _bookContributorFileAs, value, "BookContributorFileAs"); } }

    private string _bookContributorType;
    [DescriptionLocalized(typeof(ResourcesClassBooks), "BookContributorTypeComment")]
    [DisplayNameLocalized(typeof(ResourcesClassBooks), "BookContributorTypeDisplayName")]
    public string BookContributorType { get { return _bookContributorType; } set { SetField(ref _bookContributorType, value, "BookContributorType"); } }


    #region handle property changes
    public event PropertyChangedEventHandler PropertyChanged;
    protected bool SetField<T>(ref T field, T value, string propertyName)
    {
        //if the value did not change, do nothing.
        if (EqualityComparer<T>.Default.Equals(field, value)) return false;
        //the value did change, so make the modification.
        field = value;
        return true;
    }
    protected virtual void OnPropertyChanged(string propertyName)
    {
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
    }
    #endregion
}

BookContributorTypes (populated with a list of bookContributor types from an XML file). Example Book contributor type ID's: "rev", "edt". Example Book contributor type descriptions: "Reviewer", "Editor".

class BookContributorTypes : INotifyPropertyChanged
{
    private string _bookContributorTypeId;
    [DescriptionLocalized(typeof(ResourcesClassBooks), "BookContributorTypeIdComment_lkp")]
    [DisplayNameLocalized(typeof(ResourcesClassBooks), "BookContributorTypeIdDisplayName_lkp")]
    public string BookContributorTypeId { get { return _bookContributorTypeId; } set { SetField(ref _bookContributorTypeId, value, "BookContributorTypeId"); } }

    private string _bookContributorTypeDescription;
    [DescriptionLocalized(typeof(ResourcesClassBooks), "BookContributorTypeComment_lkp")]
    [DisplayNameLocalized(typeof(ResourcesClassBooks), "BookContributorTypeDisplayName_lkp")]
    public string BookContributorTypeDescription { get { return _bookContributorTypeDescription; } set { SetField(ref _bookContributorTypeDescription, value, "BookContributorTypeDescription"); } }


    #region handle property changes
    public event PropertyChangedEventHandler PropertyChanged;
    protected bool SetField<T>(ref T field, T value, string propertyName)
    {
        //if the value did not change, do nothing.
        if (EqualityComparer<T>.Default.Equals(field, value)) return false;
        //the value did change, so make the modification.
        field = value;
        return true;
    }
    protected virtual void OnPropertyChanged(string propertyName)
    {
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
    }
    #endregion
}

All I have working right now, without the combo box, because I do not know how to implement a combobox to show the selection options from the BookContributorTypes class:

dataGridView1.DataSource = bookContributors;

Thanks for ay help you can provide.

Bill
  • 582
  • 1
  • 7
  • 21
  • What you have so far? Show working code which you used at this moment. And please explain what is the different between "quick fix" and "good repeatable process". – Fabio Feb 08 '16 at 16:43
  • Hi there. I added the working classes, which is not much more detail. The only 'working' code I have for the datagridview at the moment is just connecting the datasource to one class. I have clarified my question in the latest edit but to directly respond to your "quick fix" vs. "good repeatable process", I mean the following: Since my code will have many of these types of situations, I want to do this right. I think this means showing the relationship between the two classes somehow so the datagridview knows to just display the combobox, but I am not sure how to go about doing this. – Bill Feb 08 '16 at 21:53

2 Answers2

0

I'm working with WinForms DataGridView for several years. AFAIK the DataGridView is not as smart as you expect. You have to setup comboBox columns manually, i.e. on event DataBindingComplete

  • create new DataGridViewComboBoxColumn
  • set its DataPropertyName property to "BookContributorType"
  • add it to DataGridViewColumnCollection
  • set its DataSource property to a collection of BookContributorTypes
  • set its ValueMember property to "BookContributorTypeId"
  • set its DisplayMember property to "BookContributorTypeDescription"
  • adopt properties from auto-generated column corresponding BookContributorTypes like DisplayIndex
  • remove auto-generated column

I'm using wrapper classes around DataGridView to get a smarter control. In this way I've reduced source code everywhere I'm using DataGridView.

Here is a code snippet to replace an auto-generated column with a comboBox column:

DataGridViewColumn auto // = ... auto-generated column to be replaced
DataGridViewComboBoxColumn combo = new DataGridViewComboBoxColumn();
combo.DataPropertyName = auto.DataPropertyName;
combo.Name = auto.Name;
DataGridView dgv = auto.DataGridView;
dgv.Columns.Add(combo);

combo.DataSource = GetBookContributorTypes; // collection of comboBox entries
combo.ValueMember = "BookContributorTypeId";
combo.DisplayMember = "BookContributorTypeDescription";

// adopt further properties if required
combo.Frozen = auto.Frozen;
combo.DisplayIndex = auto.DisplayIndex;
combo.Visible = auto.Visible;
combo.ReadOnly = auto.ReadOnly;
combo.HeaderText = auto.HeaderText;
combo.HeaderCell.ToolTipText = auto.HeaderCell.ToolTipText;
combo.SortMode = auto.SortMode;
combo.Width = auto.Width;

dgv.Columns.Remove(auto);
VV5198722
  • 374
  • 5
  • 19
  • Thank you for the response but I feel like there is something missing here. There is a primary DataGridView datasource called `BookContributors`. There is a DataGridViewComboBoxColumn with a datasource called `BookContributorTypes`. How does the combobox column know where to feed the selected item back to the appropriate property of the primary datasource? Do you happen to have a code sample? Thanks. – Bill Feb 09 '16 at 20:49
  • Figured it out and posted an answer. – Bill Feb 09 '16 at 22:35
  • @Bill: You're right. I missed `DataPropertyName`. I've completed my answer for further reading. – VV5198722 Feb 10 '16 at 12:59
  • Do you remove the auto-generated column or hide it? When I tested the `remove` method, it simply takes the column and moves it to the end of the datagridview and [I think] disconnects it from the class. I used `Visible=false` to remove it but am not sure if that will cause problems down the road. – Bill Feb 10 '16 at 16:06
  • @Bill: I `Remove` the auto-generated column, but after adding comboBox columns. I wasn't aware that auto-generation takes place between `Remove` and `Add`. I've corrected my answer and added a code snippet. – VV5198722 Feb 11 '16 at 10:15
0

This ended up being a lot more difficult to research than I originally thought. It turns out, if you want to place a column in a DataGridView with a combobox and have that combobox interact with another class, it is not a quick one-liner.

My example has two classes:

  • BookContributors (bookContributors) - A list of people that have contributed to a book. For example, this stores a person's name and the type of contribution they made.
  • BookContributorTypes (bookContributorTypes) - A list of possible types of contributions to a book, like Editor or Contributor.
  • I want the ContributorTypes class to be a combobox and store the user's selection into the BookContributors class, specifically the BookContributorType property.

Here is the code I came up with, along with some additional notes I found along the way. To the best of my knowledge, the information I provide is accurate :-)

    gvContributors.DataSource = bookContributors; //set the datgridview's datasource so it displays the class you want.
    gvContributors.Columns["BookContributorType"].Visible = false; //hide the column (property) you want to replace with a combobox.

    DataGridViewComboBoxColumn contribType = new DataGridViewComboBoxColumn(); //create a combobox object.
    contribType.HeaderText = "My Column"; //the text to display in the column header.
    contribType.Name = "BookContributorType"; //name of the class property you set to Visible=false. Note sure if this is needed.
    contribType.DataSource = bookContributorTypes; //name of the class that provides the combobox data.
    contribType.DisplayMember = "BookContributorTypeDescription"; //data the user will see when clicking the combobox.
    contribType.ValueMember = "BookContributorTypeId"; //data to store in the class property.
    contribType.DataPropertyName = "BookContributorType"; //the class property you are binding to in order to store the data.

    gvContributors.Columns.Add(contribType); //add the new combobox to the datagridview.

What you have now is the following:

  1. A DataGridView with a datasource. In my case, the datasource is a class.
  2. A hidden (Visible=false) column that you want to make a combobox.
  3. A DataGridViewComboBox that links to another datasource so the user will see a list of values. By using the DataPropertyName as the same name as the hidden column, you are now binding to the DataGridView's DataSource.

Another answer on this post suggests you Remove the column you are replacing with a combobox but Visible=false seems to work for me.

When you run the solution, you will find the user has to click twice on the combobox. I have not tried this code yet, but think this post will resolve that problem: Open dropdown(in a datagrid view) items on a single click

Community
  • 1
  • 1
Bill
  • 582
  • 1
  • 7
  • 21