Exploring the TimeCheck Application (Part 14)

Today we’ll wrap up the discussion about frmConfigure. If you remember from the Exploring the TimeCheck Application (Part 13) post, we’ve finished up the major configuration controls, but there are three areas that you still need to know about:

 

  • Constructors
  • Form Loading
  • Form Closing


All three of these elements appear in this post. Let’s begin with the constructors. The application can either create frmConfigure by defining a blank form or by passing information needed to populate the form with data. The following code shows both of the constructors needed to fulfill these tasks:

// Create a variable to hold the user data.
public UserSettings UserData;
 
// Create a variable to hold the group data.
public GroupSettings GroupData;
 
public frmConfigure()
{
   // Perform the default configuration.
   InitializeComponent();
 
   // Check for the user data.
   if (UserData == null)
 
      // Initialize the user data.
      UserData = new UserSettings();
 
   // Check for the group data.
   if (GroupData == null)
 
      // Initialize the group data.
      GroupData = new GroupSettings();
 
   // Fill the administrator provided task and project lists.
   FillLists();
}
 
public frmConfigure(UserSettings UserData, GroupSettings GroupData)
{
   // Perform the default configuration.
   InitializeComponent();
 
   // Check for the user data.
   if (UserData == null)
 
      // Initialize the user data.
      this.UserData = new UserSettings();
 
   // Otherwise, use the supplied settings.
   else
      this.UserData = UserData;
 
   // Check for the group data.
   if (GroupData == null)
 
      // Initialize the group data.
      this.GroupData = new GroupSettings();
 
   // Otherwise, use the supplied settings.
   else
      this.GroupData = GroupData;
 
   // Fill the administrator provided task and project lists.
   FillLists();
}

In both cases, the constructor must create the UserData and GroupData local variables that you’ve seen used for so many purposes in the application. When the caller supplies the required data, frmConfigure uses it. Otherwise, frmConfigure creates new, blank variables that the application can fill with data. No matter how the data are supplied, the constructors call FillLists() to fill the user interface with data so that the user can see the settings (if any).

The form isn’t completely populated with data, nor is it stable at this point. The application raises the FormLoad() event at some point, which calls this event handler:

private void frmConfigure_Load(object sender, EventArgs e)
{
   // When there is user data to use, display it.
   if (UserData != null)
   {
      // Configure the form fields.
      cbProjectName.SelectedText = UserData.DefaultProject;
      cbWorkType.SelectedText = UserData.DefaultTask;
      txtNetLocation.Text = UserData.NetworkPath;
      chkCustomProject.Checked = GroupData.CustomProject;
      chkCustomWork.Checked = GroupData.CustomTask;
   }
}

At this point, the form is ready for the user to interact with. We’ll be looking at some additional code later to handle the requirements for working with administrators, versus standard users. For now, just consider that the form is ready to use. The user does whatever he/she needs to do, and then clicks either Cancel or OK to close the form. Now, some actions automatically save data because you simply don’t want to take a chance that the user will close the form in some other way and lose settings. However, it’s important to save the data one last time when the user clicks OK to ensure absolutely every change appears on disk. That’s the purpose of the two event handlers shown here.

private void btnCancel_Click(object sender, EventArgs e)
{
   // Close this dialog box.
   Close();
}
 
private void btnOK_Click(object sender, EventArgs e)
{
   // Save the changes to the user data.
   if (cbProjectName.SelectedText != "")
      UserData.DefaultProject = cbProjectName.SelectedText;
   if (cbWorkType.SelectedText != "")
      UserData.DefaultTask = cbWorkType.SelectedText;
   UserData.NetworkPath = txtNetLocation.Text;
   UserSettings.SetUserSettings(UserData);
 
   // Save the changes to the group data.
   GroupData.CustomProject = chkCustomProject.Checked;
   GroupData.CustomTask = chkCustomWork.Checked;
   GroupData.ProjectList.Clear();
   foreach (String Item in lstStandardProjects.Items)
      GroupData.ProjectList.Add(new Project(Item));
   GroupData.TaskList.Clear();
   foreach (String Item in lstStandardTasks.Items)
      GroupData.TaskList.Add(new Task(Item));
   GroupSettings.SaveSettings(GroupData);
 
   // Close the dialog box.
   Close();
}

As you can see, clicking Cancel simply closes the form. However, when you click OK, the application saves the data as it appears on screen. This is important because it’s possible that things could have gotten out of sync as the user interacted with the form. The result of the btnOK_Click() event handler is that the data on disk appears precisely the same as the user saw it on screen.

At this point, you have a basic frmConfigure completed. The next post will focus on the code used to make frmMain work. In the meantime, let me know if you have any questions about any of the frmConfigure code at John@JohnMuellerBooks.com.

 

Exploring the TimeCheck Application (Part 13)

The previous post (Exploring the TimeCheck Application (Part 12)) discussed how to manage the project and task entries that the user needs to sign into the system. In this post, you see the code used to manage the remaining configuration options on frmConfigure. The last administrator-specific configuration options are the Allow Custom Project and Allow Custom Task Entry check boxes. Checking these two boxes will allow the user to type non-standard entries for signing into the system. Otherwise, the user must sign in using the administrator-provided options. Providing separate configuration options makes sense because there are times when you want to control these features individually. Here is the simple code used for the check box CheckChanged event handlers.

private void chkCustomProject_CheckedChanged(object sender, EventArgs e)
{
   // Modify the status of the group data to match the
   // checked status of the checkbox.
   GroupData.CustomProject = chkCustomProject.Checked;
 
   // Save the data to disk.
   GroupSettings.SaveSettings(GroupData);
}
 
private void chkCustomWork_CheckedChanged(object sender, EventArgs e)
{
   // Modify the status of the group data to match the
   // checked status of the checkbox.
   GroupData.CustomTask = chkCustomWork.Checked;
 
   // Save the data to disk.
   GroupSettings.SaveSettings(GroupData);
}

As you can see, the event handlers simply make a change to the GroupData properties and then save GroupData to disk. Other parts of the application read the GroupData information and use it to configure the interface as appropriate. As with other administrator-level settings, you’ll see how to hide these options in a later post. For now, all you’re trying to do is create a working application.

One of the configuration options, Network Location, is administrator-specific, even though the application stores it as part of the user settings. Hiding the setting from view is helpful, but realistically, this is one option that the user can change without permission by editing the configuration file directly. Most users won’t have the required skills, but you need to be aware of the potential for problems with this one setting. Here is the code used to set the network location that is used to provide a pointer to group data on the server.

private void btnNetSelect_Click(object sender, EventArgs e)
{
   // Display the network location selector.
   if (NetworkSelect.ShowDialog(this) == DialogResult.OK)
   {
 
      // Place the selected location in the textbox.
      txtNetLocation.Text = NetworkSelect.SelectedPath;
 
      // Save the data to disk.
      UserData.NetworkPath = NetworkSelect.SelectedPath;
      UserSettings.SetUserSettings(UserData);
   }
}

