Team LiB
Previous Section Next Section

The Data Structures

You should start programming this project by looking at the data structures. As I mentioned, they are a set of collections based on some abstracted database. The collections are homegrown for two reasons:

The last reason is why I use homegrown collections the most. Consider that a collection has these five methods in common:

The methods are Add, Remove, Item, and Count.

If I made a DLL that exposed some collection of GolfCourses, I would not want to expose a raw collection because the client would be able to call the Add and Remove functions of the collection directly. There would be no way for the DLL to track what is going in and out of the collection.

A better way would be to make your own collection and expose only the Count and Item properties. Adding and removing an object from the collection should be done by methods in the class that holds the collection. This allows you to control what gets added to the collection, and it also allows you to do some other processing that may be needed when something is added to or deleted from the collection. You can limit the exposure of some object's method with the Friend keyword in VB and the Internal keyword in C#.

Listings 11-1a and 11-1b show the code for the IHoleDetails collection. Note that a collection of an item has the suffix s . The object is called IHoleDetail and the collection is called IHoleDetails.

Listing 11-1a: C# Code for the IHoleDetails Collection Classes
Start example
  #region Hole Detail collection Classes

  public class IHoleDetailInfos : IEnumerable
  {
     //Slower than Hash table but more flexible
     //Can get item three ways
     //Most like VB type collection
     private SortedList mCol;

     public IHoleDetailInfos()
     {
       mCol = new SortedList();
     }

     #region collection methods

     // enables foreach processing
     private mEnum GetEnumerator()
     {
       return new mEnum(this);
     }

     //Property count
     public int Count
     {
       get { return mCol.Count; }
     }

     // ----- add method ------
     public void Add(IHoleDetailInfo hole)
     {
       mCol.Add(hole.ToString(), hole);
       mCol.TrimToSize();
     }

     // ----- overloaded remove method ------
     public void Remove(int Index)
     {
       mCol.RemoveAt(Index);
     }
     public void Remove(string key)
     {
       mCol.Remove(key);
     }

     // ----- overloaded item method ------
     public IHoleDetailInfo Item(int index)
     {
       return (IHoleDetailInfo) mCol.GetByIndex(index);
     }
     public IHoleDetailInfo Item(string key)
     {
       return (IHoleDetailInfo) mCol[key];
     }

     #endregion

     #region enumeration methods

     // Implement the GetEnumerator() method:
     IEnumerator IEnumerable.GetEnumerator()
     {
       return GetEnumerator();
     }
     // Declare the enumerator and implement the IEnumerator interface:
     private class mEnum: IEnumerator
     {
       private int nIndex;
       private IHoleDetailInfos collection;

       // constructor. make the collection
       public mEnum(IHoleDetailInfos coll)
       {
         collection = coll;
         nIndex = -1;
       }

       // start over
       public void Reset()
       {
         nIndex = -1;
       }

       // bump up the index
       public bool MoveNext()
       {
         nIndex++;
         return(nIndex < collection.mCol.Count);
       }

       // get the current object
       // The current property on the IEnumerator interface:
       object IEnumerator.Current
       {
         get { return(collection.mCol.GetByIndex(nIndex)); }
       }
     }

     #endregion
   }


   public class IHoleDetailInfo
   {
     #region Locals
     private YardMarker  mTeeBox;
     private int         mHole;
     private int         mPar;
     private GolfClubs   mTeeClub;
     private GolfClubs   mSecondClub;
     private bool        mHitfairway;
     private bool        mGood2Shot;
     private int         mShots2Green;
     private int         mPutts;
     private int         mTotalShots;

     #endregion

     public IHoleDetailInfo()
     {
       mTotalShots = 0;
     }

     public override string ToString()
     {
       return mHole.ToString();
     }

     #region Properties

     public YardMarker TeeBox
     {
       get{return mTeeBox;}
       set{mTeeBox = value;}
     }

     public GolfClubs ScondClub
     {
       get{return mSecondClub;}
       set{mSecondClub = value;}
     }

     public GolfClubs TeeClub
     {
       get{return mTeeClub;}
       set{mTeeClub = value;}
     }

     public bool GoodSecondShot
     {
       get{return mGood2Shot;}
       set{mGood2Shot = value;}
     }

     public bool HitFairway
     {
       get{return mHitfairway;}
       set{mHitfairway = value;}
     }

     public int TotalShots
     {
       get{return mTotalShots;}
       set{mTotalShots = value;}
     }

     public int Putts
     {
       get{return mPutts;}
       set{mPutts = value;}
     }

     public int ShotsToGreen
     {
       get{return mShots2Green;}
       set{mShots2Green = value;}
     }

     public bool GreenInReg
     {
       get{return((mPar - mShots2Green >= 2) ? true : false);}
     }

     public int Par
     {
       get{return mPar;}
       set{mPar = value;}
     }

     public int Hole
     {
       get{return mHole;}
       set{mHole = value;}
     }

     #endregion
   }

   #endregion
End example
Listing 11-1b: VB Code for the IHoleDetails Collection Classes
Start example
#Region "Hole Detail collection Classes"

Public Class IHoleDetailInfos
  Implements IEnumerable

  'Slower than Hash table but more flexible
  'Can get item three ways
  'Most like VB type collection
  Private mCol As SortedList

  Public Sub New()
    mCol = New SortedList()
  End Sub

  ' enables foreach processing
  Private Function GetMyEnumerator() As mEnum
    Return New mEnum(Me)
  End Function

  ' Implement the GetEnumerator() method:
  Public Function GetEnumerator() As IEnumerator Implements
   IEnumerable.GetEnumerator
    Return GetMyEnumerator()
  End Function

  'Property count
  Public ReadOnly Property Count() As Int32
    Get
      Return mCol.Count
    End Get
  End Property

  ' ----- add method ------
  Public Sub Add(ByVal hole As IHoleDetailInfo)
    mCol.Add(hole.ToString(), hole)
    mCol.TrimToSize()
  End Sub

  ' ----- overloaded remove method ------
  Public Sub Remove(ByVal Index As Int32)
    mCol.RemoveAt(Index)
  End Sub

  Public Sub Remove(ByVal key As String)
    mCol.Remove(key)
  End Sub

  ' ----- overloaded item method ------
  Public Function Item(ByVal index As Int32) As IHoleDetailInfo
    Return CType(mCol.GetByIndex(index), IHoleDetailInfo)
  End Function

  Public Function Item(ByVal key As String) As IHoleDetailInfo
    Return CType(mCol(key), IHoleDetailInfo)
  End Function

  ' Declare the enumerator and implement the IEnumerator interface:
  Private Class mEnum
    Implements IEnumerator

    Private nIndex As Int32
    Private collection As IHoleDetailInfos

    ' constructor. make the collection
    Public Sub New(ByVal coll As IHoleDetailInfos)
      collection = coll
      nIndex = -1
    End Sub

    ' start over
    Public Sub Reset() Implements IEnumerator.Reset
      nIndex = -1
    End Sub
      ' bump up the index
      Public Function MoveNext() As Boolean Implements IEnumerator.MoveNext
        nIndex += 1
        Return (nIndex < collection.mCol.Count)
      End Function

      ' The current property on the IEnumerator interface:
      Public ReadOnly Property Current() As Object Implements IEnumerator.Current
        Get
          Return collection.mCol.GetByIndex(nIndex)
        End Get
      End Property
    End Class
 
 End Class


  Public Class IHoleDetailInfo

  #Region "Locals"

    Private mTeeBox As YardMarker
    Private mHole As Int32
    Private mPar As Int32
    Private mTeeClub As GolfClubs
    Private mSecondClub As GolfClubs
    Private mHitfairway As Boolean
    Private mGood2Shot As Boolean
    Private mShots2Green As Int32
    Private mPutts As Int32
    Private mTotalShots As Int32

  #End Region

    Public Sub New()
      mTotalShots = 0
    End Sub

    Public Overrides Function ToString() As String
      Return mHole.ToString()
    End Function

  #Region "Properties"

  Public ReadOnly Property GreenInReg() As Boolean
    Get
      Return (IIf(mPar - mShots2Green >= 2, True, False))
    End Get
  End Property

  Public Property TeeBox() As YardMarker
    Get
      Return mTeeBox
    End Get
    Set(ByVal Value As YardMarker)
      mTeeBox = Value
    End Set
  End Property

  Public Property ScondClub() As GolfClubs
    Get
      Return mSecondClub
    End Get
    Set(ByVal Value As GolfClubs)
      mSecondClub = Value
    End Set
  End Property

  Public Property TeeClub() As GolfClubs
    Get
      Return mTeeClub
    End Get
    Set(ByVal Value As GolfClubs)
      mTeeClub = Value
    End Set
  End Property

  Public Property GoodSecondShot() As Boolean
    Get
      Return mGood2Shot
    End Get
    Set(ByVal Value As Boolean)
      mGood2Shot = Value
    End Set
  End Property

  Public Property HitFairway() As Boolean
    Get
      Return mHitfairway
    End Get
    Set(ByVal Value As Boolean)
      mHitfairway = Value
    End Set
  End Property

  Public Property TotalShots() As Int32
    Get
      Return mTotalShots
    End Get
    Set(ByVal Value As Int32)
      mTotalShots = Value
    End Set
  End Property

  Public Property Putts() As Int32
    Get
      Return mPutts
    End Get
    Set(ByVal Value As Int32)
      mPutts = Value
    End Set
  End Property

  Public Property ShotsToGreen() As Int32
    Get
      Return mShots2Green
    End Get
    Set(ByVal Value As Int32)
      mShots2Green = Value
    End Set
  End Property

  Public Property Par() As Int32
    Get
      Return mPar
    End Get
    Set(ByVal Value As Int32)
      mPar = Value
    End Set
  End Property

  Public Property Hole() As Int32
    Get
      Return mHole
    End Get
    Set(ByVal Value As Int32)
      mHole = Value
    End Set
  End Property

