DataGridViewComboBoxColumn's ValueMember from different source

150 Views Asked by At

I have a binding source creditUserBindingSource in my below class which recieves a list of CreditUser class as datasource. In my DataGridView I have a DataGridViewComboBoxColumn called ResponsibleList which receives a list of string as DataSource ["Production", "Distribution", "Customer Service", "Sales"]. I want the ResponsibleList to display the responsible from the responsible variable from the list of CrteditUsers for each user.

public partial class CreditUserLimitsForm : Form
    {
        private List<CreditUser> creditUser;
        private bool SetupCheckStatus = false;
        //private Dictionary<string, string> fbu;

        public CreditUserLimitsForm()
        {
            InitializeComponent();
        }

        private void CreditUserLimitsForm_Load(object sender, EventArgs e)
        {
            //fbu = MainForm.srv.GetFBU();
            //foreach (KeyValuePair<string, string> k in fbu)
            //{
            //    lvwFBU.Items.Add(new ListViewItem(new string[] { k.Key, k.Value }));
            //}
            try
            {
                creditUser = MainForm.srv.GetCreditUser("","").ToList();
                creditUserBindingSource.DataSource = creditUser;
                SetAlternateChoicesUsingDataSource(ResponsibleList);
            }
            catch (Exception ex)
            {
                Cursor = Cursors.Default;
                NutraMsg.DisplayError(this, ex, MainForm.GetMessageDisplayType());
            }
        }
        private void SetAlternateChoicesUsingDataSource(DataGridViewComboBoxColumn comboboxColumn)
        {
            {
                comboboxColumn.DataSource = MainForm.srv.GetResponsibleList();
                comboboxColumn.ValueMember = "Responsible";
                comboboxColumn.DisplayMember = comboboxColumn.ValueMember;
            }
        }
}

Here's the code for CreditUser class

   public class CreditUser : INotifyPropertyChanged
    {
        public string Responsible { get; set; }
        public int UserId { get; set; }
        public int RoutingId { get; set; }
        public string UserName { get; set; }
        public List<string> AllowedCustomerTypes { get; set; }
        public decimal CreditLimit { get; set; }
        public bool Restricted
        {
            get
            {
                foreach (UserCatalog uc in Catalogs)
                {
                    if (uc.Restricted)
                    {
                        return true;
                    }
                }
                return false;
            }
        }
}
2

There are 2 best solutions below

2
user18387401 On

If you're binding a list of string values then don't set the DisplayMember or ValueMember. The point of those is to specify members of the items you want to use but you don't want to use members of the items. You want to use the items themselves. Here is a simple example that demonstrates this:

private class Record
{
    public int Id { get; set; }
    public string Name { get; set; }
}

private void Form1_Load(object sender, EventArgs e)
{
    var idColumn = new DataGridViewTextBoxColumn { HeaderText = "Id", DataPropertyName = "Id" };
    var nameColumn = new DataGridViewComboBoxColumn
                     {
                         HeaderText = "Name",
                         DataPropertyName = "Name",
                         DataSource = new[] {"First", "Second", "Third"}
                     };

    dataGridView1.Columns.AddRange(idColumn, nameColumn);
    dataGridView1.DataSource = new BindingList<Record>
    {
        new() {Id = 1, Name = "First"},
        new() {Id = 2, Name = "Second"},
        new() {Id = 3, Name = "Third"}
    };
}
3
IV. On

I see that you have made a custom DataGridComboBoxColumn and have implemented a version of SetAlternateChoicesUsingDataSource that seems to be modeled after the method of the same name in the Microsoft code example for DataGridViewComboBoxColumn.

The purpose of SetAlternateChoicesUsingDataSource in that example is to provide ComboBox drop down options that are tailored and specified for each user using the AllowedCustomerTypes property that you show in your CreditUser class. Something like this:

screenshot

But based on your comment, the choices are the same for each row. More like this:

new screenshot

This means that your code can be simplified.


DataSources for DataGridViewComboBoxColumn and DataGridView

I believe what might be causing the confusion is that the data sources for DataGridView and for DataGridViewComboBoxColumn are completely unrelated in this case. Since there is no need to provide each user with individualized options, the source of drop down items only needs to be set one time for the entire column.

The DataSource for DataGridViewComboBoxColumn is an array of strings named ResponsibleList that will not change.

private readonly string[] ResponsibleList = new []
{
    "Production", 
    "Distribution", 
    "Customer Service", 
    "Sales",
    String.Empty
};

The DataSource for dataGridViewCreditUser is a binding list named CreditUsers.

readonly BindingList<CreditUser> CreditUsers = new BindingList<CreditUser>();

Initialize

