DataBinding a Combobox to custom object fails to show data - vb.net

I'm having a problem with databinding an object to a combobox in VB.NET (VS2008/.NET 3.5). Please take a look at this simplified version of my code:
Friend Class clDocument
Private _items as New List(Of clDocumentItems)
<System.ComponentModel.DisplayName("Items")> _
<System.ComponentModel.Bindable(True)> _
Public Property Items() As List(Of clDocumentItems)
Get
Return _items
End Get
Set(ByVal value As List(Of clDocumentItems))
_items = value
RaiseEvent ItemsChanged(Me, New EventArgs)
End Set
End Property
Public Event ItemsChanged As EventHandler
End Class
Friend Class clDocumentItems
Private _uid as String = ""
Private _docnumber as String = ""
<System.ComponentModel.DisplayName("UID")> _
<System.ComponentModel.Bindable(True)> _
Public Property UID() As String
Get
Return _uid
End Get
Set(ByVal value As String)
_uid = value
RaiseEvent UIDChanged(Me, New EventArgs)
End Set
End Property
<System.ComponentModel.DisplayName("Document")> _
<System.ComponentModel.Bindable(True)> _
Public Property DocNumber() As String
Get
Return _docnumber
End Get
Set(ByVal value As String)
_docnumber = value
RaiseEvent DocNumberChanged(Me, New EventArgs)
End Set
End Property
Public Event UIDChanged As EventHandler
Public Event DocNumberChanged As EventHandler
End Class
Somewhere else, we got this code:
Private Sub cmd_go_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles cmd_go.Click
'Try to load the object with data, this works well
Dim _document as New clDocument
_document.Load(somevalue)
cmb_docs.DataSource = Nothing
cmb_docs.Items.Clear()
If _document.UID = "" Then Exit Sub 'Object wasn't loaded so get out
'Create the binding.
cmb_docs.ValueMember = "UID"
cmb_docs.DisplayMember = "DocNumber"
cmb_docs.DataSource = _document.Items
End Sub
Now, the problem is that the ComboBox is populated with as many items as there are objects in _document.Items, but it doesn't passes real data -The combobox is filled with "Namespace.clDocumentItems" strings. Please note that similar code works perfectly when bound to regular class properties (String, integer, etc).
Now, I can guess by using the debugger's reflection that it is because the Datsource is receiving a List of objects instead of fields, but then again I don't know how to avoid that without having to create another Array or List with just those values and passing THAT to the Datasource property...
I explored the site for something similar, the only thing that came close was this question that went unanswered 3 years ago, so I hope to have better luck today ;)
Thanks for your time!
EDIT: Below I add the code I used for databinding to a DataGridView, as requested in the comments.
Private WithEvents _bs as New BindingSource
Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
DataGridView1.AutoGenerateColumns = False
Dim column As DataGridViewColumn = New DataGridViewTextBoxColumn()
column.DataPropertyName = "Document"
column.Name = "colDoc"
DataGridView1.Columns.Add(column)
_bs.DataSource = _document.Items
Me.DataGridView1.DataSource = _bs
End Sub

There is actually nothing at all wrong with your code as you have it written above (I copied it into a new WinForms app, supplied some default values, and it worked correctly).
There are two possible reasons that your implementation may not be working:
If there is a typo in DisplayMember.
If the access level of the DisplayMember property prevents the
combobox from accessing it.
If the property cannot be found or accessed, .Net drops back to the default implementation of using the object's ToString method. So one quick and dirty fix would be to override the ToString method of clDocument and return DocNumber. That should resolve the display issue, but not the underlying cause of the problem, which will require a little more research.

ok, The problem was the comboBox was faulty. Removing the control from the form and adding it again solved the problem. I accept the answer because it contains useful information.
Thanks

Related

Get Label through Function

