Additional OOP Techniques
>> Aug 20, 2009
In this chapter, you continue exploring the C# language by looking at a few bits and pieces that haven ’ t quite fit in elsewhere. This isn ’ t to say that these techniques aren ’ t useful — just that they don ’ t fall under any of the headings you ’ ve worked through so far.
Specifically, you will look at the following:
. The ::operator and the global namespace qualifier
. Custom exceptions and exception recommendations
. Events
. Anonymous methods
You also make some final modifications to the CardLib code that you ’ ve been building in the last few chapters, and even use CardLib to create a card game.
The :: Operator and the Global Namespace Qualifier
The :: operator provides an alternative way to access types in namespaces. This may be necessary if you want to use a namespace alias and there is ambiguity between the alias and the actual namespace hierarchy. If that ’ s the case, then the namespace hierarchy is given priority over the namespace alias. To see what this means, consider the following code:
using MyNamespaceAlias = MyRootNamespace.MyNestedNamespace;
namespace MyRootNamespace
{
namespace MyNamespaceAlias
{
public class MyClass
{
}
}
namespace MyNestedNamespace
{
public class MyClass
{
}
}
}
Code in MyRootNamespace might use the following to refer to a class:
MyNamespaceAlias.MyClass
The class referred to by this code is the MyRootNamespace.MyNamespaceAlias.MyClassclass, not the MyRootNamespace.MyNestedNamespace.MyClassclass. That is, the namespace MyRootNamespace.MyNamespaceAliashas hidden the alias defined by the usingstatement, which refers to MyRootNamespace.MyNestedNamespace. You can still access the MyRootNamespace .MyNestedNamespace namespace and the class contained within, but you require different syntax:
MyNestedNamespace.MyClass
Alternatively, you can use the ::operator:
MyNamespaceAlias::MyClass
Using this operator forces the compiler to use the alias defined by the using statement, and therefore the code refers to MyRootNamespace.MyNestedNamespace.MyClass.
You can also use the keyword globalwith the :: operator, which is essentially an alias to the top - level, root namespace. This can be useful to make it clearer which namespace you are referring to, as shown here:
global::System.Collections.Generic.List < int >
This is the class you’d expect it to be, the generic List < T > collection class. It definitely isn ’ t the class efined with the following code:
namespace MyRootNamespace
{
namespace System
{
namespace Collections
{
namespace Generic
{
class List < T >
{
}
}
}
}
}
Of course, you should avoid giving your namespaces names that already exist as .NET namespaces, although this problem may arise in large projects, particularly if you are working as part of a large team. Using the ::operator and the global keyword may be the only way you can access the types you want.
Custom Exceptions
Chapter 7 covered exceptions and explained how you can use try...catch...finallyblocks to act on them. You also saw several standard .NET exceptions, including the base class for exceptions, System.Exception. Sometimes it ’ s useful to derive your own exception classes from this base class for use in your applications, instead of using the standard exceptions. This enables you to be more specific with the information you send to whatever code catches the exception, and it enables catching code to be more specific about which exceptions it handles. For example, you might add a new property to your exception class that permits access to some underlying information, making it possible for the exception ’ s receiver to make the required changes, or just provide more information about the exception ’ s cause.
Once you have defined an exception class, you can add it to the list of exceptions recognized by VS using the Debug > Exceptions dialog ’ s Add button, and then define exception - specific behavior as shown in Chapter 7 .
Exception Base Classes
While you can derive exceptions from System.Exception as described in the previous section, best practices dictate that you don ’ t. Instead, you should derive custom exceptions from System.ApplicationException.
Two fundamental exception classes exist in the System namespace and derive from Exception: ApplicationExceptionand SystemException. SystemExceptionis used as the base class for exceptions that are predefined by the .NET Framework. ApplicationException is provided for developers to derive their own exception classes. If you follow this model, then you will always be able to differentiate between predefined and custom exceptions by catching exceptions that derive from one of these two base classes.
Neither ApplicationExceptionnor SystemExceptionextend the functionality of the Exception class in any way. They exist purely so that you can differentiate between exceptions in the manner described here.
Adding Custom Exceptions to CardLib
How to use custom exceptions is, once again, best illustrated by upgrading the CardLib project. The Deck.GetCard()method currently throws a standard .NET exception if an attempt is made to access a card with an index less than 0 or greater than 51, but you ’ ll modify that to use a custom exception.
First, you need to create a new class library project called Ch13CardLib and copy the classes from Ch12CardLib as before, changing the namespace to Ch13CardLib as applicable. Next, define the exception. You do this with a new class defined in a new class file called CardOutOfRangeException .cs, which you can add to the Ch13CardLib project with Project > Add Class:
public class CardOutOfRangeException : ApplicationException
{
private Cards deckContents;
public Cards DeckContents
{
get
{
return deckContents;
}
}
public CardOutOfRangeException(Cards sourceDeckContents) :
base(“There are only 52 cards in the deck.”)
{
deckContents = sourceDeckContents;
}
}
An instance of the Cards class is required for the constructor of this class. It allows access to this Cards object through a DeckContents property and supplies a suitable error message to the base Exception constructor, so that it is available through the Message property of the class.
Next, add code to throw this exception to Deck.cs (replacing the old standard exception):
public Card GetCard(int cardNum)
{
if (cardNum > = 0 & & cardNum < = 51)
return cards[cardNum];
else
throw new CardOutOfRangeException(cards.Clone() as Cards);
}
The DeckContents property is initialized with a deep copy of the current contents of the Deckobject, in the form of a Cards object. This means that you see what the contents were at the point where the exception was thrown, so subsequent modification to the deck contents won ’ t “ lose ” this information.
To test this, use the following client code (in Ch13CardClient in the downloadable code for this chapter):
Deck deck1 = new Deck();
try
{
Card myCard = deck1.GetCard(60);
}
catch (CardOutOfRangeException e)
{
Console.WriteLine(e.Message);
Console.WriteLine(e.DeckContents[0]);
}
Console.ReadKey();
This code results in the output shown in Figure 13 - 1 .
Figure 13-1
Here, the catching code has written the exception Message property to the screen. You also displayed the first card in the Cards object obtained through DeckContents, just to prove that you can access the Cards collection through your custom exception object.
Events
This section covers one of the most frequently used OOP techniques in .NET: events. You start, as usual, with the basics — looking at what events actually are. After that, you ’ ll see some simple events in action and learn what you can do with them. Then you learn how you can create and use events of your own.
At the end of this chapter you ’ ll complete your CardLib class library by adding an event. Finally, because this is the last port of call before arriving at some advanced topics, you ’ ll have a bit of fun creating a card game application that uses this class library.
What Is an Event?
Events are similar to exceptions in that they are raised (thrown) by objects, and you can supply code that acts on them. However, there are several important differences, the most important of which is that there is no equivalent to the try...catch structure for handling events. Instead, you must subscribe to them. Subscribing to an event means supplying code that will be executed when an event is raised, in the form of an event handler.
Many handlers can be subscribed to an event, all of which are called when the event is raised. This may include event handlers that are part of the class of the object that raises the event, but event handlers are just as likely to be found in other classes.
Event handlers themselves are simply functions. The only restriction on an event handler function is that it must match the return type and parameters required by the event. This restriction is part of the definition of an event and is specified by a delegate.
The fact that delegates are used in events is what makes delegates so useful. This is why some space was devoted to them in Chapter 6 . You may want to review that material to refresh your memory about delegates and how you use them.
The basic sequence of processing is as follows: First, an application creates an object that can raise an event. For example, suppose an instant messaging application creates an object that represents a connection to a remote user. That connection object might raise an event when a message arrives through the connection from the remote user (see Figure 13 - 2 ).
Figure 13-2
Next, the application subscribes to the event. Your instant messaging application would do this by defining a function that could be used with the delegate type specified by the event, passing a reference to this function to the event. The event handler function might be a method on another object, such as an object representing a display device to display instant messages when they arrive (see Figure 13 - 3 ).
Figure 13-3
When the event is raised, the subscriber is notified. When an instant message arrives through the connection object, the event handler method on the display device object is called. Because you are using a standard method, the object that raises the event may pass any relevant information via parameters, making events very versatile. In the example case, one parameter might be the text of the instant message, which the event handler could display on the display device object. This is shown in Figure 13 - 4 .
Figure 13-4
Handling Events
As previously discussed, to handle an event you need to subscribe to it by providing an event handler function whose return type and parameters match that of the delegate specified for use with the event. The following example uses a simple timer object to raise events, which results in a handler function being called.
Try It Out : Handling Events
1. Create a new console application called Ch13Ex01 and save it in the directory C:\BegVCSharp\Chapter13.
2. Modify the code in Program.csas follows:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Timers;
namespace Ch13Ex01
{
class Program
{
static int counter = 0;
static string displayString =
“This string will appear one letter at a time. “;
static void Main(string[] args)
{
Timer myTimer = new Timer(100);
myTimer.Elapsed += new ElapsedEventHandler(WriteChar);
myTimer.Start();
Console.ReadKey();
}
static void WriteChar(object source, ElapsedEventArgs e)
{
Console.Write(displayString[counter++ % displayString.Length]);
}
}
}
3. Run the application (once it is running, pressing a key will terminate the application). The result, after a short period, is shown in Figure 13 - 5 .
Figure 13-5
How It Works
The object you are using to raise events is an instance of the System.Timers.Timerclass. This object is initialized with a time period (in milliseconds). When the Timerobject is started using its Start()method, a stream of events is raised, spaced out in time according to the specified time period. Main()initializes a Timerobject with a timer period of 100 milliseconds, so it will raise events 10 times a second when started:
static void Main(string[] args)
{
Timer myTimer = new Timer(100);
The Timerobject possesses an event called Elapsed, and the event handler required by this event must match the return type and parameters of the System.Timers.ElapsedEventHandlerdelegate type, which is one of the standard delegates defined in the .NET Framework. This delegate specifies the following return type and parameters:
void functionName(object source, ElapsedEventArgs e);
The Timer object sends a reference to itself in the first parameter and an instance of an ElapsedEventArgs object in its second parameter. It is safe to ignore these parameters for now; you ’ ll take a look at them a little later.
In your code you have a suitable method:
static void WriteChar(object source, ElapsedEventArgs e)
{
Console.Write(displayString[counter++ % displayString.Length]);
}
This method uses the two static fields of Class1, counterand displayString, to display a single character. Every time the method is called the character displayed is different.
The next task is to hook this handler up to the event — to subscribe to it. To do this, you use the += operator to add a handler to the event in the form of a new delegate instance initialized with your event handler method:
static void Main(string[] args)
{
Timer myTimer = new Timer(100);
myTimer.Elapsed += new ElapsedEventHandler(WriteChar);
This command (which uses slightly strange - looking syntax, specific to delegates) adds a handler to the list that will be called when the Elapsed event is raised. You can add as many handlers as you like to this list as long as they all meet the criteria required. Each handler is called in turn when the event is raised. All that remains for Main()to do is to start the timer running:
myTimer.Start();
You don ’ t want the application terminating before you have handled any events, so you put the Main()function on hold. The simplest way to do this is to request user input, as this command won ’ t finish processing until the user has pressed a key:
Console.ReadKey();
Although processing in Main()effectively ceases here, processing in the Timerobject continues. When it raises events it calls the WriteChar()method, which runs concurrently with the Console.ReadLine()statement.
Defining Events
Now it ’ s time to define and use your own events. The following Try It Out implements an example version of the instant messaging scenario introduced earlier in this chapter, creating a Connection object that raises events that are handled by a Displayobject.
Try It Out: Defining Events
1. Create a new console application called Ch13Ex02 and save it in the directory C:\BegVCSharp\Chapter13.
2. Add a new class called Connectionand modify Connection.csas follows:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Timers;
namespace Ch13Ex02
{
public delegate void MessageHandler(string messageText);
public class Connection
{
public event MessageHandler MessageArrived;
private Timer pollTimer;
public Connection()
{
pollTimer = new Timer(100);
pollTimer.Elapsed += new ElapsedEventHandler(CheckForMessage);
}
public void Connect()
{
pollTimer.Start();
}
public void Disconnect()
{
pollTimer.Stop();
}
private static Random random = new Random();
private void CheckForMessage(object source, ElapsedEventArgs e)
{
Console.WriteLine(“Checking for new messages.”);
if ((random.Next(9) == 0) & & (MessageArrived != null))
{
MessageArrived(“Hello Mum!”);
}
}
}
}
3. Add a new class called Displayand modify Display.csas follows:
namespace Ch13Ex02
{
public class Display
{
public void DisplayMessage(string message)
{
Console.WriteLine(“Message arrived: {0}”, message);
}
}
}
4. Modify the code in Program.csas follows:
static void Main(string[] args)
{
}
Connection myConnection = new Connection();
Display myDisplay = new Display();
myConnection.MessageArrived +=
new MessageHandler (myDisplay.DisplayMessage);
myConnection.Connect();
Console.ReadKey();
5. Run the application. The result is shown in Figure 13 - 6 .
Figure 13-6
How It Works
The Connectionclass does most of the work in this application. Instances of this class make use of a Timer object much like the one shown in the first example of this chapter, initializing it in the class constructor and providing access to its state (enabled or disabled) via Connect()and Disconnect():
public class Connection
{
private Timer pollTimer;
public Connection()
{
pollTimer = new Timer(100);
pollTimer.Elapsed += new ElapsedEventHandler(CheckForMessage);
}
public void Connect()
{
pollTimer.Start();
}
public void Disconnect()
{
pollTimer.Stop();
}
...
}
Also in the constructor, you register an event handler for the Elapsedevent, just as you did in the first example. The handler method, CheckForMessage(), raises an event on average once every 10 times it is called. You will look at the code for this, but first it would be useful to look at the event definition itself.
Before you define an event, you must define a delegate type to use with the event — that is, a delegate type that specifies the return type and parameters that an event handling method must conform to. You do this using standard delegate syntax, defining it as public inside the Ch13Ex02namespace to make the type available to external code:
namespace Ch13Ex02
{
public delegate void MessageHandler(string messageText);
This delegate type, called MessageHandler here, is a voidfunction that has a single string parameter. You can use this parameter to pass an instant message received by the Connectionobject to the Displayobject. Once a delegate has been defined (or a suitable existing delegate has been located), you can define the event itself, as a member of the Connectionclass:
public class Connection
{
public event MessageHandler MessageArrived;
You simply name the event (here it is MessageArrived) and declare it by using the eventkeyword and specifying the delegate type to use (the MessageHandler delegate type defined earlier). After you have declared an event in this way, you can raise it simply by calling it by name as if it were a method with the return type and parameters specified by the delegate. For example, you could raise this event using the following:
MessageArrived(“This is a message.”);
If the delegate had been defined without any parameters, then you could simply use the following:
MessageArrived();
Alternatively, you could have defined more parameters, which would have required more code to raise the event. The CheckForMessage()method looks like this:
private static Random random = new Random();
private void CheckForMessage(object source, ElapsedEventArgs e)
{
Console.WriteLine(“Checking for new messages.”);
if ((random.Next(9) == 0) & & (MessageArrived != null))
{
MessageArrived(“Hello Mum!”);
}
}
You use an instance of the Randomclass shown in earlier chapters to generate a random number between 0 and 9, and raise an event if the number generated is 0, which should happen 10 percent of the time. This simulates polling the connection to determine whether a message has arrived, which won ’ t be the case every time you check. To separate the timer from the instance of Connection , you use a private static instance of the Randomclass.
Note that you supply additional logic. You only raise an event if the expression MessageArrived != nullevaluates to true. This expression, which again uses the delegate syntax in a slightly unusual way, means: “ Does the event have any subscribers? ” If there are no subscribers, then MessageArrivedevaluates to null, and there is no point in raising the event.
The class that will subscribe to the event is called Displayand contains the single method, DisplayMessage(), defined as follows:
public class Display
{
public void DisplayMessage(string message)
{
Console.WriteLine(“Message arrived: {0}”, message);
}
}
This method matches the delegate type (and is public, which is a requirement of event handlers in classes other than the class that generates the event), so you can use it to respond to the MessageArrivedevent.
All that is left now is for the code in Main()to initialize instances of the Connectionand Display classes, hook them up, and start things going. The code required here is similar to that from the first example:
static void Main(string[] args)
{
Connection myConnection = new Connection();
Display myDisplay = new Display();
myConnection.MessageArrived +=
new MessageHandler(myDisplay.DisplayMessage);
myConnection.Connect();
Console.ReadKey();
}
Again, you call Console.ReadKey()to pause the processing of Main()once you have started things moving with the Connect()method of the Connectionobject.
Multipurpose Event Handlers
The delegate you saw earlier, for the Timer.Elapsed event, contained two parameters that are of a type often seen in event handlers:
. object source — A reference to the object that raised the event
. ElapsedEventArgs e— Parameters sent by the event
The reason why the objecttype parameter is used in this event, and indeed in many other events, is that you often need to use a single event handler for several identical events generated by different objects and still tell which object generated the event.
To explain and illustrate this, we ’ ll extend the last example a little.
Try It Out : Using a Multipurpose Event Handler
1. Create a new console application called Ch13Ex03 and save it in the directory C:\BegVCSharp\Chapter13.
2. Copy the code across for Program.cs, Connection.cs, and Display.cs from Ch13Ex02, making sure that you change the namespaces in each file from Ch13Ex02 to Ch13Ex03.
3. Add a new class called MessageArrivedEventArgsand modify MessageArrivedEventArgs.csas follows:
namespace Ch13Ex03
{
public class MessageArrivedEventArgs : EventArgs
{
private string message;
public string Message
{
get
{
return message;
}
}
public MessageArrivedEventArgs()
{
message = “No message sent.”;
}
public MessageArrivedEventArgs(string newMessage)
{
message = newMessage;
}
}
}
4. Modify Connection.csas follows:
namespace Ch13Ex03
{
public delegate void MessageHandler(Connection source,
MessageArrivedEventArgs e);
public class Connection
{
public event MessageHandler MessageArrived;
private string name;
public string Name
{
get
{
return name;
}
set
{
name = value;
}
}
...
private void CheckForMessage(object source, EventArgs e)
{
Console.WriteLine(“Checking for new messages.”);
if ((random.Next(9) == 0) & & (MessageArrived != null))
{
MessageArrived(this, new MessageArrivedEventArgs(“Hello Mum!”));
}
}
...
}
}
5. Modify Display.csas follows:
public void DisplayMessage(Connection source, MessageArrivedEventArgs e)
{
Console.WriteLine(“Message arrived from: {0}”, source.Name);
Console.WriteLine(“Message Text: {0}”, e.Message);
}
6. Modify Program.csas follows:
static void Main(string[] args)
{
Connection myConnection1 = new Connection();
myConnection1.Name = “First connection.”;
Connection myConnection2 = new Connection();
myConnection2.Name = “Second connection.”;
Display myDisplay = new Display();
myConnection1.MessageArrived +=
new MessageHandler(myDisplay.DisplayMessage);
myConnection2.MessageArrived +=
new MessageHandler(myDisplay.DisplayMessage);
myConnection1.Connect();
myConnection2.Connect();
Console.ReadKey();
}
7. Run the application. The result is shown in Figure 13 - 7 .
Figure 13-7
How It Works
By sending a reference to the object that raises an event as one of the event handler parameters you can customize the response of the handler to individual objects. The reference gives you access to the source object, including its properties.
By sending parameters that are contained in a class that inherits from System.EventArgs(as ElapsedEventArgsdoes), you can supply whatever additional information is necessary as parameters (such as the Messageparameter on the MessageArrivedEventArgsclass).
In addition, these parameters will benefit from polymorphism. You could define a handler for the MessageArrivedevent such as this:
public void DisplayMessage(object source, EventArgs e)
{
Console.WriteLine(“Message arrived from: {0}”,
((Connection)source).Name);
Console.WriteLine(“Message Text: {0}”,
((MessageArrivedEventArgs)e).Message);
}
Then you could modify the delegate definition in Connection.csas follows:
public delegate void MessageHandler(object source, EventArgs e);
The application will execute exactly as it did before, but you have made the DisplayMessage()function more versatile (in theory at least — more implementation would be needed to make this production quality). This same handler could work with other events, such as the Timer.Elapsed, although you ’ d have to modify the internals of the handler a bit more such that the parameters sent when this event is raised are handled properly (casting them to Connectionand MessageArrivedEventArgsobjects in this way will cause an exception; you should use the asoperator instead and check for nullvalues).
Return Values and Event Handlers
All the event handlers you ’ ve seen so far have had a return type of void. It is possible to provide a return type for an event, but this can lead to problems because a given event may result in several event handlers being called. If all of these handlers return a value, then it may be unclear which value was actually returned.
The system deals with this by only allowing you access to the last value returned by an event handler. That will be the value returned by the last event handler to subscribe to an event. Although this functionality might be of use in some situations, it is recommended that you use voidtype event handlers, and avoid outtype parameters.
Anonymous Methods
Instead of defining event handler methods, you can choose to use anonymous methods . An anonymous method is one that doesn ’ t actually exist as a method in the traditional sense — that is, it isn ’ t a method on any particular class. Instead, an anonymous method is created purely for use as a target for a delegate.
To create an anonymous method, you need the following code:
delegate(parameters)
{
// Anonymous method code.
};
parameters is a list of parameters matching those of the delegate type you are instantiating, as used by the anonymous method code:
delegate(Connection source, MessageArrivedEventArgs e)
{
// Anonymous method code matching MessageHandler event in Ch13Ex03.
};
For example, you could use this code to completely bypass the Display,DisplayMessage() method in Ch13Ex03:
myConnection1.MessageArrived +=
delegate(Connection source, MessageArrivedEventArgs e)
{
Console.WriteLine(“Message arrived from: {0}”, source.Name);
Console.WriteLine(“Message Text: {0}”, e.Message);
};
An interesting point about anonymous methods is that they are effectively local to the code block that contains them, and they have access to local variables in this scope. If you use such a variable, then it becomes an outer variable. Outer variables are not disposed of when they go out of scope like other local variables are; instead, they live on until the anonymous methods that use them are destroyed. This may be some time later than you expect, so it ’ s definitely something to be careful about.
Expanding and Using CardLib
Now that you ’ ve had a look at defining and using events, you can use them in Ch13CardLib. The event you ’ ll add to your library will be generated when the last Cardobject in a Deckobject is obtained by using GetCard, and will be called LastCardDrawn. The event enables subscribers to reshuffle the deck automatically, cutting down on the processing necessary by a client. The delegate defined for this event (LastCardDrawnHandler) needs to supply a reference to the Deckobject such that the Shuffle()method will be accessible from wherever the handler is. Add the following code to Deck.cs:
namespace Ch13CardLib
{
public delegate void LastCardDrawnHandler(Deck currentDeck);
The code to define the event and raise it is simple:
public event LastCardDrawnHandler LastCardDrawn;
public Card GetCard(int cardNum)
{
if (cardNum > = 0 & & cardNum < = 51)
{
if ((cardNum == 51) & & (LastCardDrawn != null))
LastCardDrawn(this);
return cards[cardNum];
}
else
throw new CardOutOfRangeException((Cards)cards.Clone());
}
This is all the code required to add the event to the Deckclass definition.
A Card Game Client for CardLib
After spending all this time developing the CardLib library, it would be a shame not to use it. Before finishing this section on OOP in C# and the .NET Framework, it ’ s time to have a little fun and write the basics of a card game application that uses the familiar playing card classes.
As in previous chapters, you ’ ll add a client console application to the Ch13CardLib solution, and add a reference to the Ch13CardLib project and make it the startup project. This application will be called Ch13CardClient.
To begin, you ’ ll create a new class called Player in a new file in Ch13CardClient, Player.cs. This class will contain a private Cardsfield called hand, a private string field called name, and two read - only
properties: Nameand PlayHand. The properties simply expose the private fields. Although the PlayHand property is read - only, you will have write access to the reference to the hand field returned, enabling you to modify the cards in the player’ s hand.
You ’ ll also hide the default constructor by making it private, and supply a public nondefault constructor that accepts an initial value for the Name property of Playerinstances.
Finally, you ’ ll provide a booltype method called HasWon(), which returns true if all the cards in the player’ s hand are of the same suit (a simple winning condition, but that doesn ’ t matter too much).
Here ’ s the code for Player.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Ch13CardLib;
namespace Ch13CardClient
{
public class Player
{
private Cards hand;
private string name;
public string Name
{
get
{
return name;
}
}
public Cards PlayHand
{
get
{
return hand;
}
}
private Player()
{
}
public Player(string newName)
{
name = newName;
hand = new Cards();
}
public bool HasWon()
{
bool won = true;
Suit match = hand[0].suit;
for (int i = 1; i < hand.Count; i++)
{
won & = hand[i].suit == match;
}
return won;
}
}
}
Next, define a class that will handle the card game itself, called Game. This class is found in the file Game.cs of the Ch13CardClient project. The class has four private member fields:
. playDeck— A Deck type variable containing the deck of cards to use .
. currentCard— An int value used as a pointer to the next card in the deck to draw.
. players — An array of Player objects representing the players of the game .
. discardedCards— A Cards collection for the cards that have been discarded by players but not shuffled back into the deck .
The default constructor for the class initializes and shuffles the Deck stored in playDeck, sets the currentCard pointer variable to 0 (the first card in playDeck), and wires up an event handler called Reshuffle()to the playDeck.LastCardDrawn event. The handler simply shuffles the deck, initializes the discardedCards collection, and resets currentCard to 0, ready to read cards from the new deck.
The Gameclass also contains two utility methods: SetPlayers()for setting the players for the game (as an array of Playerobjects) and DealHands()for dealing hands to the players (7 cards each). The allowed number of players is restricted from 2 to 7 to ensure that there are enough cards to go around.
Finally, there is a PlayGame()method that contains the game logic itself. You ’ ll come back to this function shortly, after you ’ ve looked at the code in Program.cs. The rest of the code in Game.csis as
follows:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Ch13CardLib;
namespace Ch13CardClient
{
public class Game
{
private int currentCard;
private Deck playDeck;
private Player[] players;
private Cards discardedCards;
public Game()
{
currentCard = 0;
playDeck = new Deck(true);
playDeck.LastCardDrawn += new LastCardDrawnHandler(Reshuffle);
playDeck.Shuffle();
discardedCards = new Cards();
}
private void Reshuffle(Deck currentDeck)
{
Console.WriteLine(“Discarded cards reshuffled into deck.”);
currentDeck.Shuffle();
discardedCards.Clear();
currentCard = 0;
}
public void SetPlayers(Player[] newPlayers)
{
if (newPlayers.Length > 7)
throw new ArgumentException(“A maximum of 7 players may play this” +
“ game.”);
if (newPlayers.Length < 2)
throw new ArgumentException(“A minimum of 2 players may play this” +
“ game.”);
players = newPlayers;
}
private void DealHands()
{
for (int p = 0; p < players.Length; p++)
{
for (int c = 0; c < 7; c++)
{
players[p].PlayHand.Add(playDeck.GetCard(currentCard++));
}
}
}
public int PlayGame()
{
// Code to follow.
}
}
}
Program.cscontains the Main()function, which initializes and runs the game. This function performs the following steps:
. An introduction is displayed.
. The user is prompted for a number of players between 2 and 7.
. An array of Player objects is set up accordingly.
. Each player is prompted for a name, used to initialize one Player object in the array.
. A Game object is created and players are assigned using the SetPlayers()method.
. The game is started by using the PlayGame()method.
. The int return value of PlayGame()is used to display a winning message (the value returned is the index of the winning player in the array of Playerobjects).
The code for this (commented for clarity) follows:
static void Main(string[] args)
{
// Display introduction.
Console.WriteLine(“KarliCards: a new and exciting card game.”);
Console.WriteLine(“To win you must have 7 cards of the same suit in” +
“ your hand.”);
Console.WriteLine();
// Prompt for number of players.
bool inputOK = false;
int choice = -1;
do
{
Console.WriteLine(“How many players (2-7)?”);
string input = Console.ReadLine();
try
{
// Attempt to convert input into a valid number of players.
choice = Convert.ToInt32(input);
if ((choice > = 2) & & (choice < = 7))
inputOK = true;
}
catch
{
// Ignore failed conversions, just continue prompting.
}
} while (inputOK == false);
// Initialize array of Player objects.
Player[] players = new Player[choice];
// Get player names.
for (int p = 0; p < players.Length; p++)
{
Console.WriteLine(“Player {0}, enter your name:”, p + 1);
string playerName = Console.ReadLine();
players[p] = new Player(playerName);
}
// Start game.
Game newGame = new Game();
newGame.SetPlayers(players);
int whoWon = newGame.PlayGame();
// Display winning player.
Console.WriteLine(“{0} has won the game!”, players[whoWon].Name);
}
Now you come to PlayGame(), the main body of the application. Space limitations preclude a lot of detail about this method, but it is commented to make it more comprehensible. None of the code is complicated; there ’ s just quite a bit of it.
Play proceeds with each player viewing his or her cards and an upturned card on the table. They may either pick up this card or draw a new one from the deck. After drawing a card, each player must discard one, replacing the card on the table with another one if it has been picked up, or placing the discarded card on top of the one on the table (also adding the discarded card to the discardedCardscollection).
As you consider this code, bear in mind how the Card objects are manipulated. The reason why these objects are defined as reference types, rather than value types (using a struct) should now be clear. A given Card object may appear to exist in several places at once because references can be held by the Deckobject, the handfields of the Playerobjects, the discardedCardscollection, and the playCard object (the card currently on the table). This makes it easy to keep track of the cards and is used in particular in the code that draws a new card from the deck. The card is accepted only if it isn ’ t in any player’ s hand or in the discardedCardscollection.
The code is as follows:
public int PlayGame()
{
// Only play if players exist.
if (players == null)
return -1;
// Deal initial hands.
DealHands();
// Initialize game vars, including an initial card to place on the
// table: playCard.
bool GameWon = false;
int currentPlayer;
Card playCard = playDeck.GetCard(currentCard++);
discardedCards.Add(playCard);
// Main game loop, continues until GameWon == true.
do
{
// Loop through players in each game round.
for (currentPlayer = 0; currentPlayer < players.Length;
currentPlayer++)
{
// Write out current player, player hand, and the card on the
// table.
Console.WriteLine(“{0}’s turn.”, players[currentPlayer].Name);
Console.WriteLine(“Current hand:”);
foreach (Card card in players[currentPlayer].PlayHand)
{
Console.WriteLine(card);
}
Console.WriteLine(“Card in play: {0}”, playCard);
// Prompt player to pick up card on table or draw a new one.
bool inputOK = false;
do
{
Console.WriteLine(“Press T to take card in play or D to “ +
“draw:”);
string input = Console.ReadLine();
if (input.ToLower() == “t”)
{
// Add card from table to player hand.
Console.WriteLine(“Drawn: {0}”, playCard);
// Remove from discarded cards if possible (if deck
// is reshuffled it won’t be there any more)
if (discardedCards.Contains(playCard))
{
discardedCards.Remove(playCard);
}
players[currentPlayer].PlayHand.Add(playCard);
inputOK = true;
}
if (input.ToLower() == “d”)
{
// Add new card from deck to player hand.
Card newCard;
// Only add card if it isn’t already in a player hand
// or in the discard pile
bool cardIsAvailable;
do
{
newCard = playDeck.GetCard(currentCard++);
// Check if card is in discard pile
cardIsAvailable = !discardedCards.Contains(newCard);
if (cardIsAvailable)
{
// Loop through all player hands to see if newCard is
// already in a hand.
foreach (Player testPlayer in players)
{
if (testPlayer.PlayHand.Contains(newCard))
{
cardIsAvailable = false;
break;
}
}
}
} while (!cardIsAvailable);
// Add the card found to player hand.
Console.WriteLine(“Drawn: {0}”, newCard);
players[currentPlayer].PlayHand.Add(newCard);
inputOK = true;
}
} while (inputOK == false);
// Display new hand with cards numbered.
Console.WriteLine(“New hand:”);
for (int i = 0; i < players[currentPlayer].PlayHand.Count; i++)
{
Console.WriteLine(“{0}: {1}”, i + 1,
players[currentPlayer].PlayHand[i]);
}
// Prompt player for a card to discard.
inputOK = false;
int choice = -1;
do
{
Console.WriteLine(“Choose card to discard:”);
string input = Console.ReadLine();
try
{
// Attempt to convert input into a valid card number.
choice = Convert.ToInt32(input);
if ((choice > 0) & & (choice < = 8))
inputOK = true;
}
catch
{
// Ignore failed conversions, just continue prompting.
}
} while (inputOK == false);
// Place reference to removed card in playCard (place the card
// on the table), then remove card from player hand and add
// to discarded card pile.
playCard = players[currentPlayer].PlayHand[choice - 1];
players[currentPlayer].PlayHand.RemoveAt(choice - 1);
discardedCards.Add(playCard);
Console.WriteLine(“Discarding: {0}”, playCard);
// Space out text for players
Console.WriteLine();
// Check to see if player has won the game, and exit the player
// loop if so.
GameWon = players[currentPlayer].HasWon();
if (GameWon == true)
break;
}
} while (GameWon == false);
// End game, noting the winning player.
return currentPlayer;
}
Figure 13 - 8 shows a game in progress.
Figure 13-8
Have fun playing the game — and make sure that you spend some time going through it in detail. Try putting a breakpoint in the Reshuffle()method and play the game with seven players. If you keep drawing cards and discarding the cards drawn, it won ’ t take long for reshuffles to occur because with seven players there are only three cards to spare. This way, you can prove to yourself that things are working properly by noting the three cards when they reappear.
Summary
This chapter explained some advanced techniques that extend your knowledge of the C# language. It contained the following highlights:
. The qualification of type names in namespaces (in more detail than you saw in earlier chapters)
. How to use the ::operator and global keyword to ensure that references to types are references to the types you want
. How to implement your own exception objects and pass more detailed information to the exception handler
. Using a custom exception in the code for CardLib — the card game library you ’ ve been developing in the last few chapters
. The important topic of events and event handling. Although quite subtle, and initially difficult to get your head around, the code involved is quite simple — and you ’ ll certainly be using event handlers a lot in the rest of the book.
. Some simple illustrative examples of events and how to handle them
You also modified the CardLib library and used it to create a simple card game application. This application demonstrates nearly all the techniques you ’ ve looked at so far in this book.
With this chapter, you have completed not only a full description of OOP as applied to C# programming but also a full description of version 2.0 of the C# language. The next chapter describes the new features of C# that have been added with version 3.0.
Exercises
1. Show the code for an event handler that uses the general - purpose (object sender, EventArgs e)syntax that will accept either the Timer.Elapsedevent or the Connection .MessageArrived event from the code shown earlier in this chapter. The handler should output a string specifying which type of event has been received, along with the Message property of the MessageArrivedEventArgsparameter or the SignalTime property of the ElapsedEventArgs parameter, depending on which event occurs.
2. Modify the card game example to check for the more interesting winning condition of the popular card game rummy. This means that a player wins the game if his or her hand contains two “ sets ” of cards, one of which consists of three cards and one of which consists of four cards. A set is defined as either a sequence of cards of the same suit (such as 3H, 4H, 5H, 6H) or several cards of the same rank (such as 2H, 2D, 2S).