Introduction
This article provides an introduction to employing LINQ to Objects queries to support a simple win forms 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 //Image missing in this article
(Edits to the grid are posted immediately to the List)
Figure 4: Rolodex Function // Image missing in this article
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:
string[] tools = { "Tablesaw", "Bandsaw", "Planer", "Jointer", "Drill",
"Sander" };
var list = from t in tools
select t;
StringBuilder sb = new StringBuilder();
foreach (string s in list)
{
sb.Append(s + Environment.NewLine);
}
MessageBox.Show(sb.ToString(), "Tools");
In the example, an array of strings (tools) is used as the collections objects to be queries using LINQ to Objects; the LINQ to Objects query is:
var 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".
string[] Birds = { "Indigo Bunting", "Rose Breasted Grosbeak", "Robin",
"House Finch", "Gold Finch", "Ruby Throated
Hummingbird","Rufous Hummingbird", "Downy Woodpecker"
};
var list = from b in Birds
where b.StartsWith("R")
select b;
StringBuilder sb = new StringBuilder();
foreach (string s in list)
{
sb.Append(s + Environment.NewLine);
}
MessageBox.Show(sb.ToString(), "R Birds");
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:
string[] Birds = { "Indigo Bunting", "Rose Breasted Grosbeak", "Robin",
"House Finch", "Gold Finch", "Ruby Throated
Hummingbird","Rufous Hummingbird", "Downy Woodpecker"
};
var list = from b in Birds
where b == "House Finch"
select b;
StringBuilder sb = new StringBuilder();
foreach (string s in list)
{
sb.Append(s + Environment.NewLine);
}
MessageBox.Show(sb.ToString(), "Found Bird");
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 "orderby b ascending"):
string[] Birds = { "Indigo Bunting", "Rose Breasted Grosbeak", "Robin",
"House Finch", "Gold Finch", "Ruby Throated
Hummingbird","Rufous Hummingbird", "Downy Woodpecker"
};
var list = from b in Birds
orderby b ascending
select b;
StringBuilder sb = new StringBuilder();
foreach (string s in list)
{
sb.Append(s + Environment.NewLine);
}
MessageBox.Show(sb.ToString(), "Alphabetized Birds");
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.
List<Parts> parts = new List<Parts>();
Parts p1 = new Parts();
p1.PartNumber = 1;
p1.PartDescription = "Cog";
parts.Add(p1);
Parts p2 = new Parts();
p2.PartNumber = 2;
p2.PartDescription = "Widget";
parts.Add(p2);
Parts p3 = new Parts();
p3.PartNumber = 3;
p3.PartDescription = "Gear";
parts.Add(p3);
Parts p4 = new Parts();
p4.PartNumber = 4;
p4.PartDescription = "Tank";
parts.Add(p4);
Parts p5 = new Parts();
p5.PartNumber = 5;
p5.PartDescription = "Piston";
parts.Add(p5);
Parts p6 = new Parts();
p6.PartNumber = 6;
p6.PartDescription = "Shaft";
parts.Add(p6);
Parts p7 = new Parts();
p7.PartNumber = 7;
p7.PartDescription = "Pulley";
parts.Add(p7);
Parts p8 = new Parts();
p8.PartNumber = 8;
p8.PartDescription = "Sprocket";
parts.Add(p8);
var list = from p in parts
orderby p.PartNumber ascending
select p;
StringBuilder sb = new StringBuilder();
foreach (Parts p in parts)
{
sb.Append(p.PartNumber + ": " + p.PartDescription +
Environment.NewLine);
}
MessageBox.Show(sb.ToString(), "Parts List");
The purpose the query is merely to sort the parts list in order of the part numbers. The results returned from this method are as follows:
Figure 9: Ordered Parts List Query
The parts class used in as the type behind the parts list is as follows:
public class Parts
{
private int mPartNumber;
private string mPartDescription;
public Parts()
{
}
public Parts(int partNum, string partDescr)
{
mPartNumber = partNum;
mPartDescription = partDescr;
}
public int PartNumber
{
get { return mPartNumber; }
set { mPartNumber = value; }
}
public string PartDescription
{
get { return mPartDescription; }
set { mPartDescription = value; }
}
}
Example 6 - Searching a List<T> 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 were the part description begins with the letter "S":
// only return parts starting with 'S'
var matchingParts = from m in parts
where m.PartDescription.StartsWith("S")
select m;
StringBuilder sb = new StringBuilder();
foreach (Parts p in matchingParts)
{
sb.Append(p.PartNumber + ": " + p.PartDescription +
Environment.NewLine);
}
MessageBox.Show(sb.ToString(), "Matching Parts List");
Figure 10: Matching Parts Query Results
Example 7 - Searching a List<T> 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, returns a single result of type "Parts":
var matchingPart = (from m in parts
where m.PartNumber.Equals(5)
select m).Single<Parts>();
MessageBox.Show(matchingPart.PartDescription, "Matching Part");
One may also use this approach to return a single value from a query:
var matchingPart = (from m in parts
where m.PartNumber.Equals(5)
select m.PartDescription).Single<String>();
MessageBox.Show(matchingPart, "Matching Part");
Both approaches yield similar results as 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 Win Forms project called "LinqToObjects"; 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 entitled, '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.cs
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:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
The next section contains the namespace and class declarations. Note that the class is declared as serializable; the serializable attribute indicates that the class can be serialized.
namespace LinqToObjects
{
[Serializable]
public class Contact
{
The region defined in the class declares the member variables used internally by the class; any member variables exposed externally are made accessible through public properties.
#region Member Variables
private Guid mId;
private string mFirstName;
private string mMiddleName;
private string mLastName;
private string mStreet;
private string mCity;
private string mState;
private string mZip;
private string mEmail;
private string mHousePhone;
private string mWorkPhone;
private string mCellPhone;
private string mFax;
#endregion
The next region of code in the class contains the constructors. Two constructors are defined; a default constructor that 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 Contact()
{
mId = Guid.NewGuid();
}
public Contact(Guid ID)
{
mId = ID;
}
#endregion
The last bit of the 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 Guid ID
{
get
{
return mId;
}
}
public string FirstName
{
get
{
return mFirstName;
}
set
{
mFirstName = value;
}
}
public string MiddleName
{
get
{
return mMiddleName;
}
set
{
mMiddleName = value;
}
}
public string LastName
{
get
{
return mLastName;
}
set
{
mLastName = value;
}
}
public string Street
{
get
{
return mStreet;
}
set
{
mStreet = value;
}
}
public string City
{
get
{
return mCity;
}
set
{
mCity = value;
}
}
public string State
{
get
{
return mState;
}
set
{
mState = value;
}
}
public string ZipCode
{
get
{
return mZip;
}
set
{
mZip = value;
}
}
public string Email
{
get
{
return mEmail;
}
set
{
mEmail = value;
}
}
public string HousePhone
{
get
{
return mHousePhone;
}
set
{
mHousePhone = value;
}
}
public string WorkPhone
{
get
{
return mWorkPhone;
}
set
{
mWorkPhone = value;
}
}
public string CellPhone
{
get
{
return mCellPhone;
}
set
{
mCellPhone = value;
}
}
public string Fax
{
get
{
return mFax;
}
set
{
mFax = value;
}
}
#endregion
}
}
That concludes the description of the 'Contact' class.
Code: Main Application Form (frmContactBook.cs)
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
- New
- Open
- Save
- Save As
- Exit
- 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.cs
The class begins with the normal and default imports:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
The next section contains the namespace and class declarations.
namespace LinqToObjects
{
public partial class frmContactBook : Form
{
The region defined in the class declares the member variables used internally by the class; any member variables exposed externally are made accessible through public properties. The comment adjacent to each declaration describes its purpose.
#region Member Variables
List<Contact> contacts; // create a typed list of contacts
Contact currentContact; // create a single contact instance
int currentPosition; // used to hold current position
string currentFilePath; // file path to current contact file
bool dirtyForm; // keep track of dirty forms
#endregion
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 dirty form Boolean to false.
#region Constructor
/// <summary>
/// Constructor
/// </summary>
public frmContactBook()
{
InitializeComponent();
// initialize a new set of contact data
// in case the user is starting a new
// file; replaces this if the user
// opens an existing file
contacts = new List<Contact>();
currentContact = new Contact();
contacts.Add(currentContact);
currentPosition = 0;
dirtyForm = false;
}
#endregion
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
/// <summary>
/// Add a new contact
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void tsbAdd_Click(object sender, EventArgs e)
{
addToolStripMenuItem_Click(this, new EventArgs());
}
The next click event handler is used to exit the application when the user clicks the toolstrip's exit button; again, this method merely calls the matching menu item function.
/// <summary>
/// Exit the application
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void tsbExit_Click(object sender, EventArgs e)
{
exitToolStripMenuItem_Click(this, new EventArgs());
}
The next click event handler does a save of the current contact data file to disk; again, this handler just calls the matching menu click event handler.
/// <summary>
/// Save the current contacts file
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void tsbSave_Click(object sender, EventArgs e)
{
saveStripMenuItem_Click(this, new EventArgs());
}
The next handler removes the current contact from the contact data list; again, this handler just calls the matching menu click event handler.
/// <summary>
/// Remove the current record
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void tsbRemoveRecord_Click(object sender, EventArgs e)
{
removeToolStripMenuItem_Click(this, new EventArgs());
}
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.
/// <summary>
/// Find a specific contact by the contact's
/// last name
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void tsbFindContact_Click(object sender, EventArgs e)
{
// return if the search term was not provided
if (String.IsNullOrEmpty(tspSearchTerm.Text))
{
MessageBox.Show("Enter a last name in the space proved.", "Missing
Search Term");
return;
}
try
{
// using linq to objects query to get first matching name
var foundGuy =
(from contact in contacts
where contact.LastName == tspSearchTerm.Text
select contact).FirstOrDefault<Contact>();
// set the current contact to the found contact
currentContact = foundGuy;
currentPosition = contacts.IndexOf(currentContact);
// update the display by loading the
// found contact
LoadCurrentContact();
// clear the search term textbox and return
tspSearchTerm.Text = string.Empty;
return;
}
catch
{
MessageBox.Show("No matches were found", "Search Complete");
}
}
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.
/// <summary>
/// Navigate back to the previous record
/// if not at the lower limit
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void tsbNavBack_Click(object sender, EventArgs e)
{
// capture form changes and plug them
// into the current contact before
// navigating off the contact
SaveCurrentContact();
// don't exceed the left limit
if (currentPosition != 0)
{
currentPosition--;
currentContact = contacts[currentPosition];
LoadCurrentContact();
}
}
The next handler is used to navigate forward one contact from the current position of the displayed contact. If the contact as at the upper limit; the button click is ignored.
/// <summary>
/// Navigate to the next record if not at the
/// upper limit
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void tsbNavForward_Click(object sender, EventArgs e)
{
// capture form changes and plug them
// into the current contact before
// navigating off the contact
SaveCurrentContact();
// don't exceed the right limit
if (currentPosition < contacts.Count - 1)
{
currentPosition++;
currentContact = contacts[currentPosition];
LoadCurrentContact();
}
}
#endregion
The next region contains the menu item click event handlers. The first menu item is used to add a new contact to the current contact list. This method calls the function "SaveCurrentContact" which saves any entries currently made to the form to the current contact object; it then creates a new instance of a contact, adds the new contact to the contact list, clears the form, and marks the form as dirty since a new contact was added thus changing the list.
#region Menu Item Click Event Handler
/// <summary>
/// Add a new contact to the current
/// contact list and update the
/// display
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void addToolStripMenuItem_Click(object sender, EventArgs e)
{
SaveCurrentContact();
currentContact = new Contact();
contacts.Add(currentContact);
ClearScreen();
dirtyForm = true;
}
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.
/// <summary>
/// Create a new contact file but check for
/// a dirty form first and allow the user to save
/// if the current data has changed but not
/// been saved.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void newToolStripMenuItem_Click(object sender, EventArgs e)
{
if (dirtyForm == true)
{
if (MessageBox.Show(this, "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)
{
saveAsMenuItem_Click(this, new EventArgs());
}
else
{
// discard and start new document
contacts = new List<Contact>();
ClearScreen();
}
}
else
{
// start new document
contacts = new List<Contact>();
ClearScreen();
}
}
The next event handler is used to open a contacts file. Again, the handler checks to 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 actually file opening operation.
/// <summary>
/// Open an existing contact data file but
/// first check for a dirty form and allow the
/// user the opportunity to save it before
/// leaving the current contact file
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void openToolStripMenuItem_Click(object sender, EventArgs e)
{
if (dirtyForm == true)
{
if (MessageBox.Show(this, "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)
{
saveAsMenuItem_Click(this, new EventArgs());
}
else
{
Open();
}
}
else
{
Open();
}
}
The next handler exits the application after again checking to see if the form is dirty.
/// <summary>
/// Exit the application but first check for
/// a dirty form and allow the user to save the file
/// before leaving the application
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void exitToolStripMenuItem_Click(object sender, EventArgs e)
{
if (dirtyForm == true)
{
if (MessageBox.Show(this, "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)
{
tsbSave_Click(this, new EventArgs());
}
else
{
Application.Exit();
}
}
else
{
Application.Exit();
}
}
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.
/// <summary>
/// Save the current file; if the file is
/// new, open the save file dialog, else
/// just save the existing file
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void saveStripMenuItem_Click(object sender, EventArgs e)
{
SaveCurrentContact();
if (String.IsNullOrEmpty(currentFilePath))
{
SaveFileDialog SaveFileDialog1 = new SaveFileDialog();
try
{
SaveFileDialog1.Title = "Save CON Document";
SaveFileDialog1.Filter = "CON Documents (*.con)|*.con";
if (SaveFileDialog1.ShowDialog() ==
System.Windows.Forms.DialogResult.Cancel)
{
return;
}
}
catch
{
return;
}
currentFilePath = SaveFileDialog1.FileName;
if (String.IsNullOrEmpty(currentFilePath))
{
return;
}
}
// persist the contacts file to disk
Serializer.Serialize(currentFilePath, contacts);
// tell the user the file was saved
MessageBox.Show("File " + currentFilePath + " saved.", "File Saved.");
// everything is saved, set the dirtyform
// boolean to false
dirtyForm = false;
}
The next bit of code us used to support the "Save As" menu item; the call is similar to the previous save method but straight opens the Save File dialog box to permit the user to name or rename the file.
/// <summary>
/// Open the save file dialog to allow the user
/// to name or rename the current file and to save
/// the file with the new name
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void saveAsMenuItem_Click(object sender, EventArgs e)
{
SaveFileDialog SaveFileDialog1 = new SaveFileDialog();
try
{
SaveFileDialog1.Title = "Save CON Document";
SaveFileDialog1.Filter = "CON Documents (*.con)|*.con";
if (SaveFileDialog1.ShowDialog() ==
System.Windows.Forms.DialogResult.Cancel)
{
return;
}
}
catch
{
return;
}
currentFilePath = SaveFileDialog1.FileName;
if (String.IsNullOrEmpty(currentFilePath))
{
return;
}
// persist the contacts file to disk
Serializer.Serialize(currentFilePath, contacts);
// tell the user the file was saved
MessageBox.Show("File " + currentFilePath + " saved.", "File Saved.");
// everything is saved, set the dirtyform
// boolean to false
dirtyForm = false;
}
The next method removes the current contact from the contact list and updates the position of the current contact.
/// <summary>
/// Delete the current contact and update
/// the display
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void removeToolStripMenuItem_Click(object sender, EventArgs e)
{
// make sure there are records
if (contacts.Count == 0)
{
// remove the current record
contacts.Remove(currentContact);
// check to see if the current
// position is at the limit
// and move up or down
// as required
if (currentPosition == 0)
currentPosition++;
else
currentPosition--;
// reload the current contact
// from the new position
currentContact = contacts[currentPosition];
LoadCurrentContact();
// dirty the form since a
// record was removed
dirtyForm = true;
}
}
The next method is used to alphabetize the contacts and pass the resulting list to a new instance of the form used to display all of the contacts in a data grid view control. The current contact list is alphabetized by last name, first name, and middle name.
/// <summary>
/// Create an ordered list of contacts and display that list
/// in a datagrid
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void listAllContactsToolStripMenuItem_Click(object sender,
EventArgs e)
{
// use linq to objects to create a list of contacts
// ordered by the contact's last name, first name,
// and middle name
var orderedCons =
(from contact in contacts
orderby contact.LastName ascending,
contact.FirstName ascending,
contact.MiddleName ascending
select contact);
// create an instance of the full list form and pass it's
// constructor the list converted to a List<Contact>
frmFullList f = new frmFullList(orderedCons.ToList<Contact>());
f.Show();
}
#endregion
The next region contains a garbage can collection of other methods maintained in a region entitled "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 the display of a replacement contact.
/// <summary>
/// Clear all the values currently
/// held in the contact display
/// area's textboxes
/// </summary>
private void 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;
}
The next method is used to load the information contained in the current contact into the controls used to display contact information.
/// <summary>
/// Display the current contact's
/// information in the contact
/// display area
/// </summary>
private void LoadCurrentContact()
{
// update the form fields
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;
// display the current user in the status bar
tslViewWho.Text = "Now Viewing " +
txtFirstName.Text + " " + txtLastName.Text;
}
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.
/// <summary>
/// Save the current contacts information from the
/// textboxes contained in the contact display area
/// </summary>
private void SaveCurrentContact()
{
if (!String.IsNullOrEmpty(txtFirstName.Text) &&
(!String.IsNullOrEmpty(txtLastName.Text)))
{
try
{
// get all of the textbox values and
// plug them into the current contact object
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;
// reorder the contacts by last, first, and
// middle name to keep everything in correct
// alphabetical order
var orderedContacts =
(from contact in contacts
orderby contact.LastName ascending,
contact.FirstName ascending,
contact.MiddleName ascending
select contact).ToList<Contact>();
// set the contacts list to the newly
// ordered list
contacts = orderedContacts;
// update the current position index value
currentPosition = contacts.IndexOf(currentContact);
// reload the current contact
LoadCurrentContact();
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "Error");
}
}
}
The next method is used to open and deserialize an existing contact file, making it available for edit and viewing within the application.
/// <summary>
/// Open a contacts file and display the contact
/// information in the application
/// </summary>
public void Open()
{
OpenFileDialog OpenFileDialog1 = new OpenFileDialog();
OpenFileDialog1.Title = "Open con Document";
OpenFileDialog1.Filter = "CON Documents (*.con)|*.con";
if (OpenFileDialog1.ShowDialog() ==
System.Windows.Forms.DialogResult.Cancel)
{
return;
}
currentFilePath = OpenFileDialog1.FileName;
if (String.IsNullOrEmpty(currentFilePath))
{
return;
}
if (System.IO.File.Exists(currentFilePath) == false)
{
return;
}
// deserialize file content into contacts
// list to make it available to the application
contacts = Serializer.Deserialize(currentFilePath);
// alphabetize the contact list
// by last, first, and middle name and
// push the results into a List<T>
var orderedContacts =
(from contact in contacts
orderby contact.LastName ascending,
contact.FirstName ascending,
contact.MiddleName ascending
select contact).ToList<Contact>();
// set the contacts to the ordered
// version of the contact list
contacts = orderedContacts;
// Load contacts at position zero
// if contacts list is not empty
if (contacts != null)
{
currentContact = contacts.ElementAt<Contact>(0);
LoadCurrentContact();
dirtyForm = false;
}
}
#endregion
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 then 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.
/// <summary>
/// Display matching contacts whose last name begins
/// with the letter selected from the alphabet
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void lstAlphas_SelectedIndexChanged(object sender, EventArgs e)
{
string alpha = lstAlphas.SelectedItem.ToString();
if (contacts != null)
{
try
{
// use linq to objects query to find
// last names matching the selected
// letter of the alphabet
var alphaGroup =
from contact in contacts
where contact.LastName.ToUpper().StartsWith(alpha)
select contact;
// clear out any names from the
// existing list
lstNames.Items.Clear();
// add the short list of matching
// names to the list box
foreach (var con in alphaGroup)
lstNames.Items.Add(con.LastName + ", " +
con.FirstName + " " + con.MiddleName);
// if not matches were found, tell the user
// with a note in the box
if (alphaGroup.Count<Contact>() < 1)
{
lstNames.Items.Clear();
lstNames.Items.Add("No matches were found");
}
}
catch
{
lstNames.Items.Clear();
lstNames.Items.Add("No matches were found");
}
}
}
The the names listbox selected index changed 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 the list navigation.
/// <summary>
/// Find the matching contact for the name picked
/// from this list box and display that contact's
/// information in the contact display area
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void lstNames_SelectedIndexChanged(object sender, EventArgs e)
{
// if there were no matches found, return from this function
if (lstNames.SelectedItem.ToString().Trim() == "No matches were found")
return;
// variables to hold parts of the name as search terms
string first, middle, last;
// get the last name
string[] arr = lstNames.SelectedItem.ToString().Trim().Split(',');
last = arr[0].Trim();
// get the first name
string[] arr2 = arr[1].ToString().Trim().Split(' ');
first = arr2[0].Trim();
// get the middle name
middle = arr2[1].Trim();
// cannot complete the query without the three values
// so return if the information is missing
if (String.IsNullOrEmpty(last) ||
String.IsNullOrEmpty(first) ||
String.IsNullOrEmpty(middle))
{
MessageBox.Show("This query requires a first, middle, and a last
name.",
"Missing Name Values");
return;
}
try
{
// using linq to objects query to get a collection of matching names
// when all three names match
var foundGuy =
(from contact in contacts
where contact.FirstName.Equals(first) &&
contact.LastName.Equals(last) &&
contact.MiddleName.Equals(middle)
select contact).FirstOrDefault<Contact>();
// set the current contact to the first found
// contact
currentContact = foundGuy;
// update the index position used to maintain
// the current position within the list
currentPosition = contacts.IndexOf(currentContact);
// reload the current contact and return
LoadCurrentContact();
return;
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "Error Encountered");
}
}
#endregion
Code: frmFullList.cs
This form class contains a data grid view control and a constructor which accepts a contact list (List<Contact>) as an argument. Upon initialization the list is bound to the data grid view 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:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
namespace LinqToObjects
{
public partial class frmFullList : Form
{
public frmFullList(List<Contact> cons)
{
InitializeComponent();
dgvFullList.DataSource = cons;
dgvFullList.Columns[0].Visible = false;
}
}
}
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.