Exploring the TypingBuddy Application (Part 12)

This week’s programming series post is going to be a bit short. Last week (Exploring the TypingBuddy Application (Part 11)) you put the finishing touches on the basic TypingBuddy application. There are a number of useful additions that you could pursue, but many of them are straightforward enough that I’ll leave them in your capable hands.

 

  • Notification Area context menu updates for returning the timer to its starting point.
  • The option to display the remaining time in hours, minutes, and seconds, rather than in seconds.
  • The option to play sounds other than the default sounds included with the application.
  • Adding custom icons or pictures to the message box display.
  • Custom typing times based on the time of day.
  • Scheduled break times that override the standard typing time as needed.
  • An option to lock the system automatically when typing time is up (rather than simply display a message).


As with the GrabAPicture series, a number of you have asked me to put this entire series in an easier to use format. As with GrabAPicture, I’ll eventually put together an e-book for this series that includes:

 

  • All 12 posts for the series.
  • My personal series notes.
  • Some useful additions for the TypingBuddy application.
  • A URL for a downloadable copy of the application.

 

If there are any special additions you’d like to see in the e-book, let me know about them at John@JohnMuellerBooks.com. I’m always looking for interesting topics to cover in my books. Of course, letting me know what interests you will result in a better book.

Next week we’ll start another new application series. This application, TimeCheck, helps you track the use of your time during the day. You log into and out of projects in order to keep better track of how you’re using your time. This particular application has helped me improve my efficiency because I was able to use it to see where I waste time. In addition, I use the information obtained to do things like making buying decisions for new equipment and software based on how much I’ll actually use the new items.

The TimeCheck program has been with me for a long time—almost 25 years. The first version was written in assembler, but quickly proved hard to update. I wrote another version in C, but that version wasn’t quite what I wanted, so I reverted to the assembler version, until I rewrote the application yet again in Java. As my network grew and I saw the advantage of using automation with this application, I decided to rewrite it in its current form in C#. This version allows you to choose a storage location for the log files (including a network drive) and provides some interesting statistics. More importantly, it hides in the Notification Area to make it easier to keep track of how things are configured without cluttering the Taskbar.

 

Exploring the TypingBuddy Application (Part 11)

The TypingBuddy application is finally nearing completion. During the previous post, Exploring the TypingBuddy Application (Part 10), you discovered how the timing works within the application for controlling typing time. This week’s post will complete the basic TypingBuddy application. It consists of a few remaining event handlers for controls on frmMain. The three buttons in question, Save Settings (btnSet), Minimize Window (btnMinimize), and Change Messages (btnChangeMessages) have the following event handlers attached to their Click events.

private void btnSet_Click(object sender, System.EventArgs e)
{
   // Set the controls.
   TimeLeft = (Int32)TypingInterval.Value * 60;
 
   // Obtain the current settings.
   Settings.DisplayMessageIcon = chkShowIcon.Checked;
   Settings.PlaySound = chkPlaySound.Checked;
   Settings.RestInterval = (Int32)RestingValue.Value;
   Settings.TypingInterval = (Int32)TypingInterval.Value;
 
   // Save the settings.
   TBMessages.SaveSettings(Settings);
 
   // Start the timer.
   TypeTimer.Start();
}
 
private void btnMinimize_Click(object sender, System.EventArgs e)
{
   // Minimize the form.
   WindowState = FormWindowState.Minimized;
   ThisFrmMain.Hide();
}
 
private void btnChangeMessages_Click(object sender, EventArgs e)
{
   // Create the required form.
   frmMessages ManageMessages = new frmMessages();
 
   // Display the form.
   ManageMessages.ShowDialog(this);
 
   // Upon return from the form, make sure you reload the settings.
   Settings = TBMessages.LoadSettings();
}

When the user clicks Save Settings, the application begins by updating the amount of time left to the new time setting the user has provided. The application then obtains updates for all of the required properties in Settings, and then saves Settings to disk. Remember the Settings is a global variable that contains the application settings and also contains a list of custom messages that the user wants to use with the application. After Settings is saved, the application starts the typing timer by calling TypeTimer.Start().

One of the mistakes that developers make when creating an application that resides in the Notification Area is to hide the form. The btnMinimize_Click() event handler shows the proper way to handle things. You minimize the form and then hide it from view. This technique ensures that the user doesn’t see anything in the Taskbar or on screen.

Previous posts have described the entire process for creating and managing messages in detail. However, you still haven’t seen the call that starts the entire process. When the user clicks Change Messages, the application calls on the btnChangeMessages_Click() event handler. The event handler creates an instance of frmMessages, ManageMessages, and displays the form on screen. Remember that for reliability purposes, this process is pretty much self-contained. When the user closes the form, the event handler calls TBMessages.LoadSettings() to update Settings with the message changes that the user had made.

At this point, you have a copy of TypingBuddy as it exists today. Of course, application developers are always tweaking their applications to make them better. What sorts of changes can you suggest for TypingBuddy? Let me know at John@JohnMuellerBooks.com.

 

Exploring the TypingBuddy Application (Part 10)

In the previous installment of this series, Exploring the TypingBuddy Application (Part 9), you discovered how the Notification Area menu works in Typing Buddy. The main purpose of this menu is to control the timer used to determine when it’s time to stop typing. Given the purpose of TypingBuddy, keeping you from working so long as to cause repetitive stress injuries, the most important code is that used by the timer to display the stop message.

The TypeTimer control relies on the Timer_Tick() event handler to respond to Tick events once each second. The Timer_Tick() method must accomplish the following tasks:

 

  • Update the counter.
  • Check for an end of typing time condition.
  • Choose a message.
  • Ensure the message falls within the required parameters for display.
  • Display the message on screen.


With these criteria in mind, it’s time to look at the method code. The following code shows how the Timer_Tick() method performs its assigned tasks.