Notice that the application shows the NetworkSelect dialog box to the user. If the user chooses a new network location and clicks OK, the application will change the network location in the txtNetLocation text box and also saves the data to disk.

The last two configuration options are user-specific, so you always see them displayed on screen. The user needs to be able to select a default project and task. Doing so makes the task of logging into the system easier and faster. Anything that speeds user activities will only give the reader more reasons to use your application. Here is the code used for these last two options.

private void cbProjectName_SelectedIndexChanged(object sender, EventArgs e)
{
   // Modify the user data to reflect the change
   // in default project selection.
   UserData.DefaultProject = cbProjectName.SelectedItem.ToString();
 
   // Save the data to disk.
   UserSettings.SetUserSettings(UserData);
}
 
private void cbWorkType_SelectedIndexChanged(object sender, EventArgs e)
{
   // Modify the user data to reflect the change
   // in default task selection.
   UserData.DefaultTask = cbWorkType.SelectedItem.ToString();
 
   // Save the data to disk.
   UserSettings.SetUserSettings(UserData);
}

As in other cases in this post, the application stores the information in the appropriate place and then stores it on disk. At this point, you have all of the code required to configure the application for use. Next week, we’ll discuss the remaining code used to make frmConfigure work. Let me know if you have any questions about this segment of the code at John@JohnMuellerBooks.com.

 

Exploring the TimeCheck Application (Part 12)

Last week, in the Exploring the TimeCheck Application (Part 11) post, you discovered some of the requirements for making the Add, Edit, and Delete buttons for the Standard Project Entries and Standard Tasks lists work. This addition included creating a new dialog box so that the user could provide the information required by the lists. Now that you have the basics down, let’s look at the code for performing the task. This first bit of code performs tasks with projects.

private void btnProjAdd_Click(object sender, EventArgs e)
{
   // Create the project dialog box.
   frmProjectTask ThisProject = new frmProjectTask();
 
   // Display the project dialog box on screen.
   if (ThisProject.ShowDialog(this) == DialogResult.OK)
   {
      // Create a new project and add it to the list.
      Project NewProject = new Project();
      NewProject.Name = ThisProject.txtProject.Text;
      GroupData.ProjectList.Add(NewProject);
      GroupSettings.SaveSettings(GroupData);
   }
 
   // Update the list information.
   ClearLists();
   FillLists();
}
 
private void btnProjEdit_Click(object sender, EventArgs e)
{
   // Make sure the user has selected a value to edit.
   if (lstStandardProjects.SelectedIndex == -1)
   {
      // Display a message box telling the user to select an item.
      MessageBox.Show("You must select an item to edit.",
         "Selection Error", MessageBoxButtons.OK,
         MessageBoxIcon.Error);
 
      // Exit the event handler.
      return;
   }
 
   // Create the project dialog box.
   frmProjectTask ThisProject = new frmProjectTask("Edit a Project",
      lstStandardProjects.SelectedItem.ToString(), "&Edit");
 
   // Display the project dialog box on screen.
   if (ThisProject.ShowDialog(this) == DialogResult.OK)
   {
      // Edit the existing project name and save it to disk.
      GroupData.ProjectList[lstStandardProjects.SelectedIndex].Name
         = ThisProject.txtProject.Text;
      GroupSettings.SaveSettings(GroupData);
   }
 
   // Update the list information.
   ClearLists();
   FillLists();
}
 
private void btnProjDelete_Click(object sender, EventArgs e)
{
   // Make sure the user has selected a value to delete.
   if (lstStandardProjects.SelectedIndex == -1)
   {
      // Display a message box telling the user to select an item.
      MessageBox.Show("You must select an item to delete.",
         "Selection Error", MessageBoxButtons.OK,
         MessageBoxIcon.Error);
 
      // Exit the event handler.
      return;
   }
 
   // Remove the selected project from the list.
   GroupData.ProjectList.RemoveAt(lstStandardProjects.SelectedIndex);
   GroupSettings.SaveSettings(GroupData);
 
   // Update the list information.
   ClearLists();
   FillLists();
}

The btnProjAdd_Click() event handler begins by creating the default frmProjectTask object, ThisProject. It then displays the dialog box. When the user clicks OK, the code creates a new Project entry, NewProject, adds the project name to it from ThisProject.txtProject.Text, adds the entry to GroupData.ProjectList, and then saves GroupData. Now you know why ThisProject.txtProject.Text has to be public. There are other ways you could handle the situation, such as using a property, but this approach is faster and shouldn’t pose any security issues. You must save the changes by calling SaveSettings(). Because the list has changed, you must call ClearLists() to remove existing data from the lists and FillLists() to add the new data.

In most respects, the btnProjEdit_Click() event handler works like the btnProjAdd_Click() event handler. However, in this case, the user must choose an entry to edit, rather than create a new entry. When the user has failed to select an entry, the code displays an error message and exits. As before, the code displays the dialog box, but this time it changes the button and title bar text to reflect the difference in task. In addition, the code modifies the existing entry, rather than creating a new one. As before, you must call both ClearLists() and FillLists() to update the application.

Deleting an entry in the btnProjDelete_Click() event handler is the simplest of the three tasks. The code begins by verifying that the user has actually selected an entry and reacting if the user hasn’t. It then looks for the requested entry in the list and calls RemoveAt() to remove the entry from the list. A call to ClearLists() and FillLists() completes the process.

The three event handlers for tasks work much like those used for projects. Of course, the code makes additional changes to the frmProjectTask dialog box to ensure the user sees the right information. Here is the code for these three event handlers.

private void btnTaskAdd_Click(object sender, EventArgs e)
{
   // Create the project dialog box.
   frmProjectTask ThisTask = new frmProjectTask("Add a Task", "&Task");
 
   // Display the project dialog box on screen.
   if (ThisTask.ShowDialog(this) == DialogResult.OK)
   {
      // Create a new task and add it to the list.
      Task NewTask = new Task();
      NewTask.Name = ThisTask.txtProject.Text;
      GroupData.TaskList.Add(NewTask);
      GroupSettings.SaveSettings(GroupData);
   }
 
   // Update the list information.
   ClearLists();
   FillLists();
}
 
private void btnTaskEdit_Click(object sender, EventArgs e)
{
   // Make sure the user has selected a value to edit.
   if (lstStandardTasks.SelectedIndex == -1)
   {
      // Display a message box telling the user to select an item.
      MessageBox.Show("You must select an item to edit.",
         "Selection Error", MessageBoxButtons.OK,
         MessageBoxIcon.Error);
 
      // Exit the event handler.
      return;
   }
 
   // Create the task dialog box.
   frmProjectTask ThisTask = new frmProjectTask("Edit a Task",
      "&Task", lstStandardTasks.SelectedItem.ToString(), "&Edit");
 
   // Display the project dialog box on screen.
   if (ThisTask.ShowDialog(this) == DialogResult.OK)
   {
      // Edit the existing project name and save it to disk.
      GroupData.TaskList[lstStandardTasks.SelectedIndex].Name
         = ThisTask.txtProject.Text;
      GroupSettings.SaveSettings(GroupData);
   }
 
   // Update the list information.
   ClearLists();
   FillLists();
}
 
