VB.NET datagridview one-to-one mapping of combobox - vb.net

I have a datagridview with two textbox columns and one combobox column. The combobox's DataSource is bound to enums for the values of the dropdown. The datagridview's DataSource is bound to a custom class with datatypes of string, string and enum.
The first two columns are pre-populated with values and in the third column the user must select a value from the dropdown. All this is working excellent so far except....
The combobox field should be a one-to-one kind of mapping, meaning no two comboboxes should have the same value. I am really not sure how to implement this kind of behavior. Should the chosen value be removed from the remaining dropdowns? should the chosen value remain in the dropdown and just give an error when two of the same are selected?
Any ideas and how to implement these ideas will be of great help.
Thanks
note the only acceptable value that can be used more than once is 'None'

I have an idea based off your intent to possibly remove the chosen value from the remaining dropdowns.
I have a class called Runner which has a similar setup to your example.
Public Class Runner
Public Property FirstName As String
Public Property LastName As String
Public Property Placement As Result
Public Sub New(fn As String, ln As String)
FirstName = fn
LastName = ln
Placement = Result.None
End Sub
End Class
I also have an enum called Result which will get populated into the ComboBoxCell:
Public Enum Result
None = 0
Bronze = 1
Silver = 2
Gold = 3
End Enum
When I create data objects and bind them to the DataGridView, I do so like this (note - Placements is a global List(Of Result) and Runners is a global List(Of Runner):
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
Placements.Add(Result.None)
Placements.Add(Result.Bronze)
Placements.Add(Result.Silver)
Placements.Add(Result.Gold)
Runners.Add(New Runner("John", "Smith"))
Runners.Add(New Runner("Jane", "Doe"))
Runners.Add(New Runner("Bill", "Jones"))
Column1.DataPropertyName = "FirstName"
Column2.DataPropertyName = "LastName"
Column3.DataPropertyName = "Placement"
Column3.DataSource = Placements
DataGridView1.DataSource = Runners
End Sub
Now, whenever the value of a cell in the ComboBoxColumn changes, you remove the new value from the list that contains the enums:
Private Sub DataGridView1_CellValueChanged(sender As Object, e As DataGridViewCellEventArgs) Handles DataGridView1.CellValueChanged
If (e.RowIndex > -1 And e.ColumnIndex = 2) Then
Dim currentvalue As Result = DataGridView1.Rows(e.RowIndex).Cells(e.ColumnIndex).Value
If currentvalue <> Result.None Then
Placements.Remove(currentvalue)
End If
End If
End Sub
Be careful when changing drop down selections... as per this Discussion, you have to trick the DataGridView into committing values as soon as the ComboBox value changes. I used an answer from that discussion and did something like this:
Private Sub DataGridView1_CurrentCellDirtyStateChanged(sender As Object, e As EventArgs) Handles DataGridView1.CurrentCellDirtyStateChanged
Dim col As DataGridViewColumn = DataGridView1.Columns(DataGridView1.CurrentCell.ColumnIndex)
If col.Name = "Column3" Then
DataGridView1.CommitEdit(DataGridViewDataErrorContexts.Commit)
Dim selected As DataGridViewCell = DataGridView1.CurrentCell
DataGridView1.CurrentCell = Nothing //This line and the next one simply hide
DataGridView1.CurrentCell = selected //an odd display effect that occurs
//because we remove a value and change the
//selection at the same time
End If
End Sub
Finally, you want to handle the DataError event for the DataGridView and leave it blank so that you don't get Exceptions thrown at you when removing values from your list of enums.
This gets you about 90% of the way there. It will not re-add items to the list when you change a value. For example if I change from Gold to Silver, Gold should be added back to the list. You can probably figure out what events to handle to get the old value and the new one, and insert the old value back into the list at the correct index based on its enum value.

I decided to use the CellValidating event of the DataGridView to check whether or not the same value is selected more than once. If it is then a error message is displayed in the row header column.
The combobox in error will not lose focus until the error is resolved.
Private Sub DataGridView1_CellValidating(ByVal sender As Object, ByVal e As System.Windows.Forms.DataGridViewCellValidatingEventArgs) Handles DataGridView1.CellValidating
Dim headerText As String = DataGridView1.Columns(e.ColumnIndex).HeaderText
'Abort validation if cell is not in the Mapping column.
If Not headerText.Equals("Column Mapping") Then Return
'Clear error on current row.
DataGridView1.Rows(e.RowIndex).ErrorText = Nothing
e.Cancel = False
Dim newMappingValue As XmlElement = DirectCast([Enum].Parse(GetType(XmlElement), e.FormattedValue), XmlElement)
' Abort validation if cell value equal XmlElement.None
If newMappingValue.Equals(XmlElement.None) Then Return
For Each dgvRow As DataGridViewRow In DataGridView1.Rows
Dim currentMappingValue As XmlElement = dgvRow.Cells.Item(headerText).Value
If dgvRow.Index <> e.RowIndex Then
If currentMappingValue.Equals(newMappingValue) Then
DataGridView1.Rows(e.RowIndex).ErrorText = "Value already selected, please select a different value."
e.Cancel = True
End If
End If
Next
End Sub

Related

How to update devexpress gridview data column value automatically if condition is met?

I need to update devexpress gridview data column value automatically if condition is met. I tried to implement it using HtmldatacellPrepared
but there is error at line e.GetValue("Status") = "Delay" : Expression is a value and therefore cannot be the target of an assignment. Is there any other method to use to implement this?
Protected Sub Grid_HtmldatacellPrepared(sender As Object, e As ASPxGridViewTableDataCellEventArgs)
if e.GetValue("Forecast") IsNot DBNull.Value Then
If e.DataColumn.VisibleIndex = 9 Then
If e.GetValue("Forecast") > Date.Now Then
e.GetValue("Status") = "Delay"
End if
End If
End If
End Sub
Use the ASPxGridView.CustomColumnDisplayText event instead of the ASPxGridView.HtmlDataCellPrepared one, and set the EventArgs e.DisplayText property. Check out my answer here.
e.GetValue("...") is a function so you cannot assign value to it.
I don't have DevExpress component handy to test but based on their online doc, I draft below how to set the cell value.
In order to set the value of the "Status" cell, handle the grid's CustomColumnDisplayText event. Detect if the target cell (i.e "Status") is currently being processed and set the value using e.DisplayText property.
Protected Sub ASPxGridView1_CustomColumnDisplayText(ByVal sender As Object, ByVal e As DevExpress.Web.ASPxGridViewColumnDisplayTextEventArgs) Handles ASPxGridView1.CustomColumnDisplayText
If e.Column.FieldName = "Status" Then
Dim grid = DirectCast(sender, ASPxGridView)
Dim forecast As Object = grid.GetRowValues(e.VisibleRowIndex,"Forecast")
If forecast IsNot DBNull.Value AndAlso CDate(forecast) > Date.Now Then
e.DisplayText = "Delay"
End If
End If
End Sub
Hopefully that will give you rough idea.

Trying to make smart combobox item interaction

I am here to find out is it possible to complete my idea to save me time writing long code.
I have 1 main combobox with various items and some other comboboxed. Each combobox of them is called "Combo" + the item from the main combobox.
and I wonder can I, when I click on an item to hide the last used combobox and to show the combobox linked to this item?
1. hide last used combobox
2. show the combobox responding to the selected item from the main combox
Public Sub ComboBox2_SelectedIndexChanged(sender As Object, e As EventArgs) Handles ComboBox2.SelectedIndexChanged
Dim SelectedAction As String = "Combo" + ComboBox2.Text
lastcombobox.visible = false
' now to assign to the new combobox
lastcombobox = (SelectedAction as name of combobox) combobox
Lastcombobox.visible = true
End Sub
Strings are not controls. Strings are the data type of Control.Name; that is the Name property of the control object. You cannot cast a string to a control but all is not lost. Create your other Combo Boxes at design time and stack them on top of each other. Notice the Static keyword before lastComboBox. This persists the value between calls to the method. You could accomplish the same thing by making this variable a class level variable. The first time the method is called their will be nothing in lastComboBox therefore the check for IsNothhing. Control.Find returns an array so we must refer to ctl(0) - the first element of the array, since we know it will return only one.
Private Sub Combo2_SelectedIndexChanged(sender As Object, e As EventArgs) Handles Combo2.SelectedIndexChanged
Dim SelectedAction As String = "Combo" & Combo2.Text
Static lastComboBox As ComboBox
If Not IsNothing(lastComboBox) Then
lastComboBox.Visible = False
End If
Dim ctl() As Control = Controls.Find(SelectedAction, True)
lastComboBox = CType(ctl(0), ComboBox)
lastComboBox.BringToFront()
lastComboBox.Visible = True
End Sub

Prevent Code Running if Combobox's SelectedIndex was Never Changed

I have a combo box that has a couple items in them, where they add text to a text box. I want to prevent the same item being selected twice in a row, because the index never changed, but it still runs the code.
Here's what I've done:
Dim intComboIndex As Integer = -1
Dim cboComboBox As ComboBox = CType(sender, ComboBox)
Dim intComboSelIndex As Integer = cboComboBox.SelectedIndex
If intComboSelIndex > -1 And intComboSelIndex <> intComboIndex Then intComboIndex = intComboSelIndex
Is there a more efficient way of doing this, without having to create a different combo box and compare the indexes?
You must store the previously selected index elsewhere, because at the moment you recreate the variable every time the event is raised.
I'd recommend storing the previously selected index at class level, and then compare that to the currently selected index.
Public Class Form1
Dim ComboBoxPrevIndex As Integer = -1 'Declared at class level, outside any Sub or Function.
Private Sub ComboBox1_SelectedIndexChanged(sender As Object, e As EventArgs) Handles ComboBox1.SelectedIndexChanged
Dim SenderBox As ComboBox = DirectCast(sender, ComboBox) 'Get the control that raised the ever.
'Has the selection changed?
If ComboBoxPrevIndex <> SenderBox.SelectedIndex _
AndAlso SenderBox.SelectedIndex > -1 Then
'Do stuff...
End If
ComboBoxPrevIndex = SenderBox.SelectedIndex 'Set the new, previous index.
End Sub
End Class
You might have noticed that I used AndAlso instead of And. This is because of AndAlso is short-circuited, meaning that it will only check the condition on it's right if the condition on it's left evaluates to True.

get value of selected item in second column... returning previous item

I get that there are other questions like this, but for some reason nobody seems to have this problem. I'm trying to populate a 2 column listview that I create on load like this.
With lstShipMethods
.View = View.Details
.FullRowSelect = True
.HeaderStyle = ColumnHeaderStyle.None ' set to whatever you need
.Columns.Clear() ' make sure collumnscollection is empty
' Add 3 columns
.Columns.AddRange(New ColumnHeader() {New ColumnHeader(), New ColumnHeader()})
End With
lstShipMethods.Items.Add(New ListViewItem({"col1data", "col2data1"}))
lstShipMethods.Items.Add(New ListViewItem({"col1data", "col2data2"}))
It populates just fine, but when I try to get the data from the selected items column like this
Private Sub lstShipMethods_SelectedIndexChanged(sender As Object, e As EventArgs) Handles lstShipMethods.SelectedIndexChanged
Dim val As String = lstShipMethods.FocusedItem.SubItems(1).Text
MessageBox.Show(val)
End Sub
after the first click it will always show both values aka second column from row one and row two will be output in the MessageBox.
You can use the ItemSelectionChanged event instead.
Private Sub lstShipMethods_SelectedIndexChanged(sender As System.Object, e As ListViewItemSelectionChangedEventArgs) Handles lstShipMethods.ItemSelectionChanged
If e.IsSelected Then
Dim val As String = lstShipMethods.FocusedItem.SubItems(1).Text
MessageBox.Show(val)
End If
End Sub

Gridviews and DropdownLists

Is it possible to change the data source of a dropdown list in a gridview from another dropdown list selected index changed method in the same gridview?
for example I have a dropdown that needs to change its contents depending on what is chosen in the previous cell of the gridview, which is also a dropdown list.
Any Help would be much appreciated
Thanks
Instead of changing the DataSource when the 1st DropDownList.SelectedIndex changes, you could set the DataSource of the 2nd DropDownList when it is being edited.
An example of how this can be achieved can be found here.
In this article, the author hooks to the EditingControlShowing event in order to change the type of the ComboBox. This can be easily modified to change the DataSource instead:
Private Sub DataGridView1_EditingControlShowing(ByVal sender As Object, ByVal e As System.Windows.Forms.DataGridViewEditingControlShowingEventArgs) Handles DataGridView1.EditingControlShowing
' make sure we are editing the 2nd ComboBox:'
Dim comboBoxColumn2 As DataGridViewComboBoxColumn = DataGridView1.Columns(2)
If (DataGridView1.CurrentCellAddress.X = comboBoxColumn2.DisplayIndex) Then
'here you retrieve the value of the 1st ComboBox:'
Dim comboBox1Value As object = DataGridView1.SelectedRow... 'fill with whatever is needed'
Dim cb As ComboBox = e.Control
If (cb IsNot Nothing) Then
cb.DataSource = Nothing 'maybe not needed, I'm not sure
cb.DataSource = 'here, set the data source based on the value of ComboBox1'
End If
End If
End Sub
Here is another way how I would do this, by example: Two columns (Types, Days), if the user drops-down and chooses 'week', a second combo populates with week days, otherwise, weekends.
For the purpose of this example, add a grid (DataGridView1) with two ComboBoxCell columns and let the first column have these items: week, weekend.
This class will be our data source:
Class WeekDataItem
Sub New(ByVal id As Integer, ByVal name As String)
Me.ID = id
Me.Name = name
End Sub
Public Property ID() As Integer
Get
Return _ID
End Get
Set(ByVal value As Integer)
_ID = value
End Set
End Property
Private _ID As Integer
Public Property Name() As String
Get
Return _Name
End Get
Set(ByVal value As String)
_Name = value
End Set
End Property
Private _Name As String
End Class
This function will return our data source, based on the key which can be 'week' or 'weekend':
Function getWeekDataSource(ByVal key As String) As List(Of WeekDataItem)
getWeekDataSource = New List(Of WeekDataItem)
If (key = "week") Then
getWeekDataSource.Add(New WeekDataItem(1, "monday"))
getWeekDataSource.Add(New WeekDataItem(2, "tuesday"))
getWeekDataSource.Add(New WeekDataItem(3, "wednesday"))
getWeekDataSource.Add(New WeekDataItem(4, "thrusday"))
getWeekDataSource.Add(New WeekDataItem(5, "friday"))
ElseIf (key = "weekend") Then
getWeekDataSource.Add(New WeekDataItem(6, "caturday"))
getWeekDataSource.Add(New WeekDataItem(7, "sunday"))
End If
End Function
And lastly, this event will fire when the Type combo value changes, and assign the appropriate data source to our days combo:
Private Sub DataGridView1_CellValueChanged(ByVal sender As System.Object, ByVal e As System.Windows.Forms.DataGridViewCellEventArgs) Handles DataGridView1.CellValueChanged
' if the type dropdown value changed
If (e.ColumnIndex = clmTypes.Index) Then
' set the week dropdown data source for the current row
If Not IsNothing(DataGridView1.CurrentRow) Then
' get the combobox cell we want to change
Dim comboCell As DataGridViewComboBoxCell
comboCell = CType(DataGridView1.CurrentRow.Cells(clmDays.Index), DataGridViewComboBoxCell)
' assign it's new data source
comboCell.DataSource = getWeekDataSource(CStr(DataGridView1.Rows(e.RowIndex).Cells(e.ColumnIndex).Value))
' update the data source members so it displays info properly
comboCell.DisplayMember = "Name"
comboCell.ValueMember = "ID"
End If
End If
End Sub
Note that this event fires after the cell is validated, ie once you tab off the cell.
It is totally possible. How are populating your dropdownlists ? If all the data is dynamic then you will have to rebuild the entire grid everytime you change the dropdownlist selected item.
If I am not wrong , you are trying to apply Filter mechanism. Are you ? Another way I have done in the past is to build my data source for DropDownList from the rows of GridView. Think about the data that you already have on the screen. Once you are in PreRender Function you can bind needed data in your dropdownlist, this way you will cut out load.