private void Timer_Tick(object sender, System.EventArgs e)
{
   frmMessage MyMessage;   // Message dialog box.
   DialogResult RetVal;    // Return value from dialog.
   Int32 Count = 0;        // Number of messages.
   Random Rand;            // A random number generator.
   Int32 Selection = -1;   // The message selection.
 
   // Set the timer value.
   TimeLeft--;
 
   // See if the timer has expired.
   if (TimeLeft == 0)
   {
      // Determine whether there are any messages to use.
      if ((Settings.MessageList != null) &&
         (Settings.MessageList.Length > 0))
      {
         // Obtain the current number of messages.
         Count = Settings.MessageList.Length;
 
         // Randomize a specific message.
         Rand = new Random(DateTime.Now.Millisecond);
         Selection = Rand.Next(Count);
 
         // Ensure that the message isn't one that has a date set.
         if (Settings.MessageList[Selection].UseDates)
 
            // When the message does have the date set,
            // ensure the date is in the right timeframe.
            Selection = VerifySelection(
               Settings.MessageList, Selection);
      }
 
      // Create the new dialog box.
      if (Selection == -1)
      {
         if (chkShowIcon.Checked)
            MyMessage = new frmMessage(
               (Int32)RestingValue.Value * 60,
               chkShowIcon.Checked);
         else
            MyMessage = new frmMessage(
               (Int32)RestingValue.Value * 60);
      }
      else
      {
         if (Settings.MessageList[Selection].ShowTitle)
         {
            if (chkShowIcon.Checked)
               MyMessage = new frmMessage(
                  (Int32)RestingValue.Value * 60,
                  Settings.MessageList[Selection].Title,
                  Settings.MessageList[Selection].MessageText,
                  chkShowIcon.Checked);
            else
               MyMessage = new frmMessage(
                  (Int32)RestingValue.Value * 60,
                  Settings.MessageList[Selection].Title,
                  Settings.MessageList[Selection].MessageText);
         }
         else
         {
            if (chkShowIcon.Checked)
               MyMessage = new frmMessage(
                  (Int32)RestingValue.Value * 60,
                  Settings.MessageList[Selection].MessageText,
                  chkShowIcon.Checked);
            else
               MyMessage = new frmMessage(
                  (Int32)RestingValue.Value * 60,
                  Settings.MessageList[Selection].MessageText);
         }
      }
       
      // Reset the controls.
      TimeLeft = (Int32)TypingInterval.Value * 60;
      TypeTimer.Stop();
 
      // Play the required sound when necessary.
      if (chkPlaySound.Checked)
         if (Selection != -1)
         {
            switch (Settings.MessageList[Selection].Sound)
            {
               case TBMessages.SystemSoundList.None:
                  break;
               case TBMessages.SystemSoundList.Asterisk:
                  System.Media.SystemSounds.Asterisk.Play();
                  break;
               case TBMessages.SystemSoundList.Beep:
                  System.Media.SystemSounds.Beep.Play();
                  break;
               case TBMessages.SystemSoundList.Exclamation:
                  System.Media.SystemSounds.Exclamation.Play();
                  break;
               case TBMessages.SystemSoundList.Hand:
                  System.Media.SystemSounds.Hand.Play();
                  break;
               case TBMessages.SystemSoundList.Question:
                  System.Media.SystemSounds.Question.Play();
                  break;
            }
         }
         else
            System.Media.SystemSounds.Beep.Play();
 
      // Display the stop typing message.
      RetVal = MyMessage.ShowDialog(this);
 
      if (RetVal == DialogResult.OK)
         // Restart the timer.
         TypeTimer.Start();
      else
         // Exit the application.
         Close();
   }
   else
   {
      // Update the time left indicator.
      txtTypingTime.Text = TimeLeft.ToString();
      ThisNotifyIcon.Text = "Time Left in Seconds: " +
                         TimeLeft.ToString();
   }
}

Let’s start with the simple decision. Most of the time, the code will decrement TimeLeft and find that it still isn’t 0. When this condition occurs, the code updates the Time Left (in Seconds) field of frmMain to display the new typing time left value. In addition, you might remember that the the user can also see the amount of time left by hovering the mouse over the icon in the Notification Area. In order to accomplish this task, the code updates the ThisNotifyIcon.Text property value.

Of course, the alternative to updating the timer is to display a message. The Selection variable contains the number of the message to display. It begins with a value of -1 to indicate there is no message to display. When the user has configured custom messages, the code chooses a random message number from that collection and places it in Selection. However, the selection process must consider whether the selected message is only usable on a specific date. When this occurs, the code calls VerifySelection() with the list of available messages and the current selection from that list. Don’t worry about the inner workings of this method for now; you’ll see it in operation later.

When the user hasn’t configured any custom messages, Selection remains at -1 and the application displays the standard message that’s provided as part of the frmMessage configuration. The only decision left to make at this point is which constructor to call. When the user wants to display an icon, the code passes the value of chkShowIcon.Checked to the constructor. Otherwise, all it passes is the amount of resting time that the message should use.

Creating the message box when working with custom messages becomes a little more complex because the code must now decide whether the user wants the message title displayed or not (along with the decision about displaying an icon). When the user wants to display both the title and a custom message, the code uses constructors that pass along Settings.MessageList[Selection].Title and Settings.MessageList[Selection].MessageText; otherwise, the code simply passes the message text to the constructor.

Now that the application has a message box ready to display, it’s time to reset the count and stop the timer. The user may want to hear a sound when the message displays. To accommodate this need, the code looks at chkPlaySound.Checked. When this property is true, it then checks for a standard or custom message and plays the appropriate sound. Note that when the custom message has TBMessages.SystemSoundList.None selected, the application doesn’t play a sound even when chkPlaySound.Checked is true.

Everything is ready to display the message to the end user. The user either waits for the rest time counter to count down or clicks Quit to exit TypingBuddy. When the user waits for the rest period to end and clicks Continue, the code restarts the timer, which starts a new typing cycle. Otherwise, the application ends.

These are the basics of how the timer handling works. However, there is another issue to consider, verifying that the message is within the correct date span. The VerifySelection() method handles this requirement as shown here.

