Introduction
This article provides an introduction to employing LINQ to Objects queries to support a simple WinForms application; the article addresses the construction of LINQ to Objects statements, and then goes on to describe how one might use LINQ to Objects within the context of an actual application.
The demonstration project included with the article is a simple contact manager which may be used to capture and store information about a person’s contacts in address book format. This demonstration application uses LINQ to Objects to manage, query, and order the list of contacts maintained by the application. The demonstration application also includes a dummy contact file with a collection of test contacts.
Figure 1: Application Main Form
The application provides the following functionality:
- Create a contact data file.
- Add contacts to the contact data file.
- Remove contacts from the contact data file.
- Search for specific contacts by last name.
- Create and edit details about the contact.
- First Name
- Middle Name
- Last Name
- Street
- City
- State
- Zip Code
- Home Phone
- Work Phone
- Cell Phone
- Email Address
- Save a contact data file.
- Reopen a contact data file.
- Navigate through all of the contacts in the contact data file.
- View a list of all contacts in the contact data file.
- Provide a Rolodex function (search by starting letter of last name).
Naturally, the approaches used within the application are representative of only one way of doing things; as with most things in the .NET world, there are several alternatives, and you can modify the code to work with the data using one of the other alternatives, if you prefer to do so.
Figure 2: Searching for a Contact by Last Name
Figure 3: Listing All Contacts (Edits to the grid are posted immediately to the List)
Figure 4: Rolodex Function
LINQ to Objects Statements
This section will discuss some of the common techniques used in LINQ to Objects statement construction. In a nutshell, LINQ to Objects provides the developer with the means to conduct queries against an in-memory collection of objects. The techniques used to query against such collections of objects are similar to but simpler than the approaches used to conduct queries against a relational database using SQL statements.
Anatomy of LINQ to Objects Statements
Example 1 – A Simple Select
This is an example of a very simple LINQ to Objects statement:
Public Sub Example1()
Dim tools() As String = {"Tablesaw", "Bandsaw", "Planer", "Jointer", _
"Drill", "Sander"}
Dim List = From t In tools _
Select t
Dim sb As New StringBuilder()
Dim s As String
For Each s In List
sb.Append(s + Environment.NewLine)
Next
MessageBox.Show(sb.ToString(), "Tools")
End Sub
In the example, an array of strings (tools
) is used as the collections object to be queried using LINQ to Objects; the LINQ to Objects query is:
Dim List = From t In tools _
Select t
In this example, an untyped variable “List
” is created, and all of the items contained in the string array are added to this object. The types are inferred (implicitly typed). For example, “t
” is a member of tools
; since it is known that tools
is a string array, the framework will infer that “t
” is also a string. Of course, this is not all that terrific since you can just iterate through the array to do essentially the same thing; however, you can create more complex queries with LINQ to Objects, and then the value of the LINQ library becomes more apparent.
If you were to create a project, add this bit of code to a method, and run it; the results would look like this:
Figure 5: Query Results
Example 2 – Select with a Where Clause
The next example shows a LINQ to Objects query that incorporates a where
clause. In this example, we start out with a collection of birds in the form of a string array; LINQ to Objects is used to query this string array to find and return a subset of the array in the form of all birds with names beginning with the letter “R”.
Public Sub Example2()
Dim Birds() As String = {"Indigo Bunting", "Rose Breasted Grosbeak", _
"Robin", "House Finch", "Gold Finch", _
"Ruby Throated Hummingbird", _
"Rufous Hummingbird", "Downy Woodpecker"}
Dim list = From b In Birds _
Where b.StartsWith("R") _
Select b
Dim sb As New StringBuilder()
Dim s As String
For Each s In list
sb.Append(s + Environment.NewLine)
Next
MessageBox.Show(sb.ToString(), "R Birds")
End Sub
If you were to run this query, the results would appear as follows (all birds with names beginning with the letter “R” are shown):
Figure 6: R Birds Query Results
Example 3 – Select with a Where Clause
In a slight variation to the previous query, this example looks for an exact match in its where
clause:
Public Sub Example3()
Dim Birds() As String = {"Indigo Bunting", "Rose Breasted Grosbeak", _
"Robin", "House Finch", "Gold Finch", _
"Ruby Throated Hummingbird", _
"Rufous Hummingbird", "Downy Woodpecker"}
Dim list = From b In Birds _
Where b = "Indigo Bunting" _
Select b
Dim sb As New StringBuilder()
Dim s As String
For Each s In list
sb.Append(s + Environment.NewLine)
Next
MessageBox.Show(sb.ToString(), "Bunting Birds")
End Sub
Running this code will result in the display of this message box:
Figure 7: Bird Query Results
Example 4 – Generating an Ordered List
In this query, the list of birds is alphabetized (using “Order By b Ascending
”):
Public Sub Example4()
Dim Birds() As String = {"Indigo Bunting", "Rose Breasted Grosbeak", _
"Robin", "House Finch", "Gold Finch", _
"Ruby Throated Hummingbird", _
"Rufous Hummingbird", "Downy Woodpecker"}
Dim list = From b In Birds _
Order By b Ascending _
Select b
Dim sb As New StringBuilder()
Dim s As String
For Each s In list
sb.Append(s + Environment.NewLine)
Next
MessageBox.Show(sb.ToString(), "Ordered Birds")
End Sub
Figure 8: Ordered Bird List Query Results
Example 5 – Working with a Custom Type
In this example, a typed list is created, populated, and then queried using LINQ to Objects.
Public Sub Example5()
Dim parts = New List(Of Parts)
Dim p1 As New Parts()
p1.PartNumber = 1
p1.PartDescription = "Cog"
parts.Add(p1)
Dim p2 As New Parts()
p2.PartNumber = 2
p2.PartDescription = "Widget"
parts.Add(p2)
Dim p3 As New Parts()
p3.PartNumber = 3
p3.PartDescription = "Gear"
parts.Add(p3)
Dim p4 As New Parts()
p4.PartNumber = 4
p4.PartDescription = "Tank"
parts.Add(p4)
Dim p5 = New Parts()
p5.PartNumber = 5
p5.PartDescription = "Piston"
parts.Add(p5)
Dim p6 As New Parts()
p6.PartNumber = 6
p6.PartDescription = "Shaft"
parts.Add(p6)
Dim p7 As New Parts()
p7.PartNumber = 7
p7.PartDescription = "Pulley"
parts.Add(p7)
Dim p8 As New Parts()
p8.PartNumber = 8
p8.PartDescription = "Sprocket"
parts.Add(p8)
Dim list = From p In parts _
Order By p.PartNumber Ascending _
Select p
Dim sb As New StringBuilder()
Dim pt As Parts
For Each pt In parts
sb.Append(pt.PartNumber.ToString() + ": " + _
pt.PartDescription.ToString() + _
Environment.NewLine)
Next
MessageBox.Show(sb.ToString(), "Parts List")
End Sub
The purpose of the query is merely to sort the parts list in the order of the part numbers. The results returned from this method are as follows:
Figure 9: Ordered Parts List Query
The Parts
class used as the type behind the parts list is as follows:
Public Class Parts
Private mPartNumber As Integer
Private mPartDescription As String
Public Sub New()
End Sub
Public Sub New(ByVal partNum As Integer, ByVal partDesc As String)
mPartNumber = partNum
mPartDescription = partDesc
End Sub
Public Property PartNumber() As Integer
Get
Return mPartNumber
End Get
Set(ByVal value As Integer)
mPartNumber = value
End Set
End Property
Public Property PartDescription() As String
Get
Return mPartDescription
End Get
Set(ByVal value As String)
mPartDescription = value
End Set
End Property
End Class
Example 6 – Searching a Typed List Using LINQ to Objects
In this example, a typed list is created (as in the previous example), populated, and then queried using LINQ to Objects. In this case, the query includes a where
clause that only returns matches where the part description begins with the letter “S”:
Dim list = From p In parts _
Where p.PartDescription.StartsWith("S") _
Order By p.PartNumber Ascending _
Select p
Dim sb As New StringBuilder()
Dim pt As Parts
For Each pt In list
sb.Append(pt.PartNumber.ToString() + ": " _
+ pt.PartDescription.ToString() + _
Environment.NewLine)
Next
MessageBox.Show(sb.ToString(), "Parts List")
Figure 10: Matching Parts Query Results
Example 7 – Searching a Typed List Using LINQ to Objects and Returning a Single Result
In this example, a typed list is created (as in the previous example), populated, and then queried using LINQ to Objects. In this case, a single result of type “Parts
” is returned:
Dim matchingPart = (From m In list _
Where m.PartNumber.Equals(5) _
Select m).Single()
MessageBox.Show(matchingPart.PartDescription, "Matching Part")
The results of this query are shown in the next figure.
Figure 11: Returning a Single Result
The preceding examples were intended to provide a simple overview as to how to conduct some basic queries against collections using LINQ to Objects; there are certainly a great number of more complex operations that can be executed using similar procedures (grouping, joins, and selects into a new custom type, etc.).
Getting Started
There is a single solution included with this download, the solution contains a WinForms project called “LinqToObjectsVB”; this project contains two forms (the main form (frmContactBook
) and a form used to display the total list of contacts (frmFullList
)), a serializable class called ‘Contact
’ (used to contain contact related data), and a class called ‘Serializer
’ which contains two static methods used to serialize and deserialize the contact data (writing it to and reading it from a file).
If you open the attached project into Visual Studio 2008, you should see the following in the Solution Explorer:
Figure 12: Solution Explorer
Code: Contact.vb
The Contact
class is the container class used to store all of the contact related data used in the application. Whilst this demonstration uses contact data, this could easily be replaced with something more useful to you.
The class begins with the normal and default imports:
Imports System
Imports System.Collections.Generic
Imports System.Linq
Imports System.Text
The next section contains the class declaration. Note that the class is declared as serializable; the Serializable
attribute indicates that the class can be serialized.
<Serializable()> _
Public Class Contact
The region defined in the class declares the member variables used internally by the class; member variables exposed externally are made accessible through public properties.
#Region "Member Variables"
Private mId As System.Guid
Private mFirstName As String
Private mMiddleName As String
Private mLastName As String
Private mStreet As String
Private mCity As String
Private mState As String
Private mZip As String
Private mEmail As String
Private mHousePhone As String
Private mWorkPhone As String
Private mCellPhone As String
Private mFax As String
#End Region
The next region of code in the class contains the constructors. Two constructors are defined. A default constructor creates a new instance of the class and assigns it an internal ID (as a GUID). The second constructor accepts an ID as an argument, and sets the contact’s internal ID to that value.
#Region "Constructor"
Public Sub New()
mId = Guid.NewGuid()
End Sub
Public Sub New(ByVal ID As System.Guid)
mId = ID
End Sub
#End Region
The last bit of code in this class is contained within the Properties region; this region contains all of the properties defined to access the member variables. Note that since the ID value is always set by the constructor, the property does not provide a public interface to set the GUID to a new value.
#Region "Properties"
Public Property FirstName() As String
Get
Return mFirstName
End Get
Set(ByVal value As String)
mFirstName = value
End Set
End Property
Public Property MiddleName() As String
Get
Return mMiddleName
End Get
Set(ByVal value As String)
mMiddleName = value
End Set
End Property
Public Property LastName() As String
Get
Return mLastName
End Get
Set(ByVal value As String)
mLastName = value
End Set
End Property
Public Property Street() As String
Get
Return mStreet
End Get
Set(ByVal value As String)
mStreet = value
End Set
End Property
Public Property City() As String
Get
Return mCity
End Get
Set(ByVal value As String)
mCity = value
End Set
End Property
Public Property State() As String
Get
Return mState
End Get
Set(ByVal value As String)
mState = value
End Set
End Property
Public Property ZipCode() As String
Get
Return mZip
End Get
Set(ByVal value As String)
mZip = value
End Set
End Property
Public Property Email() As String
Get
Return mEmail
End Get
Set(ByVal value As String)
mEmail = value
End Set
End Property
Public Property HousePhone() As String
Get
Return mHousePhone
End Get
Set(ByVal value As String)
mHousePhone = value
End Set
End Property
Public Property WorkPhone() As String
Get
Return mWorkPhone
End Get
Set(ByVal value As String)
mWorkPhone = value
End Set
End Property
Public Property CellPhone() As String
Get
Return mCellPhone
End Get
Set(ByVal value As String)
mCellPhone = value
End Set
End Property
Public Property Fax() As String
Get
Return mFax
End Get
Set(ByVal value As String)
mFax = value
End Set
End Property
#End Region
End Class
That concludes the description of the ‘Contact
’ class.
Code: Main Application Form (frmContactBook.vb)
The is the main form of the application; much of the code provides the framework for the application, and does not really pertain to LINQ to Objects. However, all of the code will be described herein to provide a proper context.
The contact application’s main form contains the following controls:
- Menu
- File
- Contacts
- Add Contact
- Remove Contact
- List All Contacts
- Toolbar
- Add
- Remove
- Find by Last Name
- Save Data
- Navigate to Previous Contact
- Navigate to Next Bird Contact
- Exit Application
- Split Container Left Hand Side
- Alphabet List
- Alphabetized Names List
- Split Container Right Hand Side
- First name text box control
- Middle name text box control
- Last name text box control
- Street text box control
- City text box control
- State text box control
- Zip code text box control
- Home phone number text box control
- Work phone number text box control
- Cell number text box control
- Fax number text box control
- Email address text box control
Figure 13: frmContactBook.vb
The class begins with the normal and default imports:
Imports System
Imports System.Collections.Generic
Imports System.ComponentModel
Imports System.Data
Imports System.Drawing
Imports System.Linq
Imports System.Text
Imports System.Windows.Forms
The next section contains the class declaration.
Public Class frmContactBook
The next region defined in the class declares the member variables used internally by the class; member variables exposed externally are made accessible through public properties. The comment adjacent to each declaration describes its purpose.
#Region "Member Variables"
Private contacts As New List(Of Contact)
Private currentContact As Contact
Private currentPosition As Integer
Private currentFilePath As String
Private dirtyForm As Boolean
#End Region
The next region of code in the class contains the constructor. Upon initialization, the application creates a new contact data list, creates a new contact data object, sets the current position indicator to zero, and sets the dirtyForm
boolean to False
.
#Region "Constructor"
Public Sub New()
InitializeComponent()
contacts = New List(Of Contact)
currentContact = New Contact()
contacts.Add(currentContact)
currentPosition = 0
dirtyForm = False
End Sub
#End Region
The next code region is called ‘Toolstrip Event Handlers’; the first event handler in this region is the Click
event handler for the Add button; this method merely calls the menu control’s Click
event handler, and the code contained in that event handler adds a new contact to the current contact data.
#Region "Toolstrip Event Handlers"
Private Sub tsbAddRecord_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles tsbAddRecord.Click
Me.addToolStripMenuItem_Click(Me, New EventArgs())
End Sub
The next Click
event handler is used to remove the current contact from the contact list when the user clicks the toolstrip’s Remove Record button; again, this method merely calls the matching menu item function.
Private Sub tsbRemoveRecord_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles tsbRemoveRecord.Click
Me.removeToolStripMenuItem_Click(Me, New EventArgs())
End Sub
The next handler is used to search for a specific contact using the contact’s last name. The code uses a LINQ to Objects query in order to find the first instance of a matching contact with that last name. The handler uses the Search Term text box control on the toolstrip to capture the last name, and it uses the Search button to execute the search. The code is annotated to describe what is going on in this method.
Private Sub tsbFindContact_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles tsbFindContact.Click
If (String.IsNullOrEmpty(tspSearchTerm.Text)) Then
MessageBox.Show("Enter a last name in the space proved.", _
"Missing Search Term")
Return
End If
Try
Dim foundGuy = _
(From contact In contacts _
Where contact.LastName = tspSearchTerm.Text _
Select contact).FirstOrDefault()
currentContact = foundGuy
currentPosition = contacts.IndexOf(currentContact)
LoadCurrentContact()
tspSearchTerm.Text = String.Empty
Return
Catch
MessageBox.Show("No matches were found", "Search Complete")
End Try
End Sub
The next handler saves the current contact list; this handler just calls the matching menu Click
event handler.
Private Sub tsbSave_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles tsbSave.Click
Me.saveStripMenuItem_Click(Me, New EventArgs())
End Sub
The next handler is used to navigate back one contact from the current position of the displayed contact. If the contact as at the lower limit, the button click is ignored.
Private Sub tsbNavBack_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles tsbNavBack.Click
SaveCurrentContact()
If (currentPosition <> 0) Then
currentPosition -= 1
currentContact = contacts(currentPosition)
LoadCurrentContact()
End If
End Sub
The next handler is used to navigate forward one contact from the current position of the displayed contact. If the contact is at the upper limit, the button click is ignored.
Private Sub tsbNavForward_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles tsbNavForward.Click
SaveCurrentContact()
If (currentPosition < contacts.Count - 1) Then
currentPosition += 1
currentContact = contacts(currentPosition)
LoadCurrentContact()
End If
End Sub
The next handler is used to exit the application. This handler merely calls the matching menu item Click
event handler.
Private Sub tsbExit_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles tsbExit.Click
Me.exitToolStripMenuItem_Click(Me, New EventArgs())
End Sub
#End Region
The next region contains the menu item Click
event handlers. The next menu item Click
event handler creates a new contact list; before following through with the creation of the new contact list, this handler checks to see if the current form is dirty to allow the user the opportunity to save before closing the current list. Following that, the contact list is replaced with a new contact list and the form’s controls are cleared.
#Region "Menu Item Click Event Handler"
Private Sub newToolStripMenuItem_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles newToolStripMenuItem.Click
If dirtyForm = True Then
If (MessageBox.Show(Me, "You have not saved the current contact data; " + _
"would you like to save before starting a new " + _
"contact database?", "Save Current Data",
MessageBoxButtons.YesNo) = _
System.Windows.Forms.DialogResult.Yes) Then
saveAsMenuItem_Click(Me, New EventArgs())
End If
Else
contacts = New List(Of Contact)
ClearScreen()
End If
End Sub
The next event handler is used to open a contacts file. Again, the handler checks for a dirty form and provides the user with an opportunity to save if the form is dirty. A separate Open
method is called to handle the actual file opening operation.
Private Sub openToolStripMenuItem_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles openToolStripMenuItem.Click
If dirtyForm = True Then
If (MessageBox.Show(Me, "You have not saved the current contact data; " + _
"would you like to save before opening a different " + _
"contact database?", "Save Current Data",
MessageBoxButtons.YesNo) = _
System.Windows.Forms.DialogResult.Yes) Then
saveAsMenuItem_Click(Me, New EventArgs())
End If
Else
Open()
End If
End Sub
The Save menu item is used to save the current contacts file to disk; the function first calls a “SaveCurrentContact
”, which is used to save the current contact to the current contact data list. Next, the function uses the Save File Dialog to capture a file name if none is currently set to the “currentFilePath
” variable, or, if the variable is set, it saves the file using that file path. The file is actually saved to disk when the call to serialize the file is made.
Private Sub saveStripMenuItem_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles saveStripMenuItem.Click
SaveCurrentContact()
If String.IsNullOrEmpty(currentFilePath) Then
Dim SaveFileDialog1 As New SaveFileDialog()
Try
SaveFileDialog1.Title = "Save CON Document"
SaveFileDialog1.Filter = "CON Documents (*.con)|*.con"
If (SaveFileDialog1.ShowDialog() = _
System.Windows.Forms.DialogResult.Cancel) Then
Return
End If
Catch
Return
End Try
currentFilePath = SaveFileDialog1.FileName
End If
If String.IsNullOrEmpty(currentFilePath) Then
Return
End If
Serializer.Serialize(currentFilePath, contacts)
MessageBox.Show("File " + currentFilePath + " saved.", "File Saved.")
dirtyForm = False
End Sub
The next bit of code is used to support the “Save As” menu item; the call is similar to the previous Save method, but straight opens the Save File Dialog to permit the user to name or rename the file.
Private Sub saveAsMenuItem_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles saveAsMenuItem.Click
SaveCurrentContact()
Dim SaveFileDialog1 As New SaveFileDialog()
Try
SaveFileDialog1.Title = "Save CON Document As"
SaveFileDialog1.Filter = "CON Documents (*.con)|*.con"
If (SaveFileDialog1.ShowDialog() = _
System.Windows.Forms.DialogResult.Cancel) Then
Return
End If
Catch
Return
End Try
currentFilePath = SaveFileDialog1.FileName
If String.IsNullOrEmpty(currentFilePath) Then
Return
End If
Serializer.Serialize(currentFilePath, contacts)
MessageBox.Show("File " + currentFilePath + " saved.", "File Saved.")
dirtyForm = False
End Sub
The next method exits the application but checks the dirtyForm
boolean prior to exiting, to give the user a chance to save their edits.
Private Sub exitToolStripMenuItem_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles exitToolStripMenuItem.Click
If dirtyForm = True Then
If (MessageBox.Show(Me, "You have not saved the current contact data; " + _
"would you like to save before exiting?", "Save Current
Data", _
MessageBoxButtons.YesNo) =
System.Windows.Forms.DialogResult.Yes) Then
tsbSave_Click(Me, New EventArgs())
End If
Else
Application.Exit()
End If
End Sub
The next method is used to add a new contact to the current list of contacts; this method saves the current contact to the open list of contacts, creates a new contact, and adds it to the list of contacts, clears the form, and marks the form as dirty:
Private Sub addToolStripMenuItem_Click(ByVal sender As System.Object, ByVal e As
System.EventArgs) Handles addToolStripMenuItem.Click
SaveCurrentContact()
currentContact = New Contact()
contacts.Add(currentContact)
ClearScreen()
dirtyForm = True
End Sub
The next method removes the current contact from the list and updates the display.
Private Sub removeToolStripMenuItem_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles removeToolStripMenuItem.Click
If contacts.Count = 0 Then
contacts.Remove(currentContact)
If (currentPosition = 0) Then
currentPosition += 1
End If
Else
currentPosition -= 1
currentContact = contacts(currentPosition)
LoadCurrentContact()
dirtyForm = True
End If
End Sub
The next method is used to open a separate form displaying all of the contacts in a DataGridView
control. This method constructs an ordered list, and passes it to a new instant of the “frmFullList
” class which binds the grid control to the list in its constructor.
Private Sub listAllContactsToolStripMenuItem_Click(ByVal sender As System.Object,
ByVal e As System.EventArgs) _
Handles
listAllContactsToolStripMenuItem.Click
Dim orderedCons = _
(From contact In contacts _
Order By contact.LastName Ascending, _
contact.FirstName Ascending, _
contact.MiddleName Ascending _
Select contact)
Dim f As New frmFullList(orderedCons.ToList())
f.Show()
End Sub
#End Region
The next region contains a garbage can collection of other methods, maintained in a region titled “Housekeeping”:
#Region "Housekeeping"
The first method contained in this section is used to clear all of the text boxes used to display contact information. This is called anytime the current contact is changed, to prevent remnants of one contact appearing in the display of a replacement contact.
Private Sub ClearScreen()
txtFirstName.Text = String.Empty
txtMiddleName.Text = String.Empty
txtLastName.Text = String.Empty
txtStreet.Text = String.Empty
txtCity.Text = String.Empty
txtState.Text = String.Empty
txtZipCode.Text = String.Empty
txtHousePhone.Text = String.Empty
txtWorkPhone.Text = String.Empty
txtCellPhone.Text = String.Empty
txtFax.Text = String.Empty
txtEmailAddress.Text = String.Empty
tslViewWho.Text = String.Empty
End Sub
The next method is used to load information contained in the current contact into the controls used to display contact information.
Private Sub LoadCurrentContact()
txtFirstName.Text = currentContact.FirstName
txtMiddleName.Text = currentContact.MiddleName
txtLastName.Text = currentContact.LastName
txtStreet.Text = currentContact.Street
txtCity.Text = currentContact.City
txtState.Text = currentContact.State
txtZipCode.Text = currentContact.ZipCode
txtHousePhone.Text = currentContact.HousePhone
txtWorkPhone.Text = currentContact.WorkPhone
txtCellPhone.Text = currentContact.CellPhone
txtFax.Text = currentContact.Fax
txtEmailAddress.Text = currentContact.Email
tslViewWho.Text = "Now Viewing " + txtFirstName.Text + " " + txtLastName.Text
End Sub
The next method captures all of the information currently on the form for the current contact, and writes it into the current contact’s properties. This is called whenever a contact is changed so that all edits to an existing contact are held within the local list until it can be written to disk. The method further updates the order of the contacts, and updates the contact list and displayed contact.
Private Sub SaveCurrentContact()
If (Not String.IsNullOrEmpty(txtFirstName.Text) And _
(Not String.IsNullOrEmpty(txtLastName.Text))) Then
Try
currentContact.FirstName = txtFirstName.Text
currentContact.MiddleName = txtMiddleName.Text
currentContact.LastName = txtLastName.Text
currentContact.Street = txtStreet.Text
currentContact.City = txtCity.Text
currentContact.State = txtState.Text
currentContact.ZipCode = txtZipCode.Text
currentContact.HousePhone = txtHousePhone.Text
currentContact.WorkPhone = txtWorkPhone.Text
currentContact.CellPhone = txtCellPhone.Text
currentContact.Fax = txtFax.Text
currentContact.Email = txtEmailAddress.Text
Dim orderedContacts = _
(From contact In contacts _
Order By contact.LastName Ascending, _
contact.FirstName Ascending, _
contact.MiddleName Ascending _
Select contact).ToList()
contacts = orderedContacts
currentPosition = contacts.IndexOf(currentContact)
LoadCurrentContact()
Catch ex As Exception
MessageBox.Show(ex.Message, "Error")
End Try
End If
End Sub
The next method is used to open and deserialize an existing contact file, making it available for edit and viewing within the application.
Public Sub Open()
Dim OpenFileDialog1 As New OpenFileDialog()
OpenFileDialog1.Title = "Open con Document"
OpenFileDialog1.Filter = "CON Documents (*.con)|*.con"
If OpenFileDialog1.ShowDialog() = System.Windows.Forms.DialogResult.Cancel
Then
Return
End If
currentFilePath = OpenFileDialog1.FileName
If String.IsNullOrEmpty(currentFilePath) Then
Return
End If
If System.IO.File.Exists(currentFilePath) = False Then
Return
End If
contacts = Serializer.Deserialize(currentFilePath)
Dim orderedContacts = _
(From contact In contacts _
Order By contact.LastName Ascending, _
contact.FirstName Ascending, _
contact.MiddleName Ascending _
Select contact).ToList()
contacts = orderedContacts
If contacts.Count > 0 Then
currentContact = contacts.ElementAt(0)
LoadCurrentContact()
dirtyForm = False
End If
End Sub
#End Region
The final region in this form class is used to handle the listbox control events. These controls are used to provide a Rolodex sort of functionality to the application. The listbox controls are loaded into the left hand split panel’s panel. The top listbox control displays all of the letters in the alphabet, whilst the lower listbox control is used to display all matching last names beginning with the letter selected in the upper listbox.
#Region "Listbox Event Handlers"
The first function handles the selected index changed event for the upper listbox containing all of the letters of the alphabet. When a new letter is selected, this method uses a simple LINQ to Objects query to find all contacts with last names beginning with the selected letter. The lower listbox is then cleared, and then the matches are formatted into a string, showing the contact’s last name, first name, and middle name, and each formatted string is then added to the lower listbox control.
Private Sub lstAlphas_SelectedIndexChanged(ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles lstAlphas.SelectedIndexChanged
Dim alpha As String = lstAlphas.SelectedItem.ToString()
If contacts.Count > 0 Then
Try
Dim alphaGroup = _
From contact In contacts _
Where contact.LastName.ToUpper().StartsWith(alpha) _
Select contact
lstNames.Items.Clear()
Dim con As Contact
For Each con In alphaGroup
lstNames.Items.Add(con.LastName + ", " + _
con.FirstName + " " + con.MiddleName)
Next
If (alphaGroup.Count < 1) Then
lstNames.Items.Clear()
lstNames.Items.Add("No matches were found")
End If
Catch
lstNames.Items.Clear()
lstNames.Items.Add("No matches were found")
End Try
End If
End Sub
The names listbox SelectedIndexChanged
event is handled in the next block of code. In it, the name string (last name, first name, middle name) is parsed and used in a LINQ to Objects query used to return a list of all matching names; the first found name is displayed in the contact form, and the index position is updated to support list navigation.
Private Sub lstNames_SelectedIndexChanged(ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles lstNames.SelectedIndexChanged
If (lstNames.SelectedItem.ToString().Trim() = "No matches were found") Then
Return
End If
Dim first As String = String.Empty
Dim middle As String = String.Empty
Dim last As String = String.Empty
Dim arr() As String = lstNames.SelectedItem.ToString().Trim().Split(",")
last = arr(0).Trim()
Dim arr2() As String = arr(1).ToString().Trim().Split(" ")
first = arr2(0).Trim()
Try
middle = arr2(1).Trim()
Catch
End Try
Try
Dim foundGuy = _
(From contact In contacts _
Where contact.FirstName.Equals(first) And _
contact.LastName.Equals(last) And _
contact.MiddleName.Equals(middle) _
Select contact).FirstOrDefault()
currentContact = foundGuy
currentPosition = contacts.IndexOf(currentContact)
LoadCurrentContact()
Return
Catch ex As Exception
MessageBox.Show(ex.Message, "Error Encountered")
End Try
End Sub
#End Region
End Class
Code: frmFullList.vb
This form class contains a DataGridView
control and a constructor which accepts a contact list (List(Of Contact)
) as an argument. Upon initialization, the list is bound to the DataGridView
control.
Changes made by edits in the grid are maintained in the contact list.
There is not much code; it is presented here in its entirety:
Imports System
Imports System.Collections.Generic
Imports System.ComponentModel
Imports System.Data
Imports System.Drawing
Imports System.Linq
Imports System.Text
Imports System.Windows.Forms
Public Class frmFullList
Public Sub New(ByVal cons As List(Of Contact))
InitializeComponent()
dgvFullList.DataSource = cons
End Sub
End Class
Summary
The article shows some simple examples of LINQ to Objects queries used in support of a sample application. LINQ to Objects may be used to generate more complex queries than are shown in the example; however, those demonstrated herein are representative of some of the more common tasks that one might choose to do within a similar application. Much of the code in the demonstration project was provided as a framework for the application, and was necessary to create an environment useful for testing some simple LINQ to Objects based queries.