Introduction
This article provides an approach to building an application that may be used to collect, store, and retrieve data without relying upon a database to get there. If you need to store a limited amount of data, if your users won't be sharing data with other users, and you do not require a full blown relational database on the backend, this article might be of interest.
The article is built around a demonstration project that allows the user to store some information gathered during bird observation; of course the same approach could be used to work with contact information, an inventory, postage stamp collection, coin collections, movie collections, or whatever else you might be interested in.
Figure 1: Application Main Form.
The application provides the following functionality:
- Create a bird data file.
- Add birds to the bird data file.
- Remove birds from the bird data file.
- Search for specific birds by name.
- Create and edit details about the bird.
- Bird name
- Bird gender
- Bird location
- Bird behavior
- Observation date
- Observation time
- Image of the bird
- Save a bird data file.
- Reopen a bird data file.
- Navigate through all of the birds in the bird data file.
- View a list of all birds in the bird data file.
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: Finding a Bird.
Figure 3: Listing All Birds.
Getting Started
There is a single solution included with this download, the solution contains a WinForms project called BirdWatcher
; this project contains three forms (the main form, a search form, and a form used to display the total list of birds), a serializable class called BirdData.cs (used to contain bird related data), and a class entitled FileSerializer.cs which contains two static
methods used to serialize and deserialize the bird 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 4: Solution Explorer.
Code: BirdData.cs
The BirdData
class is the container class used to store all of the bird related data used in the application. Whilst this demonstration uses bird 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.Drawing;
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 BirdWatcher
{
[Serializable]
public class BirdData
{
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 mBirdName;
private string mGender;
private string mLocation;
private string mBehaviorObserved;
private DateTime mDateViewed;
private DateTime mTimeViewed;
private byte[] mPicture;
private string mPictureName;
#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 a bird name as an argument and, when called, this constructor generates both an ID and assigns the bird name property to the bird name member variable.
#region Constructors
public BirdData()
{
mId = Guid.NewGuid();
}
public BirdData(string birdname)
{
mId = Guid.NewGuid();
mBirdName = birdname;
}
#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 BirdName
{
get
{
return mBirdName;
}
set
{
mBirdName = value;
}
}
public string Gender
{
get
{
return mGender;
}
set
{
mGender = value;
}
}
public string Location
{
get
{
return mLocation;
}
set
{
mLocation = value;
}
}
public string BehaviorObserved
{
get
{
return mBehaviorObserved;
}
set
{
mBehaviorObserved = value;
}
}
public DateTime DateViewed
{
get
{
return mDateViewed;
}
set
{
mDateViewed = value;
}
}
public DateTime TimeViewed
{
get
{
return mTimeViewed;
}
set
{
mTimeViewed = value;
}
}
public byte[] Picture
{
get
{
return mPicture;
}
set
{
mPicture = value;
}
}
public string PictureName
{
get
{
return mPictureName;
}
set
{
mPictureName = value;
}
}
#endregion
}
}
That concludes the description of the BirdData
class.
Code: Main Application Form (Form1.cs)
The Bird Tracker main form contains the following controls:
- Menu
- File
- New
- Open
- Save
- Save As
- Exit
- View
- Toolbar
- Add Bird
- Remove Bird
- Find Bird
- Save Bird Data
- Navigate to Previous Bird
- Navigate to Next Bird
- Exit Application
- Bird name text box control
- Bird gender combo box
- Bird location multiline text box
- Bird Behavior multiline text box
- Date of observation date time picker
- Time of observation date time picker
- Group container
- Picture box
- Set Picture button
Figure 5: Form1.cs.
The class begins with the normal and default imports:
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.IO;
using System.Windows.Forms;
The next section contains the namespace and class declarations.
namespace BirdWatcher
{
public partial class Form1 : 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 Variable Declarations
private List<BirdData> birds;
BirdData currentBird;
string currentFilePath;
int currentPosition;
bool dirtyForm;
#endregion
The next region of code in the class contains the constructor. Upon initialization, the application creates a new bird data list, creates a new bird data object, sets the date and time picker controls to the current data, sets the current position indicator to zero, and sets the dirty form Boolean
to false
.
#region Constructor
public Form1()
{
InitializeComponent();
birds = new List<BirdData>();
currentBird = new BirdData();
dtpDate.Value = DateTime.Now;
dtpTime.Value = DateTime.Now;
currentPosition = 0;
dirtyForm = false;
}
#endregion
The next code region is called Housekeeping
. This region contains some basic functionality used to operate the application. The first code section is for the Exit toolbar button click event handler; this function merely calls the exit menu item option’s event handler rather than duplicate the code in both functions.
#region Housekeeping
private void tsbExit_Click(object sender, EventArgs e)
{
exitToolStripMenuItem_Click(this, new EventArgs());
}
The next section of code is the exit menu option’s click event handler. This code first checks to see if the form is marked as dirty (based upon user changes made during the operation of the application); if the form is dirty and the user attempts to exit the application, users will be presented with a message box which will ask them whether or not they wish to exit the application without saving. If users indicates that they would prefer to save prior to exiting the application, the application will present the save file dialog to the user; otherwise the application will exit.
private void exitToolStripMenuItem_Click(object sender, EventArgs e)
{
if (dirtyForm == true)
{
if (MessageBox.Show(this, "You have not saved the current bird data;
" + "would you like to save before exiting?", "Save Current Data",
MessageBoxButtons.YesNo) == System.Windows.Forms.DialogResult.Yes)
{
saveToolStripMenuItem_Click(this, new EventArgs());
}
else
{
Application.Exit();
}
}
else
{
Application.Exit();
}
}
The next method is used to clear out all of the information displayed in the main form controls.
public void ClearForm()
{
dirtyForm = true;
txtBirdName.Text = string.Empty;
txtLocation.Text = string.Empty;
txtBehavior.Text = string.Empty;
cboGender.SelectedIndex = -1;
dtpDate.Value = DateTime.Now;
dtpTime.Value = DateTime.Now;
picBird.Image = null;
}
The next function contained in the form is used to set the image associated with the current bird. Since we are adding new data to the current instance of bird data, the form is marked as dirty – this will allow the functions used to exit the application to check to see if the current data needs to be saved prior to exiting the application. The function uses the open file dialog to allow the user to set a path to an image file; once a file path is set; the function opens the files, converts it into a byte array and stores it in the current bird data object’s image property. The function wraps up by setting the form’s image to display the selected bird picture.
private void btnSetImage_Click(object sender, EventArgs e)
{
dirtyForm = true;
string imageFilePath = string.Empty;
OpenFileDialog OpenFileDialog1 = new OpenFileDialog();
OpenFileDialog1.Title = "Open Image File";
OpenFileDialog1.Filter = "JPEG Documents (*.jpg)|*.jpg|Gif Files|*.gif";
if (OpenFileDialog1.ShowDialog() ==
System.Windows.Forms.DialogResult.Cancel)
{
return;
}
imageFilePath = OpenFileDialog1.FileName;
if (String.IsNullOrEmpty(imageFilePath))
{
return;
}
if (System.IO.File.Exists(imageFilePath) == false)
{
return;
}
byte[] bArrImage = new byte[0];
try
{
System.IO.FileStream fsImage = null;
fsImage = System.IO.File.Open(imageFilePath, FileMode.Open,
FileAccess.Read);
bArrImage = new byte[fsImage.Length];
fsImage.Read(bArrImage, 0, (int)fsImage.Length);
fsImage.Close();
currentBird.Picture = bArrImage;
currentBird.PictureName = imageFilePath;
MemoryStream ms = new MemoryStream(bArrImage);
picBird.Image = Image.FromStream(ms);
ms.Dispose();
}
catch(Exception ex)
{
MessageBox.Show(ex.Message, "Error Storing Image");
}
}
The next function is used to handle the ‘New’ menu option’s click event; this function begins by checking the dirty form Boolean
to see if the current data should be saved prior to starting the new bird data file. If the current form is dirty; the user will be prompted to save the current data, else, a new collection of bird data is created and the form is cleared of all prior entries.
private void newToolStripMenuItem_Click(object sender, EventArgs e)
{
if (dirtyForm == true)
{
if (MessageBox.Show(this, "You have not saved the current bird data;
" + "would you like to save before starting a new "
+ "bird database?", "Save Current Data",
MessageBoxButtons.YesNo) ==
System.Windows.Forms.DialogResult.Yes)
{
saveToolStripMenuItem_Click(this, new EventArgs());
}
else
{
birds = new List<BirdData>();
ClearForm();
}
}
else
{
birds = new List<BirdData>();
ClearForm();
}
}
The open file menu option’s click event handler is next; this function also checks for a dirty form prior to opening a new bird data file into the application. If the form is not dirty, the application will call the Open
function which will in turn allow the user to navigate to and open a file for viewing or edit.
private void openStripMenuItem2_Click(object sender, EventArgs e)
{
if (dirtyForm == true)
{
if (MessageBox.Show(this, "You have not saved the current bird data; "
+ "would you like to save before opening a different " +
"bird database?", "Save Current Data", MessageBoxButtons.YesNo) ==
System.Windows.Forms.DialogResult.Yes)
{
saveToolStripMenuItem_Click(this, new EventArgs());
}
else
{
Open();
}
}
else
{
Open();
}
}
Next up, the open
method uses the open file dialog control to allow the user to navigate to and select a bird data file. Once selected, the file is deserialized into the local bird collection and made available to the display.
public void Open()
{
OpenFileDialog OpenFileDialog1 = new OpenFileDialog();
OpenFileDialog1.Title = "Open BRD Document";
OpenFileDialog1.Filter = "BRD Documents (*.brd)|*.brd";
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;
}
birds = FileSerializer.Deserialize(currentFilePath);
if (birds != null)
{
currentBird = birds.ElementAt<BirdWatcher.BirdData>(0);
LoadCurrentBird();
dirtyForm = false;
}
}
The save menu option click event handler is up next. This function uses the save file dialog to allow the user to name or set the path to the file. Once the file path is set, the application serializes the data contained in the bird collection, saving it to disk.
private void saveToolStripMenuItem_Click(object sender, EventArgs e)
{
SaveCurrentBird();
if (String.IsNullOrEmpty(currentFilePath))
{
SaveFileDialog SaveFileDialog1 = new SaveFileDialog();
try
{
SaveFileDialog1.Title = "Save BRD Document";
SaveFileDialog1.Filter = "BRD Documents (*.brd)|*.brd";
if (SaveFileDialog1.ShowDialog() ==
System.Windows.Forms.DialogResult.Cancel)
{
return;
}
}
catch
{
return;
}
currentFilePath = SaveFileDialog1.FileName;
if (String.IsNullOrEmpty(currentFilePath))
{
return;
}
}
FileSerializer.Serialize(currentFilePath, birds);
MessageBox.Show("File " + currentFilePath + " saved.", "File Saved.");
dirtyForm = false;
}
The next bit of code is very similar to save; it allows the user to save the current bird data file to the local hardware using a replacement name.
private void saveAsToolStripMenuItem_Click(object sender, EventArgs e)
{
SaveFileDialog SaveFileDialog1 = new SaveFileDialog();
try
{
SaveFileDialog1.Title = "Save BRD Document";
SaveFileDialog1.Filter = "BRD Documents (*.brd)|*.brd";
if (SaveFileDialog1.ShowDialog() ==
System.Windows.Forms.DialogResult.Cancel)
{
return;
}
}
catch
{
return;
}
currentFilePath = SaveFileDialog1.FileName;
if (String.IsNullOrEmpty(currentFilePath))
{
return;
}
FileSerializer.Serialize(currentFilePath, birds);
MessageBox.Show("File " + currentFilePath + " saved.", "File Saved.");
dirtyForm = false;
}
The toolbar’s save button is up next; it merely calls the save menu item click event to save the bird data file.
private void tsbSave_Click(object sender, EventArgs e)
{
saveToolStripMenuItem_Click(this, new EventArgs());
}
The save current bird function is used to set the values of the current bird data object to match the content from the form controls.
private void SaveCurrentBird()
{
if(!String.IsNullOrEmpty(txtBirdName.Text))
{
try
{
currentBird.BirdName = txtBirdName.Text;
currentBird.Location = txtLocation.Text;
currentBird.BehaviorObserved = txtBehavior.Text;
currentBird.Gender = cboGender.Text;
currentBird.DateViewed = dtpDate.Value;
currentBird.TimeViewed = dtpTime.Value;
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "Error");
}
}
}
The next region is called Bird Data Management
; within this section are the methods used to manage the bird data and the bird data collection. The first function contained in this region is used to add a new bird to the collection. The function saves the current bird, creates a new empty bird object, adds the new bird data object to the birds
collection, and then marks the form as dirty.
#region Bird Data Management
private void tsbAdd_Click(object sender, EventArgs e)
{
SaveCurrentBird();
currentBird = new BirdData();
ClearForm();
birds.Add(currentBird);
dirtyForm = true;
}
The next function is used to remove a bird from the collection. After a bird is removed; the current position is updated and the form reloaded with the replacement bird information.
private void tsbRemoveBird_Click(object sender, EventArgs e)
{
birds.RemoveAt(currentPosition);
if (currentPosition == 0)
currentPosition++;
else
currentPosition--;
currentBird = birds[currentPosition];
LoadCurrentBird();
dirtyForm = true;
}
The next function is used to support finding a specific bird. When the click event handler is executed, a new instance of the find form (Form3
) is created and an event handler for the find form’s BirdNameUpdated
event is set.
private void tsbFindBird_Click(object sender, EventArgs e)
{
Form3 f = new Form3(birds);
f.BirdNameUpdated += new Form3.BirdNameUpdateHandler(FindBird);
f.Show();
}
The next function is used to find a specific bird by its name. When the find form raises the event indicating that the user wants the bird of a particular name found in the collection, this function will iterate through the collection until the match is found and then it will make that bird the current bird and update the form to display that bird’s information.
private void FindBird(object sender, BirdNameUpdateEventArgs e)
{
for (int i = 0; i < birds.Count; i++)
{
if (birds[i].BirdName == e.BirdName)
{
currentBird = birds[i];
LoadCurrentBird();
currentPosition = i;
}
}
}
The next function is used to display a form containing all of the birds contained in the current bird data list. Form2
contains a single data grid view control and it accepts a list of bird data as an argument for the constructor. When the form is created and passed a copy of the current bird data, that data is bound to the data grid view control and displayed to the user.
private void listAllBirdsToolStripMenuItem_Click(object sender, EventArgs e)
{
Form2 f = new Form2(birds);
f.Show();
}
The next function is called whenever the current bird is changed. It reloads all of the form controls from the current bird object.
private void LoadCurrentBird()
{
try
{
txtBirdName.Text = currentBird.BirdName;
txtLocation.Text = currentBird.Location;
txtBehavior.Text = currentBird.BehaviorObserved;
}
catch { }
try
{
cboGender.Text = currentBird.Gender;
}
catch { }
try
{
dtpDate.Value = currentBird.DateViewed;
}
catch { }
try
{
dtpTime.Value = currentBird.TimeViewed;
}
catch { }
try
{
if (currentBird.Picture != null)
{
MemoryStream ms = new MemoryStream(currentBird.Picture);
picBird.Image = Image.FromStream(ms);
ms.Dispose();
}
else
{
picBird.Image = null;
}
}
catch
{
picBird.Image = null;
}
}
The next region contained within this class contains two functions used to control navigation forwards and backwards through the bird data list. In either case, the current bird is saved by calling the SaveCurrentBird
function. After the current bird is saved locally, the position is checked to see if it is at the bottom or top limit and if possible, the position is incremented or decremented. Once the current position is updated, the current bird data is set to the bird data at the updated position and for the form reloaded.
#region Navigation
private void tsbNavBack_Click(object sender, EventArgs e)
{
SaveCurrentBird();
if (currentPosition != 0)
{
currentPosition--;
currentBird = birds[currentPosition];
LoadCurrentBird();
}
}
private void tsbNavForward_Click(object sender, EventArgs e)
{
SaveCurrentBird();
if (currentPosition < birds.Count - 1)
{
currentPosition++;
currentBird = birds[currentPosition];
LoadCurrentBird();
}
}
#endregion
The last region of code in this class is used to mark the form as dirty in response to changes. Note that, rather than using the value changed events to set the form dirty, other events such as the text box control’s key press event were used to provide an indication as to whether or not something was changed. The reason being that, if one were to open a bird data file and navigate through the list, the values would change and the form would be marked as dirty even though in reality no changes took place.
#region Dirty the Form
private void txtBirdName_KeyPress(object sender, KeyPressEventArgs e)
{
dirtyForm = true;
}
private void cboGender_MouseClick(object sender, MouseEventArgs e)
{
dirtyForm = true;
}
private void txtLocation_KeyPress(object sender, KeyPressEventArgs e)
{
dirtyForm = true;
}
private void txtBehavior_KeyPress(object sender, KeyPressEventArgs e)
{
dirtyForm = true;
}
private void dtpDate_CloseUp(object sender, EventArgs e)
{
dirtyForm = true;
}
private void dtpTime_CloseUp(object sender, EventArgs e)
{
dirtyForm = true;
}
#endregion
That wraps up the discussion of the code contained in the main form of the application.
Code: List All Birds Form (Form2.cs)
The form used to display the entire list of birds contained in the current bird data list is pretty simple. The form contains a single data grid view control which is used to display the bird data list. The constructor was modified to accept a bird data list as an argument, the constructor binds the data grid view control to the bird data list and hides the first column (containing a Guid
used to uniquely identify the record).
The code for this class is provided in total:
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 BirdWatcher
{
public partial class Form2 : Form
{
public Form2(List<BirdData> bd)
{
InitializeComponent();
dataGridView1.DataSource = bd;
dataGridView1.Columns[0].Visible = false;
}
}
}
That concludes the description of the Form2
class.
Code: Find Birds Form (Form3.cs)
This form is used to provide the user with a list of the names of all of the birds contained in the current bird data list. The user can view and select bird names from the list and then click on the form’s Find button; this will raise an event which will be used by the main form to load the matching bird into the form.
The class begins with the normal and default library imports, namespace declaration, and class declaration:
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 BirdWatcher
{
public partial class Form3 : Form
{
Following the class declaration, a delegate
and event
are declared; these are used to notify the main form whenever the user selects a bird from the form’s list and clicks the form’s Find button. The BirdNameUpdateEventArgs
is declared as a separate class; that class will be used to contain the selected bird name and to make that information available to the main form whenever the event is raised.
public delegate void BirdNameUpdateHandler(object sender,
BirdNameUpdateEventArgs e);
public event BirdNameUpdateHandler BirdNameUpdated;
The constructor was modified to accept a bird data list as an argument; whenever an instance of the form is created, the constructor will iterate through the bird data list and add each bird name to the listbox
control used to display the bird names.
public Form3(List<BirdData> bd)
{
InitializeComponent();
foreach (BirdData bird in bd)
listBox1.Items.Add(bird.BirdName);
}
The find button click event handler creates an instance of the event arguments and adds the listbox
control’s selected item to the event arguments. Once set, the event is raised and raising the event will force the main form to load the selected bird’s information into the form’s controls.
private void btnFind_Click(object sender, EventArgs e)
{
BirdNameUpdateEventArgs args =
new BirdNameUpdateEventArgs(listBox1.SelectedItem.ToString());
BirdNameUpdated(this, args);
}
}
The BirdNameUpdateEventArgs
class is used to contain the bird name and to facilitate passing the bird name to the main form whenever the bird name updated event is fired. This class could be expanded to contain greater numbers of properties.
public class BirdNameUpdateEventArgs : System.EventArgs
{
private string mBirdName;
public BirdNameUpdateEventArgs(string sBirdName)
{
this.mBirdName = sBirdName;
}
public string BirdName
{
get
{
return mBirdName;
}
}
}
}
That wraps up the discussion of the find form class (Form3.cs).
Summary
The article shows one approach that may be used to persist and manage data within a Winforms application without actually backing the application with a database. Such an approach might be useful in a number of different applications where the user does not share information with other users. Even though the example shows the approach used to manage a collection of information regarding birds, it could be used for any number of other purposes such as maintaining a library of fly patterns, or your favorite insect collection, inventories of tools or equipment, etc.
History
- 3rd June, 2008: Initial version