private void btnTaskDelete_Click(object sender, EventArgs e)
{
   // Make sure the user has selected a value to edit.
   if (lstStandardTasks.SelectedIndex == -1)
   {
      // Display a message box telling the user to select an item.
      MessageBox.Show("You must select an item to edit.",
         "Selection Error", MessageBoxButtons.OK,
         MessageBoxIcon.Error);
 
      // Exit the event handler.
      return;
   }
 
   // Remove the selected task from the list.
   GroupData.TaskList.RemoveAt(lstStandardTasks.SelectedIndex);
   GroupSettings.SaveSettings(GroupData);
 
   // Update the list information.
   ClearLists();
   FillLists();
}

As you can see, the differences aren’t all that much. Well, that’s it for this week. Please let me know if you have any questions at all about the code for these six event handlers. Next week we’ll begin looking at the code for some of the other configuration dialog box controls. In the meantime, contact me at John@JohnMuellerBooks.com with your questions and concerns. You can read the next segment in this series at Exploring the TimeCheck Application (Part 13).

 

Exploring the TimeCheck Application (Part 11)

The focus of this post is frmConfigure, which you created in Exploring the TimeCheck Application (Part 3). In Exploring the TimeCheck Application (Part 10) you saw that this form has several lists that need to be maintained. This post begins the process of making the controls on this form functional. To begin with, there are two distinct lists for this application. The first contains projects that the user works on, while the second contains tasks that the user can perform with those projects. Each list requires that the administrator be able to add, edit, and delete entries, so there is a total of six buttons. In addition, any changes to these two lists necessarily modifies the user’s default selections. We’ll talk about the underpinnings for these six buttons today.

There are three essential tasks to consider: deleting, adding, and editing. Deleting an entry is a matter of removing it from the list and then re-displaying the list on screen. Adding and editing entries requires user input, which means you need another form, frmProjectTask, to the application using the same techniques you used earlier. The new form looks like this:

TimeCheck1101

You use the following settings to create this dialog box:

Control

Property

Value

Form1

(Name)

frmProjectTask

AcceptButton

btnAdd

CancelButton

btnCancel

FormBorderStyle

FixedDialog

Size

290, 110

Text

Add a Project

ToolTip1

(Name)

toolTip1

Button1

(Name)

btnAdd

AccessibleDescription

Add the new project to the list.

DialogResult

OK

Location

197, 12

Size

75, 23

TabIndex

1

Text

&Add

ToolTip on toolTip1

Add the new project to the list.

Button2

(Name)

btnCancel

AccessibleDescription

Exit without making a change.

DialogResult

Cancel

Location

197, 41

Size

75, 23

TabIndex

2

Text

&Cancel

ToolTip on toolTip1

Exit without making a change.

Label1

(Name)

lblProject

Location

12, 9

Size

40, 13

TabIndex

3

Text

&Project

TextBox1

(Name)

txtProject

AccessibleDescription

Contains the project text.

Location

12, 25

Modifiers

Public

Size

179, 20

TabIndex

0

ToolTip on toolTip1

Contains the project text.

In order to make frmProjectTask usable in the application, you need to define a number of constructors for it. These constructors make it possible to use the same basic form for all four needs:

  • Add a project
  • Edit a project
  • Add a task
  • Edit a task


In each case, you interact with the form in a slightly different way. The default setup lets you add a project to the list. In order to edit a project, you must supply the current project text, along with changing the dialog box title and the add button to appropriate values. The following code shows the four constructors used for the purpose.

public partial class frmProjectTask : Form
{
   // Add a project constructor.
   public frmProjectTask()
   {
      // Perform the default task.
      InitializeComponent();
   }
 
   // Edit a project constructor.
   public frmProjectTask(String Title, String Value, String ButtonText)
   {
      // Perform the default task.
      InitializeComponent();
 
      // Set the dialog box title.
      this.Text = Title;
 
      // Set the Accept button title.
      btnAdd.Text = ButtonText;
 
      // Provide a value for the textbox.
      txtProject.Text = Value;
  }
 
   // Add a task constructor.
   public frmProjectTask(String Title, String ValueName)
   {
      // Perform the default task.
      InitializeComponent();
 
      // Set the dialog box title.
      this.Text = Title;
 
      // Set the value label title.
      lblProject.Text = ValueName;
   }
    
   // Edit a task constructor.
   public frmProjectTask(String Title, String ValueName,
      String Value, String ButtonText)
   {
      // Perform the default task.
      InitializeComponent();
 
      // Set the dialog box title.
      this.Text = Title;
 
      // Set the value label title.
      lblProject.Text = ValueName;
 
      // Set the Accept button title.
      btnAdd.Text = ButtonText;
 
      // Provide a value for the textbox.
      txtProject.Text = Value;
   }
}

It’s actually possible to use the same constructor for all four tasks, but this approach makes it easier for the developer interacting with the form to create the form needed. Of course, it’s also nice to get by with a little less typing whenever possible.

Now that you have a form to use to get data from the user, it’s time to get back to frmConfigure. The previous post discussed a special method for filling the lists, FillLists(). It turns out that there is also a generic need for a method to clear the lists before filling them, so the application also has a ClearLists() method as shown here.

private void ClearLists()
{
   // Clear all of the lists on the dialog box.
   cbProjectName.Items.Clear();
   cbWorkType.Items.Clear();
   lstStandardProjects.Items.Clear();
   lstStandardTasks.Items.Clear();
 
   // Verify that the user's default selections are
   // still valid.
   if (!GroupData.ProjectList.Contains(new Project(UserData.DefaultProject)))
      UserData.DefaultProject = "None";
   if (!GroupData.TaskList.Contains(new Task(UserData.DefaultTask)))
      UserData.DefaultTask = "None";
 
   // Save the data to disk.
   UserSettings.SetUserSettings(UserData);
}

I’m sure some readers are wondering why I didn’t include this functionality with FillLists(), but there are genuinely times when you only need to fill an empty list. In addition, using two separate methods makes the purpose of each task clear and simple. Methods should focus on one task whenever possible to keep things simple. When you start making methods too complicated, debugging becomes a problem and it’s much harder for other developers to understand your code.

This part of the example starts out by clearing the lists. However, in clearing the lists you may actually end up clearing the user’s default selections as well. If the administrator has removed those default selections, the user will need a new default selection or the combo boxes will appear blank. The application sets the default value to “None” when the user’s original choice is no longer available.

Notice the use of the Contains() method to determine whether the default selection is still available. The Contains() method requires that you provide an object of the type that you want to use for comparison purposes. Our original version of the Project and Task classes doesn’t include a constructor that makes creating an object of this type easy, so this example adds it (more about this addition in a few moments).