#End Region
End Class
End example

I have three such sets of classes: one for the individual holes, one for the scorecard, and one for the golf course itself. Figure 11-7 shows the graphical view of the data structure.

Click To expand
Figure 11-7: The data structure for the Golf project

What you have here is a collection of GolfCourse objects. Each of these GolfCourse objects has a collection of ScoreCard objects. Each of these ScoreCard objects has a collection of HoleDetail objects. Collections of collections of collections.

There is nothing too exciting about the IHoleDetail object except for one thing: I override the ToString() method. You have seen me do this before. I do it so that I can add an IHoleDetail object to a ComboBox list. The ComboBox always uses the ToString() method as the text to show in the control.

Examining the Collection

The collection is a bit more exciting. Notice that the IHoleDetails collection implements the IEnumerable interface.[4] I use a SortedList as the base collection for all the homegrown collections in this project. I do this because you can get and remove objects via an index or a key. It has the added benefit of keeping the list sorted, which can come in handy. The methods I have in here are as follows:

  • Count

  • Add

  • Remove

  • Item

These are the classic collection methods.

The IEnumerable interface gives me the ability to enumerate over the collection using the foreach construct. This code implements the following functions:

  • Reset: Enumeration is done in a forward-only fashion. This function starts at the beginning.

  • MoveNext: This function bumps up the index into the collection. It also makes sure you cannot go past the end of the collection.

  • Current: This function returns the current object in the collection as defined by the index.

Here is the code for the IScoreCardInfo object.

C#

  public class IScoreCardInfo
  {
    public IHoleDetailInfos holes;
    private DateTime mDate;
    public IScoreCardInfo()
    {
      holes = new IHoleDetailInfos();
      mDate = DateTime.Now;
    }

    public IScoreCardInfo(int numHoles)
    {
      holes = new IHoleDetailInfos();
      IHoleDetailInfo h;
      for(int k=0; k<numHoles; k++)
      {
        h = new IHoleDetailInfo();
        h.TotalShots = 0;
        h.Hole = k+1;
        h.Putts = 0;
        holes.Add(h);
      }

      mDate = DateTime.Now;
    }

    public DateTime PlayDate
    {
      get{return mDate;}
      set{mDate = value;}
    }

    public int RoundScore
    {
      get
      {
        int score = 0;
        foreach(IHoleDetailInfo h in holes)
          score += h.TotalShots;
        return score;
      }
    }
  }

VB

Public Class IScoreCardInfo

Public holes As IHoleDetailInfos
Private mDate As DateTime

Public Sub New()
  holes = New IHoleDetailInfos()
  mDate = DateTime.Now
End Sub

Public Sub New(ByVal numHoles As Int32)

  holes = New IHoleDetailInfos()
  Dim h As IHoleDetailInfo
  Dim k As Int32
  For k = 0 To numHoles - 1
    h = New IHoleDetailInfo()
    h.TotalShots = 0
    h.Hole = k + 1
    h.Putts = 0
    holes.Add(h)
  Next

  mDate = DateTime.Now
End Sub

Public Property PlayDate() As DateTime
  Get
    Return mDate
  End Get
  Set(ByVal Value As DateTime)
    mDate = Value
  End Set
End Property

  Public ReadOnly Property RoundScore() As Int32
    Get
      Dim score As Int32 = 0
      Dim h As IHoleDetailInfo
      For Each h In holes
        score += h.TotalShots
      Next
      Return score
    End Get
  End Property
End Class

You can see that there is a public collection of IHoleDetailInfo objects. Each scorecard has its own collection. The constructor is responsible for making this collection and initializing some variables.

Notice also that the RoundScore property of this object gets the score in real time. It enumerates over its collection of holes and totals the individual scores. If I did not implement the IEnumerable interface on the IHoleDetails object, I would not be able to do this.

The final collection here is the GolfCourse collection. This contains a list of tee distances and a collection of scorecards. I refer you to the book's code (which you can download from the Downloads section of the Apress Web site at http://www.apress.com) to see this.

The rest of this CollectionClass code contains a static Globals class and some enums. The Globals class is an interesting way to have global variables within a program. It is even thread-safe because I only read variables from it.

The Database Class

I have a class that acts as a buffer between the program and the database. As I mentioned, I do not keep a database for this project, but the code in this class demonstrates how to work with whatever database you do use. This database class shows how to construct the golf course from the holes to the scorecards on up.

Listings 11-2a and 11-2b show the complete database class.

Listing 11-2a: C# Code for the Database Class
Start example

using System;

namespace Golf_c
{
  /// <summary>
  /// This is the database abstraction code.
  /// Note that the rest of the program knows nothing
  /// about the database or even that there is one.
  /// The only thing it knows about is the GolfCourse collection
  /// </summary>
  public class Database
  {

    #region locals

    public ICourseInfos GolfCourses;

    #endregion

    public Database()
    {
      GolfCourses = new ICourseInfos();

      //You could either put some code here to get the complete database at the
      //start or make a method that needs to be called explicitly to
      //do it. This database, even after years of playing, would be very small.
      //Caching the whole thing in memory is not a big deal.

      //This simulates getting the data from a database.
      GetGolfCourses();
    }

    public void SaveCourse(ICourseInfo GolfCourse)
    {

      if(GolfCourses.Item(GolfCourse.Name) == null)
        GolfCourses.Add(GolfCourse);
      //Put some code in here to save to a database.
    }

    private void GetGolfCourses()
    {
      //Go out to the database and get the golf course info here
      ICourseInfo course = new ICourseInfo();
      course.NumberOfHoles  = 9;
      course.Name           = "My Back Yard";
      course.Par            = CoursePar._35;
      course.Slope          = 127;

      Tees tee;
      for(int k=0; k<course.Hole.Count; k++)
      {
        tee = (Tees)course.Hole[k];
        tee.BlueDistance    = 450;
        tee.WhiteDistance   = 430;
        tee.RedDistance     = 400;
        tee.Par             = k<5 ? 4 : 5;
        tee.HoleNumber      = k+1;
      }

      IScoreCardInfo card = new IScoreCardInfo();
      card.PlayDate = DateTime.Now;

      IHoleDetailInfo detail;
      for(int k=0; k<course.NumberOfHoles; k++)
      {
        detail = new IHoleDetailInfo();
        detail.Hole = k+1;
        detail.TeeClub = GolfClubs.Driver;
        detail.HitFairway = true;
        detail.ScondClub = GolfClubs.Nine_wood;
        detail.GoodSecondShot = true;
        detail.ShotsToGreen = 2;
        detail.Putts = 2;
        detail.TotalShots = 4;
        detail.Par = ((Tees)course.Hole[k]).Par;
        detail.TeeBox = YardMarker.White;
        card.holes.Add(detail);
      }
      course.ScoreCards.Add(card);

      GolfCourses.Add(course);
    }
  }
}
End example
Listing 11-2b: VB Code for the Database Class
Start example
Option Strict On

' This is the database abstraction code.
' Note that the rest of the program knows nothing
' about the database or even that there is one.
' The only thing it knows about is the GolfCourse collection
' This database abstraction includes some multithreaded code.
Public Class Database

#Region "locals"

  Public GolfCourses As ICourseInfos