private Int32 VerifySelection(TBMessage[] List, Int32 Selection)
{
   // Check the date range.
   if ((DateCompare(Settings.MessageList[Selection].StartDate) <= 0) &&
      (DateCompare(Settings.MessageList[Selection].EndDate) >= 0))
 
      // The Selection value is in the correct date range.
      return Selection;
 
   // Obtain the message list count.
   Int32 Count = List.Length;
 
   // When the selected message isn't in the right timeframe, look for
   // a message that does fit within the required timeframe starting
   // from the current selection.
   for (Int32 NewSel = Selection + 1; NewSel < Count; NewSel++)
 
      // Check to see if the message has a date usage requirement.
      if (List[NewSel].UseDates)
      {
 
         // Check the date range.
         if ((DateCompare(List[NewSel].StartDate) <= 0) &&
            (DateCompare(List[NewSel].EndDate) >= 0))
 
            // Return a new selection.
            return NewSel;
      }
      else
 
         // This entry doesn't use a date, so return it.
         return NewSel;
 
   // We've gotten to the end of the list without finding a message.
   // Let's start over from the beginning of the list.
   for (Int32 NewSel = 0; NewSel < Selection; NewSel++)
 
      // Check to see if the message has a date usage requirement.
      if (List[NewSel].UseDates)
      {
 
         // Check the date range.
         if ((DateCompare(List[NewSel].StartDate) <= 0) &&
            (DateCompare(List[NewSel].EndDate) >= 0))
 
            // Return a new selection.
            return NewSel;
      }
      else
 
         // This entry doesn't use a date, so return it.
         return NewSel;
 
   // There aren't any messages in the database that will work in
   // the current date range. In short, even if there are messages
   // in the list, it's as if the list is empty because none of the
   // message are useful.
   return -1;
}

The code begins by verifying that the date is within correct time span. However, you don’t want to compare years, just the month and day, so the code relies on a special method, DateCompare(), to perform this task. You’ll see how DateCompare() works in a few moments, so don’t worry about it for now. All you really need to know is that DateCompare() returns -1 when the date supplied as part of the custom message is less than the current date, 0 when the month and day are precisely the same as the current date, and 1 when the custom message date is greater than the current date. When the selected message dates fall within the current date, then the code returns the current selection.

There are going to be times when the custom message dates won’t include the current date. When that happens, the application begins with the current message and looks at each message in the list until it gets to the end of the list looking for a message that either doesn’t have the dates set (which means that the reader would like to see the message at any time) or the current date falls within the message date. The application uses a simple for loop to perform the task.

It could happen that none of the custom messages starting with the current message to the end of the list are useable. In this case, the code starts looking for a useable message from the beginning of the list to the original selected message location. The criteria are the same—the code looks for a message that has no dates set or one that has a range that includes the current date.

The application has reviewed the entire list of custom messages at this point. It could happen that the application gets to the end of the list and still doesn’t find a useable message. When that situation occurs, VerifySelection() returns -1. If you review the Timer_Tick() code, you’ll see that this condition automatically selects the default message, even if the user has created custom messages.

The .NET Framework doesn’t provide an easy method for comparing just the month and the day of two dates. You usually compare the entire date or a specific date element. In order to perform the correct level of comparison, the example uses the DateCompare() method shown here.

private Int32 DateCompare(DateTime Date)
{
   // Create variables to hold the checked date information.
   Int32 Month = Date.Month;
   Int32 Day = Date.Day;
 
   // Create variables to hold the current date information.
   Int32 CMonth = DateTime.Now.Month;
   Int32 CDay = DateTime.Now.Day;
 
   // Compare the month and the day.
   if (Month < CMonth)
 
      // When the month specified by the user is less
      // than the current month, return -1.
      return -1;
   else if (Month > CMonth)
 
      // When the month specified by the user is greater
      // than the current month, return 1.
      return 1;
 
   // The months are equal, so look at the dates.
   else
   {
      if (Day < CDay)
 
         // When the day specified by the user is less than
         // the current day, then reutrn -1.
         return -1;
      else if (Day > CDay)
 
         // When the day specified by the user is greater
         // than the current day return 1.
         return 1;
      else
 
         // Both the month and the day are equal, so
         // return 0.
         return 0;
   }
}

As you can see, this method first compares the month values. When the two months are precisely equal, it then compares the day values. When the two days are also equal, the method returns a value of 0. Otherwise, the method returns either -1 or 1 depending on whether the custom message date is greater or less than the current date.

Now you have the code for the entire application except for the event handlers in frmMain itself. Next week you’ll receive this last bit of code. 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 11).

 

Exploring the TypingBuddy Application (Part 9)

In the Exploring the TypingBuddy Application (Part 8) post you discovered how the TypingBuddy form loads and closes. However, after the initial configuration, you might never see that form again because TypingBuddy loads itself in the Notification Area. The first thing to notice is that the Notification Area contains an icon for this application named ThisNotifyIcon. You can assign a default behavior to this icon by creating an event handler for the DoubleClick event named NotifyIcon_DoubleClick().  All that this event handler does is display the form as shown in the following code.

private void NotifyIcon_DoubleClick(object sender, EventArgs e)
{
   // Display the form.
   ThisFrmMain.Show();
   WindowState = FormWindowState.Normal;
}

When the user right clicks on the TypingBuddy icon in the notification, the application displays a menu containing options for opening the window, pausing the countdown, and exiting the application. Of course, you could easily add any other functionality required by your personal needs, including the ability to configure the application.

TypingBuddy0901

In order to create this menu, you must select the NotificationMenu component (added as part of the Exploring the TypingBuddy Application (Part 2) post) and click on the right pointing arrow that appears in the upper right corner of the control. You’ll see an Edit Menu link on a menu like this one shown here.

TypingBuddy0902

Clicking Edit Menu adds the context menu to the design area. You can then add the menu entries to it as shown here.

TypingBuddy0903

Each of these menu entries requires some configuration as shown in the following table.