Assigning these data sources is done in the override of OnLoad (there's no need to have the form subscribe to its own Load event). Allow me to explain what I've done and you can modify this flow to your specific requirements.

protected override void OnLoad(EventArgs e)
{
    dataGridViewCreditUser.DataSource = CreditUsers;

Adding one or more items will autogenerate the columns.

    // Calls a mock method that returns a simulated response of three CreditUsers.
    foreach (var creditUser in mockMainForm_srv_GetCreditUser("", ""))
    {
        CreditUsers.Add(creditUser);
    }

Create a ComboBox column that will be swapped out for the autogenerated one. This is where ResponsibleList becomes the DataSource for the ComboBox.

    var colCB = new DataGridViewComboBoxColumn
    {
        Name = nameof(CreditUser.Responsible),
        // Connects the column value to the Responsible property of CreditUser
        DataPropertyName = nameof(CreditUser.Responsible),
        AutoSizeMode = DataGridViewAutoSizeColumnMode.AllCells,
        // Connects the four drop-down options in ResponsibleList to the ComboBox
        DataSource = ResponsibleList,
    };

Perform the swap. Remove the autogenerated column and replace it with the custom version.

    var index = dataGridViewCreditUser.Columns[nameof(CreditUser.Responsible)].Index;
    dataGridViewCreditUser.Columns.RemoveAt(index);
    dataGridViewCreditUser.Columns.Insert(index, colCB);

Make sure the cell is NOT left in an editing state after change of ComboBox or CheckBox.

    dataGridViewCreditUser.CurrentCellDirtyStateChanged += (sender, e) =>
    {
        switch (dataGridViewCreditUser.Columns[dataGridViewCreditUser.CurrentCell.ColumnIndex].Name)
        {
            case nameof(CreditUser.Responsible):
            case nameof(CreditUser.Restricted):
                dataGridViewCreditUser.CommitEdit(DataGridViewDataErrorContexts.Commit);
                break;
        }
    };

To monitor ongoing changes, update the Title bar whenever the source list is modified.

    CreditUsers.ListChanged += (sender, e) =>
    {
        if ((dataGridViewCreditUser.CurrentCell != null) && (dataGridViewCreditUser.CurrentCell.RowIndex < CreditUsers.Count))
        {
            var creditUser = CreditUsers[dataGridViewCreditUser.CurrentCell.RowIndex];
            Text = creditUser.ToString();
        }
    };

Now that the DataGridView is all set up the columns can be formatted.

    foreach (DataGridViewColumn col in dataGridViewCreditUser.Columns)
    {
        if (col.Name == nameof(CreditUser.UserName))
        {
            col.AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
        }
        else
        {
            col.AutoSizeMode = DataGridViewAutoSizeColumnMode.AllCells;
        }
    }
}

MOCK QUERY FOR TESTING

// MOCK for minimal example
private List<CreditUser> mockMainForm_srv_GetCreditUser(string v1, string v2)
{
    return new List<CreditUser>
    {
        new CreditUser
        {
            UserName = "Tom",
            CreditLimit=10000m,
        },
        new CreditUser
        {
            UserName = "Richard",
            CreditLimit=1250m,
            Restricted = true
        },
        new CreditUser
        {
            UserName = "Harry",
            CreditLimit=10000m,
        },
    };
}

CreditUser class

// REDUCED for minimal example
public class CreditUser : INotifyPropertyChanged
{
    string _UserName = string.Empty;
    public string UserName
    {
        get => _UserName;
        set
        {
            if (!Equals(_UserName, value))
            {
                _UserName = value;
                OnPropertyChanged();
            }
        }
    }
    string _Responsible = String.Empty;
    public string Responsible
    {
        get => _Responsible;
        set
        {
            if (!Equals(_Responsible, value))
            {
                _Responsible = value;
                OnPropertyChanged();
            }
        }
    }
    decimal _CreditLimit = 0;
    public decimal CreditLimit
    {
        get => _CreditLimit;
        set
        {
            if (!Equals(_CreditLimit, value))
            {
                _CreditLimit = value;
                OnPropertyChanged();
            }
        }
    }
    bool _Restricted = false;
    public bool Restricted
    {
        get => _Restricted;
        set
        {
            if (!Equals(_Restricted, value))
            {
                _Restricted = value;
                OnPropertyChanged();
            }
        }
    }
    public event PropertyChangedEventHandler PropertyChanged;
    private void OnPropertyChanged([CallerMemberName] string propertyName = null) =>
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));

    public override string ToString()
    {
        var ctype = Responsible == string.Empty ? "Not Specified" : $"{Responsible}";
        return Restricted ?
        $"{UserName} ({ctype}) Restricted" :
        $"{UserName} ({ctype}) {CreditLimit}";
    }
}