#End Region

  Public Sub New()
    GolfCourses = New ICourseInfos()

    'You could either put some code here to get the complete database at the
    'start or make a method that needs to be called explicitly to
    'do it. This database, even after years of playing, would be very small.
    'Caching the whole thing in memory is not a big deal.

    'This simulates getting the data from a database.

    GetGolfCourses()
  End Sub

  Public Sub SaveCourse(ByVal GolfCourse As ICourseInfo)

    If GolfCourses.Item(GolfCourse.Name) Is Nothing Then
      GolfCourses.Add(GolfCourse)
    End If
      'Put some code in here to save to a database.
    End Sub

    Private Sub GetGolfCourses()
      'Go out to the database and get the golf course info here
      Dim course As ICourseInfo = New ICourseInfo()
      course.NumberOfHoles = 9
      course.Name = "My Back Yard"
      course.Par = CoursePar._35
      course.Slope = 127

      Dim tee As Tees
      Dim k As Int32
      For k = 0 To course.Hole.Count - 1
        tee = CType(course.Hole(k), Tees)
        tee.BlueDistance = 450
        tee.WhiteDistance = 430
        tee.RedDistance = 400
        If k < 5 Then
          tee.Par = 4
        Else
          tee.Par = 5
        End If
        tee.HoleNumber = k + 1
      Next

      Dim card As IScoreCardInfo = New IScoreCardInfo()
      card.PlayDate = DateTime.Now

      Dim detail As IHoleDetailInfo

      For k = 0 To course.NumberOfHoles - 1
        detail = New IHoleDetailInfo()
        detail.Hole = k + 1
        detail.TeeClub = GolfClubs.Driver
        detail.HitFairway = True
        detail.ScondClub = GolfClubs.Nine_wood
        detail.GoodSecondShot = True
        detail.ShotsToGreen = 2
        detail.Putts = 2
        detail.TotalShots = 4
        detail.Par = (CType(course.Hole(k), Tees)).Par
        detail.TeeBox = YardMarker.White
        card.holes.Add(detail)
      Next
      course.ScoreCards.Add(card)

      GolfCourses.Add(course)

    End Sub

  End Class
End example

What I have here is a class local variable that holds the collection of golf courses associated with this database. When the constructor is called, I go and create a single IGolfCourseInfo object on the fly that has just one IScoreCardInfo object.

You can see from this code how easy it is to work with homegrown collections. Try adding an object to the GolfCourses collection that is not an IGolfCourseInfo object. You will get a compiler error. You never even get to run the code.

If you had a simple collection such as an ArrayList, you would be able to put any object in it. This would cause problems down the line. Catching errors at compile time is the way to go if you can.

Starting Up the Program

The initial screen for this program is the MDI parent screen, of course. Let's look at the code for that.

Basically, what I have in this class is a class local variable called "db" that is an instantiated database object. Now when I bring up some of these other child forms, such as the course form or the statistics form, I need access to this database object. After all, it is not only the interface to the actual database, but also it is the keeper of the GolfCourses collection.

Although I have quite a few items in the menu, only a few are wired up to anything. They are the menu choices to open up a course for editing and to view statistics. Here is the code to instantiate these two child forms.

C#

    private void EditCourse(object sender, EventArgs e)
    {
      Course frm = new Course();
      frm.MdiParent = this;
      frm.Show();
    }

    private void Stats(object sender, EventArgs e)
    {
      Statistics frm = new Statistics();
      frm.MdiParent = this;
      frm.Show();
    }

VB

  Private Sub EditCourse(ByVal sender As Object, ByVal e As EventArgs)
    Dim frm As Course = New Course()
    frm.MdiParent = Me
    frm.Show()
  End Sub

  Private Sub Stats(ByVal sender As Object, ByVal e As EventArgs)
    Dim frm As Statistics = New Statistics()
    frm.MdiParent = Me
    frm.Show()
  End Sub

All I do is make a new object of the child form I want and set its MdiParent property to the current form. Setting the MdiParent property does two things. First, it tells .NET that this is a child form and that it should reside in the parent's container. Second, it gives the child form a mechanism to call back into the parent form whenever it needs to.

Calling back into the parent form is how the child forms access the database object. It is also a standard way for child forms to communicate with each other. In this case, the parent form acts as an intermediary for child-to-child communication.

The first child form you will look at is the courses form. This form has a tab control that separates editing of the course itself and the course scores. Let's look at the first tab again, as shown in Figure 11-8.

Click To expand
Figure 11-8: The Course Setup tab

The first thing you may notice is that I swapped the location of the tabs via code. The design-time version of this form has the tabs on the top. For a program like this, I prefer the Excel tab locations.

This form is currently in edit mode. As soon as I click the Edit button, I disable it and I also disable the ability to create a new course. This prevents the user from doing anything he or she should not do. It also prevents me from having to write tons of code that checks if a certain operation is allowed if I am in edit mode. I just do not allow it.

While in edit mode, I enable the Holes, Par, and Slope fields, as well as the Save button. Now, I could have made the Holes and the Par fields TextBoxes and left it at that. If I did that, though, I would need to write a fair bit of validation code to make sure that the choices matched a certain range. I would have the added problem that the range of Par values differs if you have 9 or 18 holes.

The best thing to do is what I do here. I use the CoursePar enumeration from the Collections class to fill in the Par ComboBox. This enum contains all the common possibilities for either 9 or 18 holes of golf. If the user chooses 9 holes, then I clear the Par ComboBox and refill it with the appropriate values.

Listings 11-3a and 11-3b show the initialization code for the Course Setup tab and the delegate that handles the Holes ComboBox click event.

Listing 11-3a: C# Code for the Course Setup Tab and cmbHoles Click Event
Start example
    private void GolfCourseTabSetup()
    {

      lblLength.BackColor = Color.LightGray;

      //Slope must be between 55 and 155
      txtSlope.MaxLength  = Globals.SlopeLen;
      txtSlope.KeyPress   += new KeyPressEventHandler(this.SlopeKeyPress);
      txtSlope.Validating += new CancelEventHandler(this.SlopeValidate);
      cmdNew.BackColor = Color.SandyBrown;
      cmdNew.Font      = cmdQuit.Font;
      cmdNew.Image     = Image.FromFile("new.ico");
      cmdNew.Click     += new EventHandler(this.NewCourse);

      cmdSave.BackColor = Color.SandyBrown;
      cmdSave.Font      = cmdQuit.Font;
      cmdSave.Click     += new EventHandler(this.SaveCourse);
      cmdSave.Enabled   = false;

      cmdEdit.BackColor = Color.SandyBrown;
      cmdEdit.Font      = cmdQuit.Font;
      cmdEdit.Click     += new EventHandler(this.EditCourse);

      cmbPar.DropDownStyle = ComboBoxStyle.DropDownList;

      cmbHoles.DropDownStyle = ComboBoxStyle.DropDownList;
      cmbHoles.Items.Add(Globals.NineHoles);
      cmbHoles.Items.Add(Globals.EighteenHoles);
      cmbHoles.SelectedIndexChanged += new EventHandler(this.SelectPar);
      cmbHoles.SelectedIndex = 1;

      cmbCourseName.MaxLength = 60;
      cmbCourseName.DropDownStyle = ComboBoxStyle.DropDownList;
      cmbCourseName.SelectedIndexChanged += new EventHandler(ChangeCourse);
      foreach(ICourseInfo c in mParent.db.GolfCourses)
      {
        cmbCourseName.Items.Add(c);
      }
      if(cmbCourseName.Items.Count>0)
        cmbCourseName.SelectedIndex = 0;

      lstTees.MouseUp += new MouseEventHandler(EditTeeBox);
    }

    private void SelectPar(object sender, EventArgs e)
    {
      Debug.Assert(sender == cmbHoles,
                   "SelectPar method called by wrong control");

      cmbPar.BeginUpdate();
      cmbPar.Items.Clear();
      if((int)cmbHoles.SelectedItem == Globals.NineHoles)
      {
        cmbPar.Items.Add((int)CoursePar._36);
        cmbPar.Items.Add((int)CoursePar._35);
        cmbPar.Items.Add((int)CoursePar._27);
      }
      else
      {
        cmbPar.Items.Add((int)CoursePar._72);
        cmbPar.Items.Add((int)CoursePar._71);
        cmbPar.Items.Add((int)CoursePar._70);
        cmbPar.Items.Add((int)CoursePar._54);
      }
      cmbPar.SelectedIndex = 0;
      cmbPar.EndUpdate();

      //While I am in here I need to create the listview on the fly
      //The listview depends upon the number of holes
      if(ThisCourse != null)
        Debug.WriteLine(ThisCourse.Name);

      if(ThisCourse != null && !cmdEdit.Enabled)
        ThisCourse.NumberOfHoles = (int)cmbHoles.SelectedItem;

      SetupTeeList();
   }