Menu Property/Event Value/Event Handler
mnuOpen Text &Open Window
  Click NotifyIcon_DoubleClick()
mnuPause Text &Pause
  Click btnPause_Click()
mnuExit Text E&xit
  Click mnuExit_Click()

You’ve already seen the code for mnuOpen, which is the same code that executes when the user double clicks the icon. Here is the code you use for mnuPause and mnuExit.

bool IsPaused;   // Is the timer paused?
 
private void btnPause_Click(object sender, System.EventArgs e)
{
   if (IsPaused)
   {
      // Start the timer.
      IsPaused = false;
      TypeTimer.Start();
 
      // Display status information.
      btnPause.Text = "&Pause";
      mnuPause.Text = "&Pause";
   }
   else
   {
      // Stop the timer.
      IsPaused = true;
      TypeTimer.Stop();
 
      // Display status information.
      btnPause.Text = "&Restart";
      mnuPause.Text = "&Restart";
      ThisNotifyIcon.Text = "Timer is Paused";
   }
}
 
private void mnuExit_Click(object sender, System.EventArgs e)
{
   // Obtain the current settings.
   Settings.DisplayMessageIcon = chkShowIcon.Checked;
   Settings.PlaySound = chkPlaySound.Checked;
   Settings.RestInterval = (Int32)RestingValue.Value;
   Settings.TypingInterval = (Int32)TypingInterval.Value;
 
   // Save the settings.
   TBMessages.SaveSettings(Settings);
 
   // Exit the application.
   Close();
}

The btnPause_Click() event handler is actually shared with the Pause button on frmMain, so you’ll see it mentioned again. Because there are two distinctly different ways to pause the application, the code requires a semaphore (a shared variable) to maintain the state information. When the application is paused, selecting Restart will change the IsPaused variable, restart the timer, and change the captions for both the Pause button and the NotificationMenu menu to Pause. The reverse happens when the application is running—IsPaused is set to true, the timer is stopped, and the captions are changed to Restart.

When the user clicks Exit on the NotificationMenu, the application calls the mnuExit_Click() event handler. This event handler changes the Settings properties to match those of frmMain, and then saves the settings to disk. It then closes frmMain, which ends the application.

That’s almost it for the Notification Area code. There is one added feature for ThisNotifyIcon. It displays the remaining time when you hover the mouse over it. This feature is covered in next week’s post, which discusses the Timer_Tick() event handler. This event handler is quite complex because it has to consider issues like which message to display, which can vary depending on whether the user has chosen to use dates with the messages. In the meantime, let me know whether you have any questions about this part of the application at John@JohnMuellerBooks.com. You can find the next post in this series at Exploring the TypingBuddy Application (Part 10).

 

Exploring the TypingBuddy Application (Part 8) Update

There is a small mistake in the first listing in Exploring the TypingBuddy Application (Part 8). I didn’t include a definition for two of the required global variables because they appear later in my personal code listing. As a result, the code won’t currently compile without error. You actually need three global variable definitions in place of the one shown for Settings. So, the global variable list in that first listing should look like this:

// Holds the main form.
static frmMain ThisFrmMain;
 
// Current typing time.
Int32 TimeLeft;
 
// Create a variable to hold the messages.
private TBMessages Settings;

Thank you to everyone who took time to write me about this error.  Let me know if you have any additional questions or observations at John@JohnMuellerBooks.com.

 

Exploring the TypingBuddy Application (Part 8)

In the previous post in this series, Exploring the TypingBuddy Application (Part 7), you saw the last underlying component of the message management part of this application. It’s time to start looking at frmMain—the part of the application that pulls everything together. From a design perspective, this part of the application has these parts:

 

  • Application loading and closing
  • Notification icon management
  • Timer management and event handling
  • User interface event handling


The posts that follow will discuss these elements one at a time. So, today, you’ll learn about how the application loads and what happens when you close it. When the application loads, it also has the load any settings that the user may have configured for it as shown here.

// Create a variable to hold the messages.
private TBMessages Settings;
 
private void frmMain_Load(object sender, System.EventArgs e)
{
   // Attempp to load the settings.
   Settings = TBMessages.LoadSettings();
 
   // See if the user has worked with this program before.
   if (Settings == null)
   {
      // If not, display a welcome message and the configuration
      // screen.
      MessageBox.Show("Welcome to TypingBuddy!\r\n" +
         "You need to set the initial typing and rest times by " +
         "entering a typing and resting interval and clicking " +
         "Save Settings. It's also a good idea to create custom " +
         "messages that will tell you specific things to do " +
         "while resting.",
         "Welcome Message",
         MessageBoxButtons.OK,
         MessageBoxIcon.Information);
      ThisFrmMain.Show();
      WindowState = FormWindowState.Normal;
 
      // Initialize Settings.
      Settings = new TBMessages();
   }
   else
   {
      // Configure the form.
      chkShowIcon.Checked = Settings.DisplayMessageIcon;
      chkPlaySound.Checked = Settings.PlaySound;
      RestingValue.Value = Convert.ToDecimal(Settings.RestInterval);
      TypingInterval.Value = Convert.ToDecimal(Settings.TypingInterval);
      TimeLeft = (Int32)TypingInterval.Value * 60;
 
      // Start the timer.
      TypeTimer.Start();
 
      // Hide the form.
      ThisFrmMain.Hide();
   }
}

The settings are stored into a TBMessages object, Settings, by calling TBMessages.LoadSettings(). When Settings is null the application either hasn’t been run before or the user has deleted the old settings. In this case, the application begins by telling the user that he/she has to provide some configuration settings. It then displays frmMain with the default configuration in place and saves a default set of settings to Settings. The user can make any required changes and close the form (a task described in an upcoming post).

When there are settings to use, the application configures itself according to those settings. It then starts the Timer object, TypeTimer. Notice that it immediately hides frmMain from view. The user sees the application in the Notification Area, but never gets any other indication that the application is running. This is how applications of this sort normally run. You could add a splash screen, if desired, to provide a visual cue to the user that the application is running. If you do add a splash screen, you need to provide a configuration setting for it. Some users will prefer not to see the splash screen.