The documentation doesn’t make it very clear that the Contains() method always returns false unless you implement it in the supporting class. However, you don’t implement it directly. What you do instead is provide a means for the List base class to make the determination based on an equality method, Equals(), that you provide. If you remember from Exploring the TimeCheck Application (Part 8) I created simple forms of the Project and Task classes for the sake of discussion. It wasn’t necessary to know about the Contains() method at the time and would have only confused matters. In order to make the Contains() method usable, you must add the IEquatable interface and the Equals() method. Here is the code used for the updated version of the Project class.

// Contains a single standard project name.
[Serializable()]
public class Project : IEquatable<Project>
{
   // Default a default constructor.
   public Project()
   {
   }
 
   // Define a constructor that accepts a project name.
   public Project(String ProjectName)
   {
      this.Name = ProjectName;
   }
 
   // Define a method for checking equality.
   public Boolean Equals(Project OtherProject)
   {
      // Verify the Name property values are equal.
      if (this.Name == OtherProject.Name)
         return true;
      else
         return false;
   }
 
   // Define the project property.
   public String Name { get; set; }
}

As you can see, the updated Project class contains a new constructor that accepts as project name as input. It then returns an object that includes the object name. The class also now includes an Equals() method that accepts a Project object as input and returns true or false based on the equality of the two project Name property values. However, the application won’t even use the Equals() method unless you also include the IEquatable<Project> entry. Even though this looks simple, a lot of developers encounter problems with it because the documentation doesn’t make the required implementation clear.

The Task class requires similar additions. You need to add the constructor, the interface reference, and the Equals() method as shown here.

// Contains a single standard task name.
[Serializable()]
public class Task : IEquatable<Task>
{
   // Create a default task constructor.
   public Task()
   {
   }
 
   // Create a task construtor that accepts a string.
   public Task(String TaskName)
   {
      this.Name = TaskName;
   }
 
   // Define a method for checking equality.
   public Boolean Equals(Task OtherTask)
   {
      // Verify the Name property values are equal.
      if (this.Name == OtherTask.Name)
         return true;
      else
         return false;
   }
 
   // Define the task property.
   public String Name { get; set; }
}

That’s it for this week. Next week we’ll start discussing the button event handlers. In the meantime, let me know if you have any questions about these important code additions or want to know why I covered them as I did in these posts (sometimes it’s important to know why something is done in a certain order). Let me know about your questions and concerns at John@JohnMuellerBooks.com. You can see the next post in this series at Exploring the TimeCheck Application (Part 12).

Exploring the TimeCheck Application (Part 10)

Last week you learned about the remaining major elements of the GroupSettings class in the Exploring the TimeCheck Application (Part 9) post. Like many modern applications, you now see how to create an application that relies on both localized user settings (the UserSettings class) and work group-wide settings (the GroupSettings class). Both classes rely on XML data files to create a database of settings that the application relies upon for configuration needs. The use of XML data files makes the application incredibly portable. It’s important to remember the sources of settings information this application:

 

  • The user manually configures some of the settings in the UserSettings class by selecting items such as the default task and project.
  • The administrator manually configures some settings in the UserSettings class by selecting items that only the administrator can see.
  • The application automatically provides default settings in the UserSettings class when the application hasn’t been configured by either the user or the administrator.
  • The administrator manually configures all settings in the GroupSettings class.
  • The application doesn’t automatically supply any defaults for the GroupSettings class and simply displays blank screen areas when there are no values to display.


I wanted to reiterate these settings requirements before moving on because there were some questions on the part of a few readers as to how the settings worked. I’m not showing the security features of this example just yet—for right now, it’s important to focus on the functionality required to perform the desired tasks. However, I do encourage developers creating production applications to include security as part of the application design and development process (as I did when putting this example together). You can’t bolt security on as an afterthought. However, I have chosen to not discuss the security features just now in order to make the example easier to understand.

Because I’m still answering some questions about this application, I’ve decided to keep today’s post simple. Look carefully at frmConfigure (see Exploring the TimeCheck Application (Part 3) for details on the frmConfigure configuration). This form contains two combo boxes and two list boxes that you must fill at various times during the use of the form. For example, you must fill all four of them when initially displaying the data. Any change to the two list boxes must also appear in the two combo boxes. In addition, changes to group data must appear every time the user decides to make any sort of change to the user settings, so you have an opportunity here for interactions between multiple sessions. Because of the potential for problems, the frmConfigure class uses a method named FillList() to fill all four lists any time there is a chance for changes. The following code shows how FillList() works.

private void FillLists()
{
   // Check for a list of tasks.
   if (GroupData.TaskList != null)
 
   // Fill the default and administrator lists with data.
   {
      foreach (Task Item in GroupData.TaskList)
      {
         lstStandardTasks.Items.Add(Item.Name);
         cbWorkType.Items.Add(Item.Name);
      }
   }
 
   // Check for a list of projects.
   if (GroupData.ProjectList != null)
 
   // Fill the default and administrator lists with data.
   {
      foreach (Project Item in GroupData.ProjectList)
      {
         lstStandardProjects.Items.Add(Item.Name);
         cbProjectName.Items.Add(Item.Name);
      }
   }
 
   // Add the "None" entry to the two user lists.
   cbWorkType.Items.Add("None");
   cbProjectName.Items.Add("None");
 
   // Choose the user-supplied default (if any).
   if (UserData.DefaultTask != null)
      cbWorkType.SelectedItem = UserData.DefaultTask;
   if (UserData.DefaultProject != null)
      cbProjectName.SelectedItem = UserData.DefaultProject;
}

Any time there is any change to any of the data that frmConfigure manages, the code calls FillList() to ensure all of the lists are updated. Using a single method like this makes it possible to update the application without any potential problem with method interactions.

In this case, the code works with GroupData.TaskList and GroupData.ProjectList to obtain entries for the two lists associated with a particular data type. The user lists must also contain a None entry, so the code adds this item to the two combo boxes after the lists are filled. Having the None option last helps ensure that the user actually reviews the lists of potential choices before accepting the None option. Using a foreach loop to fill each list is extremely efficient.

The FillList() method must also interact with UserData to automatically choose the user’s selection of a default task and a default project. Remember that the default selection, the one automatically created by the application, is None. When the user hasn’t made a selection, the Configure TimeCheck dialog box displays None for these two entries.

This code provides a subtle form of error handling. Theoretically, GroupData.TaskList, GroupData.ProjectList, UserData.DefaultTask, and UserData.DefaultProject should never be null when this piece of code is called. However, the code makes a quick null check anyway just to ensure it doesn’t attempt to work with a null data value. This quick check will reduce potential errors without affecting performance in any noticeable way. Using this sort of check will help make your applications more robust too.

Well, that’s it for this week. Please keep those questions coming. I realize that this example is a bit more complicated than the ones you’re used to seeing in books, so I want to be sure I don’t leave anyone behind. Be sure to send your questions to me at John@JohnMuellerBooks.com.

 