End example
Listing 11-3b: VB Code for the Course Setup Tab and cmbHoles Click Event
Start example
  Private Sub GolfCourseTabSetup()

    lblLength.BackColor = Color.LightGray

    'Slope must be between 55 and 155
    txtSlope.MaxLength = Globals.SlopeLen
    AddHandler txtSlope.KeyPress, New KeyPressEventHandler(AddressOf SlopeKeyPress)
    AddHandler txtSlope.Validating, New CancelEventHandler(AddressOf SlopeValidate)

    cmdNew.BackColor = Color.SandyBrown
    cmdNew.Font = cmdQuit.Font
    cmdNew.Image = Image.FromFile("new.ico")
    AddHandler cmdNew.Click, New EventHandler(AddressOf NewCourse)

    cmdSave.BackColor = Color.SandyBrown
    cmdSave.Font = cmdQuit.Font
    AddHandler cmdSave.Click, New EventHandler(AddressOf SaveCourse)
    cmdSave.Enabled = False

    cmdEdit.BackColor = Color.SandyBrown
    cmdEdit.Font = cmdQuit.Font
    AddHandler cmdEdit.Click, New EventHandler(AddressOf EditCourse)

    cmbPar.DropDownStyle = ComboBoxStyle.DropDownList

    cmbHoles.DropDownStyle = ComboBoxStyle.DropDownList
    cmbHoles.Items.Add(Globals.NineHoles)
    cmbHoles.Items.Add(Globals.EighteenHoles)
    AddHandler cmbHoles.SelectedIndexChanged, _
                           New EventHandler(AddressOf SelectPar)
    cmbHoles.SelectedIndex = 1

    cmbCourseName.MaxLength = 60
    cmbCourseName.DropDownStyle = ComboBoxStyle.DropDownList
    AddHandler cmbCourseName.SelectedIndexChanged, _
                             New EventHandler(AddressOf ChangeCourse)
    Dim c As ICourseInfo
    For Each c In mParent.db.GolfCourses

      cmbCourseName.Items.Add(c)
    Next
    If cmbCourseName.Items.Count > 0 Then
      cmbCourseName.SelectedIndex = 0
    End If

    AddHandler lstTees.MouseUp, New MouseEventHandler(AddressOf EditTeeBox)

  Private Sub SelectPar(ByVal sender As Object, ByVal e As EventArgs)
    Debug.Assert(sender Is cmbHoles, _
                "SelectPar method called by wrong control")

    cmbPar.BeginUpdate()
    cmbPar.Items.Clear()
    If CType(cmbHoles.SelectedItem, Int32) = Globals.NineHoles Then
     cmbPar.Items.Add(CType(CoursePar._36, Int32))
     cmbPar.Items.Add(CType(CoursePar._35, Int32))
     cmbPar.Items.Add(CType(CoursePar._27, Int32))

   Else
     cmbPar.Items.Add((CType(CoursePar._72, Int32)))
     cmbPar.Items.Add(CType(CoursePar._71, Int32))
     cmbPar.Items.Add(CType(CoursePar._70, Int32))
     cmbPar.Items.Add(CType(CoursePar._54, Int32))
   End If
   cmbPar.SelectedIndex = 0
   cmbPar.EndUpdate()

   'While I am in here I need to create the listview on the fly
   'The listview depends upon the number of holes

   If Not ThisCourse Is Nothing And Not cmdEdit.Enabled Then
     ThisCourse.NumberOfHoles = CType(cmbHoles.SelectedItem, Int32)
   End If

   SetupTeeList()
 End Sub
End example

The setup routine makes sure that I have a validation and KeyPress handler for the Slope field. It also goes out to the static Globals class and gets the maximum length that the field can be. Doing this allows me to change the Slope parameters in the Globals class, and all other code that uses these parameters will change automatically.

You may be wondering why I did not do any setup of the cmbPar ComboBox in the setup routine, yet when the program runs this field is set up correctly. Note the code that sets up the cmbHoles ComboBox:

    cmbHoles.DropDownStyle = ComboBoxStyle.DropDownList;
    cmbHoles.Items.Add(Globals.NineHoles);
    cmbHoles.Items.Add(Globals.EighteenHoles);
    cmbHoles.SelectedIndexChanged += new EventHandler(this.SelectPar);
    cmbHoles.SelectedIndex = 1;

I wire up the delegate and then set the selected item to a value. Setting the SelectedIndex value automatically fires the SelectedIndexChanged event. I rely on the delegate to set up the cmbPar ComboBox correctly. If I had reversed these last two lines of code, the program would not work correctly.

I override the ToString method in several classes. The ICourseInfo class is one of them. Note this code:

      foreach(ICourseInfo c in mParent.db.GolfCourses)
      {
        cmbCourseName.Items.Add(c);
      }

I am adding the ICourseInfo object directly to the cmbCourseName ComboBox. The ComboBox uses the ToString method of the object to display information about the object. You saw me demonstrate this in previous chapters.

The delegate shown here is no big surprise. Note, though, that I use the Debug.Assert function to make sure that this delegate is not being called by something that should not call it. This assertion code will get compiled out of the release code.

Here is the code that handles the Slope field.

C#

    private void SlopeKeyPress(object sender, KeyPressEventArgs e)
    {
      Debug.Assert(sender == txtSlope,
                   "SlopeKeyPress method called by wrong control");

      if(e.KeyChar < '0' || e.KeyChar > '9')
        e.Handled = true;

        //0 cannot be leading digit
        if(txtSlope.Text == "" && e.KeyChar == '0')
          e.Handled = true;
      }

      private void SlopeValidate(object sender, CancelEventArgs e)
      {
        Debug.Assert(sender == txtSlope,
                    "SlopeValidate method called by wrong control");

        try
        {
          int slope = int.Parse(txtSlope.Text);
          if(slope < Globals.MinSlope || slope > Globals.MaxSlope)
          {
            err.SetError(txtSlope, "Slope must be between 55 and 155");
            e.Cancel = true;
          }
      }
      catch
      {
        err.SetError(txtSlope, "BUG: Slope must be an integer");
        e.Cancel = true;
      }

      if(e.Cancel)
        txtSlope.SelectAll();
      else
        err.SetError(txtSlope, "");
    }

VB

  Private Sub SlopeKeyPress(ByVal sender As Object, ByVal e As KeyPressEventArgs)
    Debug.Assert(sender Is txtSlope, _
                  "SlopeKeyPress method called by wrong control")

    If e.KeyChar < "0" Or e.KeyChar > "9" Then
      e.Handled = True
    End If

    '0 cannot be leading digit
    If txtSlope.Text = "" AndAlso e.KeyChar = "0" Then
      e.Handled = True
    End If
  End Sub

  Private Sub SlopeValidate(ByVal sender As Object, ByVal e As CancelEventArgs)
    Debug.Assert(sender Is txtSlope, _
                "SlopeValidate method called by wrong control")
    Try
      Dim slope As Int32 = Int32.Parse(txtSlope.Text)
      If slope < Globals.MinSlope Or slope > Globals.MaxSlope Then
        err.SetError(txtSlope, "Slope must be between 55 and 155")
        e.Cancel = True
      End If
    Catch
      err.SetError(txtSlope, "BUG: Slope must be an integer")
      e.Cancel = True
    End Try
    If (e.Cancel) Then
      txtSlope.SelectAll()
    Else
      err.SetError(txtSlope, "")
    End If
  End Sub

You can see here that I use the ErrorProvider control to note any problems with the validation of this field. I use this error control within the confines of a Try-Catch block. In previous chapters I used Catch blocks to show message boxes. As you see here, the Catch block is also a good place to put an ErrorProvider control.

By the way, do you know what error I am catching here? Yup, the integer parse. I could have tested for the TextBox value to be an integer before trying to parse. This is an alternate way.

Examining the Course Scores Tab

The next tab contains quite a bit of setup and event handling. Figure 11-9 shows the Course scores tab.

Click To expand
Figure 11-9: The Course scores tab in action

As you can see, I started with an original scorecard and then clicked the New Card button several times. Each time I clicked this button I added a new IScoreCardInfo object to the ScoreCard collection within the current course.

Notice that there is no new record indicator on the DataGrid. I prevent the user from generating a new record by using the DataGrid. I want complete control over what is happening.

Listings 11-4a and 11-4b show the code that gets run when the user switches from the Course Setup tab to the Course scores tab.