all! I'm developing a BlackJack game but I've run into a little bit of a problem. When calculating score, I have to type YourCard1.Text, YourCard2.Text, YourCard3.Text, etc.
Can I make a function that gets the right label each time it's called? I want to do this so I don't have to type so much...
For example, instead of typing out "YourCard1.Text", I want to be able to type "card(1)" Is this possible? I've tried multiple ways of doing this, but to no avail. I'm having trouble figuring out how to make it work.
Assuming you have those labels on your form, YourCard1.Text, YourCard2.Text, YourCard3.Text, etc., This function should work for you. It returns the Label itself, not the Text property.
Private Function card(index As Integer) As Label
Try
Return Me.Controls.
OfType(Of Label).
Where(Function(l) l.Name = "YourCard" & index.ToString()).
Single()
Catch
Return Nothing
End Try
End Function
Note: Me.Controls returns the controls directly inside the form, but doesn't return controls inside containers in the form. If your cards are inside a panel, Panel1 for example, you would do Return Panel1.Controls.OfType(Of Label)...
Usage:
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
card(1).Text = "Hello"
card(2).Text = "World"
End Sub
Edit to address comment.
You are pidgeonholed into only those semantics. So there is another way I could think of. But I wouldn't personally do this.
Public Class Form1
Private Class cardClass
Private myContainer As Control
Sub New(container As Control)
myContainer = container
End Sub
Default Public WriteOnly Property Item(ByVal index As Long) As String
Set(value As String)
card(index).Text = value
End Set
End Property
Private Function card(index As Integer) As Label
Try
Return myContainer.Controls.
OfType(Of Label).
Where(Function(l) l.Name = "YourCard" & index.ToString()).
Single()
Catch
Return Nothing
End Try
End Function
End Class
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
Dim card As New cardClass(Me)
card(1) = "Hello"
card(2) = "World"
End Sub
End Class
The reason it's so complex is that though String is a reference type, it uses value type semantics. So when returning a string from a function, it can't refer back to the original memory location: it actually creates a copy of the string. So using function semantics won't work. Same would go for an array. It would be difficult (impossible?) to modify a string from either a function or array and have it modify the Label's Text property.

vb.net - how to change a property of a form element from another module

I am trying to change the background color of a button (cmdLogQry) from a Set-procedure of a public property (LogQry) - according to the new value of the property.
It works if the property is being changed in the code belongig to the form containing the button (in the Click method of the same or even another button). But it does not work if the property is being changed from another module (handler procedure for COM ports DataReceived event). No error message or anything - the LogQry gets its value changed all right, but the color of the button does not change.
What do I do wrong?
Public Class Handler
Private _logQry As Boolean = False
Public Property LogQry() As Boolean
Get
Return _logQry
End Get
Set(ByVal value As Boolean)
_logQry = value
If value Then
frmMain.cmdLogQry.BackColor = Color.Red
Else
frmMain.cmdLogQry.BackColor = Color.Blue
End If
End Set
End Property
Private Sub comPort_DataReceived(ByVal sender As Object, ByVal e As SerialDataReceivedEventArgs)
...
LogQry = Not LogQry ' does NOT change color
...
End Sub
End Class
Public Class frmMain
Private comm As New Handler()
...
Private Sub cmdLogQry_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles cmdLogQry.Click
comm.LogQry = Not comm.LogQry ' does change color
End Sub
...
End Class
This problem is caused by the default instance of the form class created by the VB.NET implementation. More on default instances could be found here and in this answer from Hans Passant.
Essentially when you define a form class, VB.NET compiler creates a default instance of that class named with the same name of the class, but this creates a lot of misunderstanding in an object oriented environment like NET.
To fix your problem you need to implement a constructor in your Handler class that receives the actual instance of frmMain, store it inside a class variable and use that instance when you want to modify something on the actual displayed form
Public Class Handler
Private _logQry As Boolean = False
Private _mainInstance As frmMain
Public Sub New(mainInstance as frmMain)
_mainInstance = mainInstance
End Sub
Public Property LogQry() As Boolean
Get
Return _logQry
End Get
Set(ByVal value As Boolean)
_logQry = value
If value Then
_mainInstance.cmdLogQry.BackColor = Color.Red
Else
_mainInstance.cmdLogQry.BackColor = Color.Blue
End If
End Set
End Property
....
End Class
Now, when you create the Handler instance pass the reference to the current frmMain
Public Class frmMain
Private comm As Handler
...
Private Sub cmdLogQry_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles cmdLogQry.Click
comm = new Handler(Me)
comm.LogQry = Not comm.LogQry ' does change color
End Sub
...
End Class
Keep in mind that this solution creates also problems. It couples the class Handler to your frmMain and the two are now inseparable. Probably a better approach is to create an Event in the Handler class so, every form that wants to be notified could subscribe to the event and receieves the information when needed.

Dim value disappears vb.net