The application also needs to perform a number of tasks when it ends. Typically, the user will click Quit on frmMain to accomplish this task or choose Exit on the Notification Area context menu (an option discussed in a later post). In both cases, there is one important task to perform—saving the settings to disk. The following code shows how this task is performed when clicking Quit on frmMain.

private void btnQuit_Click(object sender, System.EventArgs e)
{
   DialogResult RetValue;   // Users selection.
 
   // Display an exit message.
   RetValue = MessageBox.Show("Are you sure you want to exit?" +
      "\r\n(Program will minimize if you select No.)",
      "Exit Application",
      MessageBoxButtons.YesNo,
      MessageBoxIcon.Question,
      MessageBoxDefaultButton.Button2);
 
   if (RetValue == DialogResult.Yes)
   {
      // Obtain the current settings.
      Settings.DisplayMessageIcon = chkShowIcon.Checked;
      Settings.PlaySound = chkPlaySound.Checked;
      Settings.RestInterval = (Int32)RestingValue.Value;
      Settings.TypingInterval = (Int32)TypingInterval.Value;
 
      // Save the settings.
      TBMessages.SaveSettings(Settings);
 
      // Exit the application.
      Close();
   }
   else
   {
      // Minimize the form.
      WindowState = FormWindowState.Minimized;
      ThisFrmMain.Hide();
   }
}

The code begins by asking the user whether he/she is sure about ending the application. In some cases, a user will click a button by accident. Adding these sorts of reminders too often in an application is annoying. However, this is one time when a reminder is helpful because the application is ended when the user clicks Quit. What many users will want to click instead is Minimize Window.

When the user clicks Yes in response to the message. The application saves its settings and closes the form, which ends the application. However, when the user clicks No instead, the application merely minimizes the window. It then hides the window from view.

Well, that’s it for loading the application settings and ending the application. Next week we’ll discuss how the Notification Area context menu works. In the meantime, let me know whether you have any questions about the application at John@JohnMuellerBooks.com. Make sure you check out the update for this post at Exploring the TypingBuddy Application (Part 8) Update. You can see the next post in this series at Exploring the TypingBuddy Application (Part 9).

 

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).

 

Exploring the TypingBuddy Application (Part 6)

The previous post, Exploring the TypingBuddy Application (Part 5), describes the technique for displaying messages of various types. A user can choose to use the default message or provide any of a number of randomly selected custom messages. This post assumes that the user has chosen the custom message route. Of course, that means having a database of messages to choose from. The act of adding and editing messages requires a special form, one that contains all of the fields required to perform these two tasks. The TypingBuddy application relies on frmAddEdit to perform this task. You saw this form described in the Exploring the TypingBuddy Application (Part 3) post. As described by its name, this is a dual use form that allows for either adding or editing message records to the XML file that contains the user’s data for this application.

A developer has a number of techniques available to create forms used for multiple purposes. In this case, the caller exercises control over public members of frmAddEdit to modify its behavior. This approach is actually the easiest way to do things. In order to make configuration easier, frmAddEdit defines three special properties:

// Create a resources property that contains the message database.
public TBMessage[] MessageList { get; set; }
 
// Make it possible to edit a particular record.
public Int32 RecordNumber { get; set; }
 
// Check whether this is an add or an edit.
public Boolean IsEdit { get; set; }

These properties contain the message database, the selected record within that database, and a simple Boolean value that determines whether the form is in adding or editing mode. Because this form is created with flexibility in mind, you could probably add other properties to better control precisely how the form appears. However, you don’t require the level of flexibility described for frmMessage, so the use of multiple constructors (as in that case) is unnecessary.

The task of configuring the form falls to the frmAddEdit_Load() event handler. There are actually two levels of configuration required—the appearance of the user interface and the data shown within the form. The appearance is handled by the caller at the time the form is created, before it gets displayed. The data is configured as shown here:

private void frmAddEdit_Load(object sender, EventArgs e)
{
   // Fill the related sound list with the correct values.
   cbSound.Items.AddRange(
      Enum.GetNames(typeof(TBMessages.SystemSoundList)));
   cbSound.SelectedIndex = 0;
 
   // When this is an edit, update the form with the correct
   // data.
   if (IsEdit)
   {
      txtTitle.Text = MessageList[RecordNumber].Title;
      chkShowTitle.Checked = MessageList[RecordNumber].ShowTitle;
      txtMessageText.Text = MessageList[RecordNumber].MessageText;
      cbSound.SelectedItem = MessageList[RecordNumber].Sound.ToString();
      chkDateSpecific.Checked = MessageList[RecordNumber].UseDates;
 
      // Don't provide date values unless the message uses dates.
      if (MessageList[RecordNumber].UseDates)
      {
         dtStartDate.Value = MessageList[RecordNumber].StartDate;
         dtEndDate.Value = MessageList[RecordNumber].EndDate;
      }
      else
      {
         dtStartDate.Enabled = false;
         dtEndDate.Enabled = false;
      }
   }
}

When the form is in adding mode, the frmAddEdit_Load() event handler only has to add the list of available sounds to cbSound, and then select one of the items on the list. Because the form is already configured for adding mode, you don’t need to do anything to the user interface.

In editing mode, the frmAddEdit_Load() event handler must perform some additional tasks. It begins by configuring all of the common message data on the form. Then, if the UseDates property is checked, the event handler must also configure the starting and ending date values.

Handling the use of dates is an important consideration. You don’t want to set any dates unless the user says to use dates. Consequently, the application also requires the chkDateSpecific_CheckedChanged() event handler shown here.

private void chkDateSpecific_CheckedChanged(object sender, EventArgs e)
{
   // Determine the checked state.
   if (chkDateSpecific.Checked)
   {
      // Enable the dates when using date information.
      dtStartDate.Enabled = true;
      dtEndDate.Enabled = true;
   }
   else
   {
      // Otherwise, disable the dates.
      dtStartDate.Enabled = false;
      dtEndDate.Enabled = false;
   }
}