Listing 11-4a: C# Code for the Course Scores Tab Setup
Start example
    private void AddScoreCardStyle()
    {
      //First clear the existing one out
      dg1.TableStyles.Clear();

      DataGridTableStyle ts1 = new DataGridTableStyle();
      ts1.MappingName = "Course Score";
      // Set other properties.
      ts1.AlternatingBackColor = Color.LightGray;
      //
      // Add textbox column style so we can catch textbox mouse clicks
      DataGridTextBoxColumn TextCol = new DataGridTextBoxColumn();
      TextCol.MappingName         = "Date";
      TextCol.HeaderText          = "Date";
      TextCol.Width               = 100;
      TextCol.TextBox.Validating  += new CancelEventHandler(DateCellValidating);
      TextCol.TextBox.DoubleClick += new EventHandler(CellDateClick);
      TextCol.TextBox.KeyPress    += new KeyPressEventHandler(CellDateKeyPress);
      ts1.GridColumnStyles.Add(TextCol);

      TextCol = new DataGridTextBoxColumn();
      TextCol.MappingName         = "Score";
      TextCol.HeaderText          = "Score";
      TextCol.Width               = 50;
      TextCol.TextBox.Enabled     = false;
      ts1.GridColumnStyles.Add(TextCol);

      for(int k=1; k<ThisCourse.NumberOfHoles+1; k++)
      {
        TextCol = new DataGridTextBoxColumn();
        TextCol.MappingName         = k.ToString();
        TextCol.HeaderText          = k.ToString();
        TextCol.Width               = 50;
        TextCol.TextBox.MaxLength   = 2;
        TextCol.TextBox.DoubleClick += new EventHandler(HoleScoreDblClick);
        TextCol.TextBox.KeyPress    += new KeyPressEventHandler(HoleScoreEntry);
        TextCol.TextBox.Validating  += new CancelEventHandler(HoleScoreValidate);
        ts1.GridColumnStyles.Add(TextCol);
      }
      dg1.TableStyles.Add(ts1);
    }

    private void SetupScoreCardDatagrid()
    {
      //This must be set up based upon the scorecard collection within
      //the thiscourse object. As each cell is clicked it brings up a hole
      //detail. If the user chooses not to click a cell he can in-place edit
      //just the total score. Either way anytime a cell is edited I will need
      //to change something in the iholedetail object that belongs to this hole.

      Debug.Assert(ThisCourse != null, "Must have a valid course");

      //Generate a column style collection that makes each cell a text box.
      AddScoreCardStyle();

      DataColumn dc;
      //Set the datasource to null at start.
      dg1.DataSource = null;
      DataSet DS = new DataSet();

      //Top-level table
      DataTable DT = new DataTable("Course Score");
      dc = new DataColumn("Date", System.Type.GetType("System.DateTime"));
      DT.Columns.Add(dc);
      dc = new DataColumn("Score", System.Type.GetType("System.Int32"));
      DT.Columns.Add(dc);
      for(int k=1; k<ThisCourse.NumberOfHoles+1; k++)
      {
        dc = new DataColumn(k.ToString(), System.Type.GetType("System.Int32"));
        DT.Columns.Add(dc);
      }
      //<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
      //Add something here to catch if the number of holedetail objects exceeds
      //the number of holes in the course. If so then write to an error file.
      //<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
      ArrayList scores = new ArrayList();
      foreach(IScoreCardInfo s in ThisCourse.ScoreCards)
      {
        scores.Clear();
        scores.Add(s.PlayDate.ToShortDateString());
        scores.Add(s.RoundScore);
        foreach(IHoleDetailInfo h in s.holes)
        {
          //Remember that the basis for this collection is a SortedList so it
          //should come out in order.
          scores.Add(h.TotalShots);
        }
        DT.Rows.Add(scores.ToArray());
      }

      DS.Tables.Add(DT);

      dg1.CaptionText = cmbCourseName.Text + " / Date of play and Hole score ";
      dg1.CaptionFont = new Font("Comic Sans MS", 10);
      dg1.Font = new Font("Arial", 8);
      dg1.DataSource = DS;
      dg1.DataMember = "Course Score";
      dg1.CurrentCellChanged += new EventHandler(this.CellChanged);

      //Remember binding the property of one control to the property of another.
      //This was managed by a PropertyManager object. When you have an
      //object that derives from the IList interface such as a collection,
      //then each of these objects has a CurrencyManager. I am changing the
      //data view object that belongs to this data source's datamember
      //(The table name in this case). I am making sure that the user cannot
      //add a new row using this dataview. See the online help
      //"Consumers of Data on Windows Forms" for more explanation.
      CurrencyManager cm = (CurrencyManager)this.BindingContext[dg1.DataSource,
                                                                dg1.DataMember];
      ((DataView)cm.List).AllowNew = false;

      cmdNewCard.BackColor = Color.SandyBrown;
      cmdNewCard.Click += new EventHandler(NewCard);
    }
End example
Listing 11-4b: VB Code for the Course Scores Tab Setup
Start example

  Private Sub AddScoreCardStyle()

    'First clear the existing one out
    dg1.TableStyles.Clear()

    Dim ts1 As DataGridTableStyle = New DataGridTableStyle()
    ts1.MappingName = "Course Score"
    ' Set other properties.
    ts1.AlternatingBackColor = Color.LightGray
    '
    ' Add textbox column style so we can catch textbox mouse clicks
    Dim TextCol As DataGridTextBoxColumn = New DataGridTextBoxColumn()
    TextCol.MappingName = "Date"
    TextCol.HeaderText = "Date"
    TextCol.Width = 100
    AddHandler TextCol.TextBox.Validating, _
                    New CancelEventHandler(AddressOf DateCellValidating)
    AddHandler TextCol.TextBox.DoubleClick, _
                    New EventHandler(AddressOf CellDateClick)
    AddHandler TextCol.TextBox.KeyPress, _
                    New KeyPressEventHandler(AddressOf CellDateKeyPress)
    ts1.GridColumnStyles.Add(TextCol)

    TextCol = New DataGridTextBoxColumn()
    TextCol.MappingName = "Score"
    TextCol.HeaderText = "Score"
    TextCol.Width = 50
    TextCol.TextBox.Enabled = False
    ts1.GridColumnStyles.Add(TextCol)

    Dim k As Int32
    For k = 1 To ThisCourse.NumberOfHoles + 1
      TextCol = New DataGridTextBoxColumn()
      TextCol.MappingName = k.ToString()
      TextCol.HeaderText = k.ToString()
      TextCol.Width = 50
      TextCol.TextBox.MaxLength = 2
      AddHandler TextCol.TextBox.DoubleClick, _
                      New EventHandler(AddressOf HoleScoreDblClick)
      AddHandler TextCol.TextBox.KeyPress, _
                      New KeyPressEventHandler(AddressOf HoleScoreEntry)
      AddHandler TextCol.TextBox.Validating, _
                    New CancelEventHandler(AddressOf HoleScoreValidate)
      ts1.GridColumnStyles.Add(TextCol)
    Next
    dg1.TableStyles.Add(ts1)
  End Sub

  Private Sub SetupScoreCardDatagrid()
    'This must be set up based upon the scorecard collection within
    'the thiscourse object. as each cell is clicked it brings up a hole
    'detail. If the user chooses not to click a cell he can in-place edit
    'just the total score. Either way anytime a cell is edited I will need
    'to change something in the iholedetail object that belongs to this hole.

    Debug.Assert(Not ThisCourse Is Nothing, "Must have a valid course")

    'Generate a column style collection that makes each cell a text box.
    AddScoreCardStyle()

    Dim dc As DataColumn
    'Set the datasource to null at start.
    dg1.DataSource = Nothing
    Dim DS As DataSet = New DataSet()

    'Top-level table
    Dim DT As DataTable = New DataTable("Course Score")
    dc = New DataColumn("Date", System.Type.GetType("System.DateTime"))
    DT.Columns.Add(dc)
    dc = New DataColumn("Score", System.Type.GetType("System.Int32"))
    DT.Columns.Add(dc)
    Dim k As Int32
    For k = 1 To ThisCourse.NumberOfHoles
      dc = New DataColumn(k.ToString(), System.Type.GetType("System.Int32"))
      DT.Columns.Add(dc)

    Next
    '<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    'Add something here to catch if the number of holedetail objects exceeds
    'the number of holes in the course. If so then write to an error file.
    '<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

    Dim scores As ArrayList = New ArrayList()
    Dim s As IScoreCardInfo
    For Each s In ThisCourse.ScoreCards
      scores.Clear()
      scores.Add(s.PlayDate.ToShortDateString())
      scores.Add(s.RoundScore)
      Dim h As IHoleDetailInfo
      For Each h In s.holes
        'Remember that the basis for this collection is a SortedList so it
        'should come out in order.
        scores.Add(h.TotalShots)
      Next
      DT.Rows.Add(scores.ToArray())
    Next

    DS.Tables.Add(DT)

    dg1.CaptionText = cmbCourseName.Text + " / Date of play and Hole score "
    dg1.CaptionFont = New Font("Comic Sans MS", 10)
    dg1.Font = New Font("Arial", 8)
    dg1.DataSource = DS
    dg1.DataMember = "Course Score"
    AddHandler dg1.CurrentCellChanged, New EventHandler(AddressOf CellChanged)

    'Remember binding the property of one control to the property of another.
    'This was managed by a PropertyManager object. When you have an
    'object that derives from the IList interface such as a collection,
    'then each of these objects has a CurrencyManager. I am changing the
    'data view object that belongs to this data source's datamember
    '(The table name in this case). I am making sure that the user cannot
    'add a new row using this dataview. See the online help
    '"Consumers of Data on Windows Forms" for more explanation.
    Dim cm As CurrencyManager = CType(Me.BindingContext(dg1.DataSource, _
                                                        dg1.DataMember), _
                                                        CurrencyManager)
    Dim dv As DataView = CType(cm.List, DataView)
    dv.AllowNew = False

    cmdNewCard.BackColor = Color.SandyBrown
    AddHandler cmdNewCard.Click, New EventHandler(AddressOf NewCard)

  End Sub