I'm trying to store a value in code behind so I don't have to clog my page with HiddenFields, but the value 'disappears'
I'm assuming it has to do with either scope or byval vs byref, but I don't know how I need to do it for it to work.
What I've tried to do is creating a Dim under my Partial Class, setting the value in a gridview.RowCommand, and then trying to get the value at a later button.Click
Partial Class CustNSSF_MinSide
Inherits System.Web.UI.Page
Dim vr As New vrClass
Dim _ActNo As String
Sub GV_Relations_RowCommand(ByVal sender As Object, ByVal e As GridViewCommandEventArgs) Handles GV_Relations.RowCommand
_ActNo = GV_Relations.DataKeys(index)("ActSeqNo")
Protected Sub btn_lagre_Click(sender As Object, e As System.EventArgs) Handles btn_lagre.Click
Dim test = _ActNo
The value disappears because all variables (or controls) are disposed at the end of every page's life-cycle. GV_Relations_RowCommand is triggered only on RowCommand and btn_lagre_Click is a different action. You could store this value in a Session variable, ViewState or (as you've done) in a HiddenField. So when the user clicked on btn_lagre, the previous action that caused RowCommand was a different postback, therefore the variable is Nothing.
So one way (apart from your HiddenField aproach), using ViewState:
Private Property ActSeqNo As System.Int32
Get
If ViewState("ActSeqNo") Is Nothing Then
ViewState("ActSeqNo") = System.Int32.MinValue
End If
Return DirectCast(ViewState("ActSeqNo"), System.Int32)
End Get
Set(value As System.Int32)
ViewState("ActSeqNo") = value
End Set
End Property
Then you can set it in this way:
Me.ActSeqNo = System.Int32.Parse(GV_Relations.DataKeys(index)("ActSeqNo")))
Nine Options for Managing Persistent User State in ASP.NET
The variable will be clear every Postback. You need to store it either in the Viewstate or in the Session. You could do this:
Property _ActNo As String
Get
Return ViewState("_ActNo")
End Get
Set(ByVal value As String)
ViewState("_ActNo") = value
End Set
End Property

Saving changed values of components on FormClosing