This event handler enables or disables the date fields as needed. Notice that the date fields are always enabled and disabled as a pair. The user must set both a starting and ending date in order for the date system to work properly.

The most complex piece of code for this part of the example appears in the btnAdd_Click() event handler. This code must incorporate a number of safety check to ensure the user doesn’t enter an invalid record into the database. In addition, the code must ensure that dates are only recorded when the user wants to employ dates as a filtering mechanism as shown here.

private void btnAdd_Click(object sender, EventArgs e)
{
   // Verify that the fields are correct.
   if ((txtTitle.TextLength == 0) || (txtMessageText.TextLength == 0))
   {
      // Display a message and exit when the user
      // hasn't provided the right information.
      MessageBox.Show(
         "You must provide values for both the Message Title " +
         "and Message Text fields!",
         "Entry Error",
         MessageBoxButtons.OK,
         MessageBoxIcon.Error);
      return;
   }
 
   // Create a temporary array.
   TBMessage[] Temp;
 
   // Determine the sort of change taking place.
   if (IsEdit)
   {
      // Initialize the temporary array.
      Temp = MessageList;
 
      // Add information to the new element.
      Temp[RecordNumber].Title = txtTitle.Text;
      Temp[RecordNumber].ShowTitle = chkShowTitle.Checked;
      Temp[RecordNumber].MessageText = txtMessageText.Text;
      Temp[RecordNumber].Sound =
         (TBMessages.SystemSoundList)Enum.Parse(
            typeof(TBMessages.SystemSoundList),
            cbSound.SelectedItem.ToString());
      Temp[RecordNumber].UseDates = chkDateSpecific.Checked;
 
      // Add dates only when required.
      if (chkDateSpecific.Checked)
      {
         Temp[RecordNumber].StartDate = dtStartDate.Value;
         Temp[RecordNumber].EndDate = dtEndDate.Value;
      }
 
      // Update the permanent message list.
      MessageList = Temp;
   }
   else
   {
      // Initialize the temporary array.
      if (MessageList != null)
      {
         // Create a temporary array that's one longer than the
         // existing array.
         Temp = new TBMessage[MessageList.Length + 1];
 
         // Copy the messages from the existing array to the
         // temporary array.
         for (Int32 Record = 0; Record < MessageList.Length; Record++)
            Temp[Record] = MessageList[Record];
 
         // Add a new record to the end of the temporary array.
         Temp[Temp.Length - 1] = new TBMessage();
      }
      else
      {
         Temp = new TBMessage[1];
         Temp[Temp.Length - 1] = new TBMessage();
      }
 
      // Add information to the new element.
      Temp[Temp.Length - 1].Title = txtTitle.Text;
      Temp[Temp.Length - 1].ShowTitle = chkShowTitle.Checked;
      Temp[Temp.Length - 1].MessageText = txtMessageText.Text;
      Temp[Temp.Length - 1].Sound =
         (TBMessages.SystemSoundList)Enum.Parse(
            typeof(TBMessages.SystemSoundList),
            cbSound.SelectedItem.ToString());
      Temp[Temp.Length - 1].UseDates = chkDateSpecific.Checked;
 
      // Add dates only when required.
      if (chkDateSpecific.Checked)
      {
         Temp[Temp.Length - 1].StartDate = dtStartDate.Value;
         Temp[Temp.Length - 1].EndDate = dtEndDate.Value;
      }
 
      // Update the permanent message list.
      MessageList = Temp;
   }
}

As a minimum, a message must include a title and some message text. If the user doesn’t include any other information, the application will either rely on defaults or not provide that information as part of the output. Consequently, txtTitle.Text and txtMessageText.Text are the only two required entries. If the user doesn’t provide these two entries, the application displays an error message and exits.

When the required input is correct, the code moves on to creating a temporary array to hold the updated data. This is a safety feature to ensure that the entire transaction takes place before being made permanent. If the application should suddenly fail between steps, the initial array will still hold valid information.

In editing mode, the btnAdd_Click() event handler begins by copying all of the records from MessageList (the permanent array) to Temp (the temporary array). It then changes each of the required values in Temp. Notice how the code handles dates. Again, it only saves dates when the user wants to use dates as a filtering mechanism. The final step is to make the change permanent by copying Temp to MessageList.

In adding mode, the application can’t assume there are any existing records. As a result, the first check is to determine whether this is a new list of messages or an addition to an existing list. When it’s an addition, the code simply adds a new entry to Temp and copies the required records from MessageList to Temp. However, when this is a new list, the code simply creates a single entry array in Temp. At this point, the process is much like editing an entry. The new values are added to the empty array element and then Temp is copied to MessageList.

Next week we’ll move one level up in the form hierarchy and examine frmMessages. In the meantime, let me know if you have any questions about this part of the example at John@JohnMuellerBooks.com. You can see the next part of this series at Exploring the TypingBuddy Application (Part 7).

 

Exploring the TypingBuddy Application (Part 5)

At this point, you have classes you can use to create and store messages in the user’s folder on the current system as described in Exploring the TypingBuddy Application (Part 4). These messages appear whenever the typing time is over in TypingBuddy. Of course, you need some way to display the messages. That’s the responsibility of frmMessage described in Exploring the TypingBuddy Application (Part 2).

One of the requirements for frmMessage is flexibility. The developer has a number of requirements for formatting the message box to convey information in the way the user wants to see it. In order to meet this requirement, frmMessage includes a number of constructors as shown here.

// Contains the current resting time.
private Int32 RemainingTime;
 
public frmMessage(Int32 RestTime)
{
   // Perform the default task.
   InitializeComponent();
 
   // Configure the resting timer.
   RemainingTime = RestTime;
 
   // Configure the Continue button.
   btnContinue.Enabled = false;
   btnContinue.Text = RemainingTime.ToString() + " Secs";
 
   // Start the timer.
   RestTimer.Start();
}
 