End example

The first function here is designed to set up a certain view of the DataGrid. I set it up so that each cell is a TextBox. Once I have done that, I can attach delegates to the TextBox events. This is how I am able to get the HoleDetail screen to pop up when the user double-clicks a cell.

Note 

It actually takes three clicks to bring up the HoleDetail screen. This is because the first click turns the cell into a TextBox. At this point, a double-click is caught by the TextBox delegate. You can think of this as one click to chose the cell and a double-click to edit it.

Consider these two lines of code from the SetupScoreCardDatagrid method:

 CurrencyManager cm = (CurrencyManager)this.BindingContext[dg1.DataSource,
                                                           dg1.DataMember];
 ((DataView)cm.List).AllowNew = false;

The CurrencyManager is the object that handles a data view of a DataGrid. In this case, I am telling the data view for this DataGrid to disallow automatic addition of new rows. Pretty cool, isn't it?

The AddCardStyle method is responsible for setting up the DataGrid so that it shows the correct number of cells according to how many holes the course has. This makes the grids dynamic and presents a nicer interface than if I just clamped everything to 18 holes. It also sets up the cell style and wires up some delegates to catch TextBox events. Notice that I do more than just catch the double-click event. I also catch the Validating and KeyPress events. I do this so that the user can just enter the hole's score by typing directly into the cell. I give the user the option of detailing the hole statistics or just recording the score.

I find that giving users some choice in what data is entered and how it is entered gives the impression that you put some thought into the program. Many times I have seen programs that are aimed at computer novices only. This may be OK for first-time users, but as people get familiar with your program, they will start searching for ways to speed up data entry. Adding alternate ways to input data can make a big difference.

Note that in this setup I disable the TextBox for the Total Score. Once the user clicks in this cell, the TextBox associated with this cell is activated and the cell goes gray, which indicates that the user cannot enter data in it.

Validating Cell Entry

Let's take a quick look at some of the data validation routines for the DataGrid cells.

C#

    private void CellDateClick(object sender, EventArgs e)
    {
      //Handle double-clicking on the date field
      //If you want you can bring up a calendar dialog here to make it
      //easy for the user to pick a date
    }

    private void CellDateKeyPress(object sender, KeyPressEventArgs e)
    {
      //Handle entering data in the date field
      if(!Regex.IsMatch(e.KeyChar.ToString(), "[0-9/-]"))
        e.Handled = true;
    }

    private void DateCellValidating(object sender, CancelEventArgs e)
    {
      Debug.Assert(sender is TextBox,
        "Sender must be a datagrid cell that is a textbox");

      string DateMatch = "[0-1]?[0-9]/[0-3]?[0-9]/[0-9]{4}$";
      if(Regex.IsMatch(((TextBox)sender).Text, DateMatch))
        if(ThisCard != null)
          ThisCard.PlayDate = DateTime.Parse(((TextBox)sender).Text);
      else
        e.Cancel = true;
    }

    private void HoleScoreEntry(object sender, KeyPressEventArgs e)
    {
      if(!Char.IsDigit(e.KeyChar))
        e.Handled = true;
    }

    private void HoleScoreValidate(object sender, CancelEventArgs e)
    {
      Debug.Assert(sender is TextBox,
                   "Sender must be a datagrid cell that is a textbox");
    if(ThisCard != null && ThisHole != null)
    {
      ThisHole.TotalShots = int.Parse(((TextBox)sender).Text);
      dg1[dg1.CurrentCell.RowNumber, 1] = ThisCard.RoundScore;
    }
  }

  private void CellChanged(object sender, EventArgs e)
 {
   try
   {
     ThisCard = ThisCourse.ScoreCards.Item(dg1.CurrentCell.RowNumber);
     ThisHole = ThisCard.holes.Item(dg1.CurrentCell.ColumnNumber-2);
   }
   catch
   {
     ThisCard = null;
     ThisHole = null;
   }
 }

VB

  Private Sub CellDateClick(ByVal sender As Object, ByVal e As EventArgs)
    'Handle double-clicking on the date field
    'If you want you can bring up a calendar dialog here to make it
    'easy for the user to pick a date
  End Sub

  Private Sub CellDateKeyPress(ByVal sender As Object, _
                                ByVal e As KeyPressEventArgs)
    'Handle entering data in the date field
    If Not Regex.IsMatch(e.KeyChar.ToString(), "[0-9/-]") Then
      e.Handled = True
    End If
  End Sub

  Private Sub DateCellValidating(ByVal sender As Object, _
                                  ByVal e As CancelEventArgs)
    Debug.Assert(TypeOf (sender) Is TextBox, _
        "Sender must be a datagrid cell that is a textbox")
    Dim DateMatch As String = "[0-1]?[0-9]/[0-3]?[0-9]/[0-9]{4}$"
    If Regex.IsMatch((CType(sender, TextBox)).Text, DateMatch) Then
      If Not ThisCard Is Nothing Then
        ThisCard.PlayDate = DateTime.Parse((CType(sender, TextBox)).Text)
      Else
        e.Cancel = True
      End If
    End If
  End Sub

  Private Sub HoleScoreEntry(ByVal sender As Object, _
                                  ByVal e As KeyPressEventArgs)
    If Not Char.IsDigit(e.KeyChar) Then
      e.Handled = True
    End If
  End Sub

  Private Sub HoleScoreValidate(ByVal sender As Object, _
                                  ByVal e As CancelEventArgs)
    Debug.Assert(TypeOf (sender) Is TextBox, _
                 "Sender must be a datagrid cell that is a textbox")

    If Not ThisCard Is Nothing AndAlso Not ThisHole Is Nothing Then
      ThisHole.TotalShots = Int32.Parse((CType(sender, TextBox).Text))
      dg1(dg1.CurrentCell.RowNumber, 1) = ThisCard.RoundScore
    End If
  End Sub

  Private Sub CellChanged(ByVal sender As Object, ByVal e As EventArgs)
    Try
      ThisCard = ThisCourse.ScoreCards.Item(dg1.CurrentCell.RowNumber)
      ThisHole = ThisCard.holes.Item(dg1.CurrentCell.ColumnNumber - 2)
    Catch
      ThisCard = Nothing
      ThisHole = Nothing
    End Try
  End Sub

I make use of the regular expression engine to validate dates and some key presses. I spent much of Chapter 9 on regular expressions. I must tell you that it really does pay to learn this syntax. It can save so much code.

The Hole Detail Screen

If the user decides he or she wants to enter statistics for each hole played, he or she needs only double-click in one of the cells that holds the hole score. The user will then see the screen shown in Figure 11-10.

Click To expand
Figure 11-10: The HoleDetail screen shows statistics for each hole played.

This detail screen is the user interface to the IHoleDetail object. The user is not able to enter any value for the yardage. The program does that when the user chooses a radio button. This is restrictive data entry, and there is no reason to add any validation handlers for this data. Whatever the user chooses as his or her tee color is correct.

The Shots GroupBox contains several fields. I decided to use ComboBoxes for the clubs as these are always known quantities. The Shots To Green and Putts fields could be any numeric value. The Total Shots field is the same.

I make good use of controls that steer the user to a predetermined valid entry. The RadioBoxes are not gratuitous and scattered. CheckBoxes are used as Boolean fields to note the quality of the shot. No elaboration is needed here.

Three fields need validation. I first set the field length to 3 for each of these fields. If someone makes more than 99 shots on any one hole, that person needs a new hobby. Next, I trap the KeyPress event to make sure that only numbers are allowed. Listings 11-5a and 11-5b show the constructor and delegates for this form.