In my application I have a TabControl with some tabs. Each tab contains many components. If the application is closing I want to check If a value of any component has changed. If so, I will ask a user if he wants to save it or not.
I want to know how you solve this situation (because it is standard behaving when application is closing). I thought that I have some flag (bool) and I set an event ValueChanged to each component. If the method handlings this event is fired, this flag is set to true. In case of closing application I will only check if flag is true.
But the problem is that there is more than 30 components and create method handling the event to each component it seems not efectve to me.
You're on the right track wit the boolean flag and the ValueChanged event. As far as I'm aware, that's really the only way to deal with this kind of thing. To accomplish this you could simply writing the handling for the event once and then copy and paste the component over and over as needed.
However, to your question of effectiveness when spread across 30 components, you should consider rolling your own component(s) that inherit from a basal class which exposes an IsDirty property or similar. When closing you could then loop through all controls on your tabs to see any have IsDirty set to true.
Unfortunately, given that you've already created the interface, neither approach will solve your current dilemma.
When you fill in the text/value for each control, also fill in the TAG with the same value. Then you can compare the TAG against the text/value for each control to see if anything changed.
To avoid having to write code for each control (when checking) you cah do a for loop on each control in [Tab Page Name].Controls().
Another way to do this is to extend each control by adding a IsDirty property and overriding the validation event. Then you can set this if it changed. You might also want to have a method to RESET the IsDirty property.
Yet another way, I always bind to a class, it just makes my code less error-prone and gives me intellisense. Also gives you tons of features that you can easily throw in like this. Then just BIND to your custom class.
Here is an example of how I would do this using a custom class. This is just an example of reading a csv and writting it but the key, to answer your question, is in the "Dirty" code.
Imports System.ComponentModel
Public Class Form1
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles Me.Load
'On form load, open the file and read the data
Using SR As New System.IO.StreamReader("Test.csv")
Do While SR.Peek >= 0
'Put each line into it's own instance of the class
BindingSource1.Add(New MyData(SR.ReadLine))
Loop
End Using
End Sub
Private Sub Form1_FormClosing(sender As Object, e As FormClosingEventArgs) Handles Me.FormClosing
'When closing, see if any data has been changed and ask to save
Dim bDirty As Boolean = False
For Each dat As MyData In BindingSource1
If dat.IsDirty Then
bDirty = True
Exit For
End If
Next
If bDirty Then
'Example code for saving
Select Case MessageBox.Show("Do you want to save your changes?", "Save Changes", MessageBoxButtons.YesNoCancel)
Case Windows.Forms.DialogResult.Cancel
e.Cancel = True
Case Windows.Forms.DialogResult.Yes
'Here you should remove the old file, I like to rename it to BAK,
' save the new file, then you can get rid of it.
' Just in case there is a problem saving.
If System.IO.File.Exists("Test.csv") Then System.IO.File.Delete("Test.csv")
Using SW As New System.IO.StreamWriter("Test.csv", False)
For Each dat As MyData In BindingSource1
SW.WriteLine(dat)
Next
End Using
End Select
End If
End Sub
End Class
Public Class MyData
Implements INotifyPropertyChanged
'Event that implements INotifyPropertyChanged. This tells the binding to refresh a property in the UI.
Public Event PropertyChanged(sender As Object, e As PropertyChangedEventArgs) Implements INotifyPropertyChanged.PropertyChanged
Public Sub New(ByVal SomeDataToLoad As String)
'Take you data and parse it or whatever into the various properties
'This example uses a comma-seperated string
Dim sWords As String() = SomeDataToLoad.Split(",")
_FirstName = sWords(0)
_LastName = sWords(1)
End Sub
''' <param name="PropertyName">Case-Sensative property name</param>
Public Sub ForcePropertyChanged(ByVal PropertyName As String)
RaiseEvent PropertyChanged(Me, New System.ComponentModel.PropertyChangedEventArgs(PropertyName))
End Sub
Private _IsDirty As Boolean
Public ReadOnly Property IsDirty As Boolean
Get
Return _IsDirty
End Get
End Property
''' <summary>Override the ToString method for getting the data back out, in this case as comma seperated again. You can then write this to file or whatever.</summary>
Public Overrides Function ToString() As String
Return FirstName & "," & LastName
End Function
'--Properties you can bind to------------------------------------------------
Private _FirstName As String
Public Property FirstName As String
Get
Return _FirstName
End Get
Set(value As String)
_FirstName = value
_IsDirty = True
ForcePropertyChanged("FirstName")
End Set
End Property
Private _LastName As String
Public Property LastName As String
Get
Return _LastName
End Get
Set(value As String)
_LastName = value
_IsDirty = True
ForcePropertyChanged("LastName")
End Set
End Property
End Class
I did not go into how to bind here, you can find that all over the web, but I did put the data in a BindingSource so the rest will be easy. Notice that when the form is closing, I can loop through EVERY record to see if there were changes, easily. If you only had a single record, you wouldn't even have to loop, just ask if it is dirty.

Working with bindinglist

I have a datagridview and a bindinglist. They work together pretty ok, but I want to make the properties appear in the rows, not on the column. Is there any way to achieve that ?
My code for anyone who is interested.
Public Class Form1
Dim listaBindingSource As New BindingList(Of pessoa)
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
Dim b1 As New pessoa()
listaBindingSource.Add(b1)
dgv.DataSource = listaBindingSource
End Sub
End Class
Public Class pessoa
Dim sells_Month1 As String
Public Sub New() 'ByVal nome_fora As String)
sells_Month1 = "0"
End Sub
Property vendas1 As String
Get
Return sells_Month1
End Get
Set(value As String)
sells_Month1 = value
End Set
End Property
The other properties are vendas2, vendas3.. and are the same as this one.
Edit:
I´m kind of lost here. What I want is to make the values of the properties of my objects appear on some kind of data visualizer. When I add new objects on the list, they appear on this data visualizer and when I change the values of the cells there, the values of the properties change. Anyone has a good sugestion ? Apparentely dgv is not the way to go.
Thanks,
Ricardo S.
I want to make the properties appear in the rows´ headers, not on
the column.
I'm afraid this is not possible, there is no built-in solution for that in DataGidView. You can display the properties in columns only.
To control the text displayed in the column header, try to set the DisplayName attribut:
<System.ComponentModel.DisplayName("DisplayedText")>
Property vendas1 As String
Get
Return sells_Month1
End Get
Set(value As String)
sells_Month1 = value
End Set
End Property
Or if you import System.ComponentModel namespace.
<DisplayName("DisplayedText")>
Property vendas1 As String
Get
Return sells_Month1
End Get
Set(value As String)
sells_Month1 = value
End Set
End Property