public frmMessage(Int32 RestTime, Boolean ShowIcon)
{
   // Perform the default task.
   InitializeComponent();
 
   // Configure the resting timer.
   RemainingTime = RestTime;
 
   // Configure the Continue button.
   btnContinue.Enabled = false;
   btnContinue.Text = RemainingTime.ToString() + " Secs";
 
   // Configure the window.
   this.ControlBox = ShowIcon;
 
   // Start the timer.
   RestTimer.Start();
}
 
public frmMessage(Int32 RestTime, String Message)
{
   // Perform the default task.
   InitializeComponent();
 
   // Configure the resting timer.
   RemainingTime = RestTime;
 
   // Configure the Continue button.
   btnContinue.Enabled = false;
   btnContinue.Text = RemainingTime.ToString() + " Secs";
 
   // Add a message.
   txtMessage.Text = Message;
 
   // Start the timer.
   RestTimer.Start();
}
 
public frmMessage(Int32 RestTime, String Title, String Message)
{
   // Perform the default task.
   InitializeComponent();
 
   // Configure the resting timer.
   RemainingTime = RestTime;
 
   // Configure the Continue button.
   btnContinue.Enabled = false;
   btnContinue.Text = RemainingTime.ToString() + " Secs";
 
   // Set the window titlebar.
   this.Text = Title;
 
   // Add a message.
   txtMessage.Text = Message;
 
   // Start the timer.
   RestTimer.Start();
}
 
public frmMessage(Int32 RestTime, String Message, Boolean ShowIcon)
{
   // Perform the default task.
   InitializeComponent();
 
   // Configure the resting timer.
   RemainingTime = RestTime;
 
   // Configure the Continue button.
   btnContinue.Enabled = false;
   btnContinue.Text = RemainingTime.ToString() + " Secs";
 
   // Configure the window.
   this.ControlBox = ShowIcon;
 
   // Add a message.
   txtMessage.Text = Message;
 
   // Start the timer.
   RestTimer.Start();
}
 
public frmMessage(Int32 RestTime, String Title,
   String Message, Boolean ShowIcon)
{
   // Perform the default task.
   InitializeComponent();
 
   // Configure the resting timer.
   RemainingTime = RestTime;
 
   // Configure the Continue button.
   btnContinue.Enabled = false;
   btnContinue.Text = RemainingTime.ToString() + " Secs";
 
   // Configure the window.
   this.Text = Title;
   this.ControlBox = ShowIcon;
 
   // Add a message.
   txtMessage.Text = Message;
 
   // Start the timer.
   RestTimer.Start();
}

All of the constructors require RestTime as input. This is the amount of time that the user must do something other than working at the computer (based on whatever messages the user has configured or using the default message). The code uses this input to initialize RemainingTime, which contains the amount of time remaining to rest. When working with the simplest constructor, the code sets btnContinue.Enabled to false so that the user must wait to clear the message box. It also sets btnContinue.Text to the initial resting time value. The code then starts the timer by calling RestTimer.Start().

Beyond this simple setup, the developer can also choose to provide additional information. For example, supplying the Boolean value, ShowIcon, determines whether frmMessage displays an icon when displayed. The String value, Message, contains a custom message that the user has defined. Otherwise, frmMessage displays a default message to the user. The developer can also choose to provide a custom title for the message box by supplying the String value, Title. The constructors can accept various combinations of these three inputs, or provide all of them.

Once the timer is started, it counts down the time for the rest period. The Timer control, RestTime performs this task for you. The Tick() event handler code is as shown here.

private void RestTimer_Tick(object sender, EventArgs e)
{
   // Check the remaining time.
   if (RemainingTime == 0)
   {
      // Stop the timer.
      RestTimer.Stop();
 
      // Configure the Continue button.
      btnContinue.Text = "&Continue";
      btnContinue.Enabled = true;
   }
   else
   {
      // Count down another second.
      RemainingTime--;
 
      // Display the update.
      btnContinue.Text = RemainingTime.ToString() + " Secs";
   }
}

During the countdown phase, the value of RemainingTime is decremented. The code then displays the new number of seconds in btnContinue.Text so the user can see it on screen. When RemainingTime reaches 0, the code calls RestTimer.Stop() to stop the timer. It then changes btnContinue.Text to “&Continue” (with the C underlined) and btnContinue.Enable to true so that the user can click the Continue button.

The code for the Continue and Quit Click() event handlers simply closes the form. The reaction differs based on the button the user clicks, but this is handled by frmMain.

Next we’ll you’ll see the code used to add and edit messages. This information is handled by frmAddEdit. In the meantime, let me know if you have questions about the TypingBuddy application code you’ve seen today at John@JohnMuellerBooks.com. You can see the next post in this series at Exploring the TypingBuddy Application (Part 6).

 

Exploring the TypingBuddy Application (Part 4)

The previous post in this series, Exploring the TypingBuddy Application (Part 3), completed the design elements of TypingBuddy. It’s time to begin looking at the application code. Of course, one of the more important issues is storing the user’s configuration information. The first version of TypingBuddy relied on the registry to store this information. However, as TypingBuddy evolved and using the registry has become more difficult due to Windows security restrictions, it’s a lot more practical to store the settings in a separate file. Using a separate file has these advantages:

 

  • No need to request a privilege elevation to write the data.
  • The user can easily create a backup of the application settings.
  • Moving the settings from one system to another is easy.
  • Errors in the file are easy to fix using any XML editor (or even Notepad for that matter).
  • Roaming users can have their settings moved automatically to whatever machine they’re using.


Because of the environment that both Vista and Windows 7 provide, using a configuration file is the only solution I use for applications today. Unless you have a significant reason to use the registry, it’s always better to use a configuration file as described in this post.

Before you do anything else, you need to add two classes to the application. The following procedure will get you started.

 

  1. Right click the TypingBuddy project entry in Solution Explorer and choose Add | Class from the context menu. You’ll see the Add New Item – TypingBuddy dialog box shown here.
    TypingBuddy0401
  2. Type TBMessage in the Name field and click Add. Visual Studio adds the TBMessage class to your application.
  3. Perform steps 1 and 2, replacing TBMessage with TBMessages. You’ll end up with a new TBMessage and TBMessages class in your project.