Listing 11-5a: C# Code for the HoleDetail Screen
Start example
    public HoleDetail(ref ICourseInfo ThisCourse, ref IHoleDetailInfo hole)
    {
      InitializeComponent();

      //Don't forget to initialize tab order and speedkeys, etc.
      this.BackColor = Color.LightGreen;
      cmdSave.BackColor = Color.SandyBrown;
      cmdCancel.BackColor = Color.SandyBrown;

      mHole = hole;
      lblCourse.Text = ThisCourse.Name;
      foreach(Tees tee in ThisCourse.Hole)
      {
        if(tee.HoleNumber == mHole.Hole)
        {
          distance = tee;
          break;
        }
      }

      lblHole.Text = "Hole: " + mHole.Hole.ToString();
      lblPar.Text = "Par: " + mHole.Par.ToString();

      optBlue.CheckedChanged += new EventHandler(this.YardClick);
      optWhite.CheckedChanged += new EventHandler(this.YardClick);
      optRed.CheckedChanged += new EventHandler(this.YardClick);
      switch(hole.TeeBox)
      {
        case YardMarker.Blue:
          optBlue.Checked = true;
          TeeBox = YardMarker.Blue;
          break;
        case YardMarker.Red:
          optRed.Checked = true;
          TeeBox = YardMarker.Red;
          break;
        case YardMarker.White:
            default:
          optWhite.Checked = true;
          TeeBox = YardMarker.White;
          break;
      }

      //This is how you enumerate an enumeration
      //Bet you didn't know you could do this.
      GolfClubs G = GolfClubs.One_iron;
      while(G <= GolfClubs.Putter)
      {
        cmbFirstClub.Items.Add(G);
        if(mHole.TeeClub == G)
          cmbFirstClub.SelectedIndex = cmbFirstClub.Items.Count-1;

        cmbSecondClub.Items.Add(G);
        if(mHole.ScondClub == G)
          cmbSecondClub.SelectedIndex = cmbSecondClub.Items.Count-1;

        G++;
      }

      cmdSave.DialogResult    = DialogResult.OK;
      cmdCancel.DialogResult  = DialogResult.Cancel;
      cmdSave.Click           += new EventHandler(SaveHole);

      chkFairway.Checked = hole.HitFairway;
      chkGoodHit.Checked = hole.GoodSecondShot;
      txtShots2Green.MaxLength = 3;
      txtShots2Green.Text = hole.ShotsToGreen.ToString();
      txtShots2Green.KeyPress += new KeyPressEventHandler(OnlyNumbers);
      txtPutts.MaxLength = 3;
      txtPutts.Text       = hole.Putts.ToString();
      txtPutts.KeyPress   += new KeyPressEventHandler(OnlyNumbers);
      txtTotal.MaxLength = 3;
      txtTotal.Text       = hole.TotalShots.ToString();
      txtTotal.KeyPress   += new KeyPressEventHandler(OnlyNumbers);

      //Consider adding a databinding or validation to the totals
      //text box so it automatically totals the shots2green and putts TextBoxes.
    }

    #region events
    private void OnlyNumbers(object sender, KeyPressEventArgs e)
    {
      //Allow only positive numbers
      if(!char.IsDigit(e.KeyChar))
        e.Handled = true;
    }

    private void YardClick(object sender, EventArgs e)
   {
     if(optBlue.Checked)
     {
       txtYards.Text = distance.BlueDistance.ToString();
       TeeBox = YardMarker.Blue;
     }
     else if(optWhite.Checked)
     {
       txtYards.Text = distance.WhiteDistance.ToString();
       TeeBox = YardMarker.White;
     }
     else if(optRed.Checked)
     {
       txtYards.Text = distance.RedDistance.ToString();
       TeeBox = YardMarker.Blue;
     }

    }

    private void SaveHole(object sender, EventArgs e)
    {
      if(txtPutts.Text != string.Empty)
        mHole.Putts = int.Parse(txtPutts.Text);
      else
        mHole.Putts = 0;
      if(txtShots2Green.Text != string.Empty)
        mHole.ShotsToGreen = int.Parse(txtShots2Green.Text);
      else
        mHole.ShotsToGreen = 0;
      if(txtTotal.Text != string.Empty)
        mHole.TotalShots = int.Parse(txtTotal.Text);
      else
        mHole.TotalShots = 0;
      mHole.GoodSecondShot = chkFairway.Checked;
      mHole.GoodSecondShot = chkGoodHit.Checked;
      mHole.TeeBox = TeeBox;
      mHole.TeeClub = (GolfClubs)cmbFirstClub.SelectedItem;
      mHole.ScondClub = (GolfClubs)cmbSecondClub.SelectedItem;
    }

    #endregion
End example
Listing 11-5b: VB Code for the HoleDetail Screen
Start example
Public Sub New(ByRef ThisCourse As ICourseInfo, ByRef hole As IHoleDetailInfo)
    MyBase.New()

    'This call is required by the Windows Form Designer.
    InitializeComponent()

    'Don't forget to initialize tab order and speedkeys, etc.
    BackColor = Color.LightGreen
    cmdSave.BackColor = Color.SandyBrown
    cmdCancel.BackColor = Color.SandyBrown

    mHole = hole
    lblCourse.Text = ThisCourse.Name
    Dim Tee As Tees
    For Each Tee In ThisCourse.Hole
      If Tee.HoleNumber = mHole.Hole Then
        distance = Tee
        Exit For
      End If
    Next

    lblHole.Text = "Hole: " + mHole.Hole.ToString()
    lblPar.Text = "Par: " + mHole.Par.ToString()

    AddHandler optBlue.CheckedChanged, New EventHandler(AddressOf YardClick)
    AddHandler optWhite.CheckedChanged, New EventHandler(AddressOf YardClick)
    AddHandler optRed.CheckedChanged, New EventHandler(AddressOf YardClick)
    Select Case hole.TeeBox
      Case YardMarker.Blue
        optBlue.Checked = True
        TeeBox = YardMarker.Blue
      Case YardMarker.Red
        optRed.Checked = True
        TeeBox = YardMarker.Red
      Case YardMarker.White
        optWhite.Checked = True
        TeeBox = YardMarker.White
    End Select

    'This is how you enumerate an enumeration
    'Bet you didn't know you could do this.
    Dim G As GolfClubs = GolfClubs.One_iron
    While G <= GolfClubs.Putter
      cmbFirstClub.Items.Add(G)
      If mHole.TeeClub = G Then
        cmbFirstClub.SelectedIndex = cmbFirstClub.Items.Count - 1
      End If

      cmbSecondClub.Items.Add(G)
      If mHole.ScondClub = G Then
        cmbSecondClub.SelectedIndex = cmbSecondClub.Items.Count - 1
      End If

      G += 1
    End While

    cmdSave.DialogResult = DialogResult.OK
    cmdCancel.DialogResult = DialogResult.Cancel
    AddHandler cmdSave.Click, New EventHandler(AddressOf SaveHole)

    chkFairway.Checked = hole.HitFairway
    chkGoodHit.Checked = hole.GoodSecondShot
    txtShots2Green.MaxLength = 3
    txtShots2Green.Text = hole.ShotsToGreen.ToString()
    AddHandler txtShots2Green.KeyPress, _
                          New KeyPressEventHandler(AddressOf OnlyNumbers)
    txtPutts.MaxLength = 3
    txtPutts.Text = hole.Putts.ToString()
    AddHandler txtPutts.KeyPress, _
                          New KeyPressEventHandler(AddressOf OnlyNumbers)
    txtTotal.MaxLength = 3
    txtTotal.Text = hole.TotalShots.ToString()
    AddHandler txtTotal.KeyPress, _
                          New KeyPressEventHandler(AddressOf OnlyNumbers)
    'Consider adding a databinding or validation to the totals text box
    'so it automatically totals the shots2green and putts TextBoxes.

  End Sub

#Region "events"

  Private Sub OnlyNumbers(ByVal sender As Object, ByVal e As KeyPressEventArgs)
    'Allow only positive numbers
    If Not Char.IsDigit(e.KeyChar) Then
      e.Handled = True
    End If
  End Sub

  Private Sub YardClick(ByVal sender As Object, ByVal e As EventArgs)
    If optBlue.Checked Then
      txtYards.Text = distance.BlueDistance.ToString()
      TeeBox = YardMarker.Blue
    ElseIf optWhite.Checked Then
      txtYards.Text = distance.WhiteDistance.ToString()
      TeeBox = YardMarker.White
    ElseIf optRed.Checked Then
      txtYards.Text = distance.RedDistance.ToString()
      TeeBox = YardMarker.Blue
    End If
  End Sub

  Private Sub SaveHole(ByVal sender As Object, ByVal e As EventArgs)
    mHole.GoodSecondShot = chkFairway.Checked
    mHole.GoodSecondShot = chkGoodHit.Checked
    mHole.TeeBox = TeeBox
    mHole.TeeClub = CType(cmbFirstClub.SelectedItem, GolfClubs)
    If txtPutts.Text = String.Empty Then
      mHole.Putts = 0
    Else
      mHole.Putts = Int32.Parse(txtPutts.Text)
    End If
    mHole.ScondClub = CType(cmbSecondClub.SelectedItem, GolfClubs)
    If txtShots2Green.Text = String.Empty Then
      mHole.ShotsToGreen = 0
    Else
      mHole.ShotsToGreen = Int32.Parse(txtShots2Green.Text)
    End If
    If txtTotal.Text = String.Empty Then
      mHole.TotalShots = 0
    Else
      mHole.TotalShots = Int32.Parse(txtTotal.Text)
    End If
  End Sub

#End Region
End example

You can see that the code for this form is not difficult at all. In fact, proper use of controls has actually eliminated quite a bit of validation code.

Note 

I did not include any tab index or speed key setup. I trust that you know to do this as a first step in aiding the user to navigate your screens.