Exploring the TimeCheck Application (Part 9)

The Exploring the TimeCheck Application (Part 8) post helped you explore the first part of the GroupSettings class. As promised, this week’s post will help you complete the GroupSettings class. Of course, one of the most essential tasks you perform with this sort of class is creating a new instance of the class as needed. In many cases, you can use the default constructor, which requires no input and produces no output. However, when working with settings, you need to create a constructor that sets the settings to a known state as shown here.

// Define a constructor to initialize default values.
public GroupSettings()
{
   CustomProject = false;
   CustomTask = false;
   ProjectList = null;
   TaskList = null;
}

Setting CustomProject and CustomTask to false makes sense because you don’t want the user to be able to create custom projects or tasks unless the administrator wants to allow it. You may have heard the phrase principle of least privilege used in regard to application security and this is an example of such a setup. The application assumes that the user doesn’t have the right to perform some special task unless the administrator specifically allows it.

What may concern some people is the setting of ProjectList and TaskList to null. When an administrator uses the application for the first time, there aren’t any projects or tasks assigned for the application, so that’s one reason to set these List objects to null. However, the second reason is to signal an error to the application. When a user starts the application and the application can’t find the network drive containing the group settings, it’s important to signal the application that an error has happened. This error won’t prevent the application from continuing, but it will prevent the user from logging in using standardized projects and tasks. As a result, the application must have some way of signaling this problem and dealing with it. You’ll see how this all works in a later post.

In addition to creating new GroupSettings objects, the class must be able to read and write GroupSettings to the network drive as an XML file. This one file contains the settings that everyone will use. There are many permutations to discuss with regard to this sort of file. For example, you need to consider whether multiple administrators could possibly modify the file at one time. If so, you need to implement some sort of locking strategy to ensure that only one administrator can open the file for modification at a time. Of course, you don’t want regular users making modifications to the file. For the purposes of clarity, I chose to make things as simple as possible. I’m assuming that there is only one administrator. In addition, I’m assuming that because of the interface modifications you’ll learn about in a later post, that only the administrator will have access to the interface elements required to make the changes. With these restrictions in mind, here is the code the example uses to save data to the file.

// Create a method for saving the settings.
public static void SaveSettings(GroupSettings Settings)
{
   // Obtain the network path.
   String NetworkPath = UserSettings.GetUserSettings().NetworkPath;
 
   // Exit when the application hasn't been configured yet.
   if (NetworkPath == null)
      return;
 
   // When the path doesn't exist, the group hasn't been set up to
   // use the application, so you need to create the path.
   if (!Directory.Exists(NetworkPath + @"\GroupData"))
      Directory.CreateDirectory(NetworkPath + @"\GroupData");
 
   // Check for the file. If it doesn't exist, create it.
   if (!File.Exists(NetworkPath + @"\GroupData\AppData.Config"))
   {
      FileStream NewFile = File.Create(
         NetworkPath + @"\GroupData\AppData.Config");
      NewFile.Close();
   }
 
   // Create an XML serializer.
   XmlSerializer DataWriter = new XmlSerializer(typeof(GroupSettings));
 
   // Define a data stream.
   StreamWriter Output = new StreamWriter(
      NetworkPath + @"\GroupData\AppData.Config");
 
   // Perform the data write.
   DataWriter.Serialize(Output, Settings);
 
   // Close the stream.
   Output.Close();
}

The code begins by creating a path variable that holds the location of the network storage location. This location is unique for an individual machine and is stored as part of the UserSettings (see Exploring the TimeCheck Application (Part 7)). When NetworkPath is null, it means that the application hasn’t been properly configured and there is nothing for the application to do, so it returns a null value to the caller. The application must also ensure that the GroupData folder exists and that it contains the AppData.Config file. When the AppData.Config file is missing, the application must create it.

 

Remember that this task takes place across a network connection. Normally, I’d include some error trapping code here, but left it out for the sake of clarity. A production version of this application will include the required error trapping and error handling code.


Once the application establishes that there is a file to use to hold the data, it creates an XmlSerializer object, DataWriter, and defines its output as type GroupSettings. The application then creates a StreamWriter to actually output the data and then serializes the data as XML in the output. This entire process is precisely the same one that is used for the UserSettings class, except that it happens across a network connection (with all of the potential for problems that a network connection can cause). In fact, I encourage you to compare this method to the one used for the UserSettings class.

Everyone will use the method to obtain the data from the file. You don’t need to implement much in the way of special handling because the file is read and immediately closed. If there are a lot of users to accommodate, you may possibly need to add file locking features to the application. However, since there is no modification of data taking place, allowing shared access works just fine in most cases. Here is the code used to read data from the AppData.Config file.

// Create a method for obtaining the current settings.
public static GroupSettings GetSettings()
{
   // Obtain the network path.
   String NetworkPath = UserSettings.GetUserSettings().NetworkPath;
 
   // Exit when the application hasn't been configured yet.
   if (NetworkPath == null)
      return null;
 
   // Check for the existence of the group settings file. If it
   // doesn't exist, exit.
   if (!File.Exists(NetworkPath + @"\GroupData\AppData.Config"))
      return null;
 
   // Create an XML deserializer.
   XmlSerializer DataReader = new XmlSerializer(typeof(GroupSettings));
 
   // Define a data stream.
   StreamReader Input = new StreamReader(
      NetworkPath + @"\GroupData\AppData.Config");
 
   // Perform the data read.
   GroupSettings GroupData = (GroupSettings)DataReader.Deserialize(Input);
 
   // Close the stream.
   Input.Close();
 
   // Return the data.
   return GroupData;
}

It’s a good idea to compare this method to the one used with the UserSettings class to read the user data. The basic concept is the same. The only difference is that this class works across a network connection, so the production version of the application will likely contain additional error trapping and error handling code.

Well, that’s it for this week. Next week we’ll discuss any remaining internal elements or possibly move on to a few of the user interface elements depending on what sorts of questions I receive about the application. In the meantime, please let me know about any questions or concerns you have about the application at John@JohnMuellerBooks.com. You can find the next post in this series at Exploring the TimeCheck Application (Part 10).

 

Exploring the TimeCheck Application (Part 8)

The previous post, Exploring the TimeCheck Application (Part 7), discussed the UserSettings class, which is used to store user settings to the local hard drive. One of the most important settings included in this class is NetworkPath, which points to the location of the group settings and time logs on the network. Using centralized storage makes it possible for an administrator to set the configuration of all systems on the network at once and to also get reports for the activities of all users, even when the target user is offline. Of course, you could always use a local drive for the NetworkPath when you’re the only one working with TimeCheck. No matter where you store the data, the next level of settings are the group settings used to control the overall functionality of the application. These group settings are the target of today’s and next week’s posts.