One of the first features you need to consider is that the user is able to assign a sound to the activities that TypingBuddy displays when it’s time to rest. In order to make working with the sounds easier, this version of the application relies on the following enumeration found in the TBMessages class.

// Defines the set of system sounds available for a message.
public enum SystemSoundList
{
   None,
   Asterisk,
   Beep,
   Exclamation,
   Hand,
   Question
}

All that this enumeration does is make it easy to provide a list of common sounds for your application. The first place you see these sounds is in the TBMessage class code shown here.

[Serializable()]
public class TBMessage
{
   // Create automatically implemented properties
   // for each of the message arguments.
   public String Title { get; set; }
   public Boolean ShowTitle { get; set; }
   public String MessageText { get; set; }
   public TBMessages.SystemSoundList Sound { get; set; }
   public Boolean UseDates { get; set; }
   public DateTime StartDate { get; set; }
   public DateTime EndDate { get; set; }
}

One of the more important features to notice is that the class uses the [Serializable()] attribute, which makes it possible to store the data found in this class in an XML file with relative ease. No matter how complex the data becomes, you can use the [Serializable()] attribute to make it easy to turn it into an XML data file. The rest of the class is a series of strongly typed properties. Normally, I’d recommend adding error trapping to the get() method for each property, but in this case, there really isn’t much a caller could do to create data that has easily detected errors. About the only thing you could do is to monitor the lengths of the two String properties, Title and MessageText. However, given the way these properties are used, it would be hard to create an injection script scenario that would present some sort of risk to the application or its data.

The TBMessage class defines the content of a single message.  However, a user is likely to create a wealth of messages. The purpose of the TBMessages class is to manage a group of TBMessage class entries. The naming of the class implies as much. A plural form of a singular class usually denotes some sort of collection or array. Here’s the code for the TBMessages class.

// Define the configuration properties.
public Int32 TypingInterval { get; set; }
public Int32 RestInterval { get; set; }
public Boolean DisplayMessageIcon { get; set; }
public Boolean PlaySound { get; set; }
 
// Define an array of messages.
public TBMessage[] MessageList { get; set; }
 
// Create a method for saving the settings.
public static void SaveSettings(TBMessages Settings)
{
   // Define a path to the user settings.
   String UserPath =
      Environment.GetFolderPath(
         Environment.SpecialFolder.ApplicationData) + @"\TypingBuddy";
 
   // Create the path if necessary.
   if (!Directory.Exists(UserPath))
      Directory.CreateDirectory(UserPath);
 
   // Create an XML serializer.
   XmlSerializer DataWriter = new XmlSerializer(typeof(TBMessages));
 
   // Define a data stream.
   StreamWriter Output = new StreamWriter(UserPath + @"\AppData.Config");
 
   // Perform the data write.
   DataWriter.Serialize(Output, Settings);
 
   // Close the stream.
   Output.Close();
}
 
// Create a method for loading the settings.
public static TBMessages LoadSettings()
{
   // Define a path to the user settings.
   String UserPath =
      Environment.GetFolderPath(
         Environment.SpecialFolder.ApplicationData) + @"\TypingBuddy";
 
   // Check for the file.
   if (!File.Exists(UserPath + @"\AppData.Config"))
      return null;
 
   // Create an XML serializer.
   XmlSerializer DataReader = new XmlSerializer(typeof(TBMessages));
 
   // Define a data stream.
   StreamReader Input = new StreamReader(UserPath + @"\AppData.Config");
 
   // Load the settings.
   TBMessages Settings = new TBMessages();
   Settings = (TBMessages)DataReader.Deserialize(Input);
   Input.Close();
 
   // Return the settings to the caller.
   return Settings;
}

The code begins with a list of properties. These properties differ from those found in the TBMessage class in that they affect the application as a whole. In other words, you only need one set of these values in the XML file, not a set for every message that the user wants to see. The properties include the typing time, resting time, whether the messages should include an icon, and whether the user wants to hear a sound play when the message is displayed.

Both of the methods in this class, SaveSettings() and LoadSettings() are static, which means that the developer can call them without creating an instance of the class. This feature is important because the interface code will use these methods to save and load the XML file to and from disk.

The SaveSettings() code requires that that caller supply an object of type TBMessages (which includes both the application level settings and all of the message settings) containing the settings to save to disk. The code begins by obtaining the location of the user’s roaming application data folder. On my machine, that’s the C:\Users\John\AppData\Roaming folder. It then adds the \TypingBuddy subfolder, which contains the settings for the application. If this folder doesn’t exist, the application creates it. The application then creates an XmlSerializer of type TBMessages named DataReader to handle the data file content. Writing is performed using a StreamWriter object, Output, that points to the AppData.Config file in the TypingBuddy folder. The application then serializes the object data in Settings to the AppData.Config file and closes the file. It’s absolutely essential that you always call Close() to close the data file or the file can become corrupted. Here’s some typical output information.

TypingBuddy0402

As you can see, this XML file reflects the sort of data you’d expect. It begins with the application level settings and then contains a list of <TBMessage> entries containing the individual messages.

The LoadSettings() method works similar to the SaveSettings() method, except in reverse. In this case, you’re loading settings from disk into a TBMessages object and passing that object back to the caller. The code begins in the same way, by defining the location of the data file on disk. Notice that that code contains error trapping that returns null when the file doesn’t exist. In place of a StreamWriter, the application uses a StreamReader, Input, to read the data from disk. The code relies on the XmlSerializer, DataReader, to deserialize the data and place it into a TBMessages object, Settings. As before, make absolutely certain to call Close() to close the file when you’re finished using it.

That’s it for the data file-specific code. Next week we’ll begin using the data to configure the application. In the meantime, please let me know if you have any questions about this example at John@JohnMuellerBooks.com. You can see the next post in this series at Exploring the TypingBuddy Application (Part 5).