0

So I work from time to time in Winforms on a legacy app and am not familiar with best practices at all times with binding objects. Basically I have a three part set where I have two people, they may have only one product, but that product could cause the possibility to have different sets of SKUs. Is there a way to trigger an event and population of a combobox from the values of a first combobox? I have been looking around and I am either finding basic data of just how to bind a combobox(I can do that fine) or do something with how you bind it. Not binding after a dependent parent change is triggered and changing the dataset. Example below:

POCOS:

Public Class Person
  Public Property PersonID As Integer
  Public Property FirstName As String
  Public Property LastName As String
  Public Property ProductId As Integer
  Public Property SkuId As Integer
End Class

Public Class Product
  Public Property ProductId As Integer
  Public Property Description As String
End Class

Public Class Sku
  Public Property SKUId As Integer
  Public Property ProductId As Integer
  Public Property Description As String
End Class

Main code example (basic UI really only has a Datset labeled 'ds' that matches nearly identically the Person and Product POCOS with datatables. A datagridview 'dgv' whose columns are bound to data in Person EXCEPT for a column called SKU that has no binding as I want to bind it after the fact and that is where I am failing miserably at.

Update 9-13-2016 I can get the below code to work EXCEPT in certain large scale solutions(the whole reason I did this). It basically will NOT execute the line that casts the cell() to a datagridviewcomboboxcell and ignores it and jumps over the line. No reason why, it just jumps over it. I am wondering if in larger classes the datagrid views can become corrupt or something.

Main Code:

Private _people As List(Of Person) = New List(Of Person)
Private _products As List(Of Product) = New List(Of Product)
Private _SKUs As List(Of Sku) = New List(Of Sku)
Private _initialLoadDone = False
Private _currentRow As Integer? = Nothing

Private Sub DynamicComboBoxDoubleFill_Load(sender As Object, e As EventArgs) Handles MyBase.Load
    _products = New List(Of Product)({
                                     New Product With {.ProductId = 1, .Description = "Offline"},
                                     New Product With {.ProductId = 2, .Description = "Online"}
                                     })

    Dim s = ""
    For Each o In _products
      Dim row As DataRow = ds.Tables("tProducts").NewRow
      row("ProductId") = o.ProductId
      row("Description") = o.Description
      ds.Tables("tProducts").Rows.Add(row)
    Next

    _SKUs = New List(Of Sku)({
     New Sku With {.SKUId = 1, .ProductId = 1, .Description = "Mail"},
     New Sku With {.SKUId = 2, .ProductId = 1, .Description = "Magazine"},
     New Sku With {.SKUId = 3, .ProductId = 2, .Description = "Email"},
     New Sku With {.SKUId = 4, .ProductId = 2, .Description = "APIRequest"}
    })

    Dim items = _SKUs

    _people = New List(Of Person)({
      New Person With {.PersonID = 1, .FirstName = "Emily", .LastName = "X", .ProductId = 1, .SkuId = 1},
      New Person With {.PersonID = 2, .FirstName = "Brett", .LastName = "X", .ProductId = 2, .SkuId = 3}
                                  })
    For Each p In _people
      Dim row As DataRow = ds.Tables("tPeople").NewRow
      row("PersonId") = p.PersonId
      row("FirstName") = p.FirstName
      row("LastName") = p.LastName
      row("ProductId") = p.ProductId
      row("SkuId") = p.SkuId
      ds.Tables("tPeople").Rows.Add(row)
    Next

    For Each row As DataGridViewRow In dgv.Rows
      ArrangeValuesForSKUComboBox(row)
    Next

    _initialLoadDone = True
  End Sub

  Private Sub ArrangeValuesForSKUComboBox(row As DataGridViewRow)
    Dim productId = CInt(row.Cells("ProductId")?.Value)
    Dim skus = _SKUs.Where(Function(x) x.ProductId = productId).ToList().Select(Function(x) New With {Key .SkuId = x.SKUId, .SkuDesc = x.Description}).ToList()

    Dim cell = row.Cells("SKU")
    'Yeah I don't always work.  In this example I do, in others I won't.
    'For this reason I just want more ideas.  I don't care if you completely blow up how the binding is done and do something else entirely.
    Dim combobox = CType(cell, DataGridViewComboBoxCell)
    combobox.DataSource = skus
    combobox.ValueMember = "SKUId"
    combobox.DisplayMember = "SkuDesc"
    combobox.Value = skus.FirstOrDefault()?.SkuId
  End Sub

  Private Sub dgv_CellValueChanged(sender As Object, e As DataGridViewCellEventArgs) Handles dgv.CellValueChanged
    If _initialLoadDone Then
      Dim headerText As String = TryCast(sender, DataGridView).Columns(e.ColumnIndex).HeaderText
      If headerText = "PRODUCT" Then
        ArrangeValuesForSKUComboBox(dgv?.CurrentRow)
      End If
    End If
  End Sub
Osama Rizwan
  • 615
  • 1
  • 7
  • 19
djangojazz
  • 14,131
  • 10
  • 56
  • 94
  • Could you give a databased example of what you are after? It sounds like you want the contents of one cbo datasource to change depending on a the selection in another column, **but** you loop thru all the rows which sounds like it doesnt change? – Ňɏssa Pøngjǣrdenlarp Sep 13 '16 at 21:21
  • It is really a UI change I am after to present to a user, not a database change. It does change in the example above the problem is in a much larger solution it will not work. I don't know why it won't work, so I was hoping someone could give a similar situation and just how they did it different and I could attempt that. Else I may have to rebuild a 1000+ line class from scratch. The data in the solution is basically a main set, A, leads to having fixed choices of 1 or 2 in set B, that then will dynamically change C to be either 1 or 2 for choice 1 of B or else 3 or 4 for choice 2 of B. – djangojazz Sep 13 '16 at 21:39
  • By "databased" I meant show us what you want using some simple sample data. I can see the contents of the 3 lists, but the only way to "see it change" is to have a DB with the underlying table, which of course we dont know anything about. I *think* I have done this, but I am having a hard time understang the rules (I read this 4 times pre-bounty) – Ňɏssa Pøngjǣrdenlarp Sep 13 '16 at 21:45
  • ...also the Person class doesnt have a `.SkuId` property in the block where you create the `List(Of Person)` – Ňɏssa Pøngjǣrdenlarp Sep 13 '16 at 22:05
  • @Plutonix Sorry I updated the POCO and removed a simple object or two. There is self extracting data in the example. Are you saying you want to see the pictures of what it is doing? It will change in the example, the issue is I was curious to see yours or another developer's code of how they did it and it may have been different. – djangojazz Sep 13 '16 at 22:27
  • You *almost* got there with the A-B-C description, just trying to work out who is A, whom is B... Based on whats there, isnt the SKU selection fixed and totally determined by A and B factors? How does a combo come into play? – Ňɏssa Pøngjǣrdenlarp Sep 13 '16 at 22:38
  • No this lambda function narrows scope and assigns it a new value type each time: Dim skus = _SKUs.Where(Function(x) x.ProductId = productId).ToList().Select(Function(x) New With {Key .SkuId = x.SKUId, .SkuDesc = x.Description}).ToList(). The first combobox sets the productId that does the narrowing to get a subset of the total. That is why the SKU Poco has a ProductId – djangojazz Sep 13 '16 at 22:55

1 Answers1

5

To have dependent (cascading or master/slave) ComboBox columns in DataGridView, you can follow this steps:

  1. Set DataSource of slave column to all available values.

    Goal: Here the goal is prevent rendering errors at first load, so all slave combo boxes can show value correctly.

  2. Hanlde EditingControlShowing event of the grid and check if the current cell is slave combo cell, then get the editing control using e.Control which is of type DataGridViewComboBoxEditingControl. Then check the value of master combo cell and set the DataSource property of editing control to a suitable subset of values based on the value of master combo cell. If the value of master cell is null, set the data source to null.

    Goal: Here the goal is setting data source of slave combo to show only suitable values when selecting values from slave combo.

  3. Handle CellValueChanged and check if the current cell is master combo, then set the value for dependent cell to null.
    Note: Instead of setting the value of slave cell to null, you can set it to first available valid value based on master cell value.

    Goal: Here the goal is prevent the slave combo to have invalid values after the value of master combo changed, so we reset the value.

Following above rules you can have as many dependent combo boxes as you need.

Example

In below example I have a Country (Id, Name) table, a State(Id, Name, CountryId) table and a Population(CountryId, StateId, Population) table. And I want to perform data entry for Population table using 2 combo columns for country and state and a text column for population. I know this is not a normal db design, but it's just for example of having master/slave (dependent) combo box columns in grid:

Private Sub EditingControlShowing(sender As Object, _
    e As DataGridViewEditingControlShowingEventArgs) _
    Handles PopulationDataGridView.EditingControlShowing

    Dim grid = DirectCast(sender, DataGridView)
    If (grid.CurrentCell.ColumnIndex = 1) Then 'State column
        Dim combo = DirectCast(e.Control, DataGridViewComboBoxEditingControl)
        If (grid.CurrentRow.Cells(0).Value IsNot DBNull.Value) Then
            Dim data = Me.DataSet1.State.AsDataView()
            data.RowFilter = "CountryId = " + grid.CurrentRow.Cells(0).Value.ToString()
            combo.DataSource = data
        Else
            combo.DataSource = Nothing
        End If
    End If
End Sub

Private Sub CellValueChanged(sender As Object, e As DataGridViewCellEventArgs) _
    Handles PopulationDataGridView.CellValueChanged
    Dim grid = DirectCast(sender, DataGridView)
    If (e.ColumnIndex = 0 And e.RowIndex >= 0) Then 'Country Column
        grid.Rows(e.RowIndex).Cells(1).Value = DBNull.Value 'State Column 
    End If
End Sub
Reza Aghaei
  • 120,393
  • 18
  • 203
  • 398
  • 1
    The solution is not dependent to `DataTable` or any other data structure. The example is just an example which uses `DataTable`. You can simply use your own POCO classes and any other storage which you have. Let me know if you need any clarification or if you have any question about the answer :) – Reza Aghaei Sep 14 '16 at 15:46
  • I like your method better than mine, thanks. Your filter example seems a little wrong though as it appears this part: 'Dim data = ds.Tables("State").AsDataView() data.RowFilter = "CountryId = " + grid.CurrentRow.Cells(0).Value.ToString() combo.DataSource = data' – djangojazz Sep 14 '16 at 16:03
  • Could be: Dim dvStates = New DataView(ds.Tables("State"), $"CountryId = '{grid.CurrentRow.Cells(0).Value}'", "Id ASC", DataViewRowState.CurrentRows) combo.DataSource = dvStates combo.ValueMember = "Id" combo.DisplayMember = "Name". I think you have to tell the combobox the 'ValueMember' and the 'DisplayMember' or it just thinks it is a datagridviewrow otherwise. – djangojazz Sep 14 '16 at 16:04
  • Regardless, I like your answer better as I never thought to include everything in the binding and apply a filter when the edit control showing event fires. I can have it work in a small test application, let me see if I can get it in my larger solution as was the problem. – djangojazz Sep 14 '16 at 16:05
  • I'll check criteria to see if they are suitable or not, but I'm sure you've got the idea :) – Reza Aghaei Sep 14 '16 at 16:07
  • I checked the criteria and it seems it's correct. It seems the criteria which you shared performs the same job. Let me know if you need full source code of example and I'll share it :) – Reza Aghaei Sep 14 '16 at 21:43
  • Here is the [full source code](http://pastebin.com/V0nhm6SH) of example. Remove `Form1` from your solution is exists. It's enough to copy the code and paste it in your solution. And set the start-up object to `Form1`. – Reza Aghaei Sep 14 '16 at 21:59
  • Oh I mocked it up on my own and got it working. Thanks for all your help though. I just forgot to mark your answer. – djangojazz Sep 15 '16 at 17:25
  • Great! You're welcome and thanks for the feedback :) – Reza Aghaei Sep 15 '16 at 17:27