C#.NET: Introduction for Developers
The .NET framework is an all-encompassing framework for development and
management of Microsoft applications. The basic structure of the .NET framework
is outlined in the diagram below. The foundation is a runtime environment, called the CLR,
based on the underlying services provided by Microsoft Windows. All .NET code is
compiled to an intermediate bytecode called MSIL, which is then interpreted by the CLR (or
compiled via just-in-time aka JIT technology at runtime).
A variety of foundation classes, collectively known as the FCL, are built on top of the runtime environment.
The foundation classes in the FCL in turn serve as a basis for additional libraries for data access,
XML processing, Web Services, and Form-Based programming. Microsoft makes the functionality in .NET mavailable
via a variety of programming languages, including VB.NET, C#.NET, and C++.NET. Third
parties have developed addional languages for the .NET framework, including Perl and Python.
Regardless of the programming language used, the underlying framework is the same.
Microsoft has also incorporated many of its existing applications into the overall .NET architecture.
For example, Active Directory for directory services, MS-DTC for transaction management,
and MSMQ for messaging. SQL Server, Exchange Server, and IIS all have .NET-friendly APIs.
VB.NET | C#.NET | Managed C++.NET etc... |
Web Services | Web Forms | Windows Forms |
Data and XML Classes (ADO.NET, SQL, XML, XSLT, XPath, etc...) |
Framework Base Classes (IO, string, net, security, threading,
text, reflection, collections, etc... |
Common Language Runtime (debug, exception, type checking, JIT) |
Windows Platform |
You can download the .NET framework separately, without Visual Studio.NET for free. It is
also included automatically as part of Visual Studio.NET. So far, to the best of my knowledge
there are not any enterprise-strength third-party IDEs for .NET, so Visual Studio.NET is probably
the best bet for real-world programming. However, for smaller applications, one can at least in
theory either use a plain text editor or third party .NET editor, such as SharpDevelop
( http://sourceforge.net/projects/sharpdevelop),
which is free.
C# is generally considered to be the core language in the .NET family of programming languages.
Its syntax is similar to C++ and Java, and like Java it runs inside a virtual machine (the CLR),
supports just-in-time (JIT) compilation, exceptions (though not checked exceptions) and
performs automatic garbage collection. C#, like Java, also supports the single model of inheritance
rather than the multiple inheritance available in C++. However, C# supports features available in C++
and not Java such as operator overloading, allows objects to be assigned to the stack as well
as to the heap (value and reference types respectively), and has features such as enumerated types,
structs, etc.
Hello World:
Below is a listing for the Hello World program. We will code it, compile, and run it using both
the Visual Studio.NET and the command line.
class HelloWorld
{
static void Main()
{
System.Console.WriteLine("Hello, world!");
}
}
|
The command line compilation program for C# is called csc.exe. It is located in
C:\Windows\Microsoft.NET\Framework\vx.x e.g.
C:\Windows\Microsoft.NET\Framework\v1.1.4322
Exercise: Modify the simple HelloWorld program to print a name passed in
as a parameter on the command line, e.g. "Hello, John!"
Exercise: Use the online debugger to inspect variables and step through code
in the HelloWorld program.
Issues to discuss:
- Namespaces: Namespaces define a naming hierarchy for classes in order to
avoid conflicts between different classes that may share the same name.
- Using keyword: Using keyword is used to import a namespace into your
code so that you don't have to reference the entire namespace every time
you use a class.
- The Dot operator (.) is used to access attributes and methods of objects
and to delimit namespace hierarchies.
- Case Sensitivity: Classes, objects, and namespaces are case sensitive in C#.
- Programming Conventions: Microsoft suggests using Camel notation for
variable names and Pascal notation for classes, methods, and properties.
Microsoft no longer recommends using the Hungarian notation. Braces
are places on their own line.
- Comments: "///" comments are used with xml documentation (csc/doc or property pages).
- Types (page 25, Programming C#, 3rd Edition):
- Intrinsic vs. user-defined types
- Value vs. reference types: classes are reference types
- Definite assignment: All variables inside a function must be initialized
- Constants
- Conditional Statements (page 35, Programming C#, 3rd Edition): if; switch; nesting.
- Loops (page 43, Programming C#, 3rd Edition): for; while; foreach; continue;
break
- Operators (page 49, Programming C#, 3rd Edition)
- Assignment
- Arithmetic
- Relational/Boolean (short-circuit evaluation, as opposed to VB)
-
- Strings (page 219, Programming C#, 3rd Edition): @ symbol denotes a verbatim string; ToString method;
StringBuilder class is mutable, and can be used to create and
modify strings without the expense of creating temporary objects; Regex class
used to define a regular expression that can then be applied to a string; Match Groups
can be used to break up a match into partitions;
- Preprocessor Directives (page 59, Programming C#, 3rd Edition): use csc /define at command line
- Exceptions (page 245, Programming C#, 3rd Edition): used to handle unusual situations,
such as unexpected/erronous input, trying to open a file that doesn't exist, and system
errors such as insufficient memory; The main components of exceptions are the try, catch,
and finally blocks. There are built-in exceptions in C#, and you can also write your own custom
exception. Unlike in Java, there is no concept of a checked exception in C#; exceptions
can be rethrown.
Exercise: Create a simple class to model a car's state and behaviour
See page 63, Programming C#, 3rd Edition.
- Classes are reference types
- Structs (page 128, Programming C#, 3rd Edition): value types. Structs also don't support
inheritance or destructors. Use structs for small, simple types. Structs are more efficient
in arrays, but less efficient in collections, where they must be boxed. Struts also do not
support initialization.
- Access Modifiers (public, private, protected, internal, protected internal)
- Constructors
- Initializer
- Copy constructor
- this keyword is a reference to the current object
- Static fields, methods constructors
- Finalize method: used to release external (unmanaged) resources; similar to a Destructor
- Destructor: Shortcut to a Finalizer method that chains up to its base class
- Dispose: If you cannot wait for the garbage collector to call Finalize, you may
implement the IDisposable interface instead. This interface requires a Dispose
method to be supplied. You must also call GC.SuppressFinalize(this) to stop
the garbage collector from automatically calling Finalize. You can then
override the Finalize method with your own public version, which calls
Dispose.using keyword to automatically call Dispose.
- The using construct insures that Dispose will be called automatically on the
object defined as its parameter.
- Parameter passing: By default, parameters are passed into methods by value.
C# supports the ref keyword to pass value objects by referemce as well as
an out keyword which allows reference parameters to be passed in un-initialized.
- Operator overloading (page 119, Programming C#, 3rd Edition): allows operators such
as "+", "-", and "=" to be overloaded by a class. For example, you may overload arithmetic
operators for a Fraction class.
- Properties allow a client to access class state as if it were accessing member
fields directly. Behind the scenes however, the state is accessed via a method.
Using introspection, one can obtain a list and get/set the values of properties for any
given class without knowing what they are ahead of time.
- readonly
- Inheritance allows you to extend the functionality of existing classes
by adding additional state variables and methods to a subclass. The
: keyword is used for to define inheritance. base keyword
references the superclass. base([params]) can also be
used to call the superclass constructor.
- Polymorphism adds a twist to inheritance: Subclass methods can
also override the functionality of their superlclass counterparts.
- base keyword can be used to reference superclass.
- The virtual keyword specifies that a base class can be overridden.
The override keyword specifies that a subclass is overriding the
base class mathod. Both are required to successfully override a function.
The new keyword specifies that a method in a subclass does not
override the virtual base class method with the same signature.
- An abstract class cannot be instantiated. It contains at least one
abstract method, which must be implemented by a subclass.
- A sealed class cannot be subclassed.
- Boxing and unboxing types: Boxing allows native types to automatically be used as
objects, and unboxing allows boxed type to be converted back to their native type.
Use casting to unbox a type, as it must be done explicitely.
- An interface (page 135, Programming C#, 3rd Edition): essentially a class with all
abstract methods. However, while a subclass can only extend one base class, it can
implement multiple interfaces.
- The convention in .NET is to preface the name of an interface with a capital 'I'.
- The colon (:) symbol is used implement or extend an interface, same as for
extending classes.
- To use an interface, you have to cast an object to that interface, e.g.
IStorable storableDocument = (IStorable) new Document("Test Document");
- You can cast to any interface and successfully compile, but you will get an
exception at runtime if the given object does not implement that interface.
- The is keyword determines whether an object can be cast safely
to a given class or interface at runtime.
- The as keyword combines the is keyword with an implicit cast. e.g.
you can write
IStorable storableDocument = doc as IStorable;
instead of
if (doc is IStorable) {
IStorable storableDocument = (IStorable) doc;
}
- An class can implement any interface method as virtual, and derived classes can
override or provide new implementation.
- When a class implements two interfaces, each of which has the same method signature,
one of the methods must be implemented explicitely, e.g.
void ITalk.Read()
Such methods are automatically public (not access modifier is allowed), and the method
can only be accessed via a cast to the interface, since the object reference will always
point to the method that implicitely implements the (other) interface.
- When using value types, it is preferable to access methods directly via the object
rather than using a interface cast. The reason is that value types will be boxed
first to a reference type before the cast, and it is this reference that is modified,
not the original value type.
See page 163, Programming C#, 3rd Edition.
Arrays are the simplest and most commonly used data structures for
managing lists of items.
- Arrays in C# can be declared using the standard C-like syntax, but they are
also objects in their own right and thus support a variety of useful methods.
Arrays are indexed starting at 0, just as in C, C++, and Java (not VB).
- foreach is a very convenient way to iterate through all of the
elements of an array, e.g.
foreach (Employee e in employeeArray) {
Console.WriteLine(e.Name);
}
- params keyword automatically creates an array so you can pass a variable number of
parameters to a method without having to manually wrap those parameters inside
of an array in the calling function.
- Rectangular arrays are 2x2, 3x5, etc arrays, similar to matrices e.g.
int[,] rectArray = new int[3,5];
nit[,,] cubeArray = new int[3,3,3];
- Arrays can also be initalized directly, e.g.
int[,] rectArray = new int[,] {{2,3,3}, {5,6,7}} or
int[,] rectArray = new {{2,3,3}, {5,6,7}} or
- Jagged arrays are not true multi-dimensional arrays. Rather, they
are single dimensional arrays where each item may contain a reference to
another array, and so on, e.g.
int[][] jaggedArray = new int[2][];
jaggedArray[0] = new int[5];
jaggedArray[1] = new int[6];
- Explore System.Array methods
- Indexers allows collections of a class to be exposed using array notation. Indexers
are a special kind of property, e.g.
public string this[int index]
{
get
{
return strings[index];
}
set
{
strings[index] = value;
}
}
- Indexers can also operate on other data types, such as strings.
See page 190, Programming C#, 3rd Edition.
The .NET framework provides a rich suite of collections classes,
including Array, ArrayList, NameValueCollection, StringCollection,
Queue, Stack, BitArray, Hashtable, Dictionary, etc...
- ArrayList: Like an array, but grows dynamically as needed.
- The ArrayList::Sort method allows any objects that implement
IComparable to be sorted.
- Queues: A queue is a FIFO collection. People waiting in line at a
fast food restaurant are an example of a queue.
- Stacks: A stack is a LIFO (Last In First Out) collection. A deck of cards
can serve as a queue, or a stack of trays or plates.
- Dictionaries: Hashtable is the primary collection that implements
the IDictionary interface. IDictionaryEnumerator can be used to
go through a list of the items in the dictionary.
See page 265, Programming C#, 3rd Edition.
A delegate lets you pass a function as a parameter.
The type safety of delegates requires the function you pass as a delegate
to have the same signature as the delegate declaration. Review example below,
from the MSDN library.
// bookstore.cs
using System;
// A set of classes for handling a bookstore:
namespace Bookstore
{
using System.Collections;
// Describes a book in the book list:
public struct Book
{
public string Title; // Title of the book.
public string Author; // Author of the book.
public decimal Price; // Price of the book.
public bool Paperback; // Is it paperback?
public Book(string title, string author, decimal price, bool paperBack)
{
Title = title;
Author = author;
Price = price;
Paperback = paperBack;
}
}
// Declare a delegate type for processing a book:
public delegate void ProcessBookDelegate(Book book);
// Maintains a book database.
public class BookDB
{
// List of all books in the database:
ArrayList list = new ArrayList();
// Add a book to the database:
public void AddBook(string title, string author, decimal price, bool paperBack)
{
list.Add(new Book(title, author, price, paperBack));
}
// Call a passed-in delegate on each paperback book to process it:
public void ProcessPaperbackBooks(ProcessBookDelegate processBook)
{
foreach (Book b in list)
{
if (b.Paperback)
// Calling the delegate:
processBook(b);
}
}
}
}
// Using the Bookstore classes:
namespace BookTestClient
{
using Bookstore;
// Class to total and average prices of books:
class PriceTotaller
{
int countBooks = 0;
decimal priceBooks = 0.0m;
internal void AddBookToTotal(Book book)
{
countBooks += 1;
priceBooks += book.Price;
}
internal decimal AveragePrice()
{
return priceBooks / countBooks;
}
}
// Class to test the book database:
class Test
{
// Print the title of the book.
static void PrintTitle(Book b)
{
Console.WriteLine(" {0}", b.Title);
}
// Execution starts here.
static void Main()
{
BookDB bookDB = new BookDB();
// Initialize the database with some books:
AddBooks(bookDB);
// Print all the titles of paperbacks:
Console.WriteLine("Paperback Book Titles:");
// Create a new delegate object associated with the static
// method Test.PrintTitle:
bookDB.ProcessPaperbackBooks(new ProcessBookDelegate(PrintTitle));
// Get the average price of a paperback by using
// a PriceTotaller object:
PriceTotaller totaller = new PriceTotaller();
// Create a new delegate object associated with the nonstatic
// method AddBookToTotal on the object totaller:
bookDB.ProcessPaperbackBooks(new ProcessBookDelegate(totaller.AddBookToTotal));
Console.WriteLine("Average Paperback Book Price: ${0:#.##}",
totaller.AveragePrice());
}
// Initialize the book database with some test books:
static void AddBooks(BookDB bookDB)
{
bookDB.AddBook("The C Programming Language",
"Brian W. Kernighan and Dennis M. Ritchie", 19.95m, true);
bookDB.AddBook("The Unicode Standard 2.0",
"The Unicode Consortium", 39.95m, true);
bookDB.AddBook("The MS-DOS Encyclopedia",
"Ray Duncan", 129.95m, false);
bookDB.AddBook("Dogbert's Clues for the Clueless",
"Scott Adams", 12.00m, true);
}
}
}
|
Events are managed using delegates. Screen controls, such as buttons produce
events. When an event occurs, the control uses a delegate to process that
event.
First one creates a delegate for change notifications:
// A delegate type for hooking up change notifications.
public delegate void ChangedEventHandler(object sender, EventArgs e);
Next, one defines an event associated with this delegate.
class ListWithNotification
{
//...other stuff
public event ChangedEventHandler Changed;
Next, one raises the even when appropriate, e.g.
//... ListWithNotification continued
public override int Add(object value)
{
int i = base.Add(value);
Changed(this, EventArgs.Empty);
return i;
}
Next, one adds a specific handler matching the delegate signature to the
event:
class EventListener
{
private void ListChangedHandler(object sender, EventArgs e)
{
Console.WriteLine("This is called when the event fires.");
}
Finally, one adds this handler to the list of handlers the event will notify when it
fires:
//...EventListener continued
this.list = list;
// Add "ListChanged" to the Changed event on "List".
list.Changed += new ChangedEventHandler(ListChangedHandler);
In the main program, one creates an instance of the list, then creates the handler
for that list, and finally uses the list.
The complete listing is below:
using System;
namespace MyCollections
{
using System.Collections;
// A delegate type for hooking up change notifications.
public delegate void ChangedEventHandler(object sender, EventArgs e);
// A class that works just like ArrayList, but sends event
// notifications whenever the list changes.
public class ListWithNotification: ArrayList
{
// An event that clients can use to be notified whenever the
// elements of the list change.
public event ChangedEventHandler Changed;
// Override some of the methods that can change the list;
// invoke event after each
public override int Add(object value)
{
int i = base.Add(value);
Changed(this, EventArgs.Empty);
return i;
}
public override void Clear()
{
base.Clear();
Changed(this, EventArgs.Empty);
}
public override object this[int index]
{
set
{
base[index] = value;
Changed(this, EventArgs.Empty);
}
}
}
}
namespace TestEvents
{
using MyCollections;
class EventListener
{
private ListWithNotification list;
public EventListener(ListWithNotification list)
{
this.list = list;
// Add "ListChanged" to the Changed event on "List".
list.Changed += new ChangedEventHandler(ListChangedHandler);
}
// This will be called whenever the list changes.
private void ListChangedHandler(object sender, EventArgs e)
{
Console.WriteLine("This is called when the event fires.");
}
public void Detach()
{
// Detach the event and delete the list
list.Changed -= new ChangedEventHandler(ListChangedHandler);
list = null;
}
}
class Test
{
// Test the ListWithNotification class.
public static void Main()
{
// Create a new list.
ListWithNotification list = new ListWithNotification();
// Create a class that listens to the list's change event.
EventListener listener = new EventListener(list);
// Add and remove items from the list.
list.Add("item 1");
list.Clear();
listener.Detach();
}
}
}
|
TDD, or Test Driven Development, popularized by Kent Beck in his book
Extreme Programming Explained, is an idea that is gaining momentum in
the software development community. The basic priciple is that any
actual program code must be preceded by an automated test before it
can be written. There are several reasons for this approach to development:
- Insures that the reason for adding a piece of code is not arbitrary, since
it has to be backed by a test
- Allows code to be developed in smaller increments so that the design
can be reviewed and improved frequently
- Reduces the number of bugs introduced into the application
- Allows the code to be refactored (i.e. reorganized) safely.
An example of refactoring is breaking up one large function
into several smaller functions in order to make the design more
modular and the code easier to understand
- The tests can be used to make the intent of the code clear to
a new developer joining the team, thus acting as a form of
specification
NUnit is a tool in the xUnit family of unit testing tools that allows
one to develop and execute unit tests. One must download the NUnit
framework and reference nunit.framework.dll in one's application.
From there, writing tests is straighforward, e.g.
namespace MyApplicationNamespace {
using System;
using NUnit.Framework;
[TestFixture]
public class TestHellowWorld
{
[Test] public void DefaultHelloWorldMessage()
{
HelloWorld hello = new HelloWorld();
Assert.AreEqual("Hello, World!", hello.GetMessage());
}
[Test] public void ParameterizedHelloWorldMessage()
{
HelloWorld hello = new HelloWorld("John");
Assert.AreEqual("Hello, John!", hello.GetMessage());
}
}
}//end namespace
Exercise: Develop a hangman game using TDD and NUnit.
See page 309, Programming C#, 3rd Edition.
Windows Forms allow you to develop applications with the standard
Microsoft Windows look and feel.
- Form
- Button
- TextBox
- Label
- PictureBox
- CheckBox
- RadioButton
- Panel
- GroupBox
- ListBox
- DataGrid
Below is a code listing for a simple form.
using System;
using System.Windows.Forms;
namespace HelloWorld
{
public class HelloWorld : Form
{
private System.Windows.Forms.Label helloWorldMessage;
private System.Windows.Forms.Button cancelButton;
public HelloWorld()
{
helloWorldMessage = new Label();
cancelButton = new Button();
this.Text = "Hello World";
helloWorldMessage.Location = new System.Drawing.Point(16,24);
helloWorldMessage.Text = "Hello, World!";
helloWorldMessage.Size = new System.Drawing.Size(216,24);
cancelButton.Location = new System.Drawing.Point(150, 200);
cancelButton.Size = new System.Drawing.Size(112,32);
cancelButton.Text = "&Cancel";
cancelButton.Click += new System.EventHandler(this.CancelButtonHandler);
AutoScaleBaseSize = new System.Drawing.Size(5, 13);
ClientSize = new System.Drawing.Size(300, 300);
Controls.Add(helloWorldMessage);
Controls.Add(cancelButton);
}
protected void CancelButtonHandler (object Sender, System.EventArgs e)
{
Application.Exit();
}
public static void Main()
{
Application.Run(new HelloWorld());
}
}
}//end namespace
|
Below is another listing for a very simple drawing program.
using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Data;
namespace GraphicalCS
{
///
/// Summary description for Form1.
///
public class MainWindow : System.Windows.Forms.Form
{
internal System.Windows.Forms.Button EraseAll;
internal System.Windows.Forms.Button FillWithHotPink;
internal System.Windows.Forms.Button AddFilledRectangle;
internal System.Windows.Forms.Button AddHollowRectangle;
internal System.Windows.Forms.Button AddPoint;
internal System.Windows.Forms.PictureBox Drawing;
internal System.Windows.Forms.GroupBox GroupBox1;
internal System.Windows.Forms.TextBox FilledCircleRadius;
internal System.Windows.Forms.Label Label3;
internal System.Windows.Forms.TextBox FilledCircleY;
internal System.Windows.Forms.Label Label2;
internal System.Windows.Forms.TextBox FilledCircleX;
internal System.Windows.Forms.Label Label1;
internal System.Windows.Forms.Button AddHollowCircle;
internal System.Windows.Forms.Button AddFilledCircleAt;
internal System.Windows.Forms.Button AddFilledCircle;
///
/// Required designer variable.
///
private System.ComponentModel.Container components = null;
public MainWindow()
{
//
// Required for Windows Form Designer support
//
InitializeComponent();
//
// TODO: Add any constructor code after InitializeComponent call
//
}
///
/// Clean up any resources being used.
///
protected override void Dispose( bool disposing )
{
if( disposing )
{
if (components != null)
{
components.Dispose();
}
}
base.Dispose( disposing );
}
#region Windows Form Designer generated code
///
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
///
private void InitializeComponent()
{
this.EraseAll = new System.Windows.Forms.Button();
this.FillWithHotPink = new System.Windows.Forms.Button();
this.AddFilledRectangle = new System.Windows.Forms.Button();
this.AddHollowRectangle = new System.Windows.Forms.Button();
this.AddPoint = new System.Windows.Forms.Button();
this.Drawing = new System.Windows.Forms.PictureBox();
this.GroupBox1 = new System.Windows.Forms.GroupBox();
this.FilledCircleRadius = new System.Windows.Forms.TextBox();
this.Label3 = new System.Windows.Forms.Label();
this.FilledCircleY = new System.Windows.Forms.TextBox();
this.Label2 = new System.Windows.Forms.Label();
this.FilledCircleX = new System.Windows.Forms.TextBox();
this.Label1 = new System.Windows.Forms.Label();
this.AddFilledCircleAt = new System.Windows.Forms.Button();
this.AddHollowCircle = new System.Windows.Forms.Button();
this.AddFilledCircle = new System.Windows.Forms.Button();
this.GroupBox1.SuspendLayout();
this.SuspendLayout();
//
// EraseAll
//
this.EraseAll.Location = new System.Drawing.Point(392, 367);
this.EraseAll.Name = "EraseAll";
this.EraseAll.Size = new System.Drawing.Size(128, 24);
this.EraseAll.TabIndex = 20;
this.EraseAll.Text = "Erase all";
this.EraseAll.Click += new System.EventHandler(this.EraseAll_Click);
//
// FillWithHotPink
//
this.FillWithHotPink.Location = new System.Drawing.Point(392, 328);
this.FillWithHotPink.Name = "FillWithHotPink";
this.FillWithHotPink.Size = new System.Drawing.Size(128, 24);
this.FillWithHotPink.TabIndex = 18;
this.FillWithHotPink.Text = "Change fills to hot pink";
this.FillWithHotPink.Click += new System.EventHandler(this.FillWithHotPink_Click);
//
// AddFilledRectangle
//
this.AddFilledRectangle.Location = new System.Drawing.Point(392, 152);
this.AddFilledRectangle.Name = "AddFilledRectangle";
this.AddFilledRectangle.Size = new System.Drawing.Size(128, 24);
this.AddFilledRectangle.TabIndex = 17;
this.AddFilledRectangle.Text = "Add Filled Rectangle";
this.AddFilledRectangle.Click += new System.EventHandler(this.AddFilledRectangle_Click);
//
// AddHollowRectangle
//
this.AddHollowRectangle.Location = new System.Drawing.Point(392, 88);
this.AddHollowRectangle.Name = "AddHollowRectangle";
this.AddHollowRectangle.Size = new System.Drawing.Size(128, 24);
this.AddHollowRectangle.TabIndex = 16;
this.AddHollowRectangle.Text = "Add Hollow Rectangle";
this.AddHollowRectangle.Click += new System.EventHandler(this.AddHollowRectangle_Click);
//
// AddPoint
//
this.AddPoint.Location = new System.Drawing.Point(392, 23);
this.AddPoint.Name = "AddPoint";
this.AddPoint.Size = new System.Drawing.Size(128, 24);
this.AddPoint.TabIndex = 15;
this.AddPoint.Text = "Add Point";
this.AddPoint.Click += new System.EventHandler(this.AddPoint_Click);
//
// Drawing
//
this.Drawing.BackColor = System.Drawing.SystemColors.ControlLightLight;
this.Drawing.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
this.Drawing.Location = new System.Drawing.Point(8, 15);
this.Drawing.Name = "Drawing";
this.Drawing.Size = new System.Drawing.Size(368, 376);
this.Drawing.TabIndex = 14;
this.Drawing.TabStop = false;
this.Drawing.Paint += new System.Windows.Forms.PaintEventHandler(this.Drawing_Paint);
//
// GroupBox1
//
this.GroupBox1.Controls.AddRange(new System.Windows.Forms.Control[] {
this.FilledCircleRadius,
this.Label3,
this.FilledCircleY,
this.Label2,
this.FilledCircleX,
this.Label1,
this.AddFilledCircleAt});
this.GroupBox1.Location = new System.Drawing.Point(384, 184);
this.GroupBox1.Name = "GroupBox1";
this.GroupBox1.Size = new System.Drawing.Size(136, 128);
this.GroupBox1.TabIndex = 21;
this.GroupBox1.TabStop = false;
this.GroupBox1.Text = "Input demo";
//
// FilledCircleRadius
//
this.FilledCircleRadius.Location = new System.Drawing.Point(80, 72);
this.FilledCircleRadius.Name = "FilledCircleRadius";
this.FilledCircleRadius.Size = new System.Drawing.Size(40, 20);
this.FilledCircleRadius.TabIndex = 9;
this.FilledCircleRadius.Text = "25";
//
// Label3
//
this.Label3.Location = new System.Drawing.Point(16, 74);
this.Label3.Name = "Label3";
this.Label3.Size = new System.Drawing.Size(56, 16);
this.Label3.TabIndex = 8;
this.Label3.Text = "Radius";
this.Label3.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
//
// FilledCircleY
//
this.FilledCircleY.Location = new System.Drawing.Point(80, 48);
this.FilledCircleY.Name = "FilledCircleY";
this.FilledCircleY.Size = new System.Drawing.Size(40, 20);
this.FilledCircleY.TabIndex = 7;
this.FilledCircleY.Text = "150";
//
// Label2
//
this.Label2.Location = new System.Drawing.Point(16, 50);
this.Label2.Name = "Label2";
this.Label2.Size = new System.Drawing.Size(56, 16);
this.Label2.TabIndex = 6;
this.Label2.Text = "Center Y";
this.Label2.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
//
// FilledCircleX
//
this.FilledCircleX.Location = new System.Drawing.Point(80, 24);
this.FilledCircleX.Name = "FilledCircleX";
this.FilledCircleX.Size = new System.Drawing.Size(40, 20);
this.FilledCircleX.TabIndex = 5;
this.FilledCircleX.Text = "100";
//
// Label1
//
this.Label1.Location = new System.Drawing.Point(16, 26);
this.Label1.Name = "Label1";
this.Label1.Size = new System.Drawing.Size(56, 16);
this.Label1.TabIndex = 4;
this.Label1.Text = "Center X";
this.Label1.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
//
// AddFilledCircleAt
//
this.AddFilledCircleAt.Location = new System.Drawing.Point(8, 96);
this.AddFilledCircleAt.Name = "AddFilledCircleAt";
this.AddFilledCircleAt.Size = new System.Drawing.Size(120, 24);
this.AddFilledCircleAt.TabIndex = 3;
this.AddFilledCircleAt.Text = "Add Filled Circle At...";
this.AddFilledCircleAt.Click += new System.EventHandler(this.AddFilledCircleAt_Click);
//
// AddHollowCircle
//
this.AddHollowCircle.Location = new System.Drawing.Point(392, 55);
this.AddHollowCircle.Name = "AddHollowCircle";
this.AddHollowCircle.Size = new System.Drawing.Size(128, 24);
this.AddHollowCircle.TabIndex = 19;
this.AddHollowCircle.Text = "Add Hollow Circle";
this.AddHollowCircle.Click += new System.EventHandler(this.AddHollowCircle_Click);
//
// AddFilledCircle
//
this.AddFilledCircle.Location = new System.Drawing.Point(392, 120);
this.AddFilledCircle.Name = "AddFilledCircle";
this.AddFilledCircle.Size = new System.Drawing.Size(128, 24);
this.AddFilledCircle.TabIndex = 22;
this.AddFilledCircle.Text = "Add Filled Circle";
this.AddFilledCircle.Click += new System.EventHandler(this.AddFilledCircle_Click);
//
// MainWindow
//
this.Controls.AddRange(new System.Windows.Forms.Control[] {
this.AddFilledCircle,
this.EraseAll,
this.FillWithHotPink,
this.AddFilledRectangle,
this.AddHollowRectangle,
this.AddPoint,
this.Drawing,
this.GroupBox1,
this.AddHollowCircle});
this.Name = "MainWindow";
this.Text = "The Canonical Polymorphism Demo as a Windows Forms app in C#";
this.GroupBox1.ResumeLayout(false);
this.ResumeLayout(false);
}
#endregion
///
/// The main entry point for the application.
///
[STAThread]
static void Main() {
Application.Run(new MainWindow());
}
DShapeList drawingList = new DShapeList();
Random randomGen = new Random();
private Point GetRandomPoint() {
return new Point(randomGen.Next(30, 320), randomGen.Next(30, 320));
}
private void Drawing_Paint(object sender,
System.Windows.Forms.PaintEventArgs e) {
drawingList.DrawList(e.Graphics);
}
private void AddPoint_Click(object sender, System.EventArgs e) {
drawingList.Add(new DPoint(GetRandomPoint(), Color.Blue));
Drawing.Invalidate();
}
private void AddHollowCircle_Click(object sender, System.EventArgs e) {
drawingList.Add(new DHollowCircle(GetRandomPoint(), 30, Color.Chocolate));
Drawing.Invalidate();
}
private void AddHollowRectangle_Click(object sender, System.EventArgs e) {
drawingList.Add(new DHollowRectangle(new Rectangle(
GetRandomPoint(), new Size(20, 30)), Color.Green));
Drawing.Invalidate();
}
private void AddFilledCircle_Click(object sender, System.EventArgs e) {
drawingList.Add(new DFilledCircle(GetRandomPoint(), 20, Color.Red, Color.LimeGreen));
Drawing.Invalidate();
}
private void AddFilledRectangle_Click(object sender, System.EventArgs e) {
drawingList.Add(new DFilledRectangle(
new Rectangle(GetRandomPoint(), new Size(40, 50)), Color.Blue, Color.Cyan));
Drawing.Invalidate();
}
private void AddFilledCircleAt_Click(object sender, System.EventArgs e) {
drawingList.Add(new DFilledCircle(
new Point(Convert.ToInt32(FilledCircleX.Text), Convert.ToInt32(FilledCircleY.Text)),
Convert.ToInt32(FilledCircleRadius.Text), Color.Green, Color.CornflowerBlue));
Drawing.Invalidate();
}
private void FillWithHotPink_Click(object sender, System.EventArgs e) {
IFillable [] filledList = drawingList.GetFilledList();
foreach (IFillable i in filledList)
i.FillBrushColor = Color.HotPink;
Drawing.Invalidate();
}
private void EraseAll_Click(object sender, System.EventArgs e) {
drawingList = new DShapeList();
Drawing.Invalidate();
}
}
}
|
using System;
using System.Drawing;
using System.Collections;
public abstract class DShape {
public abstract void Draw(Graphics g);
protected Rectangle bounding;
protected Color penColor; // should have property, too
// should also have methods to move, resize, etc.
}
public interface IFillable {
void Fill(Graphics g);
Color FillBrushColor { get; set; }
}
public class DPoint : DShape {
public DPoint(Point p, Color penColor) {
bounding = new Rectangle(p, new Size(1, 1));
this.penColor = penColor;
}
public override void Draw(Graphics g) {
using (Pen p = new Pen(penColor)) {
g.DrawRectangle(p, bounding);
}
}
}
public class DHollowCircle : DShape
{
public DHollowCircle(Point p, int radius, Color penColor) {
p.Offset(-radius, -radius); // need to convert to upper left
int diameter = radius * 2;
bounding = new Rectangle(p, new Size(diameter, diameter));
this.penColor = penColor;
}
public override void Draw(Graphics g) {
using (Pen p = new Pen(penColor)) {
g.DrawEllipse(p, bounding);
}
}
}
public class DFilledCircle : DHollowCircle, IFillable
{
public DFilledCircle(Point center, int radius, Color penColor, Color brushColor)
: base(center, radius, penColor) {
this.brushColor = brushColor;
}
public void Fill(Graphics g) {
using (Brush b = new SolidBrush(brushColor)) {
g.FillEllipse(b, bounding);
}
}
protected Color brushColor;
public Color FillBrushColor {
get {
return brushColor;
}
set {
brushColor = value;
}
}
public override void Draw(Graphics g) {
Fill(g);
base.Draw(g);
}
}
public class DHollowRectangle : DShape {
public DHollowRectangle(Rectangle rect, Color penColor) {
bounding = rect;
this.penColor = penColor;
}
public override void Draw(Graphics g) {
using (Pen p = new Pen(penColor)) {
g.DrawRectangle(p, bounding);
}
}
}
public class DFilledRectangle : DHollowRectangle, IFillable {
public DFilledRectangle(Rectangle rect, Color penColor,
Color brushColor) : base(rect, penColor) {
this.brushColor = brushColor;
}
public void Fill(Graphics g) {
using (Brush b = new SolidBrush(brushColor)) {
g.FillRectangle(b, bounding);
}
}
protected Color brushColor;
public Color FillBrushColor {
get {
return brushColor;
}
set {
brushColor = value;
}
}
public override void Draw(Graphics g) {
Fill(g);
base.Draw(g);
}
}
public class DShapeList {
ArrayList wholeList = new ArrayList();
ArrayList filledList = new ArrayList();
public void Add(DShape d) {
wholeList.Add(d);
if (d is IFillable)
filledList.Add(d);
}
public void DrawList(Graphics g) {
if (wholeList.Count == 0)
{
Font f = new Font("Arial", 10);
g.DrawString("Nothing to draw; list is empty...",
f, Brushes.Gray, 50, 50);
}
else
{
foreach (DShape d in wholeList)
d.Draw(g);
}
}
public IFillable[] GetFilledList() {
return (IFillable[])filledList.ToArray(typeof(IFillable));
}
}
|
Exercise: Develop a game of Hangman using Windows Forms.
See page 479, Programming C#, 3rd Edition.
Assemblies
Assemblies are Portable Executable (PE) files. They can either be implemented
as EXE or DLL files. Assemblies can consit of multiple modules, though for
simple projects, just one module is included with the assembly itself. Modules
are created as DLLs and cannot be executed on their own; they have to part of an assembly.
Assemblies are deployed, used, and versioned as a unit. The manifest contains the
assembly meta-data, including the name and version of the assembly, a list of types
and resources in the assembly, and a map that connects the types in the assembly
to the implementing code.
You can use ILDASM.EXE to view assembly metadata.
Multi-Module Assemblies
Multi-module assemblies consist at least one DLL or EXE file. The assembly
manifest can reside in a separate assembly or can be incorporate into one
of the modules. The benefits of multi-module assemblies is that they can
be developed/deployed independently. Modules within an assembly can also
be loaded independently.
Exercise: Create a multi-module assembly as per p.482 of Programming
C#, 3rd Edition.
Here is the makefile for this project (use nmake /fmakefile.mk /A to compile):
ASSEMBLY=MultiModuleAssembly.dll
BIN=.\bin
SRC=.
DEST=.\bin
CSC=csc /nologo /debug+ /d:DEBUG /d:TRACE
MODULETARGET=/t:module
LIBTARGET=/t:library
EXETARGET=/t:exe
REFERENCES=System.dll
MODULES=$(DEST)\Fraction.dll $(DEST)\Calc.dll
METADATA=$(SRC)\AssemblyInfo.cs
all:$(DEST)\MultiModuleAssembly.dll
$(DEST)\$(ASSEMBLY): $(METADATA) $(MODULES) $(DEST)
$(CSC) $(LIBTARGET) /addmodule:$(MODULES: =;) /out:$@ %s
$(DEST)\Calc.dll: Calc.cs $(DEST)
$(CSC) $(MODULETARGET) /r:$(REFERENCES: =;) /out:$@ %s
$(DEST)\Fraction.dll: Fraction.cs $(BIN)
$(CSC) $(MODULETARGET) /r:$(REFERENCES: =;) /out:$@ %s
$(DEST)::
!if !EXISTS($(BIN))
mkdir $(BIN)
!endif
|
Public and Private Assemblies
The example above shows how to use a private assembly. A private assembly
must be stored along with its application in the same directory tree.
No other applications can access such an assembly. If an assembly is going
to be used by multiple application, you need to create a public assembly:
- Create a strong name using the sn utility.
- Edit the AssemblyInfo.cs file.
- Rebuild the assembly.
- Use GACUTIL.exe to add the assembly to the Global Assmbly Cache (GAC).
- Recompile client.
See page 349 of Programming C#, 3rd Edition.
There are a number of options for deploying an application:
Cab Project, Merge Module Project, Setup Project, Setup Wizard,
Remote Deploy Wizard, and Web Setup Project.
See chapter page 641, Programming C#, 3rd Edition.
Importing ActiveX Controls
Once you have a registered OCX control (use Regsvr32 to register the control, e.g.
Regsvr32 CalcControl.ocx), you can import the control into a .NET project.
In Visual Studio, choose Tools, Customize Toolbox. On the COM Components tab, find the
ActiveX control (e.g. CalcControl). Alternatively, you can generate the assembly for
the control using the AXIMP tool, e.g. aximp CalcControl.ocx.
Once this is done, you can return to Customize Toolbox, select .NET Framework Components,
and import the AxCalcControl.dll file. You can now drag this control onto your form
and use it just as you would a normal ActiveX control.
Importing COM Components
If you have a COM type library for your COM component, you can use early binding
using TLBIMP, e.g. tlbimp ComCalculator.dll /out:ComCalculatorDOTNET.dll.
You can now use the control, e.g.
ComCalculator calc = new ComCalculator();
calc.Add(x, y);
If the type library is not available, you must use late binding with
reflection, e.g.
Type calcType = Type.GetTypeFromProgId(ComCalculator");
object calcObject = Activator.CreateInstance(calcType);
Double result = (Double) calcType.InvokeMember("Add", BindingFlags.InvokeMethod,
null, calcObject, new object[] {(Double)2.2, (Double)3.3});
Exporting .NET components
Once you have a public assembly, you can use the REGASM tool to export
the component, e.g. regasm MyAssembly.dll. You can now use this component
in VBScript, e.g.
dim calc
dim msg
dim result
set calc = CreateObject("MyAssemblyNamespace.Calculator");
result = calc.Multiply(7, 3);
msg = "7 * 3 = : & result & ".";
Call MsgBox(msg);
Creating a Type Library
If you wish to use early binding, you can create a type library using
TLBEXP, e.g. tlbexp MyAssembly.dll /out:Calc.tlb
P/Invoke
You can make calls to unmanaged code by declaring a static extern function, e.g.
[DllImport("kernel32.dll", EntryPoint="MoveFile", ExactSpelling=false, CharSet=Charset.Unicode,
SetLastError=true)]
public static extern bool MoveFile(string sourceFile, string destinationFile);
See page 660 of Programming C#, 3rd Edition for more details.
See page 359, Programming C#, 3rd Edition for more information.
The primary objects in the ADO.NET object model are as follows:
- DataSet: Represents a cached subset of the database
- DataTable: Represents a database table
- DataRelation: Represents a foreign key link
- DataAdapter: Accesses the database
- DBCommand: The command sent to the database
- DBConnection: The connection to the database
There are two kinds of managed providers included with ADO.NET:
- SQL Server Managed Provider (SqlDataAdapter)
- OLE DB Manager Provider (OleDbDataAdapter)
You can navigate relationships between tables with ADO.NET (page 377, Programming C#, 3rd Edition).
Create a DataRelation object, e.g.
parentColumn = dataSet.Tables["Customer"].Columns["CustomerId"];
childColumn = dataSet.Tables["Order"].Columns["CustomerId"];
DataRelation dataRelation = new System.Data.DataRelation("CustomersToOrders",
parentColumn, childColumn);
grid1.DataSource = dataSet.DefaultViewManager;
grid1.DataMember = "Customer";
Exercise: Review example on page 383 of Programming C#, 3rd Edition. Also, create a form using
the DataForm Wizard. Explore the code in both cases.
See page 421, Programming C#, 3rd Edition for more information.
Web Services is a suite of technologies designed to facilitate
the deployment of applications as services over the Internet.
One of the reasons for the development of the Web Services model
is to make it easy for people to make various services available
in a standard and generic way to other parties. A company might
make services available to other companies, or a department
might make services available to other departments. In this
respect, Web Services is a model to facilitate enterprise
integration. For example, a pipeline management company may
have a department that stores the most up-to-date information
about gas pool and well locations, production, etc. It may
then encapsulate that information in the form of web wervices
so that other departments may be able to integrate that information
into their own applications. Another aspect of Web Services
concerns E-Commerce. In this regard, Web Services can be used
as a platform to process micro-payments for per-access use
of an application. A company may expose a Web service that
yields real time stock information, and any other applications
that use this Web Service would incur an automatic debit of
some kind each time they request that service.
- SOAP: SOAP is basically a remote method invocation protocol similar
to RPC, CORBA remote method calls, and Java RMI. The
difference is that it is encapsulated in straightforward
XML streams transmitted over the HTTP protocol, which
makes it more friendly to Web applications.
- WSDL: Web Service Descriptin Language is basically
a directory (implemented as an XML schema)
that lists the available interfaces supported by a given
web service. WSDL can be accessed via SOAP or HTTP.
- UDDI and Discovery are two protocols that used to provide
a registry of web services providers. Discovery is a proprietary Microsoft
technology.
Creating a Web Service
Creating a Web service in .NET and Visual Studio with Internet Information
Server is quite trivial. Simply add the [WebMethod] attribute to any
method you wish to expose as a Web Service. A simple way to create
a Web Service is to create a new Web Service Project in Visual
Studio (you will need to have Internet Information Service running).
Creating a Web Client
Use WSDL to create a proxy for your Web Service client, e.g.
wsdl http://localhost/WSCalc/service1.asmx?wsdl. Add the appropriate
namespace to the generated code and build a library (DLL) from it.
Next, write a simple client using this proxy referencing that DLL.
Simply instantiate the client, and away you go. You can access
your Web Service directly over the Web also, e.g.
http://localhost/WSCalc/Service1.asmx/Add?x=32&y=22
See page 401, Programming C#, 3rd Edition.
ASP.NET uses Web Forms technology to create Web applications (as
opposed to Windows forms, which are used to create Windows applications).
Each Web Form consists of two files, the GUI and the back-end code:
The gui which contains all of the controls is stored in a .aspx file.
The code-behind file, which handles all of the events is in a normal
C# file (using the same name as the form is a reasonable convention).
ASP.NET is event-driven. In other words, when a form is submitted,
the server issues events based on the state of the form. For example,
if you've selected a particular radio button on the form, the CheckedChanged
event will fire for that radio button. Postback events cause the form
to actually submit, so asp:Button is an obvious example. asp:RadioButton
is not a PostBack object, so the event for button selection will only
be fired once the submit button is clicked. One can generally cause
a non-postback control to become a postback control by setting AutoPostback=True,
e.g.
<asp:RadioButton runat="server" id="button1"
Text="One" GroupName="radioGroup"
AutoPostBack="true" />
Unit tests
namespace HangmanServer {
using System;
using NUnit.Framework;
using System.Collections;
[TestFixture]
public class TestHangman
{
[Test] public void CreateGame()
{
Hangman hangman = new Hangman("happy");
Assert.AreEqual(GameState.IN_PROGRESS, hangman.GetGameState());
Assert.AreEqual("happy", hangman.GetSecretWord());
Assert.AreEqual(0, hangman.GetErrorCount());
char[] letters = hangman.GetLetters();
for (int i=0; i<5; i++) {
Assert.AreEqual('\0', letters.GetValue(i));
}
}
[Test] public void SecretWordAlwaysLowerCase()
{
Hangman hangman = new Hangman("HaPPy");
Assert.AreEqual("happy", hangman.GetSecretWord());
}
[Test] public void CorrectGuess()
{
Hangman hangman = new Hangman("happy");
hangman.Guess('a');
Assert.AreEqual(GameState.IN_PROGRESS, hangman.GetGameState());
Assert.AreEqual(0, hangman.GetErrorCount());
char[] letters = hangman.GetLetters();
Assert.AreEqual('\0', letters.GetValue(0));
Assert.AreEqual('a', letters.GetValue(1));
Assert.AreEqual('\0', letters.GetValue(2));
Assert.AreEqual('\0', letters.GetValue(3));
Assert.AreEqual('\0', letters.GetValue(4));
}
[Test] public void CorrectGuessUppercase()
{
Hangman hangman = new Hangman("happy");
hangman.Guess('P');
Assert.AreEqual(GameState.IN_PROGRESS, hangman.GetGameState());
Assert.AreEqual(0, hangman.GetErrorCount());
char[] letters = hangman.GetLetters();
Assert.AreEqual('\0', letters.GetValue(0));
Assert.AreEqual('\0', letters.GetValue(1));
Assert.AreEqual('p', letters.GetValue(2));
Assert.AreEqual('p', letters.GetValue(3));
Assert.AreEqual('\0', letters.GetValue(4));
}
[Test] public void WrongGuess()
{
Hangman hangman = new Hangman("happy");
hangman.Guess('x');
Assert.AreEqual(GameState.IN_PROGRESS, hangman.GetGameState());
Assert.AreEqual(1, hangman.GetErrorCount());
char[] letters = hangman.GetLetters();
for (int i=0; i<5; i++) {
Assert.AreEqual('\0', letters.GetValue(i));
}
}
[Test] public void GameLost()
{
Hangman hangman = new Hangman("sad");
hangman.Guess('b');
Assert.AreEqual(1, hangman.GetErrorCount());
hangman.Guess('c');
Assert.AreEqual(2, hangman.GetErrorCount());
hangman.Guess('d');
Assert.AreEqual(2, hangman.GetErrorCount());
hangman.Guess('e');
Assert.AreEqual(3, hangman.GetErrorCount());
hangman.Guess('f');
Assert.AreEqual(4, hangman.GetErrorCount());
hangman.Guess('g');
Assert.AreEqual(5, hangman.GetErrorCount());
hangman.Guess('h');
Assert.AreEqual(6, hangman.GetErrorCount());
hangman.Guess('i');
Assert.AreEqual(7, hangman.GetErrorCount());
Assert.AreEqual(GameState.LOST, hangman.GetGameState());
char[] letters = hangman.GetLetters();
Assert.AreEqual('\0', letters.GetValue(0));
Assert.AreEqual('\0', letters.GetValue(1));
Assert.AreEqual('d', letters.GetValue(2));
}
[Test] public void GameWon()
{
Hangman hangman = new Hangman("happy");
hangman.Guess('h');
hangman.Guess('a');
hangman.Guess('p');
hangman.Guess('y');
Assert.AreEqual(0, hangman.GetErrorCount());
Assert.AreEqual(GameState.WON, hangman.GetGameState());
char[] letters = hangman.GetLetters();
Assert.AreEqual('h', letters.GetValue(0));
Assert.AreEqual('a', letters.GetValue(1));
Assert.AreEqual('p', letters.GetValue(2));
Assert.AreEqual('p', letters.GetValue(3));
Assert.AreEqual('y', letters.GetValue(4));
}
[Test] public void RepeatedWrongGuessIsFree()
{
Hangman hangman = new Hangman("repeat");
hangman.Guess('x');
hangman.Guess('x');
Assert.AreEqual(1, hangman.GetErrorCount());
}
[Test] public void RepeatedCorrectGuessIsFree()
{
Hangman hangman = new Hangman("repeat");
hangman.Guess('e');
hangman.Guess('e');
Assert.AreEqual(0, hangman.GetErrorCount());
}
[Test] public void SecretWordCannotBeNull()
{
try
{
Hangman hangman = new Hangman(null);
Assert.Fail("secret word cannot be null");
}
catch (InvalidSecretWordException e)
{
Assert.AreEqual("secret word cannot be null", e.Message);
}
}
[Test] public void SecretWordMinimumTwoCharacters()
{
try
{
Hangman hangman = new Hangman("x");
Assert.Fail(
"secret word must be at least two characters long");
}
catch (InvalidSecretWordException e)
{
Assert.AreEqual(
"secret word must be at least two characters long",
e.Message);
}
}
[Test] public void LostGameCannotBeContinued()
{
Hangman hangman = new Hangman("sad");
hangman.Guess('b');
hangman.Guess('c');
hangman.Guess('e');
hangman.Guess('f');
hangman.Guess('g');
hangman.Guess('h');
hangman.Guess('i');
try
{
hangman.Guess('x');
Assert.Fail("game is over");
}
catch (GameOverException)
{
}
}
[Test] public void WonGameCannotBeContinued()
{
Hangman hangman = new Hangman("happy");
hangman.Guess('h');
hangman.Guess('a');
hangman.Guess('p');
hangman.Guess('y');
try
{
hangman.Guess('x');
Assert.Fail("game is over");
}
catch (GameOverException)
{
}
}
[Test] public void GuessHistoryIsTracked()
{
Hangman hangman = new Hangman("repeat");
hangman.Guess('r');
hangman.Guess('x');
hangman.Guess('r');
hangman.Guess('t');
IEnumerator guessHistory = hangman.GetGuessHistory();
guessHistory.MoveNext();
GameGuess gameGuess = (GameGuess) guessHistory.Current;
Assert.AreEqual('r', gameGuess.Letter);
Assert.AreEqual(GameGuess.CORRECT, gameGuess.Status);
guessHistory.MoveNext();
gameGuess = (GameGuess) guessHistory.Current;
Assert.AreEqual('x', gameGuess.Letter);
Assert.AreEqual(GameGuess.WRONG, gameGuess.Status);
guessHistory.MoveNext();
gameGuess = (GameGuess) guessHistory.Current;
Assert.AreEqual('r', gameGuess.Letter);
Assert.AreEqual(GameGuess.REPEAT, gameGuess.Status);
guessHistory.MoveNext();
gameGuess = (GameGuess) guessHistory.Current;
Assert.AreEqual('t', gameGuess.Letter);
Assert.AreEqual(GameGuess.CORRECT, gameGuess.Status);
}
/* note: should make a test to make sure secret
* word is a dictionary word */
}
} //end namespace
|
Hangman Component
namespace HangmanServer
{
using System;
using System.Collections;
public class Hangman
{
private int errorCount;
private string secretWord;
private char[] letters;
private ArrayList previousGuesses = new ArrayList();
private GameState gameState = GameState.IN_PROGRESS;
public static void Main(string[] args)
{
}
public Hangman(string secretWord)
{
ValidateSecretWord(secretWord);
this.secretWord = secretWord.ToLower();
letters = new char[secretWord.Length];
}
public GameState GetGameState()
{
return gameState;
}
public int GetErrorCount()
{
return errorCount;
}
public char[] GetLetterMask()
{
return letters;
}
public void Guess(char guess)
{
CheckGameOver();
GameGuess guessHistoryItem;
if (IsRepeatGuess(guess))
{
guessHistoryItem = new GameGuess(guess, GameGuess.REPEAT);
previousGuesses.Add(guessHistoryItem);
return;
}
bool correctGuess = false;
for (int i=0; i<secretWord.Length; i++)
{
char letter = secretWord[i];
if (guess == letter ||
char.ToLower(guess) == letter)
{
letters[i] = letter;
correctGuess = true;
}
}
if (correctGuess == false)
{
guessHistoryItem = new GameGuess(guess, GameGuess.WRONG);
errorCount++;
} else
{
guessHistoryItem = new GameGuess(guess, GameGuess.CORRECT);
}
previousGuesses.Add(guessHistoryItem);
CheckGameOver();
}
public IEnumerator GetGuessHistory()
{
IEnumerable a;
return previousGuesses.GetEnumerator();
}
internal string GetSecretWord()
{
return secretWord;
}
private bool IsSecretSolved()
{
for (int i=0; i<letters.Length; i++)
{
if (letters[i] != secretWord[i])
{
return false;
}
}
return true;
}
private void CheckGameOver()
{
if (gameState == GameState.WON
|| gameState == GameState.LOST)
{
throw new GameOverException();
}
if (errorCount == 7)
{
gameState = GameState.LOST;
}
bool solved = IsSecretSolved();
if (solved)
{
gameState = GameState.WON;
}
}
private bool IsRepeatGuess(char guess)
{
foreach (GameGuess previouslyGuessedLetter
in previousGuesses)
{
if (previouslyGuessedLetter.Letter == guess)
{
return true;
}
}
return false;
}
private void ValidateSecretWord(String secretWord)
{
if (secretWord == null)
{
throw new InvalidSecretWordException(
"secret word cannot be null");
}
if (secretWord.Length < 2)
{
throw new InvalidSecretWordException(
"secret word must be at least two characters long");
}
}
}
} //end namespace
|
GameGuess
namespace HangmanServer {
public struct GameGuess
{
public const string CORRECT = "CORRECT";
public const string WRONG = "WRONG";
public const string REPEAT = "REPEAT";
private char letter;
private string status;
public GameGuess(char letter, string status)
{
this.letter = letter;
this.status = status;
}
public char Letter
{
get
{
return letter;
}
}
public string Status
{
get
{
return status;
}
}
}
} //end namespace
|
GameState
namespace HangmanServer
{
public enum GameState
{
IN_PROGRESS,
WON,
LOST
}
} //end namespace
|
GameOverException
namespace HangmanServer
{
public class GameOverException : System.ApplicationException
{
public GameOverException ()
{
}
}
} //end namespace
|
InvalidSecretWordException
namespace HangmanServer
{
public class InvalidSecretWordException : System.ApplicationException
{
public InvalidSecretWordException (string message) : base(message)
{
}
}
} //end namespace
|