The first step in this process is to create some classes. The GroupSettings class contains the actual settings you work with. However, to ensure that you can define tasks (the kind of work the user performs) and projects (the client or area of the company that is the client for the work) as you see fit, you also need to create the Project and Task classes. You can create all three classes (GroupSettings, Project, and Task) using the techniques found in Exploring the TimeCheck Application (Part 7).

The Project class is quite simple in this example. All you really need is the name of the project as shown in the following listing.

namespace TimeCheck
{
   // Contains a single standard project name.
   [Serializable()]
   public class Project
   {
      public String Name { get; set; }
   }
}

Notice that this class is marked as [Serializable()], which is a requirement when working with XML storage files. An implementation for a larger organization could include additional information, such as the client contact information. The idea is to create properties that define what a project is to your organization. In my case, all I need is the project name.

The Task class for this example is similarly simple. All it contains is the name of a task that the user might perform, such as word processing or graphics. Here is the source code for this part of the example.

namespace TimeCheck
{
   // Contains a single standard task name.
   [Serializable()]
   public class Task
   {
      public String Name { get; set; }
   }
}

As with the Project class, you must mark the Task class as [Serializable()]. Even though my class contains only the name of the task, you could easily expand this class to include other information, such as a list of work groups that normally perform this task. The use of a class makes it possible to expand the definition of a task as needed to match the requirements of your organization.

In order to start coding the GroupSettings class, you must provide both file and XML support. You need to add the following using statements at the beginning of the file.

// Add these statements to your project for XML support.
using System.Xml;
using System.Xml.Serialization;
 
// Add this statement for file system support.
using System.IO;

Look again at frmConfigure in Exploring the TimeCheck Application (Part 3). The top half of this form relies on the UserSettings class, while the bottom half relies on the GroupSettings class. There are four properties that support the four fields shown in the bottom of frmConfigure as shown here.

// Determines whether custom projects are allowed.
public Boolean CustomProject { get; set; }
 
// Determines whether custom tasks are allowed.
public Boolean CustomTask { get; set; }
 
// Contains the standard list of group projects.
public List<Project> ProjectList { get; set; }
 
// Contains the standard list of group tasks.
public List<Task> TaskList { get; set; }

Notice that the CustomProject and CustomTask properties are simple Boolean values. They never require any error handling because the properties are set using a simple check on the form.

The ProjectList and TaskList properties are a List object of type Project and Task. Using generics solves some problems later in the application and makes the code easier to understand and work with. The example application uses the simplified property setup for these properties as well because the application never does anything with the strings except to display and store them. If the application had processed the strings in some way or the Project or Task classes were more complex, then the properties would have to include some sort of error trapping. Keep this in mind if you decide to enhance the example code in some way.

That’s it for this week. Next week you’ll see how to complete the GroupSettings class. Please let me know if you have any questions about this class or any other part of the TimeCheck application at John@JohnMuellerBooks.com.

 

Exploring the TimeCheck Application (Part 7)

I’m sure a number of you were wondering when I was going to get back to the TimeCheck application (if ever). I always want to be sure that I provide good material for you and things have been hectic this summer. Of course, we were off on vacation for a while, then there was my little equipment failure, and the drought has brought problems of it’s own. Now we’re back to working on this example.

If you’ll remember from the last post, Exploring the TimeCheck Application (Part 6), this application actually manages several databases and they’re in different places. In fact, this example will eventually handle more data situations than any other example I’ve ever written for use as a teaching aid—books simply haven’t offered me enough space to explore this sort of application before. This post will discuss the user settings that were described in Part 6. To start this part of the example, you must add a class to the application using the following steps:

 

  1. Right click the TimeCheck entry in Solution Explorer and choose Add | New Item from the context menu. You’ll see the Add New Item – TimeCheck dialog box shown here.
    TimeCheck0701
  2. Highlight the Class template in the middle pane. You see a description of the purpose for this template in the right pane.
  3. Type UserSettings.cs in the Name field and click Add. Visual Studio adds the new class file for you and creates some basic code for it.


Let’s begin at the top of the new class file you’ve created. In order to support the code for this example, you must add some using statements to the top of the UserSettings class file as shown here.

// Add these statements to your project for XML support.
using System.Xml;
using System.Xml.Serialization;
 
// Add this statement for file system support.
using System.IO;

The user’s settings for this example reside on the user’s hard drive. The application must know where to find the network drive with the group settings. In addition, each user can select a default task and a default project. Consequently, the user settings consist of these three properties.

// Contains the user's project preference.
public String DefaultProject { get; set; }
 
// Contains the user's task preference.
public String DefaultTask { get; set; }
 
// Contains the location of the data on the network.
public String NetworkPath { get; set; }

You don’t need to perform any special checks in this case because the application will perform them automatically and the user won’t be able to enter anything that will crash the application. Using this shortcut method of creating properties works just fine for this part of the example.

The default constructor for any class doesn’t do anything. Yes, C# supplies a constructor because you absolutely must provide one, but it’s useless in this case because you need to initialize the properties. The next step is to create a new default constructor that performs the tasks needed as shown here.

// Create a default constructor.
public UserSettings()
{
   // Create default properties.
   this.DefaultProject = "None";
   this.DefaultTask = "None";
   this.NetworkPath = "None";
}

Now we get to the meat of the class. The class must provide static methods for getting and setting the user settings. These static methods help the application work with existing data more efficiently and reduce the need to create unnecessary objects that simply use memory. The following code shows the static methods used to get and set user settings for this application.

// Obtains the user settings.
public static UserSettings GetUserSettings()
{
   // Define a path to the user settings.
   String UserPath =
      Environment.GetFolderPath(
         Environment.SpecialFolder.ApplicationData) + @"\TimeCheck";
 
   // When the path doesn't exist, the user hasn't been set up to
   // use the application, so you need to exit.
   if (!Directory.Exists(UserPath))
      return null;
 
   // Check for the file.
   if (!File.Exists(UserPath + @"\AppData.Config"))
      return null;
 
   // Create an XML serializer.
   XmlSerializer DataReader = new XmlSerializer(typeof(UserSettings));
 
   // Define a data stream.
   StreamReader Input = new StreamReader(UserPath + @"\AppData.Config");
 
   // Load the settings.
   UserSettings Settings = new UserSettings();
   Settings = (UserSettings)DataReader.Deserialize(Input);
   Input.Close();
 
   // Return the user settings to the caller.
   return Settings;
}
 
public static void SetUserSettings(UserSettings Settings)
{
   // Define a path to the user settings.
   String UserPath =
      Environment.GetFolderPath(
         Environment.SpecialFolder.ApplicationData) + @"\TimeCheck";
 
   // When the path doesn't exist, the user hasn't been set up to
   // use the application, so you need to create the path.
   if (!Directory.Exists(UserPath))
      Directory.CreateDirectory(UserPath);
 
   // Check for the file. If it doesn't exist, create it.
   if (!File.Exists(UserPath + @"\AppData.Config"))
   {
      FileStream NewFile = File.Create(UserPath + @"\AppData.Config");
      NewFile.Close();
   }
 
   // Create an XML serializer.
   XmlSerializer DataWriter = new XmlSerializer(typeof(UserSettings));
 
   // Define a data stream.
   StreamWriter Output = new StreamWriter(UserPath + @"\AppData.Config");
 
   // Load the settings.
   DataWriter.Serialize(Output, Settings);
   Output.Close();
}