The last screen I present in this project is the Statistics screen. In this screen I show the avid golfer where his or her game falls short (or left, or right, or in the pond, or ). Figure 11-11 shows this screen with some scorecards added to the original one.

Click To expand
Figure 11-11: The Statistics screen for an avid golfer

I will tell you now that I did not add the code for the Totals GroupBox statistics. I also did not calculate the raw handicap. My intention with this form is to show you how to present a different view to the user.

You have seen this view in the form of the DataGrid page. There I displayed the scorecard in a row-by-row format. Each cell contained only some of the information you see in Figure 11-11. For instance, the date was a cell in the DataGrid page, and here it is a node in a tree. Also, the only detailed information I gave in the DataGrid form was the individual hole total. You could gather some of this information from the DataGrid using a calculator and a piece of paper, but that is not what that view is for.

Let's look at how I set up this form. Listings 11-6a and 11-6b show all the relevant code for this form.

Listing 11-6a: C# Code for the Statistics Form
Start example
  #region locals

  private GolfStat        mParent;
  private ICourseInfo     ThisCourse;
  private IScoreCardInfo  ThisCard;
  private IHoleDetailInfo ThisHole;

  #endregion

  public Statistics()
  {
    InitializeComponent();
    Init();
  }
  private void Statistics_Load(object sender, System.EventArgs e)
  {
    mParent = (GolfStat)this.MdiParent;
    SetupTree();
  }

  #region Setup

  private void Init()
  {
    this.BackColor = Color.LightGreen;
    cmdClose.BackColor = Color.SandyBrown;
    cmdClose.Text = "&Close";
    cmdClose.TabIndex = 0;
    cmdClose.Click += new EventHandler(CloseMe);
    imgIcons.Images.Add(Image.FromFile("flag.ico"));
  }

  private void SetupTree()
  {
    TreeNode CourseNode;
    TreeNode CardNode;

    tvwCourse.Nodes.Clear();
    tvwCourse.BeginUpdate();

    tvwCourse.ImageList = imgIcons;
    tvwCourse.ImageIndex = 0;
    tvwCourse.SelectedImageIndex = 0;
    foreach(ICourseInfo c in mParent.db.GolfCourses)
    {
      CourseNode = tvwCourse.Nodes.Add(c.Name);
      CourseNode.Tag = c;
      foreach(IScoreCardInfo card in c.ScoreCards)
      {
        CardNode = CourseNode.Nodes.Add(card.PlayDate.ToShortDateString());
        CardNode.Tag = card;
      }
    }

    tvwCourse.AfterSelect += new TreeViewEventHandler(CourseStats);
    tvwCourse.EndUpdate();
  }

  #endregion

  #region events

  public void CloseMe(object sender, EventArgs e)
  {
    this.Close();
  }

  private void CourseStats(object s, TreeViewEventArgs e)
  {
    float FairwaysHit = 0;
    float GreensInReg = 0;
    float Putting     = 0;
    float MaxPutts    = 0;
    float MinPutts    = 99;
    float AvgPutts    = 0;

    if(e.Node.Tag is IScoreCardInfo)
    {
      ThisCard = (IScoreCardInfo)e.Node.Tag;
      foreach(IHoleDetailInfo h in ThisCard.holes)
      {
        FairwaysHit += h.HitFairway ? 1 : 0;
        GreensInReg += h.GreenInReg ? 1 : 0;
        Putting     += h.Putts;
        if(h.Putts > MaxPutts) MaxPutts = h.Putts;
        if(h.Putts < MinPutts) MinPutts = h.Putts;
        AvgPutts += h.Putts;
      }
      FairwaysHit /= ThisCard.holes.Count;
      Putting /= (ThisCard.holes.Count * 2);
      GreensInReg /= ThisCard.holes.Count;
      AvgPutts /= ThisCard.holes.Count;

      lblFairwaysHit.Text = (FairwaysHit*100).ToString();
      lblGreens.Text = (GreensInReg*100).ToString();
      lblPutting.Text = (Putting*100).ToString();
      lblMaxPutts.Text = MaxPutts.ToString();
      lblAvgPutts.Text = AvgPutts.ToString();
      lblMinPutts.Text = MinPutts.ToString();
      //I will let you figure out the rest of the stats. :)

    }
  }

  #endregion
End example
Listing 11-6b: VB Code for the Statistics Form
Start example
#Region "locals"

  Private mParent As GolfStat
  Private ThisCourse As ICourseInfo
  Private ThisCard As IScoreCardInfo
  Private ThisHole As IHoleDetailInfo

#End Region

  Public Sub New()
    MyBase.New()

    'This call is required by the Windows Form Designer.
    InitializeComponent()
    Init()

  End Sub

  Private Sub Statistics_Load(ByVal sender As System.Object, _
                              ByVal e As System.EventArgs) Handles MyBase.Load
    mParent = CType(Me.MdiParent, GolfStat)
    SetupTree()

  End Sub

#Region "Setup"

  Private Sub Init()
    BackColor = Color.LightGreen
    cmdClose.BackColor = Color.SandyBrown
    cmdClose.Text = "&Close"
    cmdClose.TabIndex = 0
    AddHandler cmdClose.Click, New EventHandler(AddressOf CloseMe)
    imgIcons.Images.Add(Image.FromFile("flag.ico"))
  End Sub

  Private Sub SetupTree()
    Dim CourseNode As TreeNode
    Dim CardNode As TreeNode

    tvwCourse.Nodes.Clear()
    tvwCourse.BeginUpdate()

    tvwCourse.ImageList = imgIcons
    tvwCourse.ImageIndex = 0
    tvwCourse.SelectedImageIndex = 0
    Dim c As ICourseInfo
    For Each c In mParent.db.GolfCourses
      CourseNode = tvwCourse.Nodes.Add(c.Name)
      CourseNode.Tag = c
      Dim card As IScoreCardInfo
      For Each card In c.ScoreCards
        CardNode = CourseNode.Nodes.Add(card.PlayDate.ToShortDateString())
        CardNode.Tag = card
      Next
    Next

    AddHandler tvwCourse.AfterSelect, _
                         New TreeViewEventHandler(AddressOf CourseStats)
    tvwCourse.EndUpdate()
  End Sub


#End Region

#Region "events"

  Public Sub CloseMe(ByVal s As Object, ByVal e As EventArgs)
    Close()
  End Sub

  Private Sub CourseStats(ByVal s As Object, ByVal e As TreeViewEventArgs)
    Dim FairwaysHit As Single = 0
    Dim GreensInReg As Single = 0
    Dim Putting As Single = 0
    Dim MaxPutts As Single = 0
    Dim MinPutts As Single = 99
    Dim AvgPutts As Single = 0
    If TypeOf (e.Node.Tag) Is IScoreCardInfo Then

      ThisCard = CType(e.Node.Tag, IScoreCardInfo)
      Dim h As IHoleDetailInfo
      For Each h In ThisCard.holes
        If h.HitFairway Then FairwaysHit += 1
        If h.GreenInReg Then GreensInReg += 1
        Putting += h.Putts
        If h.Putts > MaxPutts Then MaxPutts = h.Putts
        If h.Putts < MinPutts Then MinPutts = h.Putts
        AvgPutts += h.Putts
      Next
      FairwaysHit /= ThisCard.holes.Count
      Putting /= (ThisCard.holes.Count * 2)
      GreensInReg /= ThisCard.holes.Count
      AvgPutts /= ThisCard.holes.Count

      lblFairwaysHit.Text = (FairwaysHit * 100).ToString()
      lblGreens.Text = (GreensInReg * 100).ToString()
      lblPutting.Text = (Putting * 100).ToString()
      lblMaxPutts.Text = MaxPutts.ToString()
      lblAvgPutts.Text = AvgPutts.ToString()
      lblMinPutts.Text = MinPutts.ToString()

      'I will let you figure out the rest of the stats. :)

    End If
  End Sub
#End Region
End example

This code enumerates through each scorecard in each golf course and displays them as nodes in the tree. As the user clicks a scorecard node, I calculate some statistics and show them on the screen.

Here is the code snippet to fill the tree:

    foreach(ICourseInfo c in mParent.db.GolfCourses)
    {
      CourseNode = tvwCourse.Nodes.Add(c.Name);
      CourseNode.Tag = c;
      foreach(IScoreCardInfo card in c.ScoreCards)
      {
        CardNode = CourseNode.Nodes.Add(card.PlayDate.ToShortDateString());
        CardNode.Tag = card;
      }
    }

You can see from this code that I use the node's Tag property to contain the actual object in question. As the user selects a node in the tree, I test the Tag object to see if it is an IScoreCardInfo object. If so, I gather some aggregate statistics for each hole played for that scorecard.

[4]Refresh your memory about the difference between a class and an interface.


Team LiB
Previous Section Next Section