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 collections are strongly typed.
The collections can have certain functionality exposed or not when used as assemblies.
The last reason is why I use homegrown collections the most. Consider that a collection has these five methods in common:
A way to add an object
A way to delete an object
A way to iterate over the collection using a foreach construct
A way to get a single object from the collection
A way to get a count of objects in the collection
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.
#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
#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
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.
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.
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;
}
}
}
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.
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.
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); } } }
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
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.
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.
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.
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();
}
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
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.
The next tab contains quite a bit of setup and event handling. Figure 11-9 shows the Course scores tab.
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.
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);
}
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
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.
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.
Let's take a quick look at some of the data validation routines for the DataGrid cells.
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.
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.
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.
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
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
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.
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.
#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
#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
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.