What I am doing
I am developing a VisualBasic application where the GUI is separated from the data manipulation (frontend and backend). This particular piece of code keeps track of the Serial Numbers already measured and displays them in the Form as a DGV with the serial number and an image or not.
My code
In a class that the GUI Form instantiates, my data is stored in a DataTable with a BindingSource, with the second column displaying an Image when I tell it to in the program (Nothing in the beginning):
Public SerialNumbersBindingSource As New BindingSource
Public SerialNumbersDataTable As New DataTable
(...)
SerialNumbersBindingSource.DataSource = SerialNumbersDataTable
SerialNumbersDataTable.Columns.Add("Serial", Type.GetType("System.String"))
SerialNumbersDataTable.Columns.Add("Pass", MyImage.GetType)
In my GUI Form code, a DataGridView has its DataSource set to the former DataTable:
DataGridViewSerialNumber.DataSource = MyObject.SerialNumbersDataTable
By just updating the DataTable in my class, the DataGridView updates automatically to reflect the current state of it. I do it like:
SerialNumbersDataTable.Add(CurrentSerial, MyImage)
The results I get
The code works, so I can modify and access the DataTable and the DataGridView autoupdates. But the images are not stretching, so I can only see a small part of them.
What I need
I need the second column named "Pass" in DataGridView to stretch the images.
What have I tried
If I access the column, it is treated like a DGVColumn and not a DGVImageColumn, so the Layout operation fails.
DataGridViewSerialNumber.Columns("Pass").DataGridViewImageCellLayout.Stretch
Microsoft's Docs page tells me to do this, which treats the columns like DGVImageColumn as I need "Pass" to. It fails because the first column is a Text one, not image.
For Each column As DataGridViewImageColumn In DataGridViewSerialNumber.Columns("Pass")
column.ImageLayout = DataGridViewImageCellLayout.Stretch
Next
Also I have tried creating a local DGVImageColumn, modify it and write it onto the original column, but it is read-only.
Dim imageColumn As DataGridViewImageColumn
imageColumn = DataGridViewSerialNumber.Columns("Pass")
imageColumn.ImageLayout = DataGridViewImageCellLayout.Stretch
DataGridViewSerialNumber.Columns("Pass") = imageColumn
I have also tried to do it from the designer. If I click the DGV, arrow to the right and 'Edit Column', I can create the two columns and setup Pass as ImageColumn with Stretched Layout. But when I set up DGVSerialNumbers.Datasource to my DataTable, it adds the DataTable's columns to the DGV's.
Failed DGV with columns added in designer
It's time for you to learn how to cast. If you want to access one column then don't use a loop. Simply access the column you want and then cast it as the type you want to use it as. You also need to actually assign the appropriate value to the appropriate property.
DirectCast(DataGridViewSerialNumber.Columns("Pass"), DataGridViewImageColumn).ImageLayout = DataGridViewImageCellLayout.Stretch
If you want to break that up for clarity:
Dim imageColumn = DirectCast(DataGridViewSerialNumber.Columns("Pass"), DataGridViewImageColumn)
imageColumn.ImageLayout = DataGridViewImageCellLayout.Stretch
Also, be aware that Stretch will not retain the original aspect ratio, so you might want to use Zoom instead.
Related
I am trying to get the results of a SQL Server stored procedure in VB.NET, parse them into a DataSet, then use the DataSet to fill a Windows Forms ListBox.
I have found many, many tutorials. The only ones that have gotten me close to a solution are ones that depend on the ListBox's DataBind() method. However:
myListBox.DataBind()
' ERROR: 'DataBind' is not a member of 'ListBox'
This is contrary to every tutorial I've found on the topic (approx a dozen so far).
Here is more code context:
Dim connection As New SqlConnection(myConnectionSecret)
Dim command As New SqlCommand("myStoredProcedureName")
command.Parameters().Add("#myParam", SqlDbType.Int).Value = myParamValue
command.Connection = connection
command.CommandType = CommandType.StoredProcedure
Dim adapter As New SqlDataAdapter(command)
adapter.SelectCommand.CommandTimeout = 300
'Fill the dataset
Dim dataSet As New DataSet
adapter.Fill(dataSet)
connection.Close()
myListBox.DataSource = dataSet
myListBox.DataBind() ' This method not found
A dataset is a collection of datatables. Typically you would bind your listbox to only one datatable. You can set the DataSource property to a dataset but you'll then also need to set the DataMember property to the name of the table in the dataset so the binding knows which table to rummage in
myListBox.DataSource = dataSet
myListBox.DataMember = "Person" 'whatever your table name is
Or set it to refer to the table directly:
myListBox.DataSource = dataSet.Tables("Person")
As other answers have commented you should then set the DisplayMember (chooses what text appears in the control) and ValueMember (chooses what value is emitted by the listbox.SelectedValue property) properties to strings representing the column names. If I want to show my persons name in the list but have their email be the selected value:
myListBox.DisplayMember = "FullName" 'fullname is a column in the datatable
myListBox.ValueMember = "Email"
If you don't set a ValueMember the whole DataRow of the selected person is returned from SelectedValue. (Clarification: Actually, it's a DataRowView because binding to a datatable actually happens to a DataView exported by the .DefaultView property - more on this later)
So, we've bound our list.DataSource to a datatable, and set the diplay/valuemember properties. How to we get the details of what Person is selected currently? Put a button on the form that has this handler code:
MessageBox.Show(DirectCast(listbox.SelectedValue, string)) 'shows e.g. john.smith#hotmail.com
Run the program, click "John Smith" in the list then click the button. The messagebox will show the selected person's email address
I mentioned earlier that databinding happens to a DataView object exported by the table's .DefaultView property. A DataView is a collection of DataRowView objects, and a DataRowView is a thin wrapper around a DataRow. DataRows exist in various versions such as original or updated values. A DataRowView is a way of selecting one of these versions for presentation, by default the Current version. You can address it like you would a DataRow:
'accessing the email address of a datatable row
Dim myDataRow = myPersonTable.Rows(0)
Dim email as String = DirectCast(myDataRow("EmailAddress"), string)
'accessing the email address of the dataview exported by the table defaultview
Dim myDataRowView = myPersonTable.DefaultView(0)
Dim email as String = DirectCast(myDataRowView("EmailAddress"), string)
As you can see, there isn't a lot of difference - as noted, the view just shows one of the various versions a datarow can exist in. If you want to get access to the underlying row you can do it via the Row property:
Dim myDataRowView = myPersonTable.DefaultView(0) 'or however you ended up holding a DataRowView object
Dim dr as DataRow = myDataRowView.Row
If you're using strongly typed datatables (discussed below), and want the strongly typed row:
Dim dr as PersonDataRow = DirectCast(myDataRowView.Row, PersonDataRow)
A useful aspect of the list binding to the .DefaultView DataView is that it can have its own filtering and sorting setup:
Dim dv = dataSet.Tables("Person").DefaultView
dv.Sort = "[FullName] ASC"
dv.RowFilter = "[FullName] LIKE 'J*'"
For more info on these see the documentation for dataview
Now, sea change: you don't have to do any of this by hand. All this can be linked up and done by visual studio and there are compelling reasons for doing so. For the same reason you don't write your form codes by hand, manually laying out all your controls etc, you can visually design and maintain your data access layer
Add a new dataSet file to your project, open it, right click the design surface and choose to add a tableadapter, go through the wizard setting your connection string and choosing that it's a stored procedure that gives you the data. At the end of it you'll see a datatable representation and all the columns returned by your stored procedure
If you now show the data sources window when you're in your forms designer you'll see a node representing your table from your dataSet, and you can either drag that node onto the form to create a DataGridview that is hooked up leafy, or you can expand the node in the data sources window to see individual columns, you can change the type of control to create for that property and you can drop them on the form. (I can't remember if listbox is one of them, but I know that ComboBox is). Dropping them on the form simply creates a control, already named and wired up with the right DataSource, Member and DisplayMember properties set, and you can change these and other properties like the value member in the properties grid.
Most critically of a difference, the controls the designer creates are all bound through a device called a bindingsource - this tracks the row in the datatable you're currently looking at, and keeps data bound controls in sync. If one control such as a grid or list is capable of showing multiple rows, clicking on different rows changes the Current property of the binding source, causing other controls (that only render one data row's data) such as textboxes to update to the new Current row values
Thus list controls may operate in one or two modes: they either serve as a device that can navigate a datatable's rows allowing you to pick one of a few rows so that you can edit the values using other textboxes etc, or lists serve as a way of showing a set of values for the user to cope from and cause another datarow's property to update to that chosen value. The differentiation in these two modes comes from whether the selectedvalue property is bound to something else or not. As a simple example in the context I've been discussing already, we could have a dataSet with two tables; person and gender; the person table has a gender column single char M or F, and the gender table has two columns one is a single char M or F and the other a text column of Male or Female (I won't get into the cases for other genders right now but the system is easy to extend by adding more rows). You could then have a form where the person table plus its binding source is causing a list of people to show in the first list box and click in different items in the list causes all the other textboxes (FullName) on the form to change so you can edit those details. You can also have a second listbox bound to the gender table via its own bindingsource (or direct, doesn't matter) that has its DisplayMember set to the "Male/female" column, its value member set to the "m/f" column and it's SelectedValue property bound to the main Person bindingsource's Person.gender column. When you choose a person from the first list, the current char value of their Gender will be used to set the current item selected in the gender list but if you change the value in the gender combo/list then it will write the new selectedvalue back into the person.gender column
That's the 101 of Windows forms binding; I recommend adding a dataset to your project because it then gives you datatables that are specifically typed with named properties. Your code looks like this:
ForEach PersonDataRow r in myDataset.Person
r.Age += 1
Instead of this:
ForEach DataRow r in myDataset.Tables("Person").Rows
r("Age") = DirectCast(r("Age"), Integer) +1
I was mid-answer when jmcilhinney commented: DataBind() is specific to ASP.Net Web Forms server controls.
Therefore, the DataBind() call isn't necessary, simply setting the DataSource property on a Windows Forms ListBox is enough.
As for parsing your results from your SQL Server, don't forget to set the ListBox's DisplayMember and ValueMember properties to correctly display your data to the user.
Here's the documentation on the DataSource property. It has a very decent example.
For filling a list box, you usually don't need DataSets or DataAdapters. Just a DataTable table will do.
The Using...End Using blocks ensure that your database objects are closed and disposed.
You can pass the connection directly to the constructor of the command.
Set the list box data source to the DataTable. Then you can use the names of the fields in the Select statement to set the .DisplayMember and .ValueMember. The display and value can be the same.
Private Sub FillListBox(myParamValue As Integer)
Dim dt As New DataTable
Using connection As New SqlConnection("myConnectionSecret")
'Pretend your stored procedure has a statement like "Select DeptarmentID, DepartmentName From Departments Where SupervisorID = #myParam;"
Using command As New SqlCommand("myStoredProcedureName", connection)
command.CommandType = CommandType.StoredProcedure
command.Parameters().Add("#myParam", SqlDbType.Int).Value = myParamValue
connection.Open()
dt.Load(command.ExecuteReader)
End Using
End Using 'Closes and disposes the connection
ListBox1.DataSource = dt
ListBox1.DisplayMember = "DepartmentName"
ListBox1.ValueMember = "DepartmentID"
End Sub
I have a datagrid that gets an extra column with checkboxes added that is not in its source. I am trying to convert the grid that the user sees to a datatable when a save button is clicked. What I am trying to do is run it through a for loop and check if the rows checkbox is checked and the best solution seems to be converting it to a datatable and checking from there. Most of what I have seen to do the conversion is Dim dt As DataTable = DirectCast(DirectCast(dtgrd, DataGrid).DataSource, DataTable) which wont work for me since the checkbox values that I am checking for are not in the datasource. I am trying to avoid a million postbacks so I am not updating anything when the checkbox changes.
I made a windows form which contains a listbox (named VenueList). The listbox is pulling it's values from a list provided by a binding source VenuesBindingSource which is pulling in a ID for the value and a name for the text.
There is a button which is causing a DialogBox to appear which is asking for values to store in the database for a NEW venue. Once the values are filled and insert the database, what's supposed to happen is that the dialog box closes and goes back to the original form which invoked it.
However, instead of updating the list. The list stays the same. But if you close the form and reopen it, you see that a new value was added.
TournamentSettings.VenuesTableAdapter.InsertVenueQuery(Trim(VenueNameTxt.Text), Trim(VenueAddress1Txt.Text), Trim(VenueAddress2Txt.Text), Trim(VenueCityTxt.Text), Trim(VenueProvinceTxt.Text), Trim(VenueZipTxt.Text), Trim(CountryBox.SelectedValue), Trim(VenuePhoneNo.Text), VenueType.SelectedText, VenueWebAddress)
TournamentSettings.VenuesTableAdapter.Fill(TournamentSettings.VenueNameList.Venues)
In the above code, InsertVenueQuery is the name of a query from the designer which is invoked to add the values onto the tableadapter VenuesTableAdapter which is used to fill the combo box on load. I also sent the Fill command to refill the table with the new value.
So the question is, should I go about doing this another way, rather than feeding the Table adapter and sending a fill command on to the datatable? Or is there something that I'm not doing here which I should to force that value into the list. I'm tempted to redo everything outside of the designer but that's a lot of code since I have to essentially run two commands (one to insert the data, and another to get the ##IDENTITY value since this is run on an access database.)
Okay. This one I had to think about for a moment.
Instead of me creating a block of done on the load event, I instead created a sub function called "FillVenueList".
I used the following block of code:
Public Sub FillVenueList()
' Adding values from database to a datatable.
' From there will add to the list box.
Dim VenueConnection As New OleDb.OleDbConnection(DBconnection)
VenueConnection.Open()
Dim VenueConnectionQuery As New OleDb.OleDbCommand("SELECT VenueID, VenueName FROM Venues", VenueConnection)
Dim VenueDataAdapter As New OleDb.OleDbDataAdapter(VenueConnectionQuery)
Dim VenueDataSet As New DataSet
VenueDataAdapter.Fill(VenueDataSet, "Venues")
TrnVenueLst.DataSource = VenueDataSet.Tables("Venues")
TrnVenueLst.DisplayMember = "VenueName"
TrnVenueLst.ValueMember = "VenueID"
VenueConnection.Close()
End Sub
From there I called THIS sub on both the form AND the Add Venue window and I can safely see that this works. SO THAT is how you get a new value onto the form, don't use it as a part of the Load Event but rather call it from the load event block, then call it when you want to add to the list.
I have a bound DGV that took a bit of work to get its columns set up. I'd like to show a 1-row version of this identical DGV on a second windows form. Is there a way to programatically place a copy on the second form. I would adjust the height and position of the 1-row version, and create a new binding source on the second form so that I could filter the data.
MyForm.Controls.Add(myDataGridView)
So further explanation:
In your first for you will need to make a variable or property that contains a reference to the DataGridView that you want to access.
I'd suggest doing something like this.
Public Shared Property myDataGridView As DataGridView
then after you get it set up in the form the way you want it set up
myDataGridView = originalDataGridView
Then in the second form
SecondForm.Controls.Add(FirstForm.myDataGridView)
Will add the DataGridView exactly as it is on the first form.
Edit
If you are creating it in a designer, you can just either copy and past it from the original for to the second form.
Or just on the Form.Shown or in the New() of the first form set the myDataGridView to the DataGridView that you created.
I have a simple VB.NET 2008 app that helps users to edit fields in the database. Simple enough as a first "real" project in .NET, right?
For one table, I am currently using a DataGridView so it can be edited straight up. However, instead of offering the user the entire table, I'd like to group the data by the 'CompanyNumber' column and use a navigator to page through. In other words, I'd like the DataGridView to show me all the lines related to one company, then click the "next" arrow to show the next company, etc.
(I know I could do this with Xceed DataGrid, but I'm using Windows Forms not WPF, and I'd really prefer to do this with "pure" ADO.NET for this project.)
Update 2009-09-28:
So I have created a ComboBox filled from the same BindingSource, and configured its SelectedIndexChanged to change the Filter value on the DataGridView.
But still, filling the ComboBox--which should be easy!--continues to be a problem. I can either:
(a) fill it from the BindingSource, in which case I see multiples of each 'CompanyNumber' and I can't figure out a way to show only distinct values, or
(b) create another TableAdapter in the data source which is just a "Select DISTINCT CompanyNumber..." query, which mostly works, except that that first value of the list changes when I change the selection (e.g. if the ComboBox shows "100, 101, 102, 103" and I pick "102", then the list will show as "102, 101, 102, 103").
Any recommendations?
(Also, bonus if you can suggest how to make the BindingNavigator's arrows page through the 'CompanyNumber' filters instead of the items in the DataGridView... which is what I'd really like to see.)
What you could do is just force the DataGridView to sort CompanyName, this way all rows with the same company name are next to each other and the user can navigate the data grids with the paging that comes with it.
Alternatly, you could follow through with your combobox/DropDownList idea, which would be best. From what I understand when you select an item in the combobox everything in it changes?
Another way is to create two separate buttons, "Previous" "Next", that when clicked, will change the DataGridView's binding source to only show a certain company. You would need to store an array of company names, then store what the current DataGridView's binding source is displaying.
I ended up figuring it out myself, and the solution is clean and simple. Here are the basic steps:
create a DataView off of the table out of the DataSet
use the DataView.ToTable() method to create a new table filtered to only distinct values from the needed column ('CompanyNumber')
create a BindingSource which uses the new DataTable as its DataSource
bind the ComboBox to the new BindingSource
bind the BindingNavigator to the new BindingSource
Because the ComboBox and the BindingNavigator use the same BindingSource, they will update each other with the changes automagically.
Here's the rough code:
Private Sub CoNumsComboxBox_LoadData()
Dim dvCoNums As DataView, dtCoNums As DataTable
dvCoNums = New DataView(Me.ODBCDataSet.Tables("CompanyFundProfile"))
dvCoNums.Sort = "CompanyNumber ASC, FundNumber ASC"
dtCoNums = dvCoNums.ToTable("CompanyFundProfile", True, "CompanyNumber")
CoNums_BindingSource.DataSource = dtCoNums
CoNumsComboBox.DataSource = CoNums_BindingSource
CoNumsComboBox.DisplayMember = "CompanyNumber"
CoNumsComboBox.ValueMember = "CompanyNumber"
'attach handler which changes DataGridView1's filter when this changes
AddHandler ToolStripComboBox1.SelectedIndexChanged, AddressOf CoNumsComboBox_SelectedIndexChanged
CompanyFundProfile_BindingNavigator.BindingSource = CoNums_BindingSource
End Sub