In both cases, the code begins by obtaining the location of the user’s data store on the local hard drive by calling Environment.GetFolderPath(). The Environment.SpecialFolder.ApplicationData enumeration contains the location of the root of the data store. This application stores its data in the \TimeCheck subdirectory.

The next step is to ensure that the \TimeCheck subdirectory actually exists and that it contains a file named AppData.Config. When the class is getting the settings, a failure to find either the directory or the file means that there is no data to obtain, so GetUserSettings() returns a value of null. However, when saving the user settings, the SetUserSettings() method must create the required directory and file when it doesn’t exist.

At this point, there is either data to get or data to save. The data is stored in XML format. In order to work with XML data, you create an XmlSerializer to either serialize (change the properties into XML) or deserialize (change the XML into properties) the data. A StreamReader or StreamWriter provides read or write access to the physical file. The code can now serialize the local data to the XML file or deserialize the data from the XML file and store it locally.

Notice that the example is careful to close files immediately after use. Even though it may seem that the application would perform this task automatically, it does so only after you exit the method and may not close it immediately. In the meantime, an application error could potentially cause damage to the data. In addition, if the application must create the AppData.Config file, the application will register an access error when you try to reopen the file for reading.

Well, that’s it for the UserSettings class. Next week will begin looking at the GroupSettings class (which is considerably more complicated than this class was). In the meantime, let me know if you have any questions about this part of the application at John@JohnMuellerBooks.com. You can see the next post in this series at Exploring the TimeCheck Application (Part 8).

 

Exploring the TimeCheck Application (Part 6)

Last week, we examined the last major user interface issue for this application, adding accessibility features so that someone using a keyboard could access all of the options simply by pressing Tab. I received some good feedback on that post. It’s important to me that applications work as well as possible for everyone. This week’s post will start to examine the underlying application source code—starting with the database used to manage the user time entries.

This application will have more database information than either of the two series presented so far (Exploring the GrabAPicture Application and Exploring the TypingBuddy Application). In fact, there are a number of different databases you need to consider before writing even a single line of code. Here are the basics—upcoming posts will examine each database in detail.

 

  • User Settings: Each user will have individual settings that are stored on the local system. For example, a user can choose to select a default project and a default task, making it easy to log into and out of the system each day. The local user settings also contain a pointer to the network drive used to store the group settings and also to store the user’s time entries. The time entries are actually the most important part of the application because they track the user’s daily activities.
  • Group Settings: The group settings control features that affect the group as a whole are are only accessible by the administrator for editing purposes. For example, you use these settings to control whether the group can use custom tasks or custom projects as part of a log entry. If not, the group is required to select one of the default projects and/or tasks. These settings appear in a central location on the network drive. In order to provide default projects and tasks for users, these settings control two sub-databases:
    • Default Projects: This is a list of the default projects that a user can select from.
    • Default Tasks: This is a list of the default tasks that the user can perform on a given project.
  • Individual Time Entries: Every time entry consists of a time, the project selected, and the task performed on that project. Every login and log out sequence comes as a pair of time entries. The first entry logs the user into the project to perform a given task, while the second entry logs the user out of the project. Each user has a single log file based on his or her name.


These databases are all implemented using XML files, as with the previous series examples, because none of them require true relational capabilities. The reports gather information from the various time logs to create an overview of how the individual or the organization as a whole is using the computer.

As part of figuring the databases out, you also need to consider the data storage requirements. Some of the data need not appear on the network drive. In fact, it has to appear on the local drive or the application won’t be able to find the network data location. Other data appears on the network to provide centralized access to it. Here are the storage locations used for the data in this application.

 

  • C:\Users\<User Name>\AppData\Roaming: Contains the user’s settings and the pointer to the network drive location.
  • \\<Server Name>\<Share Name>\TimeCheck\GroupData: Contains the group settings. Only the administrator can read and write this folder. Regular users can only read this folder.
  • \\<Server Name>\<Share Name>\TimeCheck\<Year>\LogFiles: Contains the individual log files. Administrators can read and write any file. However, regular users can only read and write a specific log file.

I had considered using separate files for each project, but this seems unnecessarily cumbersome. Obviously, you may prefer a different log file and folder setup than the one used in the example. The example is extensible so you can modify the locations as needed for your organization. The reason for using a year folder is to make it easier to archive a specific year’s entries when they’re no longer needed.

Now that you have a better idea of how the databases are arranged, we’ll begin looking at the code used to implement them starting with the next post. I’m going to be out of the office until July 17th, so we’ll talk about the next post in this series on the Friday that follows (July 20th). Until that time, please let me know if you have any questions about the databases or where they’re stored at John@JohnMuellerBooks.com. You can see the next post in this series at Exploring the TimeCheck Application (Part 7).

 

Exploring the TypingBuddy Application (Part 7)

In the previous post, Exploring the TypingBuddy Application (Part 6), you learned how the user can create custom messages for TypingBuddy. These messages make it possible for TypingBuddy to suggest activities that interest the user. The form used for this activity, frmAddEdit, can display a single message at a time for either adding or editing a message. The focus of this week’s post, frmMessages, makes it possible to manage the list of configured messages, as well as add new ones. It provides access to the functionality required to add, edit, and delete messages as shown here.

TypingBuddy0701

The setup required to create this form appears in the Exploring the TypingBuddy Application (Part 3) post. Before this form can do anything else, it must have a copy of the current message list. It uses this list to populate the Messages list box when the form loads. Here’s the code needed to perform this task:

// Create a copy of the database.
TBMessages Settings;
 
private void frmMessages_Load(object sender, EventArgs e)
{
   // Initialize the settings.
   Settings = TBMessages.LoadSettings();
 
   // Determine if there are any messages to load.
   if (Settings.MessageList != null)
 
      // If there are messages to process, check them one at a time.
      foreach (TBMessage ThisMessage in Settings.MessageList)
 
         // Display just the title in the listbox.
         lstMessages.Items.Add(ThisMessage.Title);
}

The example begins by creating a TBMessages object, Settings, to hold the list of messages along with the other settings used by the application. In frmMessages_Load(), the application instantiates this object by calling TBMessages.LoadSettings(). When Settings contains one or more TBMessage entries, the application loads them into lstMessages so that the user can see them immediately after the form loads.

The form contains four buttons. The code for Close is very simple—all it does is pass back a DialogResult value of Cancel. The Add button is a little more complex as shown here:

private void btnAdd_Click(object sender, EventArgs e)
{
   // Create the required form.
   frmAddEdit AddMessage = new frmAddEdit();
 
   // Provide the required resources.
   if (Settings.MessageList != null)
      AddMessage.MessageList = Settings.MessageList;
 
   // Display the form.
   if (AddMessage.ShowDialog(this) == DialogResult.OK)
   {
      // Retrieve the new settings.
      Settings.MessageList = AddMessage.MessageList;
 
      // Save the settings.
      TBMessages.SaveSettings(Settings);
   }
 
   // Clear the message list.
   lstMessages.Items.Clear();
 
   // Determine if there are any messages to load.
   if (Settings.MessageList != null)
 
      // If there are messages to process, check them one at a time.
      foreach (TBMessage ThisMessage in Settings.MessageList)
 
         // Display just the title in the listbox.
         lstMessages.Items.Add(ThisMessage.Title);
}

The code begins by creating a frmAddEdit object, AddMessage. It then passes the current list of messages to AddMessage and displays the form on screen where the user will provide values for a new message. When the return value is DialogResult.OK, the code retrieves the updated list of messages and places them in Settings.MessageList. The code then saves the new list of messages to disk.

At this point, the code must update the user’s view of the messages. It does this by calling lstMessages.Items.Clear() first to clear any existing messages. When Settings.MessageList contains messages (it may not because the user may have chosen to click Cancel instead of adding a message), the code adds each of the message titles to lstMessages.

The Edit button is the most complex of the four buttons. It not only must choose which message to edit, but it must also configure the frmAddEdit object to display a message editing dialog box, rather than a message adding dialog box. Of course, this event handler will also need to perform all of the tasks performed when adding a new message. Here’s the code you use to perform these tasks.

private void btnEdit_Click(object sender, EventArgs e)
{
   // Create the required form.
   frmAddEdit AddMessage = new frmAddEdit();
 
   // Configure the form.
   AddMessage.Text = "Edit Existing Message";
   AddMessage.btnAdd.Text = "&Edit";
   AddMessage.btnAdd.AccessibleDescription =
      "Edits an existing item in the list.";
   AddMessage.toolTip1.SetToolTip(AddMessage.btnAdd,
      "Edits an existing item in the list.");
 
   // Set up for an edit.
   AddMessage.IsEdit = true;
 
   // Make sure there are records to edit.
   if (Settings.MessageList != null)
   {
      // Provide the required resources.
      AddMessage.MessageList = Settings.MessageList;
 
      // Set the record number.
      AddMessage.RecordNumber = lstMessages.SelectedIndex;
   }
   else
   {
      // Display a message and exit when there are no records to edit.
      MessageBox.Show(
         "There are no messages to edit!",
         "Editing Error",
         MessageBoxButtons.OK,
         MessageBoxIcon.Error);
      return;
   }
 
   // Make sure the user has selected an item to edit.
   if (lstMessages.SelectedIndex == -1)
   {
      // Display a message and exit when there are no records to edit.
      MessageBox.Show(
         "You must select a message to edit!",
         "Selection Error",
         MessageBoxButtons.OK,
         MessageBoxIcon.Error);
      return;
   }
 
   // Display the form.
   if (AddMessage.ShowDialog(this) == DialogResult.OK)
   {
      // Retrieve the new settings.
      Settings.MessageList = AddMessage.MessageList;
 
      // Save the settings.
      TBMessages.SaveSettings(Settings);
   }
 
   // Clear the message list.
   lstMessages.Items.Clear();
 
   // Determine if there are any messages to load.
   if (Settings.MessageList != null)
 
      // If there are messages to process, check them one at a time.
      foreach (TBMessage ThisMessage in Settings.MessageList)
 
         // Display just the title in the listbox.
         lstMessages.Items.Add(ThisMessage.Title);
}

This part of the example begins by creating a frmAddEdit object, AddMessage (I simply recycled the name from the previous event handler). The first two changes configure the form so that the user sees the right messages. However, because we’re also focusing on accessibility in this example, the application must also change the settings that affect accessibility. For example, this changes would modify what a user hears through a screen reader. When making your application accessibility friendly, you need to keep these sorts of changes in mind.

Because the application is editing an existing message, rather than editing an existing message, it must also set AddMessage.IsEdit to true. As mentioned in the previous posting, this setting affects how the form reacts to user input and also controls initial configuration.

At this point, the application must pass the list of messages along to AddMessage, just as it did when adding a new message. However, notice the error trapping code that is added in this case. For example, you can add a message when the message list is null, but you can edit a message that doesn’t exist. Likewise, a user must actually select a message in the list in order to edit it—you can’t simply assume which message the user intended to edit.

Only after the conditions are right can the application display the editing dialog box. When the user makes changes and clicks Edit, the application updates the message list and updates the list of messages, just as it does when adding a new message.

The Delete button is unique in that it does change the message list, but it doesn’t rely on frmAddEdit to do it. All that really needs to happen is that the application removes the required record from the message database as shown here:

private void btnDelete_Click(object sender, EventArgs e)
{
   // Make sure the user has selected an item to edit.
   if (lstMessages.SelectedIndex == -1)
   {
      // Display a message and exit when
      // the user hasn't selected a record.
      MessageBox.Show(
         "You must select a message to edit!",
         "Selection Error",
         MessageBoxButtons.OK,
         MessageBoxIcon.Error);
      return;
   }
 
   // Create a temporary message database.
   TBMessage[] Temp = new TBMessage[Settings.MessageList.Length - 1];
 
   // Create a counter for the temporary database.
   Int32 Count = 0;
 
   // Check each of the records for the defined message and delete
   // the selected message by not copying it to the message list.
   foreach (TBMessage ThisMessage in Settings.MessageList)
      if (ThisMessage.Title != (String)lstMessages.SelectedItem)
      {
         // Copy the record.
         Temp[Count] = ThisMessage;
 
         // Update the counter.
         Count++;
      }
 
   // Update the settings.
   Settings.MessageList = Temp;
    
   // Save the new message list.
   TBMessages.SaveSettings(Settings);
 
   // Update the message list.
   lstMessages.Items.RemoveAt(lstMessages.SelectedIndex);
}

The main requirement that the application must adhere to in this case is that the user must select a record to delete. Again, the application can’t assume anything about what the user really intended to do. Good design practices dictate that you make this check and then ask the user to make a selection when there isn’t any selection.

There are many different methods you can use to delete a record from MessageList. The example chooses to use a simple approach that works quite fast. It simply creates a temporary list of messages, Temp, of type TBMessage[] that contains one less record than the original array. It then copies all of the original records to the temporary array, except for the one record that matches the one that the user has selected. After it completes the copying process, the application saves the message list and updates lstMessages so the user sees that the entry is deleted.

We’ve moved up to frmMain. This particular form will require a little more work than you’ve done so far because it’s the focal point of everything that TypingBuddy does. Next week we’ll start a series of posts about frmMain. In the meantime, let me know if you have any questions at John@JohnMuellerBooks.com. You can see the next post in this series at Exploring the TypingBuddy Application (Part 8).