This content was uploaded by our users and we assume good faith they have the permission to share this book. If you own the copyright to this book and it is wrongfully on our website, we offer a simple DMCA procedure to remove your content from our site. Start by pressing the button below!
C# FOR PROGRAMMERS SECOND EDITION DEITEL DEVELOPER SERIES ®
Deitel S er ®
How To Program Series Advanced Java 2 Platform How to Program ™
C How to Program, 4/e C++ How to Program, 5/e – Including Cyber Classroom Visual C# 2005 How to Program, 2/e ®
e-Business and e-Commerce How to Program Internet and World Wide Web How to Program, 3/e Java How to Program, 6/e – Including Cyber Classroom ™
Small C++ How to Program, 5/e – Including Cyber Classroom Small Java How to Program, 6/e – Including Cyber Classroom ™
Perl How to Program Python How to Program Visual C++ .NET How to Program ®
Visual Basic 2005 How to Program, 3/e ®
Wireless Internet & Mobile Business How to Program XML How to Program
i e s Page Simply Series Simply C++: An Application-Driven Tutorial Approach Simply C#: An Application-Driven Tutorial Approach Simply Java Programming: An Application-Driven Tutorial Approach ™
Simply Visual Basic .NET: An Application Driven Tutorial Approach (Visual Studio .NET 2002 Edition) Simply Visual Basic .NET 2003: An Application Driven Tutorial Approach ®
®
Also Available SafariX Web Books www.SafariX.com
To follow the Deitel publishing program, please register at: www.deitel.com/newsletter/subscribe.html
for the free DEITEL® BUZZ ONLINE e-mail newsletter. To communicate with the authors, send e-mail to: [email protected]
For information on corporate on-site seminars offered by Deitel & Associates, Inc. worldwide, visit: www.deitel.com or write to [email protected]
For continuing updates on Prentice Hall/Deitel publications visit: www.deitel.com www.prenhall.com/deitel www.InformIT.com/deitel
ISBN 0-13-134591-5 Text printed in the United States on recycled paper at RR Donnelley Crawfordsville in Crawfordsville, Indiana. 4th Printing June 2008
C# FOR PROGRAMMERS SECOND EDITION DEITEL DEVELOPER SERIES ®
Harvey M. Deitel Deitel & Associates, Inc. Paul J. Deitel Deitel & Associates, Inc.
Upper Saddle River, NJ • Boston • Indianapolis • San Francisco New York • Toronto • Montreal • London • Munich • Paris • Madrid Capetown • Sydney • Tokyo • Singapore • Mexico City
Trademarks Microsoft Visual Studio 2005 are either registered trademarks or trademarks of Microsoft Corporation in the United States and/or other countries. Object Management Group, OMG, Unified Modeling Language and UML are trademarks of Object Management Group, Inc. ®
®
To Janie Schwark of Microsoft With sincere gratitude for the privilege and the pleasure of working closely with you for so many years. Harvey M. Deitel and Paul J. Deitel
This page intentionally left blank
Contents Preface
1 1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.8 1.9 1.10 1.11
2 2.1 2.2 2.3 2.4
Introduction to Computers, the Internet and Visual C# Introduction Microsoft’s Windows® Operating System C# The Internet and the World Wide Web Extensible Markup Language (XML) Microsoft’s .NET The .NET Framework and the Common Language Runtime Test-Driving a C# Application (Only Required Section of the Case Study) Software Engineering Case Study: Introduction to Object Technology and the UML Wrap-Up Web Resources
Introduction to the Visual C# 2005 Express Edition IDE
2.7 2.8
Introduction Overview of the Visual Studio 2005 IDE Menu Bar and Toolbar Navigating the Visual Studio 2005 IDE Solution Explorer 2.4.1 2.4.2 Toolbox 2.4.3 Properties Window Using Help Using Visual Programming to Create a Simple Program Displaying Text and an Image Wrap-Up Web Resources
3
Introduction to C# Applications
3.1
Introduction
2.5 2.6
xxiii 1 2 2 3 4 5 5 6 8 10 16 16
18 19 19 25 27 31 32 33 35 37 49 50
51 52
x
Contents
3.2 3.3 3.4 3.5 3.6 3.7 3.8 3.9 3.10 3.11
A Simple C# Application: Displaying a Line of Text Creating Your Simple Application in Visual C# Express Modifying Your Simple C# Application Formatting Text with Console.Write and Console.WriteLine Another C# Application: Adding Integers Memory Concepts Arithmetic Decision Making: Equality and Relational Operators (Optional) Software Engineering Case Study: Examining the ATM Requirements Document Wrap-Up
4
Introduction to Classes and Objects
92
4.1 4.2 4.3 4.4 4.5 4.6 4.7 4.8 4.9 4.10 4.11
93 93 95 99 102 107 108 109 111 113
4.12
Introduction Classes, Objects, Methods, Properties and Instance Variables Declaring a Class with a Method and Instantiating an Object of a Class Declaring a Method with a Parameter Instance Variables and Properties UML Class Diagram with a Property Software Engineering with Properties and set and get Accessors Value Types vs. Reference Types Initializing Objects with Constructors Floating-Point Numbers and Type decimal (Optional) Software Engineering Case Study: Identifying the Classes in the ATM Requirements Document Wrap-Up
Introduction Control Structures if Single-Selection Statement if…else Double-Selection Statement while Repetition Statement Formulating Algorithms: Counter-Controlled Repetition Formulating Algorithms: Sentinel-Controlled Repetition Formulating Algorithms: Nested Control Statements Compound Assignment Operators Increment and Decrement Operators Simple Types (Optional) Software Engineering Case Study: Identifying Class Attributes in the ATM System Wrap-Up
Introduction Essentials of Counter-Controlled Repetition for Repetition Statement Examples Using the for Statement do…while Repetition Statement switch Multiple-Selection Statement break and continue Statements Logical Operators (Optional) Software Engineering Case Study: Identifying Objects’ States and Activities in the ATM System Wrap-Up
7
Methods: A Deeper Look
7.1 7.2 7.3 7.4 7.5 7.6 7.7 7.8 7.9
7.16
Introduction Packaging Code in C# static Methods, static Variables and Class Math Declaring Methods with Multiple Parameters Notes on Declaring and Using Methods Method Call Stack and Activation Records Argument Promotion and Casting The Framework Class Library Case Study: Random-Number Generation 7.9.1 Scaling and Shifting Random Numbers 7.9.2 Random-Number Repeatability for Testing and Debugging Case Study: A Game of Chance (Introducing Enumerations) Scope of Declarations Method Overloading Recursion Passing Arguments: Pass-by-Value vs. Pass-by-Reference (Optional) Software Engineering Case Study: Identifying Class Operations in the ATM System Wrap-Up
8
Arrays
8.1 8.2 8.3 8.4 8.5 8.6 8.7 8.8
Introduction Arrays Declaring and Creating Arrays Examples Using Arrays Case Study: Card Shuffling and Dealing Simulation foreach Statement Passing Arrays and Array Elements to Methods Passing Arrays by Value and by Reference
Case Study: Class GradeBook Using an Array to Store Grades Multidimensional Arrays Case Study: Class GradeBook Using a Rectangular Array Variable-Length Argument Lists Using Command-Line Arguments (Optional) Software Engineering Case Study: Collaboration Among Objects in the ATM System Wrap-Up
Introduction Time Class Case Study Controlling Access to Members Referring to the Current Object’s Members with the this Reference Indexers Time Class Case Study: Overloaded Constructors Default and Parameterless Constructors Composition Garbage Collection and Destructors static Class Members readonly Instance Variables Software Reusability Data Abstraction and Encapsulation Time Class Case Study: Creating Class Libraries internal Access Class View and Object Browser (Optional) Software Engineering Case Study: Starting to Program the Classes of the ATM System Wrap-Up
10
Object-Oriented Programming: Inheritance
10.1 10.2 10.3 10.4
Introduction Base Classes and Derived Classes protected Members Relationship between Base Classes and Derived Classes 10.4.1 Creating and Using a CommissionEmployee Class 10.4.2 Creating a BasePlusCommissionEmployee Class without Using Inheritance 10.4.3 Creating a CommissionEmployee–BasePlusCommissionEmployee Inheritance Hierarchy 10.4.4 CommissionEmployee–BasePlusCommissionEmployee Inheritance Hierarchy Using protected Instance Variables 10.4.5 CommissionEmployee–BasePlusCommissionEmployee Inheritance Hierarchy Using private Instance Variables
Constructors in Derived Classes Software Engineering with Inheritance Class object Wrap-Up
11
Polymorphism, Interfaces & Operator Overloading
11.1 11.2 11.3 11.4 11.5
Introduction Polymorphism Examples Demonstrating Polymorphic Behavior Abstract Classes and Methods Case Study: Payroll System Using Polymorphism 11.5.1 Creating Abstract Base Class Employee 11.5.2 Creating Concrete Derived Class SalariedEmployee 11.5.3 Creating Concrete Derived Class HourlyEmployee 11.5.4 Creating Concrete Derived Class CommissionEmployee 11.5.5 Creating Indirect Concrete Derived Class BasePlusCommissionEmployee
11.5.6 11.5.7
Polymorphic Processing, Operator is and Downcasting Summary of the Allowed Assignments Between Base Class and Derived Class Variables 11.6 sealed Methods and Classes 11.7 Case Study: Creating and Using Interfaces 11.7.1 Developing an IPayable Hierarchy 11.7.2 Declaring Interface IPayable 11.7.3 Creating Class Invoice 11.7.4 Modifying Class Employee to Implement Interface IPayable 11.7.5 Modifying Class SalariedEmployee for Use in the IPayable Hierarchy 11.7.6 Using Interface IPayable to Process Invoices and Employees Polymorphically 11.7.7 Common Interfaces of the .NET Framework Class Library 11.8 Operator Overloading 11.9 (Optional) Software Engineering Case Study: Incorporating Inheritance and Polymorphism into the ATM System 11.10 Wrap-Up
12
Exception Handling
12.1 12.2 12.3 12.4
Introduction Exception Handling Overview Example: Divide by Zero Without Exception Handling Example: Handling DivideByZeroExceptions and FormatExceptions 12.4.1 Enclosing Code in a try Block 12.4.2 Catching Exceptions
12.4.3 Uncaught Exceptions 12.4.4 Termination Model of Exception Handling 12.4.5 Flow of Control When Exceptions Occur .NET Exception Hierarchy 12.5.1 Classes ApplicationException and SystemException 12.5.2 Determining Which Exceptions a Method Throws finally Block Exception Properties User-Defined Exception Classes Wrap-Up
Introduction Windows Forms Event Handling 13.3.1 A Simple Event-Driven GUI 13.3.2 Another Look at the Visual Studio Generated Code 13.3.3 Delegates and the Event-Handling Mechanism 13.3.4 Other Ways to Create Event Handlers 13.3.5 Locating Event Information Control Properties and Layout Labels, TextBoxes and Buttons GroupBoxes and Panels CheckBoxes and RadioButtons PictureBoxes ToolTips NumericUpDown Control Mouse-Event Handling Keyboard-Event Handling Wrap-Up
14
Graphical User Interface Concepts: Part 2
12.5
14.1 Introduction 14.2 Menus 14.3 MonthCalendar Control 14.4 DateTimePicker Control 14.5 LinkLabel Control 14.6 ListBox Control 14.7 CheckedListBox Control 14.8 ComboBox Control 14.9 TreeView Control 14.10 ListView Control 14.11 TabControl Control 14.12 Multiple Document Interface (MDI) Windows
Introduction Thread States: Life Cycle of a Thread Thread Priorities and Thread Scheduling Creating and Executing Threads Thread Synchronization and Class Monitor Producer/Consumer Relationship without Thread Synchronization Producer/Consumer Relationship with Thread Synchronization Producer/Consumer Relationship: Circular Buffer Multithreading with GUIs Wrap-Up
Introduction Fundamentals of Characters and Strings string Constructors string Indexer, Length Property and CopyTo Method Comparing strings Locating Characters and Substrings in strings Extracting Substrings from strings Concatenating strings Miscellaneous string Methods Class StringBuilder Length and Capacity Properties, EnsureCapacity Method and Indexer of Class StringBuilder Append and AppendFormat Methods of Class StringBuilder Insert, Remove and Replace Methods of Class StringBuilder Char Methods Card Shuffling and Dealing Simulation Regular Expressions and Class Regex 16.16.1 Regular Expression Example 16.16.2 Validating User Input with Regular Expressions 16.16.3 Regex methods Replace and Split Wrap-Up
17
Graphics and Multimedia
17.1 17.2 17.3
Introduction Drawing Classes and the Coordinate System Graphics Contexts and Graphics Objects
Color Control Font Control Drawing Lines, Rectangles and Ovals Drawing Arcs Drawing Polygons and Polylines Advanced Graphics Capabilities Introduction to Multimedia Loading, Displaying and Scaling Images Animating a Series of Images Windows Media Player Microsoft Agent Wrap-Up
Introduction Data Hierarchy Files and Streams Classes File and Directory Creating a Sequential-Access Text File Reading Data from a Sequential-Access Text File Serialization Creating a Sequential-Access File Using Object Serialization Reading and Deserializing Data from a Sequential-Access Text File Wrap-Up
Introduction XML Basics Structuring Data XML Namespaces Document Type Definitions (DTDs) W3C XML Schema Documents (Optional) Extensible Stylesheet Language and XSL Transformations (Optional) Document Object Model (DOM) (Optional) Schema Validation with Class XmlReader (Optional) XSLT with Class XslCompiledTransform Wrap-Up Web Resources
SQL 20.4.1 Basic SELECT Query 20.4.2 WHERE Clause 20.4.3 ORDER BY Clause 20.4.4 Merging Data from Multiple Tables: INNER JOIN 20.4.5 INSERT Statement 20.4.6 UPDATE Statement 20.4.7 DELETE Statement 20.5 ADO.NET Object Model 20.6 Programming with ADO.NET: Extracting Information from a Database 20.6.1 Displaying a Database Table in a DataGridView 20.6.2 How Data Binding Works 20.7 Querying the Books Database 20.8 Programming with ADO.NET: Address Book Case Study 20.9 Using a DataSet to Read and Write XML 20.10 Wrap-Up 20.11 Web Resources
21 21.1 21.2 21.3 21.4
ASP.NET 2.0, Web Forms and Web Controls
Introduction Simple HTTP Transactions Multitier Application Architecture Creating and Running a Simple Web-Form Example 21.4.1 Examining an ASPX File 21.4.2 Examining a Code-Behind File 21.4.3 Relationship Between an ASPX File and a Code-Behind File 21.4.4 How the Code in an ASP.NET Web Page Executes 21.4.5 Examining the XHTML Generated by an ASP.NET Application 21.4.6 Building an ASP.NET Web Application 21.5 Web Controls 21.5.1 Text and Graphics Controls 21.5.2 AdRotator Control 21.5.3 Validation Controls 21.6 Session Tracking 21.6.1 Cookies 21.6.2 Session Tracking with HttpSessionState 21.7 Case Study: Connecting to a Database in ASP.NET 21.7.1 Building a Web Form That Displays Data from a Database 21.7.2 Modifying the Code-Behind File for the Guestbook Application 21.8 Case Study: Secure Books Database Application 21.8.1 Examining the Completed Secure Books Database Application 21.8.2 Creating the Secure Books Database Application 21.9 Wrap-Up 21.10 Web Resources
Introduction .NET Web Services Basics 22.2.1 Creating a Web Service in Visual Web Developer 22.2.2 Discovering Web Services 22.2.3 Determining a Web Service’s Functionality 22.2.4 Testing a Web Service’s Methods 22.2.5 Building a Client to Use a Web Service Simple Object Access Protocol (SOAP) Publishing and Consuming Web Services 22.4.1 Defining the HugeInteger Web Service 22.4.2 Building a Web Service in Visual Web Developer 22.4.3 Deploying the HugeInteger Web Service 22.4.4 Creating a Client to Consume the HugeInteger Web Service 22.4.5 Consuming the HugeInteger Web Service Session Tracking in Web Services 22.5.1 Creating a Blackjack Web Service 22.5.2 Consuming the Blackjack Web Service Using Web Forms and Web Services 22.6.1 Adding Data Components to a Web Service 22.6.2 Creating a Web Form to Interact with the Airline Reservation Web Service User-Defined Types in Web Services Wrap-Up Web Resources
Introduction Connection-Oriented vs. Connectionless Communication Protocols for Transporting Data Establishing a Simple TCP Server (Using Stream Sockets) Establishing a Simple TCP Client (Using Stream Sockets) Client/Server Interaction with Stream-Socket Connections Connectionless Client/Server Interaction with Datagrams Client/Server Tic-Tac-Toe Using a Multithreaded Server WebBrowser Control .NET Remoting Wrap-Up
24
Data Structures
24.1 24.2
Introduction Simple-Type structs, Boxing and Unboxing
Self-Referential Classes Linked Lists Stacks Queues Trees 24.7.1 Binary Search Tree of Integer Values 24.7.2 Binary Search Tree of IComparable Objects Wrap-Up
25
Generics
25.1 25.2 25.3 25.4 25.5 25.6 25.7 25.8
Introduction Motivation for Generic Methods Generic Method Implementation Type Constraints Overloading Generic Methods Generic Classes Notes on Generics and Inheritance Wrap-Up
26
Collections
26.1 26.2 26.3 26.4
26.6 26.7
Introduction Collections Overview Class Array and Enumerators Non-Generic Collections 26.4.1 Class ArrayList 26.4.2 Class Stack 26.4.3 Class Hashtable Generic Collections 26.5.1 Generic Class SortedDictionary 26.5.2 Generic Class LinkedList Synchronized Collections Wrap-Up
Introduction Abbreviating Binary Numbers as Octal and Hexadecimal Numbers Converting Octal and Hexadecimal Numbers to Binary Numbers Converting from Binary, Octal or Hexadecimal to Decimal Converting from Decimal to Binary, Octal or Hexadecimal Negative Binary Numbers: Two’s Complement Notation
26.5
1076 1078 1090 1094 1098 1099 1106 1112
1114 1115 1116 1118 1120 1123 1124 1133 1133
1134
1165 1168 1169 1169 1170 1172
xx
Contents
C
Using the Visual Studio® 2005 Debugger
C.1 C.2 C.3 C.4
C.6
Introduction Breakpoints and the Continue Command The Locals and Watch Windows Controlling Execution Using the Step Into, Step Over, Step Out and Continue Commands Other Features C.5.1 Edit and Continue C.5.2 Exception Assistant C.5.3 Just My Code™ Debugging C.5.4 Other New Debugger Features Wrap-Up
D
ASCII Character Set
1191
E
Unicode®
1192
E.1 E.2 E.3 E.4 E.5 E.6
Introduction Unicode Transformation Formats Characters and Glyphs Advantages/Disadvantages of Unicode Using Unicode Character Ranges
F
Introduction to XHTML: Part 1
F.1 F.2 F.3 F.4 F.5 F.6 F.7 F.8 F.9 F.10 F.11
Introduction Editing XHTML First XHTML Example W3C XHTML Validation Service Headers Linking Images Special Characters and More Line Breaks Unordered Lists Nested and Ordered Lists Web Resources
G
Introduction to XHTML: Part 2
G.1 G.2 G.3 G.4
Introduction Basic XHTML Tables Intermediate XHTML Tables and Formatting Basic XHTML Forms
ATM Case Study Implementation Class ATM Class Screen Class Keypad Class CashDispenser Class DepositSlot Class Account Class BankDatabase Class Transaction Class BalanceInquiry Class Withdrawal Class Deposit Class ATMCaseStudy Wrap-Up
Preface “Live in fragments no longer, only connect.” —Edgar Morgan Foster Welcome to C# and the world of Windows, Internet and Web programming with Visual Studio 2005 and the .NET 2.0 platform! This book presents leading-edge computing technologies to software developers and IT professionals. At Deitel & Associates, we write computer science textbooks for college students and professional books for software developers. We also teach this material in industry seminars at organizations worldwide. This book was a joy to create. To start, we put the previous edition under the microscope: • We audited our C# presentation against the most recent Ecma and Microsoft C# Language Specifications, which can be found at www.ecma-international.org/ publications/standards/Ecma-334.html and msdn.microsoft.com/vcsharp/ programming/language/, respectively. • All of the chapters have been significantly updated and upgraded. • We changed to an early classes and objects pedagogy. Now readers build reusable classes starting in Chapter 4. • We updated our object-oriented presentation to use the latest version of the UML (Unified Modeling Language)—UML™ 2.0—the industry-standard graphical language for modeling object-oriented systems. • We added an optional OOD/UML automated teller machine (ATM) case study in Chapters 1, 3–9 and 11. The case study includes a complete C# code implementation of the ATM. • We added several multi-section object-oriented programming case studies. • We incorporated key new features of Microsoft’s latest release of C#—Visual C# 2005—and added discussions on generics, .NET remoting and debugging. • We significantly enhanced our treatment of XML, ADO.NET, ASP.NET and Web services. All of this has been carefully scrutinized by a team of distinguished .NET industry developers, academic professionals and members of the Microsoft C# development team.
Who Should Read This Book We have several C# publications, intended for different audiences. C# for Programmers, 2/e, is part of the Deitel® Developer Series, intended for professional software developers who want a deep treatment of a new technology with minimal
xxiv
Preface
introductory material. The book emphasizes achieving program clarity through the proven techniques of structured programming, object-oriented programming (OOP) and eventdriven programming. It continues with upper-level topics such as XML, ASP.NET 2.0, ADO.NET 2.0 and Web services. Unlike our How to Program Series college textbooks, the Deitel® Developer Series books do not include the extensive pedagogic features and ancillary support materials required for college courses. C# for Programmers, 2/e presents many complete, working C# programs and depicts their inputs and outputs in actual screen shots of running programs. This is our signature “live-code” approach—we present concepts in the context of complete working programs. The book’s source code is available free for download at www.deitel.com/books/ csharpforprogrammers2/. We assume in our Chapter 1 “test-drive” instructions that you extract these examples to the C:\ folder on your computer. This will create an examples folder that contains subfolders for each chapter (e.g., ch01, ch02, etc.). As you read this book, if you have questions, send an e-mail to [email protected]; we will respond promptly. For updates on this book and the status of C# software, and for the latest news on all Deitel publications and services, please visit www.deitel.com regularly and be sure to sign up for the free Deitel® Buzz Online e-mail newsletter at www.deitel.com/newsletter/subscribe.html.
Downloading Microsoft Visual C# 2005 Express Edition Software Microsoft makes available a free version of its C# development tool called the Visual C# 2005 Express Edition. You may use it to compile the example programs in the book. You can download the Visual C# 2005 Express Edition and the Visual Web Developer 2005 Express Edition at: lab.msdn.microsoft.com/express/
Microsoft provides a dedicated forum for help using the Express Editions: forums.microsoft.com/msdn/ShowForum.aspx?ForumID=24
We provide updates on the status of this software at www.deitel.com and in our free e-mail newsletter www.deitel.com/newsletter/subscribe.html.
Features in C# for Programmers, 2/e This new edition contains many new and enhanced features.
Updated for Visual Studio 2005, C# 2.0 and .NET 2.0 We updated the entire text to reflect Microsoft’s latest release of Visual C# 2005. New items include: • Screenshots updated to the Visual Studio 2005 IDE. • Property accessors with different access modifiers. • Viewing exception data with the Exception Assistant (a new feature of the Visual Studio 2005 Debugger). • Using drag-and-drop techniques to create data-bound windows forms in ADO.NET 2.0.
Features in C# for Programmers, 2/e
xxv
•
Using the Data Sources window to create application-wide data connections.
•
Using a BindingSource to simplify the process of binding controls to an underlying data source.
•
Using a BindingNavigator to enable simple navigation, insertion, deletion and editing of database data on a Windows Form.
•
Using the Master Page Designer to create a common look and feel for ASP.NET Web pages.
•
Using Visual Studio 2005 smart tag menus to perform many of the most common programming tasks when new controls are dragged onto a Windows Form or ASP.NET Web page.
•
Using Visual Web Developer’s built-in Web server to test ASP.NET 2.0 applications and Web services.
•
Using an XmlDataSource to bind XML data sources to a control.
•
Using a SqlDataSource to bind a SQL Server database to a control or set of controls.
•
Using an ObjectDataSource to bind a control to an object that serves as a data source.
•
Using the ASP.NET 2.0 login and create new user controls to personalize access to Web applications.
•
Using generics and generic collections to create general models of methods and classes that can be declared once, but used with many types of data. Using generic collections from the Systems.Collections.Generic namespace.
•
New Interior Design Working with the creative services team at Prentice Hall, we redesigned the interior styles for our Deitel Developer Series books. In response to reader requests, we now place the key terms and the index’s page reference for each defining occurrence in bold italic text for easier reference. We emphasize on-screen components in the bold Helvetica font (e.g., the File menu) and emphasize C# program text in the Lucida font (for example, int x = 5). Syntax Shading We syntax shade all the C# code, similar to the way most C# integrated-development environments and code editors syntax color code. This greatly improves code readability— an especially important goal, given that this book contains 16,800+ lines of code. Our syntax-shading conventions are as follows: comments appear in italic keywords appear in bold, italic errors and JSP scriptlet delimiters appear in bold, black constants and literal values appear in bold, gray all other code appears in plain, black
Code Highlighting Extensive code highlighting makes it easy for readers to spot each program’s featured code segments—we place light gray rectangles around the key code.
xxvi
Preface
Early Classes and Objects Approach We still introduce the basic object-technology concepts and terminology in Chapter 1. In the previous edition, we developed custom classes in Chapter 9, but in this edition, we start doing that in our new Chapter 4. Chapters 5–8 have been carefully rewritten from an “early classes and objects approach.” Carefully Tuned Treatment of Object-Oriented Programming in Chapters 9–11 We performed a high-precision upgrade of C# for Programmers, 2/e. This edition is clearer and more accessible—especially if you are new to object-oriented programming (OOP). We completely rewrote the OOP chapters, integrating an employee payroll class hierarchy case study and motivating interfaces with an accounts payable hierarchy. Case Studies We include many case studies, some spanning multiple sections and chapters: • The GradeBook class in Chapters 4, 5, 6 and 8. • The optional, OOD/UML ATM system in the Software Engineering sections of Chapters 1, 3–9 and 11. • The Time class in several sections of Chapter 9. • The Employee payroll application in Chapters 10 and 11. • The GuestBook ASP.NET application in Chapter 21. • The secure book database ASP.NET application in Chapter 21. • The airline reservation Web service in Chapter 22. Integrated GradeBook Case Study To reinforce our early classes presentation, we present an integrated case study using classes and objects in Chapters 4–6 and 8. We incrementally build a GradeBook class that represents an instructor’s grade book and performs various calculations based on a set of student grades—finding the average, finding the maximum and minimum, and printing a bar chart. Our goal is to familiarize you with the important concepts of objects and classes through a real-world example of a substantial class. We develop this class from the ground up, constructing methods from control statements and carefully developed algorithms, and adding instance variables and arrays as needed to enhance the functionality of the class. The Unified Modeling Language (UML)—Using the UML 2.0 to Develop an Object-Oriented Design of an ATM The Unified Modeling Language™ (UML™) has become the preferred graphical modeling language for designing object-oriented systems. All the UML diagrams in the book comply with the UML 2.0 specification. We use UML class diagrams to visually represent classes and their inheritance relationships, and we use UML activity diagrams to demonstrate the flow of control in each of C#’s several control statements. This Second Edition includes a new, optional (but highly recommended) case study on object-oriented design using the UML. The case study was reviewed by a distinguished team of OOD/UML academic and industry professionals, including leaders in the field from Rational (the creators of the UML and now a division of IBM) and the Object Management Group (responsible for maintaining and evolving the UML). In the case study,
Features in C# for Programmers, 2/e
xxvii
we design and fully implement the software for a simple automatic teller machine (ATM). The Software Engineering Case Study sections at the ends of Chapters 1, 3–9 and 11 present a carefully paced introduction to object-oriented design using the UML. We introduce a concise, simplified subset of the UML 2.0, then guide the reader through a first design experience intended for the novice object-oriented designer/programmer. The case study is not an exercise; rather, it is an end-to-end learning experience that concludes with a detailed walkthrough of the complete C# code. The Software Engineering Case Study sections help readers develop an object-oriented design to complement the object-oriented programming concepts they begin learning in Chapter 1 and implementing in Chapter 4. In the first of these sections at the end of Chapter 1, we introduce basic OOD concepts and terminology. In the optional Software Engineering Case Study sections at the ends of Chapters 3–6, we consider more substantial issues, as we undertake a challenging problem with the techniques of OOD. We analyze a typical requirements document that specifies a system to be built, determine the classes needed to implement that system, determine the attributes the classes need to have, determine the behaviors the classes need to exhibit and specify how the classes must interact with one another to meet the system requirements. In Appendix J, we include a complete C# implementation of the object-oriented system that we design in the earlier chapters. We employ a carefully developed, incremental object-oriented design process to produce a UML model for our ATM system. From this design, we produce a substantial working C# implementation using key programming notions, including classes, objects, encapsulation, visibility, composition, inheritance and polymorphism.
Web Forms, Web Controls and ASP.NET 2.0 The .NET platform enables developers to create robust, scalable Web-based applications. Microsoft’s .NET server-side technology, Active Server Pages (ASP) .NET, allows programmers to build Web documents that respond to client requests. To enable interactive Web pages, server-side programs process information users input into HTML forms. ASP .NET provides enhanced visual programming capabilities, similar to those used in building Windows forms for desktop programs. Programmers can create Web pages visually, by dragging and dropping Web controls onto Web forms. Chapter 21, ASP.NET, Web Forms and Web Controls, introduces these powerful technologies. Web Services and ASP.NET 2.0 Microsoft’s .NET strategy embraces the Internet and Web as integral to software development and deployment. Web services technology enables information sharing, e-commerce and other interactions using standard Internet protocols and technologies, such as Hypertext Transfer Protocol (HTTP), Extensible Markup Language (XML) and Simple Object Access Protocol (SOAP). Web services enable programmers to package application functionality in a manner that turns the Web into a library of reusable software components. In Chapter 22, we present a Web service that allows users to manipulate huge integers— integers too large to be represented with C#’s built-in data types. In this example, a user enters two huge integers and presses buttons to invoke Web services that add, subtract and compare the two integers. We also present a Blackjack Web service and a database-driven airline reservation system.
xxviii
Preface
Object-Oriented Programming Object-oriented programming is the most widely employed technique for developing robust, reusable software. This text offers a rich treatment of C#’s object-oriented programming features. Chapter 4, introduces how to create classes and objects. These concepts are extended in Chapter 9. Chapter 10 discusses how to create powerful new classes quickly by using inheritance to “absorb” the capabilities of existing classes. Chapter 11 familiarizes the reader with the crucial concepts of polymorphism, abstract classes, concrete classes and interfaces, which facilitate powerful manipulations among objects belonging to an inheritance hierarchy. XML Use of the Extensible Markup Language (XML) is exploding in the software-development industry and in the e-business community, and is pervasive throughout the .NET platform. Because XML is a platform-independent technology for describing data and for creating markup languages, XML’s data portability integrates well with C#-based portable applications and services. Chapter 19 introduces XML, XML markup and the technologies, such as DTDs and Schema, which are used to validate XML documents’ contents. We also explain how to manipulate XML documents programmatically using the Document Object Model (DOM™) and how to transform XML documents into other types of documents via Extensible Stylesheet Language Transformation (XSLT) technology. ADO.NET 2.0 Databases store vast amounts of information that individuals and organizations must access to conduct business. As an evolution of Microsoft’s ActiveX Data Objects (ADO) technology, ADO.NET represents a new approach for building applications that interact with databases. ADO.NET uses XML and an enhanced object model to provide developers with the tools they need to access and manipulate databases for large-scale, extensible, mission-critical multi-tier applications. Chapter 20 introduces the capabilities of ADO.NET and the Structured Query Language (SQL) to manipulate databases. Visual Studio 2005 Debugger In Appendix C we explain how to use key debugger features, such as setting “breakpoints’ and “watches,” stepping into and out of methods, and examining the method call stack.
Teaching Approach C# for Programmers, 2/e contains a rich collection of examples that have been tested on Windows 2000 and Windows XP. The book concentrates on the principles of good software engineering and stresses program clarity. We avoid arcane terminology and syntax specifications in favor of teaching by example. We are educators who teach leading-edge topics in industry classrooms worldwide. Dr. Harvey M. Deitel has 20 years of college teaching experience and 15 years of industry teaching experience. Paul Deitel has 12 years of industry teaching experience and is an experienced corporate trainer, having taught courses at all levels to government, industry, military and academic clients of Deitel & Associates.
Teaching Approach
xxix
Learning C# via the Live-Code Approach C# for Programmers 2/e, is loaded with live-code examples—each new concept is presented in the context of a complete working C# application that is immediately followed by one or more sample executions showing the program’s inputs and outputs. This style exemplifies the way we teach and write about programming. We call this method of teaching and writing the live-code approach. We use programming languages to teach programming languages. World Wide Web Access All of the source-code examples for C# for Programmers, 2/e, (and for our other publications) are available on the Internet as downloads from the following Web sites: www.deitel.com/books/csharpforprogrammers2 www.phptr.com/title/0131345915
Registration is quick and easy, and the downloads are free. Download all the examples, then run each program as you read the corresponding text discussions. Making changes to the examples and immediately seeing the effects of those changes is a great way to enhance your C# learning experience.
Objectives Each chapter begins with a statement of objectives. Quotations The learning objectives are followed by quotations. Some are humorous, philosophical or offer interesting insights. Outline The chapter outline helps you approach the material in a top-down fashion, so you can anticipate what is to come, and set a comfortable and effective learning pace. 16,875 Lines of Code in 213 Example Programs (with Program Outputs) Our live-code programs range in size from just a few lines of code to substantial examples containing hundreds of lines of code (e.g., our ATM system implementation contains 655 lines of code). Each program is followed by a window containing the outputs produced when the program is run, so you can confirm that the programs run as expected. Our programs demonstrate the diverse features of C#. The code is syntax shaded, with C# keywords, comments and other program text emphasized with variations of bold, italic and gray text. This facilitates reading the code, especially when you’re reading the larger programs. 700 Illustrations/Figures An abundance of charts, tables, line drawings, programs and program outputs is included. We model the flow of control in control statements with UML activity diagrams. UML class diagrams model the fields, constructors and methods of classes. We use additional types of UML diagrams throughout our optional OOD/UML ATM case study. 316 Programming Tips We include programming tips to emphasize important aspects of program development. We highlight these tips in the form of Good Programming Practices, Common Programming
xxx
Preface
Errors, Error-Prevention Tips, Look-and-Feel Observations, Performance Tips, Portability Tips and Software Engineering Observations. These tips and practices represent the best we have gleaned from a combined six decades of programming and teaching experience. This approach is like the highlighting of axioms, theorems and corollaries in mathematics books; it provides a basis on which to build good software.
Good Programming Practice Good Programming Practices call attention to techniques that will help developers produce programs that are clearer, more understandable and more maintainable. 3.0
Common Programming Error Developers learning a language tend to make certain kinds of errors frequently. Pointing out these Common Programming Errors reduces the likelihood that readers will make the same mistakes. 3.0
Error-Prevention Tip When we first designed this tip type, we thought the tips would contain suggestions strictly for exposing bugs and removing them from programs. In fact, many of the tips describe aspects of C# that prevent bugs from getting into programs in the first place, thus simplifying the testing and debugging processes. 3.0
Look-and-Feel Observation We provide Look-and-Feel Observations to highlight graphical-user-interface conventions. These observations help you design attractive, user-friendly graphical user interfaces that conform to industry norms. 3.0
Performance Tip Developers like to “turbo charge” their programs. We include Performance Tips that highlight opportunities for improving program performance—making programs run faster or minimizing the amount of memory that they occupy. 3.0
Portability Tip We include Portability Tips to help you write portable code and to explain how C# achieves its high degree of portability. 3.0
Software Engineering Observation The object-oriented programming paradigm necessitates a complete rethinking of the way we build software systems. C# is an effective language for achieving good software engineering. The Software Engineering Observations highlight architectural and design issues that affect the construction of software systems, especially large-scale systems. 3.0
Wrap-Up Section Each chapter ends with a brief “wrap-up” section that recaps the chapter content and transitions to the next chapter. Approximately 5500 Index Entries We have included an extensive index which is especially useful to developers who use the book as a reference.
A Tour of the Optional Case Study on Object-Oriented Design with the UML
xxxi
“Double Indexing” of C# Live-Code Examples C# for Programmers, 2/e has 213 live-code examples, which we have double indexed. For every source-code program in the book, we indexed the figure caption both alphabetically and as a subindex item under “Examples.” This makes it easier to find examples using particular features.
A Tour of the Optional Case Study on Object-Oriented Design with the UML In this section we tour the book’s optional case study on object-oriented design with the UML. This tour previews the contents of the nine Software Engineering Case Study sections (in Chapters 1, 3–9 and 11). After completing this case study, you will be thoroughly familiar with an object-oriented design and implementation for a significant C# application. The design presented in the ATM case study was developed at Deitel & Associates, Inc. and scrutinized by industry professionals. Our primary goal throughout the design process was to create a simple design that would be clear to OOD and UML novices, while still demonstrating key OOD concepts and the related UML modeling techniques. Section 1.9—(Only Required Section of the Case Study) Software Engineering Case Study: Introduction to Object Technology and the UML—introduces the objectoriented design case study with the UML. The section presents the basic concepts and terminology of object technology, including classes, objects, encapsulation, inheritance and polymorphism. We discuss the history of the UML. This is the only required section of the case study. Section 3.10—(Optional) Software Engineering Case Study: Examining the ATM Requirements Document—discusses a requirements document that specifies the requirements for a system that we will design and implement—the software for a simple automated teller machine (ATM). We investigate the structure and behavior of object-oriented systems in general. We discuss how the UML will facilitate the design process in subsequent Software Engineering Case Study sections by providing several additional types of diagrams to model our system. We include a list of URLs and book references on objectoriented design with the UML. We discuss the interaction between the ATM system specified by the requirements document and its user. Specifically, we investigate the scenarios that may occur between the user and the system itself—these are called use cases. We model these interactions, using UML use case diagrams. Section 4.11—(Optional) Software Engineering Case Study: Identifying the Classes in the ATM Requirements Documents—begins to design the ATM system. We identify its classes by extracting the nouns and noun phrases from the requirements document. We arrange these classes into a UML class diagram that describes the class structure of our simulation. The class diagram also describes relationships, known as associations, among classes. Section 5.12—(Optional) Software Engineering Case Study: Identifying Class Attributes in the ATM System—focuses on the attributes of the classes discussed in Section 3.10. A class contains both attributes (data) and operations (behaviors). As we see in later sections, changes in an object’s attributes often affect the object’s behavior. To determine the attributes for the classes in our case study, we extract the adjectives describing the nouns and noun phrases (which defined our classes) from the requirements document, then place the attributes in the class diagram we created in Section 3.10.
xxxii
Preface
Section 6.9—(Optional) Software Engineering Case Study: Identifying Objects’ States and Activities in the ATM System—discusses how an object, at any given time, occupies a specific condition called a state. A state transition occurs when that object receives a message to change state. The UML provides the state machine diagram, which identifies the set of possible states that an object may occupy and models that object’s state transitions. An object also has an activity—the work it performs in its lifetime. The UML provides the activity diagram—a flowchart that models an object’s activity. In this section, we use both types of diagrams to begin modeling specific behavioral aspects of our ATM system, such as how the ATM carries out a withdrawal transaction and how the ATM responds when the user is authenticated. Section 7.15—(Optional) Software Engineering Case Study: Identifying Class Operations in the ATM System—identifies the operations, or services, of our classes. We extract from the requirements document the verbs and verb phrases that specify the operations for each class. We then modify the class diagram of Section 3.10 to include each operation with its associated class. At this point in the case study, we will have gathered all information possible from the requirements document. However, as future chapters introduce such topics as inheritance, we will modify our classes and diagrams. Section 8.14—(Optional) Software Engineering Case Study: Collaboration Among Objects in the ATM System—provides a “rough sketch” of the model for our ATM system. In this section, we see how it works. We investigate the behavior of the simulation by discussing collaborations—messages that objects send to each other to communicate. The class operations that we discovered in Section 6.9 turn out to be the collaborations among the objects in our system. We determine the collaborations, then collect them into a communication diagram—the UML diagram for modeling collaborations. This diagram reveals which objects collaborate and when. We present a communication diagram of the collaborations among objects to perform an ATM balance inquiry. We then present the UML sequence diagram for modeling interactions in a system. This diagram emphasizes the chronological ordering of messages. A sequence diagram models how objects in the system interact to carry out withdrawal and deposit transactions. Section 9.17—(Optional) Software Engineering Case Study: Starting to Program the Classes of the ATM System—takes a break from designing the behavior of our system. We begin the implementation process to emphasize the material discussed in Chapter 8. Using the UML class diagram of Section 3.10 and the attributes and operations discussed in Section 4.11 and Section 6.9, we show how to implement a class in C# from a design. We do not implement all classes—because we have not completed the design process. Working from our UML diagrams, we create code for the Withdrawal class. Section 11.9—(Optional) Software Engineering Case Study: Incorporating Inheritance and Polymorphism into the ATM System—continues our discussion of objectoriented programming. We consider inheritance—classes sharing common characteristics may inherit attributes and operations from a “base” class. In this section, we investigate how our ATM system can benefit from using inheritance. We document our discoveries in a class diagram that models inheritance relationships—the UML refers to these relationships as generalizations. We modify the class diagram of Section 3.10 by using inheritance to group classes with similar characteristics. This section concludes the design of the model portion of our simulation. We implement this model as C# code in Appendix J.
Deitel® Buzz Online Free E-mail Newsletter
xxxiii
Appendix J—ATM Case Study Code—The majority of the case study involved designing the model (i.e., the data and logic) of the ATM system. In this appendix, we implement that model in C#. Using all the UML diagrams we created, we present the C# classes necessary to implement the model. We apply the concepts of object-oriented design with the UML and object-oriented programming in C# that you learned in the chapters. By the end of this appendix, you will have completed the design and implementation of a real-world system, and should now feel confident tackling larger systems, such as those that professional software engineers build. Appendix K—UML 2: Additional Diagrams Types—Overviews the UML 2 diagram types not found in the OOD/UML Case Study.
DEITEL® Buzz Online Free E-mail Newsletter Our free e-mail newsletter, the Deitel® Buzz Online, includes commentary on industry trends and developments, links to free articles and resources from our published books and upcoming publications, product-release schedules, errata, challenges, anecdotes, information on our corporate instructor-led training courses and more. It’s also a good way for you to keep up-to-date about issues related to C# for Programmers, 2/e. To subscribe, visit www.deitel.com/newsletter/subscribe.html
Acknowledgments It is a great pleasure to acknowledge the efforts of many people whose names may not appear on the cover, but whose hard work, cooperation, friendship and understanding were crucial to the production of the book. Many people at Deitel & Associates, Inc. devoted long hours to this project. •
Andrew B. Goldberg is a Computer Science graduate of Amherst College. Andrew’s contributions to C# for Programmers, 2/e included updating Chapters 19– 22. He co-designed and co-authored the new, optional OOD/UML ATM case study. He also co-authored Appendix K.
•
Su Zhang holds B.Sc. and a M.Sc. degrees in Computer Science from McGill University. Su contributed to Chapters 25 and 26 as well as Appendix J.
•
Cheryl Yaeger graduated from Boston University with a bachelor’s degree in Computer Science. Cheryl helped update Chapters 3–14 of this publication.
•
Barbara Deitel, Chief Financial Officer at Deitel & Associates, Inc. applied copyedits to the book.
•
Abbey Deitel, President of Deitel & Associates, Inc., and an Industrial Management graduate of Carnegie Mellon University co-authored Chapter 1.
•
Christi Kelsey, a graduate of Purdue University with a degree in business and a minor in information systems, co-authored Chapter 2, the Preface and Appendix C. She edited the Index and paged the entire manuscript. She also worked closely with the production team at Prentice Hall coordinating virtually every aspect of the production of the book.
xxxiv
Preface
We would also like to thank three participants of our Honors Internship and Co-op programs who contributed to this publication—Nick Santos, a Computer Science major at Dartmouth College; Jeffrey Peng, a Computer Science student at Cornell University and William Chen, a Computer Science student at Cornell University. We are fortunate to have worked on this project with the talented and dedicated team of publishing professionals at Pearson Education/PTG. We especially appreciate the extraordinary efforts of Mark Taub, Publishing Partner of Prentice Hall/PTR, and Marcia Horton, Editorial Director of Prentice Hall’s Engineering and Computer Science Division. Noreen Regina and Jennifer Cappello did an extraordinary job recruiting the review teams for the book and managing the review process. Sandra Schroeder did a wonderful job completely redesigning the book’s cover. Vince O’Brien, Bob Engelhardt, Donna Crilly and Marta Samsel did a marvelous job managing the production of the book. We’d like to give special thanks to Dan Fernandez, C# Product Manager, and Janie Schwark, Senior Business Manager, Division of Developer Marketing, both of Microsoft for their special effort in working with us on this project. And thanks to the many other members of the Microsoft team who took time to answer our questions throughout this process: Anders Hejlsburg, Technical Fellow (C#) Brad Abrams, Lead Program Manager (.NET Framework) Jim Miller, Software Architect (.NET Framework) Joe Duffy, Program Manager (.NET Framework) Joe Stegman, Lead Program Manager (Windows Forms) Kit George, Program Manager (.NET Framework) Luca Bolognese, Lead Program Manager (C#) Luke Hoban, Program Manager (C#) Mads Torgersen, Program Manager (C#) Peter Hallam, Software Design Engineer (C#) Scott Nonnenberg, Program Manager (C#) Shamez Rajan, Program Manager (Visual Basic) We wish to acknowledge the efforts of our reviewers. Adhering to a tight time schedule, they scrutinized the text and the programs, providing countless suggestions for improving the accuracy and completeness of the presentation. Microsoft Reviewers George Bullock, Program Manager at Microsoft, Microsoft.com Community Team Dharmesh Chauhan, Microsoft Shon Katzenberger, Microsoft Matteo Taveggia, Microsoft Matt Tavis, Microsoft Industry Reviewers Alex Bondarev, Investor’s Bank and Trust Peter Bromberg, Senior Architect Merrill Lynch and C# MVP Vijay Cinnakonda, TrueCommerce, Inc. Jay Cook, Alcon Laboratories
About the Authors
xxxv
Jeff Cowan, Magenic, Inc. Ken Cox, Independent Consultant, Writer and Developer and ASP.NET MVP Stochio Goutsev, Independent Consultant, writer and developer and C# MVP James Huddleston, Independent Consultant Rex Jaeschke, Independent Consultant Saurabh Nandu, AksTech Solutions Pvt. Ltd. Simon North, Quintiq BV Mike O’Brien, State of California Employment Development Department José Antonio González Seco, Andalucia’s Parliamient Devan Shepard, Xmalpha Technologies Pavel Tsekov, Caesar BSC John Varghese, UBS Stacey Yasenka, Software Developer at Hyland Software and C# MVP Academic Reviewers Rekha Bhowmik, California Lutheran University Ayad Boudiab, Georgia Perimiter College Harlan Brewer, University of Cincinnati Sam Gill, San Francisco State University Gavin Osborne, Saskatchewan Institute of Applied Science and Technology Catherine Wyman, DeVry-Phoenix Well, there you have it! C# is a powerful programming language that will help you write programs quickly and effectively. C# scales nicely into the realm of enterprise-systems development to help organizations build their business-critical and mission-critical information systems. As you read the book, we would sincerely appreciate your comments, criticisms, corrections and suggestions for improvement. Please address all correspondence to: [email protected]
We will respond promptly, and we will post corrections and clarifications on our Web site: www.deitel.com
We hope you enjoy reading C# for Programmers, Second Edition as much as we enjoyed writing it! Dr. Harvey M. Deitel Paul J. Deitel
About the Authors Dr. Harvey M. Deitel, Chairman and Chief Strategy Officer of Deitel & Associates, Inc., has 44 years experience in the computer field, including extensive industry and academic experience. Dr. Deitel earned B.S. and M.S. degrees from the Massachusetts Institute of Technology and a Ph.D. from Boston University. He worked on the pioneering virtual-memory operating-systems projects at IBM and MIT that developed techniques now widely implemented in systems such as UNIX, Linux and Windows XP. He has 20 years of college teaching experience, including earning tenure and serving as the Chairman of the Computer
xxxvi
Preface
Science Department at Boston College before founding Deitel & Associates, Inc., with his son, Paul J. Deitel. He and Paul are the co-authors of several dozen books and multimedia packages and they are writing many more. With translations published in Japanese, German, Russian, Spanish, Traditional Chinese, Simplified Chinese, Korean, French, Polish, Italian, Portuguese, Greek, Urdu and Turkish, the Deitels’ texts have earned international recognition. Dr. Deitel has delivered hundreds of professional seminars to major corporations, academic institutions, government organizations and the military. Paul J. Deitel, CEO and Chief Technical Officer of Deitel & Associates, Inc., is a graduate of the MIT’s Sloan School of Management, where he studied Information Technology. Through Deitel & Associates, Inc., he has delivered Java, C, C++, Internet and World Wide Web courses to industry clients, including IBM, Sun Microsystems, Dell, Lucent Technologies, Fidelity, NASA at the Kennedy Space Center, the National Severe Storm Laboratory, White Sands Missile Range, Rogue Wave Software, Boeing, Stratus, Cambridge Technology Partners, Open Environment Corporation, One Wave, Hyperion Software, Adra Systems, Entergy, CableData Systems and many more. Paul is one of the most experienced Java trainers, having taught about 100 professional Java courses. He has also lectured on C++ and Java for the Boston Chapter of the Association for Computing Machinery. He and his father, Dr. Harvey M. Deitel, are the world’s best-selling programming language textbook authors.
About Deitel & Associates, Inc. Deitel & Associates, Inc., is an internationally recognized corporate training and contentcreation organization specializing in computer programming languages, Internet/World Wide Web software technology, object technology education and Internet business development. The company provides instructor-led courses on major programming languages and platforms, such as Java, Advanced Java, C, C++, .NET programming languages, XML, Perl, Python; object technology; and Internet and World Wide Web programming. The founders of Deitel & Associates, Inc., are Dr. Harvey M. Deitel and Paul J. Deitel. The company’s clients include many of the world’s largest computer companies, government agencies, branches of the military and business organizations. Through its 29-year publishing partnership with Prentice Hall, Deitel & Associates, Inc. publishes leadingedge programming textbooks, professional books, interactive multimedia Cyber Classrooms, Complete Training Courses, Web-based training courses and e-content for popular course management systems such as WebCT, Blackboard and Pearson’s CourseCompass. Deitel & Associates, Inc., and the authors can be reached via e-mail at: [email protected]
To learn more about Deitel & Associates, Inc., its publications and its worldwide DIVE INTO™ Series Corporate Training curriculum, see the last few pages of this book or visit: www.deitel.com
and subscribe to the free Deitel® Buzz Online e-mail newsletter at: www.deitel.com/newsletter/subscribe.html
About Deitel & Associates, Inc.
xxxvii
Individuals wishing to purchase Deitel books, Cyber Classrooms, Complete Training Courses and Web-based training courses can do so through: www.deitel.com/books/index.html
Bulk orders by corporations and academic institutions should be placed directly with Prentice Hall.
This page intentionally left blank
1 Introduction to Computers, the Internet and Visual C# The chief merit of language is clearness. ——Galen
High thoughts must have high language.
OBJECTIVES
—Aristophanes
In this chapter you will learn:
Our life is frittered away with detail. . . . Simplify, simplify.
I
The history of the Visual C# programming language.
I
Some basics of object technology.
—Henry David Thoreau
I
The history of the UML—the industry-standard objectoriented system modeling language.
My object all sublime I shall achieve in time.
I
The history of the Internet and the World Wide Web.
—W. S. Gilbert
I
Man is still the most extraordinary computer of all.
The motivation behind and an overview of the Microsoft’s .NET initiative, which involves the Internet in developing and using software systems.
I
To test-drive a Visual C# 2005 application that enables you to draw on the screen.
——John F. Kennedy
Outline
2
Chapter 1
Introduction to Computers, the Internet and Visual C#
1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.8 1.9
Introduction Microsoft’s Windows® Operating System C# The Internet and the World Wide Web Extensible Markup Language (XML) Microsoft’s .NET The .NET Framework and the Common Language Runtime Test-Driving a C# Application (Only Required Section of the Case Study) Software Engineering Case Study: Introduction to Object Technology and the UML 1.10 Wrap-Up 1.11 Web Resources
1.1 Introduction Welcome to Visual C# (pronounced “C-Sharp”) 2005! We have worked hard to provide you with accurate and complete information regarding this powerful computer programming language, which from this point forward, we shall generally refer to simply as C#. C# is appropriate for building substantial information systems. We hope that working with this book will be an informative, challenging and entertaining learning experience for you. The core of this book emphasizes achieving program clarity through the proven techniques of object-oriented programming (OOP) and event-driven programming. Perhaps most important, the book presents hundreds of complete, working C# programs and depicts their inputs and outputs. We call this the live-code approach. All of the book’s examples may be downloaded from www.deitel.com/books/csharpforprogrammers2/ index.html and www.prenhall.com/deitel. We hope that you will enjoy learning with C# For Programmers, Second Edition. You are embarking on a challenging and rewarding path. If you have any questions as you proceed, please send e-mail to [email protected]
To keep current with C# developments at Deitel & Associates and to receive updates to this book, please register for our free e-mail newsletter, the Deitel® Buzz Online, at www.deitel.com/newsletter/subscribe.html
1.2 Microsoft’s Windows® Operating System Microsoft Corporation became the dominant software company in the 1980s and 1990s. In 1981, Microsoft released the first version of its DOS operating system for the IBM personal computer. In the mid-1980s, Microsoft developed the Windows operating system, a graphical user interface built on top of DOS. Microsoft released Windows 3.0 in 1990; this new version featured a user-friendly interface and rich functionality. The Windows operating system became incredibly popular after the 1992 release of Windows 3.1, whose
1.3 C#
3
successors, Windows 95 and Windows 98, virtually cornered the desktop operating systems market by the late 1990s. These operating systems, which borrowed many concepts (such as icons, menus and windows) popularized by early Apple Macintosh operating systems, enabled users to navigate multiple applications simultaneously. Microsoft entered the corporate operating systems market with the 1993 release of Windows NT®. Windows XP, which is based on the Windows NT operating system, was released in 2001 and combines Microsoft’s corporate and consumer operating system lines. Windows is by far the world’s most widely used operating system. The biggest competitor to the Windows operating system is Linux. The name Linux derives from Linus (after Linus Torvalds, who developed Linux) and UNIX—the operating system upon which Linux is based; UNIX was developed at Bell Laboratories and was written in the C programming language. Linux is a free, open source operating system, unlike Windows, which is proprietary (owned and controlled by Microsoft)—the source code for Linux is freely available to users, and they can modify it to fit their needs.
1.3 C# The advancement of programming tools and consumer-electronic devices (e.g., cell phones and PDAs) created problems and new requirements. The integration of software components from various languages proved difficult, and installation problems were common because new versions of shared components were incompatible with old software. Developers also discovered they needed Web-based applications that could be accessed and used via the Internet. As a result of the popularity of mobile electronic devices, software developers realized that their clients were no longer restricted to desktop computers. Developers recognized the need for software that was accessible to anyone and available via almost any type of device. To address these needs, in 2000, Microsoft announced the C# programming language. C#, developed at Microsoft by a team led by Anders Hejlsberg and Scott Wiltamuth, was designed specifically for the .NET platform (which is discussed in Section 1.6) as a language that would enable programmers to migrate easily to .NET. It has roots in C, C++ and Java, adapting the best features of each and adding new features of its own. C# is object oriented and contains a powerful class library of prebuilt components, enabling programmers to develop applications quickly—C# and Visual Basic share the Framework Class Library (FCL), which is discussed in Section 1.6. C# is appropriate for demanding application development tasks, especially for building today’s popular Web-based applications. The .NET platform is one over which Web-based applications can be distributed to a great variety of devices (even cell phones) and to desktop computers. The platform offers a new software-development model that allows applications created in disparate programming languages to communicate with each other. C# is an event-driven, visual programming language in which programs are created using an Integrated Development Environment (IDE). With the IDE, a programmer can create, run, test and debug C# programs conveniently, thereby reducing the time it takes to produce a working program to a fraction of the time it would have taken without using the IDE. The .NET platform enables language interoperability: Software components from different languages can interact as never before. Developers can package even old software to work with new C# programs. Also, C# applications can interact via the Internet, using
4
Chapter 1
Introduction to Computers, the Internet and Visual C#
industry standards such as XML, which we discuss in Chapter 19, and the XML-based Simple Object Access Protocol (SOAP), which we discuss in Chapter 22, Web Services. The original C# programming language was standardized by Ecma International (www.ecma-international.org) in December, 2002 as Standard ECMA-334: C# Language Specification (located at www.ecma-international.org/publications/standards/ Ecma-334.htm). Since that time, Microsoft proposed several language extensions that have been adopted as part of the revised Ecma C# standard. Microsoft refers to the complete C# language (including the adopted extensions) as C# 2.0. [Note: Throughout this book, we provide references to specific sections of the C# Language Specification. We use the section numbers specified in Microsoft’s version of the specification, which is composed of two documents—the C# Language Specification 1.2 and the C# Language Specification 2.0 (an extension of the 1.2 document that contains the C# 2.0 language enhancements). Both documents are located at: msdn.microsoft.com/ vcsharp/programming/language/.]
1.4 The Internet and the World Wide Web The Internet—a global network of computers—was initiated almost four decades ago with funding supplied by the U.S. Department of Defense. Originally designed to connect the main computer systems of about a dozen universities and research organizations, its chief benefit proved early on to be the capability for quick and easy communication via what came to be known as electronic mail (e-mail). This is true even on today’s Internet, with e-mail, instant messaging and file transfer facilitating communications among hundreds of millions of people worldwide. The Internet has exploded into one of the world’s premier communication mechanisms and continues to grow rapidly. The World Wide Web allows computer users to locate and view multimedia-based documents on almost any subject over the Internet. Even though the Internet was developed decades ago, the introduction of the Web was a relatively recent event. In 1989, Tim Berners-Lee of CERN (the European Organization for Nuclear Research) began to develop a technology for sharing information via hyperlinked text documents. Berners-Lee called his invention the HyperText Markup Language (HTML). He also wrote communication protocols to form the backbone of his new information system, which he referred to as the World Wide Web. In the past, most computer applications ran on computers that were not connected to one another. Today’s applications can be written to communicate among the world’s computers. The Internet mixes computing and communications technologies, making our work easier. It makes information instantly and conveniently accessible worldwide, and enables individuals and small businesses to get worldwide exposure. It is changing the way business is done. People can search for the best prices on virtually any product or service, while special-interest communities can stay in touch with one another, and researchers can be made instantly aware of the latest breakthroughs. The Internet and the World Wide Web are surely among humankind’s most profound creations. In Chapters 19–22, you will learn how to build Internet- and Web-based applications. In 1994, Tim Berners-Lee founded an organization, called the World Wide Web Consortium (W3C), that is devoted to developing nonproprietary, interoperable technologies for the World Wide Web. One of the W3C’s primary goals is to make the Web universally accessible—regardless of disabilities, language or culture.
1.5 Extensible Markup Language (XML)
5
The W3C (www.w3.org) is also a standardization organization. Web technologies standardized by the W3C are called Recommendations. Current W3C Recommendations include the Extensible Markup Language (XML). We introduce XML in Section 1.5 and present it in detail in Chapter 19, Extensible Markup Language (XML). It is the key technology underlying the next version of the Word Wide Web, sometimes called the “semantic Web.” It is also one of the key technologies that underlies Web services, which we discuss in Chapter 22.
1.5 Extensible Markup Language (XML) As the popularity of the Web exploded, HTML’s limitations became apparent. HTML’s lack of extensibility (the ability to change or add features) frustrated developers, and its ambiguous definition allowed erroneous HTML to proliferate. The need for a standardized, fully extensible and structurally strict language was apparent. As a result, XML was developed by the W3C. Data independence, the separation of content from its presentation, is the essential characteristic of XML. Because XML documents describe data in a machine independent manner, any application conceivably can process them. Software developers are integrating XML into their applications to improve Web functionality and interoperability. XML is not limited to Web applications. For example, it is increasingly being employed in databases—the structure of an XML document enables it to be integrated easily with database applications. As applications become more Web enabled, it is likely that XML will become the universal technology for data representation. All applications employing XML would be able to communicate with one another, provided they can understand their respective XML markup schemes, called vocabularies. The Simple Object Access Protocol (SOAP) is a technology for the transmission of objects (marked up as XML) over the Internet. Microsoft’s .NET technologies (discussed in the next two sections) use XML and SOAP to mark up and transfer data over the Internet. XML and SOAP are at the core of .NET—they allow software components to interoperate (i.e., communicate easily with one another). Since SOAP’s foundations are in XML and HTTP (Hypertext Transfer Protocol—the key communication protocol of the Web), it is supported on most types of computer systems. We discuss XML in Chapter 19, Extensible Markup Language (XML), and SOAP in Chapter 22, Web Services.
1.6 Microsoft’s .NET In 2000, Microsoft announced its .NET initiative (www.microsoft.com/net), a new vision for embracing the Internet and the Web in the development and use of software. One key aspect of .NET is its independence from a specific language or platform. Rather than being forced to use a single programming language, developers can create a .NET application in any .NET-compatible language. Programmers can contribute to the same software project, writing code in the .NET languages (such as Microsoft’s Visual C#, Visual C++, Visual Basic and many others) in which they are most competent. Part of the initiative includes Microsoft’s ASP.NET technology, which allows programmers to create applications for the Web. We discuss ASP.NET in Chapter 21, ASP.NET 2.0, Web Forms and Web Controls. We use ASP.NET technology in Chapter 22 to build applications that use Web services.
6
Chapter 1
Introduction to Computers, the Internet and Visual C#
The .NET architecture can exist on multiple platforms, not just Microsoft Windowsbased systems, further extending the portability of .NET programs. One example is Mono (www.mono-project.com/Main_Page), an open-source project by Novell. Another is DotGNU Portable .NET (www.dotgnu.org). A key component of the .NET architecture is Web services, which are reusable application software components that can be used over the Internet. Clients and other applications can use Web services as reusable building blocks. One example of a Web service is Dollar Rent a Car’s reservation system (www.microsoft.com/resources/casestudies/ CaseStudy.asp?CaseStudyID=11626). An airline partner wanted to enable customers to make rental-car reservations from the airline’s Web site. To do so, the airline needed to access Dollar’s reservation system. In response, Dollar created a Web service that allowed the airline to access Dollar’s database and make reservations. Web services enable computers at the two companies to communicate over the Web, even though the airline uses UNIX systems and Dollar uses Microsoft Windows. Dollar could have created a one-time solution for that particular airline, but it would not have been able to reuse such a customized system. Dollar’s Web service enables many airlines, hotels and travel companies to use its reservation system without creating a custom program for each relationship. The .NET strategy extends the concept of software reuse to the Internet, allowing programmers and companies to concentrate on their specialties without having to implement every component of every application. Instead, companies can buy Web services and devote their resources to developing their own products. For example, a single application using Web services from various companies could manage bill payments, tax refunds, loans and investments. An online merchant could buy Web services for online credit-card payments, user authentication, network security and inventory databases to create an e-commerce Web site.
1.7 The .NET Framework and the Common Language Runtime The Microsoft .NET Framework is at the heart of the .NET strategy. This framework manages and executes applications and Web services, contains a class library (called the .NET Framework Class Library, or FCL), enforces security and provides many other programming capabilities. The details of the .NET Framework are found in the Common Language Infrastructure (CLI), which contains information about the storage of data types (i.e., data that has predefined characteristics such as a date, percentage or currency amount), objects and so on. The CLI has been standardized by Ecma International (originally known as the European Computer Manufacturers Association), making it easier to create the .NET Framework for other platforms. This is like publishing the blueprints of the framework—anyone can build it by following the specifications. The Common Language Runtime (CLR) is another central part of the .NET Framework—it executes .NET programs. Programs are compiled into machine-specific instructions in two steps. First, the program is compiled into Microsoft Intermediate Language (MSIL), which defines instructions for the CLR. Code converted into MSIL from other languages and sources can be woven together by the CLR. The MSIL for an application’s components is placed into the application’s executable file (known as an assembly). When the application executes, another compiler (known as the just-in-time compiler or JIT compiler) in the CLR translates the MSIL in the executable file into machine-language code (for a particular platform), then the machine-language code executes on that platform.
1.7 The .NET Framework and the Common Language Runtime
7
[Note: MSIL is Microsoft’s name for what the C# language specification refers to as Common Intermediate Language (CIL).] If the .NET Framework exists (and is installed) for a platform, that platform can run any .NET program. The ability of a program to run (without modification) across multiple platforms is known as platform independence. Code written once can be used on another type of computer without modification, saving both time and money. In addition, software can target a wider audience—previously, companies had to decide whether converting their programs to different platforms (sometimes called porting) was worth the cost. With .NET, porting programs is no longer an issue (once .NET itself has been made available on the platforms). The .NET Framework also provides a high level of language interoperability. Programs written in different languages are all compiled into MSIL—the different parts can be combined to create a single unified program. MSIL allows the .NET Framework to be language independent, because .NET programs are not tied to a particular programming language. Any language that can be compiled into MSIL is called a .NET-compliant language. Figure 1.1 lists many of the programming languages that are available for the .NET platform (msdn.microsoft.com/netframework/technologyinfo/overview/default.aspx). Language interoperability offers many benefits to software companies. For example, C#, Visual Basic and Visual C++ developers can work side-by-side on the same project without having to learn another programming language—all of their code compiles into MSIL and links together to form one program. The .NET Framework Class Library (FCL) can be used by any .NET language. The FCL contains a variety of reusable components, saving programmers the trouble of creating new components. This book explains how to develop .NET software with C#. .NET programming languages APL
Mondrian
C#
Oberon
COBOL
Oz
Component Pascal
Pascal
Curriculum
Perl
Eiffel
Python
Forth
RPG
Fortran
Scheme
Haskell
Smalltalk
Java
Standard ML
JScript
Visual Basic
Mercury
Visual C++
Fig. 1.1 | .NET programming languages.
8
Chapter 1
Introduction to Computers, the Internet and Visual C#
1.8 Test-Driving a C# Application In this section, you will “test-drive” a C# application that enables you to draw on the screen using the mouse. You will run and interact with a working application. You will build a similar application in Chapter 13, Graphical User Interface Concepts: Part 1. The Drawing application allows you to draw with different brush sizes and colors. The elements and functionality you see in this application are typical of what you will learn to program in this text. We use fonts to distinguish between IDE features (such as menu names and menu items) and other elements that appear in the IDE. Our convention is to emphasize IDE features (such as the File menu) in a semibold sans-serif Helvetica font and to emphasize other elements, such as file names (e.g., Form1.cs), in a sans-serif Lucida font. The following steps show you how to test-drive the application. 1. Checking your setup. Confirm that you have installed Visual C# 2005 Express or Visual Studio 2005 as discussed in the Preface. 2. Locating the application directory. Open Windows Explorer and navigate to the C:\examples\Ch01\Drawing directory. 3. Running the Drawing application. Now that you are in the correct directory, double click the file name Drawing.exe to run the application. In Fig. 1.2, several graphical elements—called controls—are labeled. The controls include two GroupBoxes (in this case, Color and Size), seven RadioButtons and a Panel (these controls will be discussed in depth later in the text). The Drawing application allows you to draw with a red, blue, green or black brush of small, medium or large size. You will explore these options in this test-drive. You can use existing controls—which are objects—to get powerful applications running in C# much faster than if you had to write all of the code yourself. In this text, you will learn how to use many preexisting controls, as well as how to write your own program code to customize your applications. The brush’s properties, selected in the RadioButtons (the small circles where you select an option by clicking the mouse) labeled Black and Small, are default settings, which are the initial settings you see when you first run the application. Programmers include default settings to provide reasonable choices that the application will use if the user chooses not to change the settings. You will now choose your own settings. 4. Changing the brush color. Click the RadioButton labeled Red to change the color of the brush. Hold the mouse button down with the mouse pointer positioned anywhere on the Panel, then drag the mouse to draw with the brush. Draw flower petals as shown in Fig. 1.3. Then click the RadioButton labeled Green to change the color of the brush again. 5. Changing the brush size. Click the RadioButton labeled Large to change the size of the brush. Draw grass and a flower stem as shown in Fig. 1.4. 6. Finishing the drawing. Click the RadioButton labeled Blue. Then click the RadioButton labeled Medium. Draw raindrops as shown in Fig. 1.5 to complete the drawing. 7. Closing the application. Click the close box,
, to close your running application.
1.8 Test-Driving a C# Application
RadioButtons Panel
GroupBoxes
Fig. 1.2 | Visual C# Drawing application.
Fig. 1.3 | Drawing with a new brush color.
Fig. 1.4 | Drawing with a new brush size.
9
10
Chapter 1
Introduction to Computers, the Internet and Visual C#
Fig. 1.5 | Finishing the drawing. Additional Applications Found in C# for Programmers, 2/e Figure 1.6 lists a few sample applications that introduce some of the powerful and entertaining capabilities of C#. We encourage you to practice running some of them. The examples folder for Chapter 1 contains all of the files required to run each application listed in Fig. 1.6. Simply double click the file name for any application you would like to run. [Note: The Garage.exe application assumes that the user inputs a value from 0 to 24.]
1.9 (Only Required Section of the Case Study) Software Engineering Case Study: Introduction to Object Technology and the UML Now we begin our early introduction to object orientation, a natural way of thinking about the world and writing computer programs. At a time when the demand for new and more powerful software is soaring, the ability to build software quickly, correctly and economically remains an elusive goal. This problem can be addressed in part through the use of objects, reusable software components that model items in the real world. A modular, object-oriented approach to design and implementation can make software development Application name
File to execute
Parking Fees
Garage.exe
Tic Tac Toe
TicTacToe.exe
Drawing Stars
DrawStars.exe
Drawing Shapes
DrawShapes.exe
Drawing Polygons
DrawPolygons.exe
Fig. 1.6 | Examples of C# programs found in C# for Programmers, 2/e.
1.9 Introduction to Object Technology and the UML
11
groups much more productive than is possible using earlier programming techniques. Furthermore, object-oriented programs are often easier to understand, correct and modify. Chapters 1, 3–9 and 11 each end with a brief “Software Engineering Case Study” section in which we present a carefully paced introduction to object orientation. Our goal here is to help you develop an object-oriented way of thinking and to introduce you to the Unified Modeling Language™ (UML™)—a graphical language that allows people who design object-oriented software systems to use an industry-standard notation to represent them. In this, the only required section of the case study, we introduce basic object-oriented concepts and terminology. The optional sections in Chapters 3–9 and 11 present an object-oriented design and implementation of the software for a simple automated teller machine (ATM) system. The “Software Engineering Case Study” sections at the ends of Chapters 3–9 •
analyze a typical requirements document that describes a software system (the ATM) to be built.
•
determine the objects required to implement the system.
•
determine the attributes the objects will have.
•
determine the behaviors the objects will exhibit.
•
specify how the objects will interact with one another to meet the system requirements.
The “Software Engineering Case Study” sections at the ends of Chapters 9 and 11 modify and enhance the design presented in Chapters 3–8. Appendix J contains a complete, working C# implementation of the object-oriented ATM system. Although our case study is a scaled-down version of an industry-level problem, we nevertheless cover many common industry practices. You will experience a solid introduction to object-oriented design with the UML. Also, you will sharpen your code-reading skills by touring a complete, straightforward and well-documented C# implementation of the ATM.
Basic Object Technology Concepts We begin our introduction to object orientation with some key terminology. Everywhere you look in the real world you see objects—people, animals, plants, cars, planes, buildings, computers and so on. Humans think in terms of objects. Telephones, houses, traffic lights, microwave ovens and water coolers are just a few more objects we see around us every day. We sometimes divide objects into two categories: animate and inanimate. Animate objects are “alive” in some sense—they move around and do things. Inanimate objects do not move on their own. Objects of both types, however, have some things in common. They all have attributes (e.g., size, shape, color and weight), and they all exhibit behaviors (e.g., a ball rolls, bounces, inflates and deflates; a baby cries, sleeps, crawls, walks and blinks; a car accelerates, brakes and turns; a towel absorbs water). We will study the kinds of attributes and behaviors that software objects have. Humans learn about objects by studying their attributes and observing their behaviors. Different objects can have similar attributes and can exhibit similar behaviors. Comparisons can be made, for example, between babies and adults and between humans and chimpanzees. Object-oriented design (OOD) models software in terms similar to those that people use to describe real-world objects. It takes advantage of class relationships, where objects
12
Chapter 1
Introduction to Computers, the Internet and Visual C#
of a certain class, such as a class of vehicles, have the same characteristics—cars, trucks, little red wagons and roller skates have much in common. OOD takes advantage of inheritance relationships, where new classes of objects are derived by absorbing characteristics of existing classes and adding unique characteristics of their own. An object of “convertible” class certainly has the characteristics of the more general class “automobile,” but more specifically, the roof goes up and down. Object-oriented design provides a natural and intuitive way to view the software design process—namely, modeling objects by their attributes, behaviors and interrelationships, just as we describe real-world objects. OOD also models communication between objects. Just as people send messages to one another (e.g., a sergeant commands a soldier to stand at attention, or a teenager text messages a friend to meet at the movies), objects also communicate via messages. A bank account object may receive a message to decrease its balance by a certain amount because the customer has withdrawn that amount of money. OOD encapsulates (i.e., wraps) attributes and operations (behaviors) into objects— an object’s attributes and operations are intimately tied together. Objects have the property of information hiding. This means that objects may know how to communicate with one another across well-defined interfaces, but normally they are not allowed to know how other objects are implemented—implementation details are hidden within the objects themselves. You can drive a car effectively, for instance, without knowing the details of how engines, transmissions, brakes and exhaust systems work internally—as long as you know how to use the accelerator pedal, the brake pedal, the steering wheel and so on. Information hiding, as you will see, is crucial to good software engineering. Languages like C# are object oriented. Programming in such a language is called object-oriented programming (OOP), and it allows computer programmers to conveniently implement an object-oriented design as a working software system. Languages like C, on the other hand, are procedural, so programming tends to be action oriented. In C, the unit of programming is the function. In C#, the unit of programming is the class, from which objects are eventually instantiated (an OOP term for “created”). C# classes contain methods (C#’s equivalent of C’s functions) that implement operations, and data that implements attributes.
Classes, Data Members and Methods C# programmers concentrate on creating their own user-defined types called classes. Each class contains data as well as the set of methods that manipulate the data and provide services to clients (i.e., other classes that use the class). The data components of a class are called attributes, or fields. For example, a bank account class might include an account number and a balance. The operation components of a class are called methods. For example, a bank account class might include methods to make a deposit (increase the balance), make a withdrawal (decrease the balance) and inquire what the current balance is. The programmer uses built-in types (and other user-defined types) as the “building blocks” for constructing new user-defined types (classes). The nouns in a system specification help the C# programmer determine the set of classes from which objects are created that work together to implement the system. Classes are to objects as blueprints are to houses—a class is a “plan” for building objects of the class. Just as we can build many houses from one blueprint, we can instan-
1.9 Introduction to Object Technology and the UML
13
tiate (create) many objects from one class. You cannot cook meals in the kitchen of a blueprint, but you can cook meals in the kitchen of a house. You cannot sleep in the bedroom of a blueprint, but you can sleep in the bedroom of a house. Classes can have relationships with other classes. For example, in an object-oriented design of a bank, the “bank teller” class needs to relate to other classes, such as the “customer” class, the “cash drawer” class, the “safe” class and so on. These relationships are called associations. Packaging software as classes makes it possible for future software systems to reuse the classes. Groups of related classes often are packaged as reusable components. Just as realtors often say that the three most important factors affecting the price of real estate are “location, location and location,” some people in the software development community often say that the three most important factors affecting the future of software development are “reuse, reuse and reuse.”
Software Engineering Observation 1.1 Reuse of existing classes when building new classes and programs saves time, money and effort. Reuse also helps programmers build more reliable and effective systems, because existing classes and components often have gone through extensive testing, debugging and performance tuning. 1.1
Indeed, with object technology, you can build much of the new software you will need by combining existing classes, just as automobile manufacturers combine interchangeable parts. Each new class you create will have the potential to become a valuable software asset that you and other programmers can reuse to speed and enhance the quality of future software development efforts.
Introduction to Object-Oriented Analysis and Design (OOAD) Soon you will be writing programs in C#. How will you create the code for your programs? Perhaps, like many beginning programmers, you will simply turn on your computer and start typing. This approach may work for small programs (like the ones we present in the early chapters of the book), but what if you were asked to create a software system to control thousands of automated teller machines for a major bank? Or what if you were asked to work as part of a team of 1,000 software developers building the next generation of the U.S. air traffic control system? For projects so large and complex, you could not simply sit down and start writing programs. To create the best solutions, you should follow a detailed process for analyzing your project’s requirements (i.e., determining what your system is supposed to do) and developing a design that satisfies them (i.e., deciding how your system should do it). Ideally, you would go through this process and carefully review the design (and have your design reviewed by other software professionals) before writing any code. If this process involves analyzing and designing your system from an object-oriented point of view, it is called object-oriented analysis and design (OOAD). Experienced programmers know that proper analysis and design can save many hours by helping avoid an ill-planned system development approach that has to be abandoned partway through its implementation, possibly wasting considerable time, money and effort. OOAD is the generic term for the process of analyzing a problem and developing an approach for solving it. Small problems like the ones discussed in the first few chapters of this book do not require an exhaustive OOAD process. It may be sufficient, before we
14
Chapter 1
Introduction to Computers, the Internet and Visual C#
begin writing C# code, to write pseudocode—an informal text-based means of expressing program logic. It is not actually a programming language, but you can use it as a kind of outline to guide you as you write your code. We introduce pseudocode in Chapter 5. As problems and the groups of people solving them increase in size, OOAD quickly becomes more appropriate than pseudocode. Ideally, a group should agree on a strictly defined process for solving its problem and a uniform way of communicating the results of that process to one another. Although many different OOAD processes exist, a single graphical language for communicating the results of any OOAD process has come into wide use. This language, known as the Unified Modeling Language (UML), was developed in the mid-1990s under the initial direction of three software methodologists: Grady Booch, James Rumbaugh and Ivar Jacobson.
History of the UML In the 1980s, increasing numbers of organizations began using OOP to build their applications, and a need developed for a standard OOAD process. Many methodologists—including Grady Booch, James Rumbaugh and Ivar Jacobson—individually produced and promoted separate processes to satisfy this need. Each process had its own notation, or “language” (in the form of graphical diagrams), to convey the results of analysis (i.e., determining what a proposed system is supposed to do) and design (i.e., determining how a proposed system should be implemented to do what it is supposed to do). By the early 1990s, different organizations were using their own unique processes and notations. At the same time, these organizations also wanted to use software tools that would support their particular processes. Software vendors found it difficult to provide tools for so many processes. A standard notation and standard process were needed. In 1994, James Rumbaugh joined Grady Booch at Rational Software Corporation (now a division of IBM), and the two began working to unify their popular processes. They soon were joined by Ivar Jacobson. In 1996, the group released early versions of the UML to the software engineering community and requested feedback. Around the same time, an organization known as the Object Management Group™ (OMG™) invited submissions for a common modeling language. The OMG (www.omg.org) is a nonprofit organization that promotes the standardization of object-oriented technologies by issuing guidelines and specifications, such as the UML. Several corporations—among them HP, IBM, Microsoft, Oracle and Rational Software—had already recognized the need for a common modeling language. In response to the OMG’s request for proposals, these companies formed the UML Partners—the consortium that developed the UML version 1.1 and submitted it to the OMG. The OMG accepted the proposal and, in 1997, assumed responsibility for the continuing maintenance and revision of the UML. We present the recently adopted UML 2 terminology and notation throughout this book. What is the UML? The Unified Modeling Language (UML) is the most widely used graphical representation scheme for modeling object-oriented systems. It has indeed unified the various popular notational schemes. Those who design systems use the language (in the form of diagrams, many of which we discuss throughout our ATM case study) to model their systems. We use several popular types of UML diagrams in this book. An attractive feature of the UML is its flexibility. The UML is extensible (i.e., capable of being enhanced with new features) and is independent of any particular OOAD pro-
1.9 Introduction to Object Technology and the UML
15
cess. UML modelers are free to use various processes in designing systems, but all developers can now express their designs with one standard set of graphical notations. The UML is a feature-rich graphical language. In our subsequent (and optional) “Software Engineering Case Study” sections on developing the software for an automated teller machine (ATM), we present a simple, concise subset of these features. We then use this subset to guide you through a first design experience with the UML. We will use some C# notations in our UML diagrams to avoid confusion and improve clarity. In industry practice, especially with UML tools that automatically generate code (a nice feature of many UML tools), you would probably adhere more closely to UML keywords and UML naming conventions for attributes and operations. This case study was carefully developed under the guidance of distinguished academic and professional reviewers. We sincerely hope you enjoy working through it. If you have any questions, please communicate with us at [email protected]. We will respond promptly.
Internet and Web UML Resources For more information about the UML, refer to the following Web sites. For additional UML sites, please refer to the Internet and Web resources listed at the end of Section 3.10. www.uml.org
This UML resource site from the Object Management Group (OMG) provides specification documents for the UML and other object-oriented technologies. www.ibm.com/software/rational/uml
This is the UML resource page for IBM Rational—the successor to the Rational Software Corporation (the company that created the UML).
Recommended Readings Many books on the UML have been published. The following recommended books provide information about object-oriented design with the UML. • Arlow, J., and I. Neustadt. UML and the Unified Process: Practical Object-Oriented Analysis and Design, Second Edition. London: Addison-Wesley, 2005. • Fowler, M. UML Distilled, Third Edition: Applying the Standard Object Modeling Language. Boston: Addison-Wesley, 2004. • Rumbaugh, J., I. Jacobson, and G. Booch. The Unified Modeling Language User Guide, Second Edition. Upper Saddle River, NJ: Addison-Wesley, 2005. For additional books on the UML, please refer to the recommended readings listed at the end of Section 3.10, or visit www.amazon.com, www.bn.com and www.informIT.com. IBM Rational, formerly Rational Software Corporation, also provides a recommended-reading list for UML books at www.ibm.com/software/rational/info/technical/books.jsp. Section 1.9 Self-Review Exercises 1.1 List three examples of real-world objects that we did not mention. For each object, list several attributes and behaviors. 1.2
Pseudocode is . a) another term for OOAD b) a programming language used to display UML diagrams c) an informal means of expressing program logic d) a graphical representation scheme for modeling object-oriented systems
16
Chapter 1
1.3
The UML is used primarily to a) test object-oriented systems b) design object-oriented systems c) implement object-oriented systems d) Both a and b
Introduction to Computers, the Internet and Visual C# .
Answers to Section 1.9 Self-Review Exercises 1.1 [Note: Answers may vary.] a) A television’s attributes include the size of the screen, the number of colors it can display, and its current channel and volume. A television turns on and off, changes channels, displays video and plays sounds. b) A coffee maker’s attributes include the maximum volume of water it can hold, the time required to brew a pot of coffee and the temperature of the heating plate under the coffee pot. A coffee maker turns on and off, brews coffee and heats coffee. c) A turtle’s attributes include its age, the size of its shell and its weight. A turtle crawls, retreats into its shell, emerges from its shell and eats vegetation. 1.2
c.
1.3
b.
1.10 Wrap-Up This chapter introduced basic object technology concepts, including classes, objects, attributes and behaviors. We presented a brief history of operating systems, including Microsoft’s Windows operating system. We discussed the history of the Internet and the Web. We presented the history of C# programming and Microsoft’s .NET initiative, which allows you to program Internet- and Web-based applications using C# (and other languages). You learned the steps for executing a C# application. You test-drove a sample C# application similar to the types of applications you will learn to program in this book. You learned about the history and purpose of the UML—the industry-standard graphical language for modeling software systems. We launched our early objects and classes presentation with the first of our “Software Engineering Case Study” sections (and the only one which is required). The remaining (all optional) sections of the case study use object-oriented design and the UML to design the software for our simplified automated teller machine system. We present the complete C# code implementation of the ATM system in Appendix J. In the next chapter, you will use the Visual Studio IDE (Integrated Development Environment) to create your first C# application using the techniques of visual programming. You will also learn about Visual Studio’s help features.
1.11 Web Resources Deitel & Associates Web Sites www.deitel.com/books/csharpforprogrammers2/index.html
The Deitel & Associates site for C# for Programmers, Second Edition includes links to the book’s examples and other resources. www.deitel.com
Please check this site for updates, corrections and additional resources for all Deitel publications. www.deitel.com/newsletter/subscribe.html
Please visit this site to subscribe to the free Deitel® Buzz Online e-mail newsletter to follow the Deitel & Associates publishing program and to receive updates on C# and this book.
1.11 Web Resources
17
www.prenhall.com/deitel
Prentice Hall’s site for Deitel publications. Includes detailed product information, sample chapters and Companion Web Sites containing book- and chapter-specific resources for students and instructors.
Microsoft Web Sites msdn.microsoft.com/vcsharp/default.aspx
The Microsoft Visual C# Developer Center site includes product information, downloads, tutorials, chat groups and more. Includes case studies on companies using C# in their businesses. msdn.microsoft.com/vcsharp/programming/language/
Microsoft’s C# Language specifications and reference page. msdn.microsoft.com/vstudio/default.aspx
Visit this site to learn more about Microsoft’s Visual Studio products and resources. www.gotdotnet.com/
This is the site for the Microsoft .NET Framework Community. It includes message boards, a resource center, sample programs and more. www.thespoke.net
Students can chat, post their code, rate other students’ code, create hubs and post questions at this site.
The Ecma International page for the C# Language Specification. www.w3.org
The World Wide Web Consortium (W3C) develops technologies for the Internet and the Web. This site includes links to W3C technologies, news and frequently asked questions (FAQs). www.error-bank.com/
The Error Bank is a collection of .NET errors, exceptions and solutions. www.csharp-station.com/
This site provides news, links, tutorials, help and other C# resources. www.csharphelp.com/
This site includes a C# help board, tutorials and articles. www.codeproject.com/index.asp?cat=3
This resource page includes C# code, articles, news and tutorials. www.dotnetpowered.com/languages.aspx
This site provides a list of languages implemented in .NET.
UML Resources www.uml.org
This UML resource page from the Object Management Group (OMG) provides specification documents for the UML and other object-oriented technologies. www.ibm.com/software/rational/uml
This is the UML resource page for IBM Rational—the successor to the Rational Software Corporation (the company that created the UML).
C# Games www.c-sharpcorner.com/Games.asp
Visit this site for numerous games developed using C#. You can also submit your own games to be posted to the site. www.gamespp.com/cgi-bin/index.cgi?csharpsourcecode
This resource site includes C# games, source code and tutorials.
2 Introduction to the Visual C# 2005 Express Edition IDE Seeing is believing. —Proverb
Form ever follows function.
OBJECTIVES In this chapter, you will learn: I
The basics of the Visual Studio Integrated Development Environment (IDE) that assists you in writing, running and debugging your Visual C# programs.
I
Visual Studio’s help features.
I
Key commands contained in the IDE’s menus and toolbars.
I
The purpose of the various kinds of windows in the Visual Studio 2005 IDE.
I
What visual programming is and how it simplifies and speeds program development.
I
To create, compile and execute a simple Visual C# program that displays text and an image using the Visual Studio IDE and the technique of visual programming.
—Louis Henri Sullivan
Intelligence …is the faculty of making artificial objects, especially tools to make tools. —Henri-Louis Bergson
Outline
2.1 Introduction
2.1 2.2 2.3 2.4
2.5 2.6 2.7 2.8
19
Introduction Overview of the Visual Studio 2005 IDE Menu Bar and Toolbar Navigating the Visual Studio 2005 IDE 2.4.1 Solution Explorer 2.4.2 Toolbox 2.4.3 Properties Window Using Help Using Visual Programming to Create a Simple Program Displaying Text and an Image Wrap-Up Web Resources
2.1 Introduction Visual Studio® 2005 is Microsoft’s Integrated Development Environment (IDE) for creating, running and debugging programs (also called applications) written in a variety of .NET programming languages. In this chapter, we provide an overview of the Visual Studio 2005 IDE and demonstrate how to create a simple Visual C# program by dragging and dropping predefined building blocks into place—a technique called visual programming. This chapter is specific to Visual C#—Microsoft’s implementation of Ecma standard C#.
2.2 Overview of the Visual Studio 2005 IDE There are many versions of Visual Studio available. For this book, we used the Microsoft Visual C# 2005 Express Edition, which supports only the Visual C# programming language. Microsoft also offers a full version of Visual Studio 2005, which includes support for other languages in addition to Visual C#, such as Visual Basic and Visual C++. Our screen captures and discussions focus on the IDE of the Visual C# 2005 Express Edition. We assume that you have some familiarity with Windows. Again, we use fonts to distinguish between IDE features (such as menu names and menu items) and other elements that appear in the IDE. We emphasize IDE features in a sans-serif bold Helvetica font (e.g., File menu) and emphasize other elements, such as file names (e.g., Form1.cs) and property names (discussed in Section 2.4), in a sans-serif Lucida font.
Introduction to Microsoft Visual C# 2005 Express Edition To start Microsoft Visual C# 2005 Express Edition in Windows XP, select Start > All Programs > Visual C# 2005 Express Edition. For Windows 2000 users, select Start > Programs > Visual C# 2005 Express Edition. Once the Express Edition begins execution, the Start Page displays (Fig. 2.1). Depending on your version of Visual Studio, your Start Page may look different. For new programmers unfamiliar with Visual C#, the Start Page contains a list of links to resources in the Visual Studio 2005 IDE and on the Internet.
20
Chapter 2
Introduction to the Visual C# 2005 Express Edition IDE
New Project
Start Page
button
tab
Hidden window
Start Page links
Empty Solution Explorer (no projects open)
Fig. 2.1 | Start Page in Visual C# 2005 Express Edition. From this point forward, we will refer to the Visual Studio 2005 IDE simply as “Visual Studio” or “the IDE.” For experienced developers, this page provides links to the latest developments in Visual C# (such as updates and bug fixes) and to information on advanced programming topics. Once you start exploring the IDE, you can return to the Start Page by selecting the page from the location bar drop-down menu (Fig. 2.2), by selecting View > Other Windows > Start Page or by clicking the Start Page icon from the IDE’s Toolbar (Fig. 2.9). We discuss the Toolbar and its various icons in Section 2.3. We use the > character to indicate the selection of a menu command from a menu. For example, we use the notation File > Open File to indicate that you should select the Open File command from the File menu.
Links on the Start Page The Start Page links are organized into sections—Recent Projects, Getting Started, Visual C# Express Headlines and MSDN: Visual Studio 2005—that contain links to helpful programming resources. Clicking any link on the Start Page displays the relevant information associated
2.2 Overview of the Visual Studio 2005 IDE
Requested Web page (URL in location bar drop-down menu)
21
Selected tab for requested Web page
Fig. 2.2 | Displaying a Web page in Visual Studio. with the specific link. We refer to single clicking with the left mouse button as selecting or clicking; we refer to double clicking with the left mouse button simply as double clicking. The Recent Projects section contains information on projects you have recently created or modified. You can also open existing projects or create new ones by clicking the links in the section. The Getting Started section focuses on using the IDE to create programs, learning Visual C#, connecting to the Visual C# developer community (i.e., other software developers with whom you can communicate through newsgroups and Web sites) and providing various development tools such as starter kits. For example, clicking the link Use a Starter Kit provides you with resources and links for building a simple screen saver application or a movie collection application. The screen saver application builds a screen saver that displays current news articles. The movie collection starter kit builds an application that lets you maintain a catalog of your DVDs and VHS movies, or the application can be changed to track anything else you might collect (e.g., CDs, video games). The Visual C# Express Headlines and MSDN: Visual Studio 2005 sections provide links to information about programming in Visual C#, including a tour of the language, new Visual C# 2005 features and online courses. To access more extensive information on Visual Studio, you can browse the MSDN (Microsoft Developer Network) online library at msdn.microsoft.com. The MSDN site contains articles, downloads and tutorials on technologies of interest to Visual Studio developers. You can also browse the Web from the IDE using Internet Explorer. For more extensive information on C#, visit the C# Developer Center at msdn.microsoft.com/vcsharp. To request a Web page, type its URL
22
Chapter 2
Introduction to the Visual C# 2005 Express Edition IDE
into the location bar (Fig. 2.2) and press the Enter key—your computer, of course, must be connected to the Internet. (If the location bar is not already present in the IDE, select View > Other Windows > Show Browser.) The Web page that you wish to view will appear as another tab, which you can select, inside the Visual Studio IDE (Fig. 2.2).
Customizing the IDE and Creating a New Project To begin programming in Visual C#, you must create a new project or open an existing one. There are two ways to create a new project or open an existing project. You can select either File > New Project… or File > Open Project… from the File menu, which creates a new project or opens an existing project, respectively. From the Start Page, under the Recent Projects section, you can also click the links Create: Project or Open: Project/Solution. A project is a group of related files, such as the Visual C# code and any images that might make up a program. Visual Studio 2005 organizes programs into projects and solutions, which contain one or more projects. Multiple-project solutions are used to create largescale programs. Each of the programs we create in this book consists of a single project. Select File > New Project… or the Create: Project… link on the Start Page to display the New Project dialog (Fig. 2.3). Dialogs are windows that facilitate user-computer communication. We will discuss the detailed process of creating new projects momentarily. Visual Studio provides templates for several project types (Fig. 2.3). Templates are the project types users can create in Visual C#—Windows applications, console applications and others (you will primarily use Windows applications and console applications in this textbook). Users can also use or create custom application templates. In this chapter, we Visual C# Windows Application (selected)
Description of selected project Default project name (provided by Visual Studio) template (provided by Visual Studio)
Fig. 2.3 | New Project dialog.
2.2 Overview of the Visual Studio 2005 IDE
23
focus on Windows applications. We discuss the Console Application template in Chapter 3, Introduction to C# Applications. A Windows application is a program that executes within a Windows operating system (e.g., Windows 2000 or Windows XP) and typically has a graphical user interface (GUI)—the visual part of the program with which the user interacts. Windows applications include Microsoft software products like Microsoft Word, Internet Explorer and Visual Studio; software products created by other vendors; and customized software that you and other programmers create. You will create many Windows applications in this text. [Note: Novell sponsors an open source project called Mono that enables developers to create .NET applications for Linux, Windows and Mac OS X. Mono is based on the Ecma standards for C# and the Common Language Infrastructure (CLI). For more information on Mono, visit www.mono-project.com.] By default, Visual Studio assigns the name WindowsApplication1 to the new project and solution (Fig. 2.3). Soon you will change the name of the project and the location where it is saved. Click OK to display the IDE in design view (Fig. 2.4), which contains all the features necessary for you to begin creating programs. The design view portion of the IDE is also known as the Windows Form Designer. The gray rectangle titled Form1 (called a Form) represents the main window of the Windows application that you are creating. C# applications can have multiple Forms (windows); however, most of the applications you will create in this text use only one Form. Later in the chapter, you will learn how to customize the Form by adding controls (i.e., reusable components) to a program—in this case, a Label and a PictureBox (as you will Solution Explorer
Active tab
Tabs
Form (Windows application)
Fig. 2.4 | Design view of the IDE.
Menu
Menu bar window
Properties window
24
Chapter 2
Introduction to the Visual C# 2005 Express Edition IDE
see in Fig. 2.27). A Label typically contains descriptive text (e.g., "Welcome to Visual C#!"), and a PictureBox displays images, such as the Deitel bug mascot. Visual Studio has over 65 preexisting controls you can use to build and customize your programs. Many of these controls are defined and used throughout this book. In addition to the controls provided with Visual C# 2005 Express or Visual Studio 2005, there are many other controls available from third parties. You can download several third party controls from msdn.microsoft.com/vcsharp/downloads/components/default.aspx. As you begin programming, you will work with controls that are not necessarily part of your program, but rather are part of the FCL. As you place controls on the form, you can modify their properties (discussed in detail in Section 2.4) by entering alternative text in a text box (Fig. 2.5) or selecting options then pressing a button, such as OK or Cancel, as shown in Fig. 2.6. Collectively, the Form and controls constitute the program’s GUI. Users enter data (input) into the program in a variety of ways, including typing at the keyboard, clicking the mouse buttons and typing into GUI controls, such as TextBoxes. Programs use the GUI to display instructions and other information (output) for users to read. For example, the New Project dialog in Fig. 2.3 presents a GUI where the user clicks the mouse button to select a project template, then inputs a project name from the keyboard (note that the figure is still showing the default project name WindowsApplication1 supplied by Visual Studio). The name of each open document is listed on a tab—in Fig. 2.4, the open documents are Form1.cs [Design] and the Start Page. To view a document, click its tab. Tabs facilitate
Text box (displaying the text Form1), which can be modified
Fig. 2.5 | Example of a text box control in the Visual Studio IDE.
2.3 Menu Bar and Toolbar
25
OK button Cancel button
Fig. 2.6 | Examples of buttons in the Visual Studio IDE. easy access to multiple open documents. The active tab (the tab of the document currently displayed in the IDE) is displayed in bold text (e.g., Form1.cs [Design] in Fig. 2.4) and is positioned in front of all the other tabs.
2.3 Menu Bar and Toolbar Commands for managing the IDE and for developing, maintaining and executing programs are contained in menus, which are located on the menu bar of the IDE (Fig. 2.7). Note that the set of menus displayed in Fig. 2.7 changes based on what you are currently doing in the IDE. Menus contain groups of related commands (also called menu items) that, when selected, cause the IDE to perform specific actions (e.g., open a window, save a file, print a file and execute a program). For example, new projects can be created by selecting File > New Project…. The menus depicted in Fig. 2.7 are summarized in Fig. 2.8. In Chapter 14, Graphical User Interface Concepts: Part 2, we discuss how to create and add your own menus and menu items to your programs.
Fig. 2.7 | Visual Studio menu bar. Menu
Description
File
Contains commands for opening, closing, adding and saving projects, as well as printing project data and exiting Visual Studio.
Edit
Contains commands for editing programs, such as cut, copy, paste, undo, redo, delete, find and select.
Fig. 2.8 | Summary of Visual Studio 2005 IDE menus. (Part 1 of 2.)
26
Chapter 2
Introduction to the Visual C# 2005 Express Edition IDE
Menu
Description
View
Contains commands for displaying windows (e.g., Solution Explorer, Toolbox, Properties window) and for adding toolbars to the IDE.
Project
Contains commands for managing projects and their files.
Build
Contains commands for compiling a program.
Debug
Contains commands for debugging (i.e., identifying and correcting problems in a program) and running a program. Debugging is discussed in detail in Appendix C.
Data
Contains commands for interacting with databases (i.e., organized collections of data stored on computers), which we discuss in Chapter 20, Database, SQL and ADO.NET).
Format
Contains commands for arranging and modifying a form’s controls. Note that the Format menu appears only when a GUI component is selected in Design view.
Tools
Contains commands for accessing additional IDE tools (e.g., the Toolbox) and options that enable you to customize the IDE.
Window
Contains commands for arranging and displaying windows.
Community
Contains commands for sending questions directly to Microsoft, checking question status, sending feedback on Visual C# and searching the CodeZone developer center and the Microsoft developers community site.
Help
Contains commands for accessing the IDE’s help features.
Fig. 2.8 | Summary of Visual Studio 2005 IDE menus. (Part 2 of 2.) Rather than navigating the menus from the menu bar, you can access many of the more common commands from the toolbar (Fig. 2.9), which contains graphics, called icons, that graphically represent commands. [Note: Figure 2.9 divides the toolbar into two parts so that we can illustrate the graphics more clearly—the toolbar appears on one line inside the IDE.] By default, the standard toolbar is displayed when you run Visual Studio for the first time; it contains icons for the most commonly used commands, such as opening a file, adding an item to a project, saving and running (Fig. 2.9). Some commands are initially disabled (i.e., unavailable to use). These commands, which are initially grayed out, are enabled by Visual Studio only when they are necessary. For example, Visual Studio enables the command for saving a file once you begin editing the file. You can customize the IDE by adding more toolbars. Select View > Toolbars (Fig. 2.10). Each toolbar you select will be displayed with the other toolbars at the top of the Visual Studio window (Fig. 2.10). Another way in which you can add toolbars to your IDE (which we do not show in this chapter) is through selecting Tools > Customize. Then, under the Toolbars tab, select the additional toolbars you would like to have appear in the IDE. To execute a command via the toolbar, click its icon. Some icons contain a down arrow that, when clicked, displays a related command or commands, as shown in Fig. 2.11.
2.4 Navigating the Visual Studio 2005 IDE
27
a) New Project
Add Item
Copy
Open File Save All
Save
Cut
Navigate backwards
Undo
Paste
Redo
Start
Navigate forwards
Solution Configurations
b) Other Windows Properties Toolbox
Solution Platforms
Find
Find in Files
window
Solution Explorer
window
Object
browser
Start Page
Fig. 2.9 | Standard toolbar in Visual Studio. It is difficult to remember what each of the icons on the toolbar represents. Positioning the mouse pointer over an icon highlights it and, after a brief delay, displays a description of the icon called a tool tip (Fig. 2.12). Tool tips help novice programmers become familiar with the IDE’s features and serve as useful reminders of each toolbar icon’s functionality.
2.4 Navigating the Visual Studio 2005 IDE The IDE provides windows for accessing project files and customizing controls. In this section, we introduce several windows that you will use frequently when developing Visual C# programs. These windows can be accessed via toolbar icons (Fig. 2.13) or by selecting the name of the desired window in the View menu. Visual Studio provides a space-saving feature called auto-hide. When auto-hide is enabled, a tab appears along either the left or right edge of the IDE window (Fig. 2.14). This tab contains one or more icons, each of which identifies a hidden window. Placing the mouse pointer over one of these icons displays that window (Fig. 2.15). The window is hidden again when the mouse pointer is moved outside of the window’s area. To “pin down” a window (i.e., to disable auto-hide and keep the window open), click the pin icon. Note that when auto-hide is enabled, the pin icon is horizontal (Fig. 2.15), whereas when a window is “pinned down,” the pin icon is vertical (Fig. 2.16).
28
Chapter 2
Introduction to the Visual C# 2005 Express Edition IDE
Standard toolbar
Build toolbar added to the Standard toolbar
Fig. 2.10 | Adding the Build toolbar to the IDE.
Toolbar
Down arrow indicates additional commands
Fig. 2.11 | IDE toolbar icon showing additional commands.
2.4 Navigating the Visual Studio 2005 IDE
Tool tip displayed when the mouse pointer rests over the New Project icon
Fig. 2.12 | Tool tip demonstration.
Properties
window
Solution Explorer
Fig. 2.13 | Toolbar icons for three Visual Studio windows.
Tab for hidden window (auto-hide enabled)
Fig. 2.14 | Auto-hide feature demonstration.
Toolbox
29
30
Chapter 2
Introduction to the Visual C# 2005 Express Edition IDE
Toolbox
title bar Horizontal orientation for pin icon
Fig. 2.15 | Displaying a hidden window when auto-hide is enabled. Toolbox
“pinned down”
Vertical orientation for pin icon
Fig. 2.16 | Disabling auto-hide (“pinning down” a window).
2.4 Navigating the Visual Studio 2005 IDE
31
The next few sections overview three of the main windows used in Visual Studio— the Solution Explorer, the Toolbox and the Properties window. These windows show information about the project and include tools that will help you build your programs. 2.4.1 Solution Explorer
The Solution Explorer window (Fig. 2.17) provides access to all of the files in a solution. If the Solution Explorer window is not shown in the IDE, select View > Solution Explorer or click the Solution Explorer icon (Fig. 2.13). When you first open Visual Studio, the Solution Explorer is empty; there are no files to display. Once you open a solution, the Solution Explorer displays the contents of the solution and its projects or when you create a new project, its contents are displayed. The solution’s startup project is the project that runs when the program executes. If you have multiple projects in a given solution, you can specify the startup project by rightclicking the project name in the Solution Explorer window, then selecting Set as StartUp Project. For a single-project solution, the startup project is the only project (in this case, WindowsApplication1) and the project name appears in bold text in the Solution Explorer window. All of the programs discussed in this text are single-project solutions. For programmers using Visual Studio for the first time, the Solution Explorer window lists only the Properties, References, Form1.cs and Program.cs files (Fig. 2.17). The Solution Explorer window includes a toolbar that contains several icons. The Visual C# file that corresponds to the form shown in Fig. 2.4 is named Form1.cs (selected in Fig. 2.17). (Visual C# files use the .cs filename extension, which is short for “C Sharp.”) By default, the IDE displays only files that you may need to edit—other files generated by the IDE are hidden. When clicked, the Show all files icon (Fig. 2.17) displays all the files in the solution, including those generated by the IDE. The plus and minus boxes that appear (Fig. 2.18) can be clicked to expand and collapse the project tree, respectively. Click the plus box to the left of Properties to display items grouped under the heading to the right of the plus box (Fig. 2.19); click the minus boxes to the left of Properties and References to collapse the tree from its expanded state (Fig. 2.20). Other Visual Studio windows also use this plus-box/minus-box convention.
Show all files icon
Toolbar Startup project
Fig. 2.17 | Solution Explorer with an open project.
32
Chapter 2
Introduction to the Visual C# 2005 Express Edition IDE
Plus box Minus box
Fig. 2.18 | Solution Explorer showing plus boxes and minus boxes for expanding and collapsing the tree to show or hide project files.
Minus box indicates that the file or folder is expanded (changed from plus box)
Fig. 2.19 | Solution Explorer expanding the Properties file after clicking its plus box.
Plus boxes indicate that the file or folder is collapsed (changed from minus box)
Fig. 2.20 | Solution Explorer collapsing all files after clicking any minus boxes. 2.4.2 Toolbox
The Toolbox contains icons representing controls used to customize forms (Fig. 2.21). Using visual programming, you can “drag and drop” controls onto the form, which is faster and simpler than building them by writing GUI code (we introduce writing this type of code in Chapter 5, Control Statements: Part 1). Just as you do not need to know how to
2.4 Navigating the Visual Studio 2005 IDE
33
Controls
…
Group names
Fig. 2.21 | Toolbox window displaying controls for the Common Controls group. build an engine to drive a car, you do not need to know how to build controls to use them. Reusing pre-existing controls saves time and money when you develop programs. The wide variety of controls contained in the Toolbox is a powerful feature of the .NET FCL. You will use the Toolbox when you create your first program later in the chapter. The Toolbox contains groups of related controls. Examples of these groups, All Windows Forms, Common Controls, Containers, Menus & Toolbars, Data, Components, Printing, Dialogs and General, are listed in Fig. 2.21. Again, note the use of plus and minus boxes to expand or collapse a group of controls. The Toolbox contains over 65 prebuilt controls for use in Visual Studio, so you may need to scroll through the Toolbox to view additional controls other than the ones shown in Fig. 2.21. We discuss many of the Toolbox’s controls and their functionality throughout the book.
2.4.3 Properties Window To display the Properties window if it is not visible, you can select View > Properties Window, click the Properties window icon shown in Fig. 2.13 or you can press the F4 key. The Properties window displays the properties for the currently selected Form (Fig. 2.22), control or file in design view. Properties specify information about the form or control, such as its size, color and position. Each form or control has its own set of properties; a property’s description is displayed at the bottom of the Properties window whenever that property is selected.
34
Chapter 2
Introduction to the Visual C# 2005 Express Edition IDE
Component selection drop-down list Toolbar
Categorized icon Alphabetical icon
Description of the Text property
Properties Property values
Fig. 2.22 | Properties window displaying the Text property of the Form. Figure 2.22 shows the form’s Properties window. The left column lists the Form’s properties; the right column displays the current value of each property. Icons on the toolbar sort the properties either alphabetically by clicking the Alphabetical icon or categorically by clicking the Categorized icon. You can sort the properties alphabetically in ascending or descending order—clicking the Alphabetical icon repeatedly toggles between sorting the properties from A–Z and Z–A. Sorting by category groups the properties according to their use (i.e., Appearance, Behavior, Design). Depending on the size of the Properties window, some of the properties may be hidden from view on the screen, in which case, users can scroll through the list of properties. We show how to set individual properties later in this chapter. The Properties window is crucial to visual programming; it allows you to modify a control’s properties visually, without writing code. You can see which properties are available for modification and, where appropriate, can learn the range of acceptable values for a given property. The Properties window displays a brief description of the selected prop-
2.5 Using Help
35
erty, helping you understand its purpose. A property can be set quickly using this window—usually, only one click is required, and no code needs to be written. At the top of the Properties window is the component selection drop-down list, which allows you to select the form or control whose properties you wish to display in the Properties window (Fig. 2.22). Using the component selection drop-down list is an alternative way to display a control’s properties without selecting the actual form or control in the GUI.
2.5 Using Help Visual Studio provides extensive help features. The Help menu commands are summarized in Fig. 2.23. Dynamic help (Fig. 2.24) is an excellent way to get information quickly about the IDE and its features. It provides a list of articles pertaining to the “current content” (i.e., the selected items). To open the Dynamic Help window, select Help > Dynamic Help. Then, when you click a word or component (such as a form or control), links to help articles appear in the Dynamic Help window. The window lists help topics, code samples and other relevant information. There is also a toolbar that provides access to the How Do I, Search, Index and Contents help features. Visual Studio also provides context-sensitive help, which is similar to dynamic help, except that it immediately displays a relevant help article rather than presenting a list of articles. To use context-sensitive help, click an item, such as the form, and press the F1 key. Figure 2.25 displays help articles related to a form. The Help options can be set in the Options dialog (accessed by selecting Tools > Options…). To display all the settings that you can modify (including the settings for the Help options), make sure that the Show all settings checkbox in the lower-left corner of the dialog is checked (Fig. 2.26). To change whether the Help is displayed internally or externally, select Help on the left, then locate the Show Help using: drop-down list on the right. Depending on your preference, selecting External Help Viewer displays a relevant help article in a separate window outside the IDE (some programmers like to view Web pages separately from the project on which they are working in the IDE). Selecting Integrated Help Viewer displays a help article as a tabbed window inside the IDE.
Command
Description
How Do I
Contains links to relevant topics, including how to upgrade programs and learn more about Web services, architecture and design, files and I/O, data, debugging and more.
Search
Finds help articles based on search keywords.
Index
Displays an alphabetized list of topics you can browse.
Contents
Displays a categorized table of contents in which help articles are organized by topic.
Fig. 2.23 | Help menu commands.
36
Chapter 2
Introduction to the Visual C# 2005 Express Edition IDE
How Do I
Selected item
Dynamic Help window
Fig. 2.24 | Dynamic Help window.
Fig. 2.25 | Using context-sensitive help.
Search Index Contents
Relevant help articles on the selected item (e.g., the form)
2.6 Create a Simple Program Displaying Text and an Image
Help options selected
37
Show Help using: box
Show all settings
check box
Fig. 2.26 | Options dialog displaying Help settings.
2.6 Using Visual Programming to Create a Simple Program Displaying Text and an Image In this section, we create a program that displays the text "Welcome to Visual C#!" and an image of the Deitel & Associates bug mascot. The program consists of a single form that uses a Label and a PictureBox. Figure 2.27 shows the results of the program as it executes. The program and the bug image are available with this chapter’s examples. You can download the examples from www.deitel.com/books/csharpforprogrammers2/index.html. To create the program whose output is shown in Fig. 2.27, you will not write a single line of program code. Instead, you will use visual programming techniques. Visual Studio processes your actions (such as mouse clicking, dragging and dropping) to generate program code. Chapter 3 begins our discussion of how to write program code. Throughout the book, you produce increasingly substantial programs that often include a combination of code written by you and code generated by Visual Studio. The generated code can be difficult for novices to understand—fortunately, you rarely need to look at this code. Visual programming is useful for building GUI-intensive programs that require a significant amount of user interaction. Visual programming cannot be used to create programs that do not have GUIs—you must write such code directly.
38
Chapter 2
Introduction to the Visual C# 2005 Express Edition IDE
Label control
PictureBox
control
Fig. 2.27 | Simple program executing. To create, run and terminate this first program, perform the following 13 steps: 1. Create the new project. If a project is already open, close it by selecting File > Close Solution. A dialog asking whether to save the current project might appear. Click Save to save any changes. To create a new Windows application for our program, select File > New Project… to display the New Project dialog (Fig. 2.28). From the template options, select Windows Application. Name the project ASimpleProgram and click OK. [Note: File names must conform to certain rules. For example, file names cannot contain symbols (e.g., ?, :, *, , # and %) or Unicode® control characters (Unicode is a special character set described in Appendix E). Also, file names cannot be system reserved names, such as “CON”, “PRN”, “AUX” and “COM1” or “.” and “..”, and cannot be longer than 256 characters in length.] We mentioned earlier in this chapter that you must set the directory in which the project will be saved. In the complete Visual Studio, you do this in the New Project dialog. To specify the directory in Visual C# Express, select File > Save All to display the Save Project dialog (Fig. 2.29). To set the project location, click the Browse… button, which opens the Project Location dialog (Fig. 2.30). Navigate through the directories, select one in which to place the project (in our example, we use a directory named MyProjects) and click OK to close the dialog. Make sure the Create directory for Solution checkbox is selected (Fig. 2.29). Click Save to close the Save Project dialog. When you first begin working in the IDE, it is in design mode (i.e., the program is being designed and is not executing). While the IDE is in design mode, you have access to all the environment windows (e.g., Toolbox, Properties), menus and toolbars, as you will see shortly.
2.6 Create a Simple Program Displaying Text and an Image
39
Template types
Type in the project name
Fig. 2.28 | New Project dialog.
Fig. 2.29 | Save Project dialog. 2. Set the text in the form’s title bar. The text in the form’s title bar is the value of the form’s Text property (Fig. 2.31). If the Properties window is not open, click the properties icon in the toolbar or select View > Properties Window. Click anywhere in the form to display the form’s properties in the Properties window. Click in the textbox to the right of the Text property box and type "A Simple Program", as in Fig. 2.31. Press the Enter key when finished; the form’s title bar is updated immediately (Fig. 2.32). 3. Resize the form. Click and drag one of the form’s enabled sizing handles (the small white squares that appear around the form, as shown in Fig. 2.32). Using the mouse, select and drag the sizing handle to resize the form (Fig. 2.33).
40
Chapter 2
Introduction to the Visual C# 2005 Express Edition IDE
Selected project location
Click to set project location
Fig. 2.30 | Setting the project location in the Project Location dialog. Name and type of object
Selected property
Property value
Property description
Fig. 2.31 | Setting the form’s Text property in the Properties window. Title bar
Enabled sizing handles
Fig. 2.32 | Form with enabled sizing handles.
2.6 Create a Simple Program Displaying Text and an Image
41
Fig. 2.33 | Resized form. 4. Change the form’s background color. The BackColor property specifies a form’s or control’s background color. Clicking BackColor in the Properties window causes a down-arrow button to appear next to the value of the property (Fig. 2.34). When clicked, the down-arrow button displays a set of other options, which vary depending on the property. In this case, the arrow displays tabs for Custom, Web and System (the default). Click the Custom tab to display the palette (a grid of colors). Select the box that represents light blue. Once you select the color, the palette closes and the form’s background color changes to light blue (Fig. 2.35). Current color
Down-arrow button Custom palette Light blue
Fig. 2.34 | Changing the form’s BackColor property.
42
Chapter 2
Introduction to the Visual C# 2005 Express Edition IDE
New background color
Fig. 2.35 | Form with new BackColor property applied. 5. Add a Label control to the Form. If the Toolbox is not already open, select View > Toolbox to display the set of controls you will use for creating your programs. For the type of program we are creating in this chapter, the typical controls we use will be located in either the All Windows Forms category of the Toolbox or the Common Controls group. If either group name is collapsed, expand it by clicking the plus sign (the All Windows Forms and Common Controls groups are shown near the top of Fig. 2.21). Next, double click the Label control in the Toolbox. This action causes a Label to appear in the upper-left corner of the form (Fig. 2.36). [Note: If the Form is behind the Toolbox, you may need to close the Toolbox to see the Label.] Although double clicking any Toolbox control places the control on the form, you also can “drag” controls from the Toolbox to the form (you may prefer dragging the control because you can position it wherever you want). Our Label displays the text label1 by default. Note that our Label’s background color is the same as the form’s background color. When a control is added to the form, its BackColor property is set to the form’s BackColor. You can change the Label’s background color to a different color than the form by changing its BackColor property. 6. Customize the Label’s appearance. Select the Label by clicking it. Its properties now appear in the Properties window (Fig. 2.37). The Label’s Text property determines the text (if any) that the Label displays. The form and Label each have their own Text property—forms and controls can have the same types of properties (such as BackColor and Text) without conflict. Set the Label’s Text property to Welcome to Visual C#!. Note that the Label resizes to fit all the typed text on one line. By default, the AutoSize property of the Label is set to True, which allows the Label to adjust its size to fit all of the text if necessary. Set the AutoSize property to False (Fig. 2.37) so that you can resize the Label on your own. Resize the Label (using the sizing handles) so that the text fits. Move the Label to the top center of the form by dragging it or by using the keyboard’s left and right arrow keys to adjust its position (Fig. 2.38). Alternatively, when the Label is selected, you can center the Label control horizontally by selecting Format > Center In Form > Horizontally.
2.6 Create a Simple Program Displaying Text and an Image
43
Label control
Fig. 2.36 | Adding a Label to the form.
AutoSize property
Fig. 2.37 | Changing the Label’s AutoSize property to False. 7. Set the Label’s font size. To change the font type and appearance of the Label’s text, select the value of the Font property, which causes an ellipsis button ( ) to appear next to the value (Fig. 2.39). When the ellipsis button is clicked, a dialog that provides additional values—in this case, the Font dialog (Fig. 2.40)—is displayed. You can select the font name (e.g., Microsoft Sans Serif, MingLiU, Mistral, Modern No. 20—the font options may vary, depending on your system), font style (Regular, Italic, Bold, etc.) and font size (16, 18, 20, etc.) in this dialog. The Sample area shows sample text with the selected font settings. Under Size, select 24 points and click OK. If the Label’s text does not fit on a single line, it wraps to the next line. Resize the Label vertically if it’s not large enough to hold the text.
44
Chapter 2
Introduction to the Visual C# 2005 Express Edition IDE
Label centered with updated Text property
Sizing handles
Fig. 2.38
|
GUI after the form and Label have been customized.
Ellipsis button
Fig. 2.39
| Properties
window displaying the Label’s properties.
Current font
Font sample
Fig. 2.40
| Font
dialog for selecting fonts, styles and sizes.
2.6 Create a Simple Program Displaying Text and an Image
45
8. Align the Label’s text. Select the Label’s TextAlign property, which determines how the text is aligned within the Label. A three-by-three grid of buttons representing alignment choices is displayed (Fig. 2.41). The position of each button corresponds to where the text appears in the Label. For this program, set the TextAlign property to MiddleCenter in the three-by-three grid; this selection causes the text to appear centered in the middle of the Label, with equal spacing from the text to all sides of the Label. The other TextAlign values, such as TopLeft, TopRight and BottomCenter, can be used to position the text anywhere within a Label. Certain alignment values may require that you resize the Label larger or smaller to better fit the text. 9. Add a PictureBox to the form. The PictureBox control displays images. The process involved in this step is similar to that of Step 5, in which we added a Label to the form. Locate the PictureBox in the Toolbox menu (Fig. 2.21) and double click it to add the PictureBox to the form. When the PictureBox appears, move it underneath the Label, either by dragging it or by using the arrow keys (Fig. 2.42). 10. Insert an image. Click the PictureBox to display its properties in the Properties window (Fig. 2.43). Locate the Image property, which displays a preview of the image, if one exists. No picture has been assigned, so the value of the Image property displays (none). Click the ellipsis button to display the Select Resource dialog (Fig. 2.44). This dialog is used to import files, such as images, to any program. Click the Import… button to browse for an image to insert. In our case, the picture is bug.png. In the dialog that appears (Fig. 2.45), click the image with the mouse and click OK. The image is previewed in the Select Resource dialog (Fig. 2.45). Click OK to place the image in your program. Supported image formats include PNG (Portable Network Graphics), GIF (Graphics Interchange Format), JPEG (Joint Photographic Experts Group) and BMP (Windows bitmap). Creating a new image requires image-editing software, such as Jasc® Paint Shop Pro™ (www.jasc.com), Adobe® Photoshop™ Elements (www.adobe.com) or Microsoft Paint (provided with Windows). To size the image to the PictureBox, change the SizeMode property to StretchImage (Fig. 2.46), which scales the image to the size of the PictureBox. Resize the PictureBox, making it larger (Fig. 2.47).
Text alignment options
Middle-center alignment option
Fig. 2.41 | Centering the Label’s text.
46
Chapter 2
Introduction to the Visual C# 2005 Express Edition IDE
Updated Label
PictureBox
Fig. 2.42 | Inserting and aligning a PictureBox.
Image property value
(no image selected)
Fig. 2.43 |
Image
property of the PictureBox.
Fig. 2.44 | Select Resource dialog to select an image for the PictureBox.
2.6 Create a Simple Program Displaying Text and an Image
Image file name
Fig. 2.45 | Select Resource dialog displaying a preview of selected image.
SizeMode property set to StretchImage
SizeMode property
Fig. 2.46 | Scaling an image to the size of the PictureBox.
Newly inserted image
Fig. 2.47 |
PictureBox
displaying an image.
47
48
Chapter 2
Introduction to the Visual C# 2005 Express Edition IDE
11. Save the project. Select File > Save All to save the entire solution. The solution file contains the name and location of its project, and the project file contains the names and locations of all the files in the project. 12. Run the project. Recall that up to this point we have been working in the IDE’s design mode (i.e., the program being created is not executing). In run mode, the program is executing, and you can interact with only a few IDE features—features that are not available are disabled (grayed out). The text Form1.cs [Design] in the tab (Fig. 2.48) means that we are designing the form visually rather than programmatically. If we had been writing code, the tab would have contained only the text Form1.cs. To run the program you must first build the solution. Select Build > Build Solution to compile the project (Fig. 2.48). Once you build the solution (the IDE will display "Build succeeded" in the lower-left corner of the IDE—also known as the status bar), select Debug > Start Debugging to execute the program. Figure 2.49 shows the IDE in run mode (indicated by the title bar text A Simple Program (Running) – Microsoft Visual C# 2005 Express Edition ). Note that many toolbar icons and menus are disabled. The running program will appear in a separate window outside the IDE (Fig. 2.49). 13. Terminate execution. To terminate the program, click the running program’s close box (the X in the top-right corner of the running program’s window). This action stops the program’s execution and returns the IDE to design mode. Build menu
Fig. 2.48 | Building a solution.
2.7 Wrap-Up
49
Form
Running program
Close box
Fig. 2.49 | IDE in run mode, with the running program in the foreground window.
2.7 Wrap-Up In this chapter, we introduced key features of the Visual Studio Integrated Development Environment (IDE). You used the technique of visual programming to create a working Visual C# program without writing a single line of code. Visual C# programming is a mixture of the two styles—visual programming allows you to develop GUIs easily and avoid tedious GUI programming; conventional programming (which we introduce in Chapter 3) allows you to specify the behavior of your programs. You created a Visual C# Windows application with one form. You worked with the Solution Explorer, Toolbox and Properties windows, which are essential to developing Visual C# programs. The Solution Explorer window allows you to manage your solution’s files visually. The Toolbox window contains a rich collection of controls for creating GUIs. The Properties window allows you to set the attributes of a form and controls.
50
Chapter 2
Introduction to the Visual C# 2005 Express Edition IDE
You explored Visual Studio’s help features, including the Dynamic Help window and the Help menu. The Dynamic Help window displays links related to the item that you click with the mouse. You learned how to set Help options to display and use help resources. We also demonstrated how to use context-sensitive help. You used visual programming to design the GUI portions of a program quickly and easily, by dragging and dropping controls (a Label and a PictureBox) onto a Form or by double clicking controls in the Toolbox. In creating the ASimpleProgram program, you used the Properties window to set the Text and BackColor properties of the form. You learned that Label controls display text and that PictureBoxes display images. You displayed text in a Label and added an image to a PictureBox. You also worked with the AutoSize, TextAlign and SizeMode properties of a Label. In the next chapter, we discuss “nonvisual,” or “conventional,” programming—you will create your first programs that contain Visual C# code that you write, instead of having Visual Studio write the code. You will study console applications (programs that display text to the screen without using a GUI). You will also learn memory concepts, arithmetic, decision making and how to use a dialog to display a message.
2.8 Web Resources msdn.microsoft.com/vs2005
Microsoft’s Visual Studio site provides news, documentation, downloads and other resources. lab.msdn.microsoft.com/vs2005/
This site provides information on the newest release of Visual Studio, including downloads, community information and resources. www.worldofdotnet.net
This site offers Visual Studio news and links to newsgroups and other resources. www.c-sharpcorner.com
This site contains articles, reviews of books and software, documentation, downloads, links and searchable information on C#. www.devx.com/dotnet
This site has a dedicated zone for .NET developers that contains articles, opinions, newsgroups, code, tips and other resources discussing Visual Studio 2005. www.csharp-station.com
This site provides articles on Visual C#, especially 2005 updates. The site also includes tutorials, downloads and other resources.
3 Introduction to C# Applications What’s in a name? That which we call a rose by any other name would smell as sweet. —William Shakespeare
When faced with a decision, I always ask, “What would be the most fun?”
OBJECTIVES In this chapter you will learn: I
To write simple C# applications using code rather than visual programming.
I
To write statements that input and output data to the screen.
I
To declare and use data of various types.
I
To store and retrieve data from memory.
I
To use arithmetic operators.
I
To determine the order in which operators are applied.
I
To write decision-making statements.
I
To use relational and equality operators.
I
To use message dialogs to display messages.
—Peggy Walker
“Take some more tea,” the March Hare said to Alice, very earnestly. “I’ve had nothing yet.” Alice replied in an offended tone, “So I can’t take more.” “You mean you can’t take less,” said the Hatter, “it’s very easy to take more than nothing.” —Lewis Carroll
Outline
52
Chapter 3
Introduction to C# Applications
3.1 3.2 3.3 3.4 3.5 3.6 3.7 3.8 3.9 3.10
Introduction A Simple C# Application: Displaying a Line of Text Creating Your Simple Application in Visual C# Express Modifying Your Simple C# Application Formatting Text with Console.Write and Console.WriteLine Another C# Application: Adding Integers Memory Concepts Arithmetic Decision Making: Equality and Relational Operators (Optional) Software Engineering Case Study: Examining the ATM Requirements Document 3.11 Wrap-Up
3.1 Introduction We now introduce C# application programming, which facilitates a disciplined approach to application design. Most of the C# applications you will study in this book process information and display results. In this chapter, we introduce console applications—these input and output text in a console window. In Microsoft Windows 95/98/ME, the console window is the MS-DOS prompt. In other versions of Microsoft Windows, the console window is the Command Prompt. We begin with several examples that simply display messages on the screen. We then demonstrate an application that obtains two numbers from a user, calculates their sum and displays the result. You will learn how to perform various arithmetic calculations and save the results for later use. Many applications contain logic that requires the application to make decisions—the last example in this chapter demonstrates decision-making fundamentals by showing you how to compare numbers and display messages based on the comparison results. For example, the application displays a message indicating that two numbers are equal only if they have the same value. We carefully analyze each example one line at a time.
3.2 A Simple C# Application: Displaying a Line of Text Let us consider a simple application that displays a line of text. (Later in this section, we discuss how to compile and run an application.) The application and its output are shown in Fig. 3.1. The application illustrates several important C# language features. For your convenience, each program we present in this book includes line numbers, which are not part of actual C# code. In Section 3.3, we show how to display line numbers for your C# code in the IDE. We will soon see that line 10 does the real work of the application—namely, displaying the phrase Welcome to C# Programming! on the screen. We now consider each line of the application. Line 1 // Fig. 3.1: Welcome1.cs
3.2 A Simple C# Application: Displaying a Line of Text
1 2 3 4 5 6 7 8 9 10 11 12
53
// Fig. 3.1: Welcome1.cs // Text-printing application. using System; public class Welcome1 { // Main method begins execution of C# application public static void Main( string[] args ) { Console.WriteLine( "Welcome to C# Programming!" ); } // end method Main } // end class Welcome1
Welcome to C# Programming!
Fig. 3.1 | Text-printing application. begins with //, indicating that the remainder of the line is a comment. We begin every application with a comment indicating the figure number and the name of the file in which the application is stored. A comment that begins with // is called a single-line comment, because it terminates at the end of the line on which it appears. A // comment also can begin in the middle of a line and continue until the end of that line (as in lines 7, 11 and 12). Delimited comments such as /* This is a delimited comment. It can be split over many lines */
can be spread over several lines. This type of comment begins with the delimiter /* and ends with the delimiter */. All text between the delimiters is ignored by the compiler. C# incorporated delimited comments and single-line comments from the C and C++ programming languages, respectively. In this book, we use only single-line comments in our programs. Line 2 // Text-printing application.
is a single-line comment that describes the purpose of the application. Line 3 using System;
is a using directive that helps the compiler locate a class that is used in this application. A great strength of C# is its rich set of predefined classes that you can reuse rather than “reinventing the wheel.” These classes are organized under namespaces—named collections of related classes. Collectively, .NET’s namespaces are referred to as the .NET Framework Class Library (FCL). Each using directive identifies predefined classes that a C# application should be able to use. The using directive in line 3 indicates that this example uses classes from the System namespace, which contains the predefined Console class (discussed shortly) used in Line 10, and many other useful classes.
54
Chapter 3
Introduction to C# Applications
Common Programming Error 3.1 All using directives must appear before any other code (except comments) in a C# source code file; otherwise a compilation error occurs. 3.1
Error-Prevention Tip 3.1 Forgetting to include a using directive for a class used in your application typically results in a compilation error containing a message such as "The name 'Console' does not exist in the current context." When this occurs, check that you provided the proper using directives and that the names in the using directives are spelled correctly, including proper use of uppercase and lowercase letters. 3.1
For each new .NET class we use, we indicate the namespace in which it is located. This information is important because it helps you locate descriptions of each class in the .NET documentation. A Web-based version of this documentation can be found at msdn2.microsoft.com/en-us/library/ms229335
This can also be found in the Visual C# Express documentation under the Help menu. You can also place the cursor on the name of any .NET class or method, then press the F1 key to get more information. Line 4 is simply a blank line. Together, blank lines, space characters and tab characters are known as whitespace. (Space characters and tabs are known specifically as whitespace characters.) Whitespace is ignored by the compiler. In this and the next several chapters, we discuss conventions for using whitespace to enhance application readability. Line 5 public class Welcome1
begins a class declaration for the class Welcome1. Every application consists of at least one class declaration that is defined by you—the programmer. These are known as user-defined classes. The class keyword introduces a class declaration and is immediately followed by the class name (Welcome1). Keywords (sometimes called reserved words) are reserved for use by C# and are always spelled with all lowercase letters. The complete list of C# keywords is shown in Fig. 3.2. C# Keywords abstract
as
base
bool
break
byte
case
catch
char
checked
class
const
continue
decimal
default
delegate
do
double
else
enum
event
explicit
extern
false
finally
fixed
float
for
foreach
goto
if
implicit
in
int
interface
internal
is
lock
long
namespace
new
null
object
operator
out
Fig. 3.2 | C# keywords. (Part 1 of 2.)
3.2 A Simple C# Application: Displaying a Line of Text
55
C# Keywords override
params
private
protected
public
readonly
ref
return
sbyte
sealed
short
sizeof
stackalloc
static
string
struct
switch
this
throw
true
try
typeof
uint
ulong
unchecked
unsafe
ushort
using
virtual
void
volatile
while
Fig. 3.2 | C# keywords. (Part 2 of 2.) By convention, all class names begin with a capital letter and capitalize the first letter of each word they include (e.g., SampleClassName). This is frequently referred to as Pascal casing. A class name is an identifier—a series of characters consisting of letters, digits and underscores ( _ ) that does not begin with a digit and does not contain spaces. Some valid identifiers are Welcome1, identifier, _value and m_inputField1. The name 7button is not a valid identifier because it begins with a digit, and the name input field is not a valid identifier because it contains a space. Normally, an identifier that does not begin with a capital letter is not the name of a class. C# is case sensitive—that is, uppercase and lowercase letters are distinct, so a1 and A1 are different (but both valid) identifiers. Identifiers may also be preceded by the @ character. This indicates that a word should be interpreted as an identifier, even if it is a keyword (e.g. @int). This allows C# code to use code written in other .NET languages where an identifier might have the same name as a C# keyword.
Good Programming Practice 3.1 By convention, always begin a class name’s identifier with a capital letter and start each subsequent word in the identifier with a capital letter. 3.1
Common Programming Error 3.2 C# is case sensitive. Not using the proper uppercase and lowercase letters for an identifier normally causes a compilation error. 3.2
In Chapters 3–8, every class we define begins with the keyword public. For now, we will simply require this keyword. You will learn more about public and non-public classes in Chapter 9. When you save your public class declaration in a file, the file name is usually the class name followed by the .cs filename extension. For our application, the file name is Welcome1.cs.
Good Programming Practice 3.2 By convention, a file that contains a single public class should have a name that is identical to the class name (plus the .cs extension) in terms of both spelling and capitalization. Naming your files in this way makes it easier for other programmers (and you) to determine where the classes of an application are located. 3.2
A left brace (in line 6 in Fig. 3.1), {, begins the body of every class declaration. A corresponding right brace (in line 12), }, must end each class declaration. Note that lines 7–11
56
Chapter 3
Introduction to C# Applications
are indented. This indentation is one of the spacing conventions mentioned earlier. We define each spacing convention as a Good Programming Practice.
Error-Prevention Tip 3.2 Whenever you type an opening left brace, {, in your application, immediately type the closing right brace, }, then reposition the cursor between the braces and indent to begin typing the body. This practice helps prevent errors due to missing braces. 3.2
Good Programming Practice 3.3 Indent the entire body of each class declaration one “level” of indentation between the left and right braces that delimit the body of the class. This format emphasizes the class declaration’s structure and makes it easier to read. You can let the IDE format your code by selecting Edit > Advanced > Format Document. 3.3
Good Programming Practice 3.4 Set a convention for the indent size you prefer, then uniformly apply that convention. The Tab key may be used to create indents, but tab stops vary among text editors. We recommend using three spaces to form each level of indentation. We show how to do this in Section 3.3. 3.4
Common Programming Error 3.3 It is a syntax error if braces do not occur in matching pairs.
3.3
Line 7 // Main method begins execution of C# application
is a comment indicating the purpose of lines 8–11 of the application. Line 8 public static void Main( string[] args )
is the starting point of every application. The parentheses after the identifier Main indicate that it is an application building block called a method. Class declarations normally contain one or more methods. Method names usually follow the same Pascal casing capitalization conventions used for class names. For each application, exactly one of the methods in a class must be called Main (which is typically defined as shown in line 8); otherwise, the application will not execute. Methods are able to perform tasks and return information when they complete their tasks. Keyword void (line 8) indicates that this method will not return any information after it completes its task. Later, we will see that many methods do return information. You will learn more about methods in Chapters 4 and 7. For now, simply mimic Main’s first line in your applications. The left brace in line 9 begins the body of the method declaration. A corresponding right brace must end the method’s body (line 11 of Fig. 3.1). Note that line 10 in the body of the method is indented between the braces.
Good Programming Practice 3.5 As with class declarations, indent the entire body of each method declaration one “level” of indentation between the left and right braces that define the method body. This format makes the structure of the method stand out and makes the method declaration easier to read. 3.5
3.2 A Simple C# Application: Displaying a Line of Text
57
Line 10 Console.WriteLine( "Welcome to C# Programming!" );
instructs the computer to perform an action—namely, to print (i.e., display on the screen) the string of characters contained between the double quotation marks. A string is sometimes called a character string, a message or a string literal. We refer to characters between double quotation marks simply as strings. Whitespace characters in strings are not ignored by the compiler. Class Console provides standard input/output capabilities that enable applications to read and display text in the console window from which the application executes. The Console.WriteLine method displays (or prints) a line of text in the console window. The string in the parentheses in line 10 is the argument to the method. Method Console.WriteLine performs its task by displaying (also called outputting) its argument in the console window. When Console.WriteLine completes its task, it positions the screen cursor (the blinking symbol indicating where the next character will be displayed) at the beginning of the next line in the console window. (This movement of the cursor is similar to what happens when a user presses the Enter key while typing in a text editor—the cursor moves to the beginning of the next line in the file.) The entire line 10, including Console.WriteLine, the parentheses, the argument "Welcome to C# Programming!" in the parentheses and the semicolon (;), is called a statement. Each statement ends with a semicolon. When the statement in line 10 executes, it displays the message Welcome to C# Programming! in the console window. A method is typically composed of one or more statements that perform the method’s task.
Common Programming Error 3.4 Omitting the semicolon at the end of a statement is a syntax error.
3.4
Some programmers find it difficult when reading or writing an application to match the left and right braces ({ and }) that delimit the body of a class declaration or a method declaration. For this reason, some programmers include a comment after each closing right brace (}) that ends a method declaration and after each closing right brace that ends a class declaration. For example, line 11 } // end method Main
specifies the closing right brace of method Main, and line 12 } // end class Welcome1
specifies the closing right brace of class Welcome1. Each of these comments indicates the method or class that the right brace terminates. Visual Studio can help you locate matching braces in your code. Simply place the cursor next to one brace and Visual Studio will highlight the other.
Good Programming Practice 3.6 Following the closing right brace of a method body or class declaration with a comment indicating the method or class declaration to which the brace belongs improves application readability. 3.6
58
Chapter 3
Introduction to C# Applications
3.3 Creating Your Simple Application in Visual C# Express Now that we have presented our first console application (Fig. 3.1), we provide a step-bystep explanation of how to compile and execute it using Visual C# Express.
Creating the Console Application After opening Visual C# 2005 Express, select File > New Project… to display the New Project dialog (Fig. 3.3), then select the Console Application template. In the dialog’s Name field, type Welcome1. Click OK to create the project. The IDE now contains the open console application, as shown in Fig. 3.4. Note that the editor window already contains some code provided by the IDE. Some of this code is similar to that of Fig. 3.1. Some is not, and uses features that we have not yet discussed. The IDE inserts this extra code to help organize the application and to provide access to some common classes in the .NET Framework Class Library—at this point in the book, this code is neither required nor relevant to the discussion of this application; delete all of it. The code coloring scheme used by the IDE is called syntax-color highlighting and helps you visually differentiate application elements. Keywords appear in blue, and other text is black. When present, comments are green. In this book, we syntax shade our code similarly—bold italic for keywords, italic for comments, bold gray for literals and constants, and black for other text. One example of a literal is the string passed to Console.WriteLine in line 10 of Fig. 3.1. You can customize the colors shown in the code editor by selecting Tools > Options…. This displays the Options dialog (Fig. 3.5). Then click the plus sign, +, next to Environment and select Fonts and Colors. Here you can change the colors for various code elements.
Project name
Fig. 3.3 | Creating a Console Application with the New Project dialog.
3.3 Creating Your Simple Application in Visual C# Express
59
Editor window
Fig. 3.4 | IDE with an open console application.
Fig. 3.5 | Modifying the IDE settings. Modifying the Editor Settings to Display Line Numbers Visual C# Express provides many ways to personalize your coding experience. In this step, you will change the settings so that your code matches that of this book. To have the IDE
60
Chapter 3
Introduction to C# Applications
display line numbers, select Tools > Options…. In the dialog that appears, click the Show all settings checkbox on the lower left of the dialog, then click the plus sign next to Text Editor in the left pane and select All Languages. On the right, check the Line Numbers check box. Keep the Options dialog open.
Setting Code Indentation to Three Spaces per Indent In the Options dialog that you opened in the previous step (Fig. 3.5), click on the plus sign next to C# in the left pane and select Tabs. Enter 3 for both the Tab Size and Indent Size fields. Any new code you add will now use three spaces for each level of indentation. Click OK to save your settings, close the dialog and return to the editor window. Changing the Name of the Application File For applications we create in this book, we change the default name of the application file (i.e., Program.cs) to a more descriptive name. To rename the file, click Program.cs in the Solution Explorer window. This displays the application file’s properties in the Properties window (Fig. 3.6). Change the File Name property to Welcome1.cs. Writing Code In the editor window (Fig. 3.4), type the code from Fig. 3.1. After you type (in line 10) the class name and a dot (i.e., Console.), a window containing a scrollbar is displayed (Fig. 3.7). This IDE feature, called IntelliSense, lists a class’s members, which include method names. As you type characters, Visual C# Express highlights the first member that matches all the characters typed, then displays a tool tip containing a description of that member. You can either type the complete member name (e.g., WriteLine), double click the member name in the member list or press the Tab key to complete the name. Once the complete name is provided, the IntelliSense window closes. Solution Explorer
Click Program.cs to display its properties Properties window
File Name property
Type Welcome1.cs here to rename the file
Fig. 3.6 | Renaming the program file in the Properties window.
3.3 Creating Your Simple Application in Visual C# Express
61
Partially-typed member Member list Highlighted member
Tool tip describes highlighted member
Fig. 3.7 | IntelliSense feature of Visual C# Express. When you type the open parenthesis character, (, after Console.WriteLine, the Parameter Info window is displayed (Fig. 3.8). This window contains information about the method’s parameters. As you will learn in Chapter 7, there can be several versions of a method—that is, a class can define several methods that have the same name as long as they have different numbers and/or types of parameters. These methods normally all perform similar tasks. The Parameter Info window indicates how many versions of the selected method are available and provides up and down arrows for scrolling through the different versions. For example, there are 19 versions of the WriteLine method—we use one of these 19 versions in our application. The Parameter Info window is one of the many features provided by the IDE to facilitate application development. In the next several chapters, you will learn more about the information displayed in these windows. The Parameter Info window is especially helpful when you want to see the different ways in which a method can be used. From the code in Fig. 3.1, we already know that we intend to display one string with WriteLine, so because you know exactly which version of WriteLine you want to use, you can simply close the Parameter Info window by pressing the Esc key. Down arrow
Up arrow
Fig. 3.8 | Parameter Info window.
Parameter Info window
62
Chapter 3
Introduction to C# Applications
Saving the Application Select File > Save All to display the Save Project dialog (Fig. 3.9). In the Location text box, specify the directory where you want to save this project. We choose to save the project in the MyProjects directory on the C: drive. Select the Create directory for solution checkbox (to enable Visual Studio to create the directory if it does not already exist), and click Save. Compiling and Running the Application You are now ready to compile and execute your application. Depending on the type of application, the compiler may compile the code into files with a .exe (executable) extension, a .dll (dynamic link library) extension or one of several other extensions. Such files are called assemblies and are the packaging units for compiled C# code. These assemblies contain the Microsoft Intermediate Language (MSIL) code for the application. To compile the application, select Build > Build Solution. If the application contains no syntax errors, your console application will compile into an executable file (named Welcome1.exe, in the project’s directory). To execute this console application (i.e., Welcome1.exe), select Debug > Start Without Debugging (or type F5), which invokes the Main method (Fig. 3.1). The statement in line 10 of Main displays Welcome to C# Programming!. Figure 3.10 shows the results of executing this application. Note that the results are displayed in a console window. Leave the application open in Visual C# Express; we will go back to it later in this section.
Fig. 3.9 | Save Project dialog.
Console window
Fig. 3.10 | Executing the application shown in Fig. 3.1.
3.3 Creating Your Simple Application in Visual C# Express
63
Running the Application from the Command Prompt As we mentioned at the beginning of the chapter, you can execute applications outside the IDE in a Command Prompt. This is useful when you simply want to run an application rather than open it for modification. To open the Command Prompt, select Start > All Programs > Accessories > Command Prompt. [Note: Windows 2000 users should replace All Programs with Programs.] The window (Fig. 3.11) displays copyright information, followed by a prompt that indicates the current directory. By default, the prompt specifies the current user’s directory on the local machine (in our case, C:\Documents and Settings\deitel). On your machine, the folder name deitel will be replaced with your username. Enter the command cd (which stands for “change directory”), followed by the /d flag (to change drives if necessary), then the directory where the application’s .exe file is located (i.e., the Release directory of your application). For example, the command cd /d C:\MyProjects\Welcome1\Welcome1\bin\Release (Fig. 3.12) changes the current directory, to the Welcome1 application’s Release directory on the C: drive. The next prompt displays the new directory. After changing to the proper directory, you can run the compiled application by entering the name of the .exe file (i.e., Welcome1). The application will run to completion, then the prompt will display again, awaiting the next command. To close the Command Prompt, type exit (Fig. 3.12) and press Enter. Note that Visual C# 2005 Express maintains a Debug and a Release directory in each project’s bin directory. The Debug directory contains a version of the application that can be used with the debugger (see Appendix C, Using the Visual Studio® 2005 Debugger). The Release directory contains an optimized version that you could provide to your clients. In the complete Visual Studio 2005, you can select the specific version you wish to build from the Solution Configurations drop-down list in the toolbars at the top of the IDE. The default is the Debug version. [Note: Many environments show Command Prompt windows with black backgrounds and white text. We adjusted these settings in our environment to make our screen captures more readable.] Syntax Errors, Error Messages and the Error List Window Go back to the application in Visual C# Express. When you type a line of code and press the Enter key, the IDE responds either by applying syntax-color highlighting or by generating a syntax error, which indicates a violation of Visual C#’s rules for creating correct Default prompt displays when Command Prompt is opened
User enters the next command here
Fig. 3.11 | Executing the application shown in Fig. 3.1 from a Command Prompt window.
64
Chapter 3
Introduction to C# Applications
Updated prompt showing the new current directory
Application’s output
Type this to change to the application’s directory
Closes the Command Prompt window
Type this to run the Welcome1.exe application
Fig. 3.12 | Executing the application shown in Fig. 3.1 from a Command Prompt window. applications (i.e., one or more statements are not written correctly). Syntax errors occur for various reasons, such as missing parentheses and misspelled keywords. When a syntax error occurs, the IDE underlines the error in red and provides a description of the error in the Error List window (Fig. 3.13). If the Error List window is Intentionally omitted parenthesis character (syntax error)
Error description(s)
Error List window
Fig. 3.13 | Syntax errors indicated by the IDE.
Red underline indicates a syntax error
3.4 Modifying Your Simple C# Application
65
not visible in the IDE, select View > Error List to display it. In Figure 3.13, we intentionally omitted the first parenthesis in line 10. The first error contains the text "; expected." and specifies that the error is in column 25 of line 10. This error message appears when the compiler thinks that the line contains a complete statement, followed by a semicolon, and the beginning of another statement. The second error contains the same text, but specifies that this error is in column 54 of line 10 because the compiler thinks that this is the end of the second statement. The third error has the text "Invalid expression term ’)’" because the compiler is confused by the unmatched right parenthesis. Although we are attempting to include only one statement in line 10, the missing left parenthesis causes the compiler to incorrectly assume that there is more than one statement on that line, to misinterpret the right parenthesis and to generate three error messages.
Error-Prevention Tip 3.3 One syntax error can lead to multiple entries in the Error List window. Each error that you address could eliminate several subsequent error messages when you recompile your application. So when you see an error you know how to fix, correct it and recompile—this may make several other errors disappear. 3.3
3.4 Modifying Your Simple C# Application This section continues our introduction to C# programming with two examples that modify the example of Fig. 3.1 to print text on one line by using several statements and to print text on several lines by using only one statement.
Displaying a Single Line of Text with Multiple Statements "Welcome to C# Programming!" can be displayed several ways. Class Welcome2, shown in Fig. 3.14, uses two statements to produce the same output as that shown in Fig. 3.1. From this point forward, we highlight the new and key features in each code listing, as shown in lines 10–11 of Fig. 3.14. 1 2 3 4 5 6 7 8 9 10 11 12 13
// Fig. 3.14: Welcome2.cs // Printing one line of text with multiple statements. using System; public class Welcome2 { // Main method begins execution of C# application public static void Main( string[] args ) { Console.Write( "Welcome to " ); Console.WriteLine( "C# Programming!" ); } // end method Main } // end class Welcome2
Welcome to C# Programming!
Fig. 3.14 | Printing one line of text with multiple statements.
66
Chapter 3
Introduction to C# Applications
The application is almost identical to Fig. 3.1. We discuss only the changes here. Line 2 // Printing one line of text with multiple statements.
is a comment stating the purpose of this application. Line 5 begins the Welcome2 class declaration. Lines 10–11 of method Main Console.Write( "Welcome to " ); Console.WriteLine( "C# Programming!" );
display one line of text in the console window. The first statement uses Console’s method Write to display a string. Unlike WriteLine, after displaying its argument, Write does not position the screen cursor at the beginning of the next line in the console window—the next character the application displays will appear immediately after the last character that Write displays. Thus, line 11 positions the first character in its argument (the letter “C”) immediately after the last character that line 10 displays (the space character before the string’s closing double-quote character). Each Write statement resumes displaying characters from where the last Write statement displayed its last character.
Displaying Multiple Lines of Text with a Single Statement A single statement can display multiple lines by using newline characters, which indicate to Console methods Write and WriteLine when they should position the screen cursor to the beginning of the next line in the console window. Like space characters and tab characters, newline characters are whitespace characters. The application of Fig. 3.15 outputs four lines of text, using newline characters to indicate when to begin each new line. Most of the application is identical to the applications of Fig. 3.1 and Fig. 3.14, so we discuss only the changes here. Line 2 // Printing multiple lines with a single statement.
is a comment stating the purpose of this application. Line 5 begins the Welcome3 class declaration. 1 2 3 4 5 6 7 8 9 10 11 12
// Fig. 3.15: Welcome3.cs // Printing multiple lines with a single statement. using System; public class Welcome3 { // Main method begins execution of C# application public static void Main( string[] args ) { Console.WriteLine( "Welcome\nto\nC#\nProgramming!" ); } // end method Main } // end class Welcome3
Welcome to C# Programming!
Fig. 3.15 | Printing multiple lines with a single statement.
3.5 Formatting Text with Console.Write and Console.WriteLine
67
Line 10 Console.WriteLine( "Welcome\nto\nC#\nProgramming!" );
displays four separate lines of text in the console window. Normally, the characters in a string are displayed exactly as they appear in the double quotes. Note, however, that the two characters \ and n (repeated three times in the statement) do not appear on the screen. The backslash (\) is called an escape character. It indicates to C# that a “special character” is in the string. When a backslash appears in a string of characters, C# combines the next character with the backslash to form an escape sequence. The escape sequence \n represents the newline character. When a newline character appears in a string being output with Console methods, the newline character causes the screen cursor to move to the beginning of the next line in the console window. Figure 3.16 lists several common escape sequences and describes how they affect the display of characters in the console window.
3.5 Formatting Text with Console.Write and Console.WriteLine
Console methods Write and WriteLine also have the capability to display formatted data. Figure 3.17 outputs the strings "Welcome to" and "C# Programming!" with WriteLine.
Line 10 Console.WriteLine( "{0}\n{1}", "Welcome to", "C# Programming!" );
calls method Console.WriteLine to display the application’s output. The method call specifies three arguments. When a method requires multiple arguments, the arguments are separated with commas (,)—this is known as a comma-separated list.
Good Programming Practice 3.7 Place a space after each comma (,) in an argument list to make applications more readable.
Escape sequence
Description
\n
Newline. Positions the screen cursor at the beginning of the next line.
\t
Horizontal tab. Moves the screen cursor to the next tab stop.
\r
Carriage return. Positions the screen cursor at the beginning of the current line—does not advance the cursor to the next line. Any characters output after the carriage return overwrite the characters previously output on that line.
\\
Backslash. Used to place a backslash character in a string.
\"
Double quote. Used to place a double-quote character (") in a string. For example, Console.Write( "\"in quotes\"" );
displays "in quotes"
Fig. 3.16 | Some common escape sequences.
3.7
68 1 2 3 4 5 6 7 8 9 10 11 12
Chapter 3
Introduction to C# Applications
// Fig. 3.17: Welcome4.cs // Printing multiple lines of text with string formatting. using System; public class Welcome4 { // Main method begins execution of C# application public static void Main( string[] args ) { Console.WriteLine( "{0}\n{1}", "Welcome to", "C# Programming!" ); } // end method Main } // end class Welcome4
Welcome to C# Programming!
Fig. 3.17 | Printing multiple lines of text with string formatting. Remember that all statements end with a semicolon (;). Therefore, line 10 represents only one statement. Large statements can be split over many lines, but there are some restrictions.
Common Programming Error 3.5 Splitting a statement in the middle of an identifier or a string is a syntax error.
3.5
Method WriteLine’s first argument is a format string that may consist of fixed text and format items. Fixed text is output by WriteLine as we demonstrated in Fig. 3.1. Each format item is a placeholder for a value. Format items also may include optional formatting information. Format items are enclosed in curly braces and contain a sequence of characters that tell the method which argument to use and how to format it. For example, the format item {0} is a placeholder for the first additional argument (because C# starts counting from 0), {1} is a placeholder for the second, etc. The format string in line 10 specifies that WriteLine should output two arguments and that the first one should be followed by a newline character. So this example substitutes "Welcome to" for the {0} and "C# Programming!" for the {1}. The output shows that two lines of text are displayed. Note that because braces in a formatted string normally indicate a placeholder for text substitution, you must type two left braces ({{) or two right braces (}}) to insert a single left or right brace into a formatted string, respectively. We introduce additional formatting features as they are needed in our examples.
3.6 Another C# Application: Adding Integers Our next application reads (or inputs) two integers (whole numbers, like –22, 7, 0 and 1024) typed by a user at the keyboard, computes the sum of the values and displays the result. This application must keep track of the numbers supplied by the user for the calculation later in the application. Applications remember numbers and other data in the computer’s memory and access that data through application elements called variables.
3.6 Another C# Application: Adding Integers
69
The application of Fig. 3.18 demonstrates these concepts. In the sample output, we highlight data the user enters at the keyboard in bold. Lines 1–2 // Fig. 3.18: Addition.cs // Displaying the sum of two numbers input from the keyboard.
state the figure number, file name and purpose of the application. Line 5 public class Addition
begins the declaration of class Addition. Remember that the body of each class declaration starts with an opening left brace (line 6), and ends with a closing right brace (line 26). The application begins execution with method Main (lines 8–25). The left brace (line 9) marks the beginning of Main’s body, and the corresponding right brace (line 25) marks the end of Main’s body. Note that method Main is indented one level within the body of class Addition and that the code in the body of Main is indented another level for readability.
// Fig. 3.18: Addition.cs // Displaying the sum of two numbers input from the keyboard. using System; public class Addition { // Main method begins execution of C# application public static void Main( string[] args ) { int number1; // declare first number to add int number2; // declare second number to add int sum; // declare sum of number1 and number2 Console.Write( "Enter first integer: " ); // prompt user // read first number from user number1 = Convert.ToInt32( Console.ReadLine() ); Console.Write( "Enter second integer: " ); // prompt user // read second number from user number2 = Convert.ToInt32( Console.ReadLine() ); sum = number1 + number2; // add numbers Console.WriteLine( "Sum is {0}", sum ); // display sum } // end method Main } // end class Addition
Enter first integer: 45 Enter second integer: 72 Sum is 117
Fig. 3.18 | Displaying the sum of two numbers input from the keyboard.
70
Chapter 3
Introduction to C# Applications
Line 10 int number1; // declare first number to add
is a variable declaration statement (also called a declaration) that specifies the name and type of a variable (number1) that is used in this application. A variable’s name enables the application to access the value of the variable in memory—the name can be any valid identifier. (See Section 3.2 for identifier naming requirements.) A variable’s type specifies what kind of information is stored at that location in memory. Like other statements, declaration statements end with a semicolon (;). The declaration in line 10 specifies that the variable named number1 is of type int— it will hold integer values (whole numbers such as 7, –11, 0 and 31914). The range of values for an int is –2,147,483,648 (int.MinValue) to +2,147,483,647 (int.MaxValue). We will soon discuss types float, double and decimal, for specifying real numbers, and type char, for specifying characters. Real numbers contain decimal points, as in 3.4, 0.0 and –11.19. Variables of type float and double store approximations of real numbers in memory. Variables of type decimal store real numbers precisely (to 28–29 significant digits), so decimal variables are often used with monetary calculations. Variables of type char represent individual characters, such as an uppercase letter (e.g., A), a digit (e.g., 7), a special character (e.g., * or %) or an escape sequence (e.g., the newline character, \n). Types such as int, float, double, decimal and char are often called simple types. Simple-type names are keywords and must appear in all lowercase letters. Appendix L summarizes the characteristics of the thirteen simple types (bool, byte, sbyte, char, short, ushort, int, uint, long, ulong, float, double, and decimal). The variable declaration statements at lines 11–12 int number2; // declare second number to add int sum; // declare sum of number1 and number2
similarly declare variables number2 and sum to be of type int. Variable declaration statements can be split over several lines, with the variable names separated by commas (i.e., a comma-separated list of variable names). Several variables of the same type may be declared in one declaration or in multiple declarations. For example, lines 10–12 can also be written as follows: int number1, // declare first number to add number2, // declare second number to add sum; // declare sum of number1 and number2
Good Programming Practice 3.8 Declare each variable on a separate line. This format allows a comment to be easily inserted next to each declaration. 3.8
Good Programming Practice 3.9 Choosing meaningful variable names helps code to be self-documenting (i.e., one can understand the code simply by reading it rather than by reading documentation manuals or viewing an excessive number of comments). 3.9
3.6 Another C# Application: Adding Integers
71
Good Programming Practice 3.10 By convention, variable-name identifiers begin with a lowercase letter, and every word in the name after the first word begins with a capital letter. This naming convention is known as camel casing. 3.10
Line 14 Console.Write( "Enter first integer: " ); // prompt user
uses Console.Write to display the message "Enter first integer: ". This message is called a prompt because it directs the user to take a specific action. Line 16 number1 = Convert.ToInt32( Console.ReadLine() );
works in two steps. First, it calls the Console’s ReadLine method. This method waits for the user to type a string of characters at the keyboard and press the Enter key to submit the string to the application. Then, the string is used as an argument to the Convert class’s ToInt32 method, which converts this sequence of characters into data of an type int. As we mentioned earlier in this chapter, some methods perform a task then return the result of that task. In this case, method ToInt32 returns the int representation of the user’s input. Technically, the user can type anything as the input value. ReadLine will accept it and pass it off to the ToInt32 method. This method assumes that the string contains a valid integer value. In this application, if the user types a noninteger value, a runtime logic error will occur and the application will terminate. Chapter 12, Exception Handling, discusses how to make your applications more robust by enabling them to handle such errors and continue executing. This is also known as making your application fault tolerant. In line 16, the result of the call to method ToInt32 (an int value) is placed in variable number1 by using the assignment operator, =. The statement is read as “number1 gets the value returned by Convert.ToInt32.” Operator = is called a binary operator because it has two operands—number1 and the result of the method call Convert.ToInt32. This statement is called an assignment statement because it assigns a value to a variable. Everything to the right of the assignment operator, =, is always evaluated before the assignment is performed.
Good Programming Practice 3.11 Place spaces on either side of a binary operator to make it stand out and make the code more readable. 3.11
Line 18 Console.Write( "Enter second integer: " ); // prompt user
prompts the user to enter the second integer. Line 20 number2 = Convert.ToInt32( Console.ReadLine() );
reads a second integer and assigns it to the variable number2. Line 22 sum = number1 + number2; // add numbers
is an assignment statement that calculates the sum of the variables number1 and number2 and assigns the result to variable sum by using the assignment operator, =. Most calcula-
72
Chapter 3
Introduction to C# Applications
tions are performed in assignment statements. When the application encounters the addition operator, it uses the values stored in the variables number1 and number2 to perform the calculation. In the preceding statement, the addition operator is a binary operator— its two operands are number1 and number2. Portions of statements that contain calculations are called expressions. In fact, an expression is any portion of a statement that has a value associated with it. For example, the value of the expression number1 + number2 is the sum of the numbers. Similarly, the value of the expression Console.ReadLine() is the string of characters typed by the user. After the calculation has been performed, line 24 Console.WriteLine( "Sum is {0}", sum ); // display sum
uses method Console.WriteLine to display the sum. The format item {0} is a placeholder for the first argument after the format string. Other than the {0} format item, the remaining characters in the format string are all fixed text. So method WriteLine displays "Sum is ", followed by the value of sum (in the position of the {0} format item) and a newline. Calculations can also be performed inside output statements. We could have combined the statements in lines 22 and 24 into the statement Console.WriteLine( "Sum is {0}", ( number1 + number2 ) );
The parentheses around the expression number1 + number2 are not required—they are included to emphasize that the value of the expression number1 + number2 is output in the position of the {0} format item.
3.7 Memory Concepts Variable names such as number1, number2 and sum actually correspond to locations in the computer’s memory. Every variable has a name, a type, a size and a value. In the addition application of Fig. 3.18, when the statement (line 16) number1 = Convert.ToInt32( Console.ReadLine() );
executes, the number typed by the user is placed into a memory location to which the name number1 has been assigned by the compiler. Suppose that the user enters 45. The computer places that integer value into location number1, as shown in Fig. 3.19. Whenever a value is placed in a memory location, the value replaces the previous value in that location and the previous value is lost. When the statement (line 20) number2 = Convert.ToInt32( Console.ReadLine() );
executes, suppose that the user enters 72. The computer places that integer value into location number2. The memory now appears as shown in Fig. 3.20.
number1
45
Fig. 3.19 | Memory location showing the name and value of variable number1.
3.8 Arithmetic
number1
45
number2
72
73
Fig. 3.20 | Memory locations after storing values for number1 and number2. After the application of Fig. 3.18 obtains values for number1 and number2, it adds the values and places the sum into variable sum. The statement (line 22) sum = number1 + number2; // add numbers
performs the addition, then replaces sum’s previous value. After sum has been calculated, memory appears as shown in Fig. 3.21. Note that the values of number1 and number2 appear exactly as they did before they were used in the calculation of sum. These values were used, but not destroyed, as the computer performed the calculation—when a value is read from a memory location, the process is nondestructive.
3.8 Arithmetic Most applications perform arithmetic calculations. The arithmetic operators are summarized in Fig. 3.22. Note the use of various special symbols not used in algebra. The asterisk (*) indicates multiplication, and the percent sign (%) is the remainder operator (called modulus in some languages), which we will discuss shortly. The arithmetic operators in Fig. 3.22 are binary operators—for example, the expression f + 7 contains the binary operator + and the two operands f and 7. Integer division yields an integer quotient—for example, the expression 7 / 4 evaluates to 1, and the expression 17 / 5 evaluates to 3. Any fractional part in integer division is simply discarded (i.e., truncated)—no rounding occurs. C# provides the remainder operator, %, which yields the remainder after division. The expression x % y yields the remainder after x is divided by y. Thus, 7 % 4 yields 3, and 17 % 5 yields 2. This operator is most commonly used with integer operands, but can also be used with floats, doubles, and decimals. In later chapters, we consider several interesting applications of the remainder operator, such as determining whether one number is a multiple of another.
number1
45
number2
72
sum
117
Fig. 3.21 | Memory locations after calculating and storing the sum of number1 and number2.
74
Chapter 3
Introduction to C# Applications
C# operation
Arithmetic operator
Algebraic expression
C# expression
Addition
+
f+7
f + 7
Subtraction
–
p–c
p - c
Multiplication
*
b⋅m
b * m
Division
/
x / y
Remainder
x / y or x-- or x ÷ y y
%
r mod s
r % s
Fig. 3.22 | Arithmetic operators. Arithmetic expressions must be written in straight-line form to facilitate entering applications into the computer. Thus, expressions such as “a divided by b” must be written as a / b, so that all constants, variables and operators appear in a straight line. The following algebraic notation is generally not acceptable to compilers: a--b
Parentheses are used to group terms in C# expressions in the same manner as in algebraic expressions. For example, to multiply a times the quantity b + c, we write a * ( b + c )
If an expression contains nested parentheses, such as ( ( a + b ) * c )
the expression in the innermost set of parentheses (a + b in this case) is evaluated first. C# applies the operators in arithmetic expressions in a precise sequence determined by the following rules of operator precedence, which are generally the same as those followed in algebra (Fig. 3.23): 1. Multiplication, division and remainder operations are applied first. If an expression contains several such operations, the operators are applied from left to right. Multiplication, division and remainder operators have the same level of precedence. 2. Addition and subtraction operations are applied next. If an expression contains several such operations, the operators are applied from left to right. Addition and subtraction operators have the same level of precedence. These rules enable C# to apply operators in the correct order. When we say that operators are applied from left to right, we are referring to their associativity. You will see that some operators associate from right to left. Figure 3.23 summarizes these rules of operator precedence. The table will be expanded as additional operators are introduced. A complete precedence chart is included in Appendix A. Now let us consider several expressions in light of the rules of operator precedence. Each example lists an algebraic expression and its C# equivalent. The following is an example of an arithmetic mean (average) of five terms:
3.8 Arithmetic
Operator(s)
Operation(s)
Order of evaluation (associativity)
Multiplication Division Remainder
If there are several operators of this type, they are evaluated from left to right.
Addition Subtraction
If there are several operators of this type, they are evaluated from left to right.
75
Evaluated first * / %
Evaluated next + -
Fig. 3.23 | Precedence of arithmetic operators. Algebra:
+ b + c + d + em = a----------------------------------------5
C#:
m = ( a + b + c + d + e ) / 5;
The parentheses are required because division has higher precedence than addition. The entire quantity ( a + b + c + d + e ) is to be divided by 5. If the parentheses are erroneously omitted, we obtain a + b + c + d + e / 5, which evaluates as a + b + c + d + --e5
The following is an example of the equation of a straight line: Algebra:
y = mx + b
C#:
y = m * x + b;
No parentheses are required. The multiplication operator is applied first because multiplication has a higher precedence than addition. The assignment occurs last because it has a lower precedence than multiplication or addition. The following example contains remainder (%), multiplication, division, addition and subtraction operations: Algebra:
z = pr%q + w/x – y
C#:
z
= 6
p
* 1
r
% 2
q
+ 4
w
/ 3
x
- y; 5
The circled numbers under the statement indicate the order in which C# applies the operators. The multiplication, remainder and division operations are evaluated first in leftto-right order (i.e., they associate from left to right), because they have higher precedence than addition and subtraction. The addition and subtraction operations are evaluated next. These operations are also applied from left to right.
76
Chapter 3
Introduction to C# Applications
To develop a better understanding of the rules of operator precedence, consider the evaluation of a second-degree polynomial (y = ax 2 + bx + c): y
= 6
a
*
x
*
1
x
2
+
b
4
* 3
x
+ c; 5
The circled numbers indicate the order in which C# applies the operators. The multiplication operations are evaluated first in left-to-right order (i.e., they associate from left to right), because they have higher precedence than addition. The addition operations are evaluated next and are applied from left to right. There is no arithmetic operator for exponentiation in C#, so x 2 is represented as x * x. Section 6.4 shows an alternative for performing exponentiation in C#. Suppose that a, b, c and x in the preceding second-degree polynomial are initialized (given values) as follows: a = 2, b = 3, c = 7 and x = 5. Figure 3.24 illustrates the order in which the operators are applied. As in algebra, it is acceptable to place unnecessary parentheses in an expression to make the expression clearer. These are called redundant parentheses. For example, the preceding assignment statement might be parenthesized to highlight its terms as follows: y = ( a * x * x ) + ( b * x ) + c;
Step 1.
y = 2 * 5 * 5 + 3 * 5 + 7;
(Leftmost multiplication)
2 * 5 is 10
Step 2.
y = 10 * 5 + 3 * 5 + 7;
(Leftmost multiplication)
10 * 5 is 50
Step 3.
y = 50 + 3 * 5 + 7;
(Multiplication before addition)
3 * 5 is 15
Step 4.
y = 50 + 15 + 7;
(Leftmost addition)
50 + 15 is 65
Step 5.
y = 65 + 7;
(Last addition)
65 + 7 is 72
Step 6.
y = 72
(Last operation—place 72 in y)
Fig. 3.24 | Order in which a second-degree polynomial is evaluated.
3.9 Decision Making: Equality and Relational Operators
77
3.9 Decision Making: Equality and Relational Operators A condition is an expression that can be either true or false. This section introduces a simple version of C#’s if statement that allows an application to make a decision based on the value of a condition. For example, the condition “grade is greater than or equal to 60” determines whether a student passed a test. If the condition in an if statement is true, the body of the if statement executes. If the condition is false, the body does not execute. We will see an example shortly. Conditions in if statements can be formed by using the equality operators (==, and !=) and relational operators (>, = and , = and
x > y
x
is greater than y
<
x < y
x
is less than y
>=
x >= y
x
is greater than or equal to y
number2 ) Console.WriteLine( "{0} > {1}", number1, number2 ); if ( number1 = number2 ) Console.WriteLine( "{0} >= {1}", number1, number2 ); } // end method Main } // end class Comparison
Fig. 3.26 | Comparing integers using if statements, equality operators and relational operators. (Part 1 of 2.)
3.9 Decision Making: Equality and Relational Operators
Enter Enter 42 == 42 =
79
first integer: 42 second integer: 42 42 42 42
Enter first integer: 1000 Enter second integer: 2000 1000 != 2000 1000 < 2000 1000 1000 2000 >= 1000
Fig. 3.26 | Comparing integers using if statements, equality operators and relational operators. (Part 2 of 2.)
Lines 18-20 // prompt user and read second number Console.Write( "Enter second integer: " ); number2 = Convert.ToInt32( Console.ReadLine() );
perform the same task, except that the input value is stored in variable number2. Lines 22–23 if ( number1 == number2 ) Console.WriteLine( "{0} == {1}", number1, number2 );
compare the values of the variables number1 and number2 to determine whether they are equal. An if statement always begins with keyword if, followed by a condition in parentheses. An if statement expects one statement in its body. The indentation of the body statement shown here is not required, but it improves the code’s readability by emphasizing that the statement in line 23 is part of the if statement that begins in line 22. Line 23 executes only if the numbers stored in variables number1 and number2 are equal (i.e., the condition is true). The if statements in lines 25–26, 28–29, 31–32, 34–35 and 37–38 compare number1 and number2 with the operators !=, , =, respectively. If the condition in any of the if statements is true, the corresponding body statement executes.
Common Programming Error 3.6 Forgetting the left and/or right parentheses for the condition in an if statement is a syntax error—the parentheses are required. 3.6
80
Chapter 3
Introduction to C# Applications
Common Programming Error 3.7 Confusing the equality operator, ==, with the assignment operator, =, can cause a logic error or a syntax error. The equality operator should be read as “is equal to,” and the assignment operator should be read as “gets” or “gets the value of.” To avoid confusion, some people read the equality operator as “double equals” or “equals equals.” 3.7
Common Programming Error 3.8 It is a syntax error if the operators ==, !=, >= and = and < =, respectively. 3.8
Common Programming Error 3.9 Reversing the operators !=, >= and and ==). Ideally, statements should be kept small, but this is not always possible.
Good Programming Practice 3.13 Place no more than one statement per line in an application. This format enhances readability.
3.13
3.10 Examining the ATM Requirements Document
81
Good Programming Practice 3.14 A lengthy statement can be spread over several lines. If a single statement must be split across lines, choose breaking points that make sense, such as after a comma in a comma-separated list, or after an operator in a lengthy expression. If a statement is split across two or more lines, indent all subsequent lines until the end of the statement. 3.14
Figure 3.27 shows the precedence of the operators introduced in this chapter. The operators are shown from top to bottom in decreasing order of precedence. All these operators, with the exception of the assignment operator, =, associate from left to right. Addition is left associative, so an expression like x + y + z is evaluated as if it had been written as ( x + y ) + z. The assignment operator, =, associates from right to left, so an expression like x = y = 0 is evaluated as if it had been written as x = ( y = 0 ), which, as you will soon see, first assigns the value 0 to variable y and then assigns the result of that assignment, 0, to x.
Good Programming Practice 3.15 Refer to the operator precedence chart (see the complete chart in Appendix A) when writing expressions containing many operators. Confirm that the operations in the expression are performed in the order you expect. If you are uncertain about the order of evaluation in a complex expression, use parentheses to force the order, as you would do in algebraic expressions. Observe that some operators, such as assignment, =, associate from right to left rather than from left to right. 3.15
3.10 (Optional) Software Engineering Case Study: Examining the ATM Requirements Document Now we begin our optional object-oriented design and implementation case study. The Software Engineering Case Study sections at the ends of this and the next several chapters will ease you into object orientation. We will develop software for a simple automated teller machine (ATM) system, providing you with a concise, carefully paced, complete design and implementation experience. In Chapters 4–9 and 11, we will perform the various steps of an object-oriented design (OOD) process using the UML, while relating these steps to the object-oriented concepts discussed in the chapters. Appendix J implements the ATM using the techniques of object-oriented programming (OOP) in C# and presents the complete case study solution. This is not an exercise; rather, it is an end-to-end learning experience that concludes with a detailed walkthrough of the complete C# code that implements our design. It will begin to acquaint you with the kinds of substantial problems encountered in industry and their solutions. Operators *
/
+
-
<
>=
Associativity
Type
left to right
multiplicative
left to right
additive
left to right
relational
left to right
equality
right to left
assignment
Fig. 3.27 | Precedence and associativity of operations discussed.
82
Chapter 3
Introduction to C# Applications
We begin our design process by presenting a requirements document that specifies the overall purpose of the ATM system and what it must do. Throughout the case study, we refer to the requirements document to determine precisely what functionality the system must include.
Requirements Document A small local bank intends to install a new automated teller machine (ATM) to allow users (i.e., bank customers) to perform basic financial transactions (Fig. 3.28). For simplicity, each user can have only one account at the bank. ATM users should be able to view their account balance, withdraw cash (i.e., take money out of an account) and deposit funds (i.e., place money into an account). The user interface of the automated teller machine contains the following hardware components: • a screen that displays messages to the user • a keypad that receives numeric input from the user • a cash dispenser that dispenses cash to the user • a deposit slot that receives deposit envelopes from the user The cash dispenser begins each day loaded with 500 $20 bills. [Note: Owing to the limited scope of this case study, certain elements of the ATM described here simplify various aspects of a real ATM. For example, a real ATM typically contains a device that reads a user’s account number from an ATM card, whereas this ATM asks the user to type an account number on the keypad (which you will simulate with your personal computer’s keypad).
Welcome! Please enter your account number: 12345 Screen Enter your PIN: 54321
Take cash here
Cash Dispenser
Keypad Insert deposit envelope here
Fig. 3.28 | Automated teller machine user interface.
Deposit Slot
3.10 Examining the ATM Requirements Document
83
Also, a real ATM usually prints a paper receipt at the end of a session, but all output from this ATM appears on the screen.] The bank wants you to develop software to perform the financial transactions initiated by bank customers through the ATM. The bank will integrate the software with the ATM’s hardware at a later time. The software should simulate the functionality of the hardware devices (e.g., cash dispenser, deposit slot) in software components, but it need not concern itself with how these devices perform their duties. The ATM hardware has not been developed yet, so instead of writing your software to run on the ATM, you should develop a first version of the software to run on a personal computer. This version should use the computer’s monitor to simulate the ATM’s screen and the computer’s keyboard to simulate the ATM’s keypad. An ATM session consists of authenticating a user (i.e., proving the user’s identity) based on an account number and personal identification number (PIN), followed by creating and executing financial transactions. To authenticate a user and perform transactions, the ATM must interact with the bank’s account information database. [Note: A database is an organized collection of data stored on a computer.] For each bank account, the database stores an account number, a PIN and a balance indicating the amount of money in the account. [Note: The bank plans to build only one ATM, so we do not need to worry about multiple ATMs accessing the database at the same time. Furthermore, we assume that the bank does not make any changes to the information in the database while a user is accessing the ATM. Also, any business system like an ATM faces reasonably complicated security issues that go well beyond the scope of a first- or second-semester programming course. We make the simplifying assumption, however, that the bank trusts the ATM to access and manipulate the information in the database without significant security measures.] Upon approaching the ATM, the user should experience the following sequence of events (see Fig. 3.28): 1. The screen displays a welcome message and prompts the user to enter an account number. 2. The user enters a five-digit account number, using the keypad. 3. For authentication purposes, the screen prompts the user to enter the PIN (personal identification number) associated with the specified account number. 4. The user enters a five-digit PIN, using the keypad. 5. If the user enters a valid account number and the correct PIN for that account, the screen displays the main menu (Fig. 3.29). If the user enters an invalid account number or an incorrect PIN, the screen displays an appropriate message, then the ATM returns to Step 1 to restart the authentication process. After the ATM authenticates the user, the main menu (Fig. 3.29) displays a numbered option for each of the three types of transactions: balance inquiry (option 1), withdrawal (option 2) and deposit (option 3). The main menu also displays an option that allows the user to exit the system (option 4). The user then chooses either to perform a transaction (by entering 1, 2 or 3) or to exit the system (by entering 4). If the user enters an invalid option, the screen displays an error message, then redisplays the main menu. If the user enters 1 to make a balance inquiry, the screen displays the user’s account balance. To do so, the ATM must retrieve the balance from the bank’s database.
84
Chapter 3
Introduction to C# Applications
Main menu 1 - View my balance 2 - Withdraw cash 3 - Deposit funds 4 - Exit Enter a choice:
Take cash here
Insert deposit envelope here
Fig. 3.29 | ATM main menu. The following actions occur when the user enters 2 to make a withdrawal: 1. The screen displays a menu (shown in Fig. 3.30) containing standard withdrawal amounts: $20 (option 1), $40 (option 2), $60 (option 3), $100 (option 4) and $200 (option 5). The menu also contains option 6 that allows the user to cancel the transaction. 2. The user enters a menu selection (1–6) using the keypad. 3. If the withdrawal amount chosen is greater than the user’s account balance, the screen displays a message stating this and telling the user to choose a smaller amount. The ATM then returns to Step 1. If the withdrawal amount chosen is less than or equal to the user’s account balance (i.e., an acceptable withdrawal amount), the ATM proceeds to Step 4. If the user chooses to cancel the transaction (option 6), the ATM displays the main menu (Fig. 3.29) and waits for user input. 4. If the cash dispenser contains enough cash to satisfy the request, the ATM proceeds to Step 5. Otherwise, the screen displays a message indicating the problem and telling the user to choose a smaller withdrawal amount. The ATM then returns to Step 1. 5. The ATM debits (i.e., subtracts) the withdrawal amount from the user’s account balance in the bank’s database. 6. The cash dispenser dispenses the desired amount of money to the user. 7. The screen displays a message reminding the user to take the money.
Fig. 3.30 | ATM withdrawal menu. The following actions occur when the user enters 3 (from the main menu) to make a deposit: 1. The screen prompts the user to enter a deposit amount or to type 0 (zero) to cancel the transaction. 2. The user enters a deposit amount or 0, using the keypad. [Note: The keypad does not contain a decimal point or a dollar sign, so the user cannot type a real dollar amount (e.g., $147.25). Instead, the user must enter a deposit amount as a number of cents (e.g., 14725). The ATM then divides this number by 100 to obtain a number representing a dollar amount (e.g., 14725 ÷ 100 = 147.25).] 3. If the user specifies a deposit amount, the ATM proceeds to Step 4. If the user chooses to cancel the transaction (by entering 0), the ATM displays the main menu (Fig. 3.29) and waits for user input. 4. The screen displays a message telling the user to insert a deposit envelope into the deposit slot. 5. If the deposit slot receives a deposit envelope within two minutes, the ATM credits (i.e., adds) the deposit amount to the user’s account balance in the bank’s database. [Note: This money is not immediately available for withdrawal. The bank first must verify the amount of cash in the deposit envelope, and any checks in the envelope must clear (i.e., money must be transferred from the check writer’s account to the check recipient’s account). When either of these events occurs, the bank appropriately updates the user’s balance stored in its database. This occurs independently of the ATM system.] If the deposit slot does not receive a deposit envelope within two minutes, the screen displays a message that the system has
86
Chapter 3
Introduction to C# Applications
canceled the transaction due to inactivity. The ATM then displays the main menu and waits for user input. After the system successfully executes a transaction, the system should redisplay the main menu (Fig. 3.29) so that the user can perform additional transactions. If the user chooses to exit the system (by entering option 4), the screen should display a thank you message, then display the welcome message for the next user.
Analyzing the ATM System The preceding statement presented a simplified requirements document. Typically, such a document is the result of a detailed process of requirements gathering that might include interviews with potential users of the system and specialists in fields related to the system. For example, a systems analyst who is hired to prepare a requirements document for banking software (e.g., the ATM system described here) might interview people who have used ATMs and financial experts to gain a better understanding of what the software must do. The analyst would use the information gained to compile a list of system requirements to guide systems designers. The process of requirements gathering is a key task of the first stage of the software life cycle. The software life cycle specifies the stages through which software evolves from the time it is conceived to the time it is retired from use. These stages typically include analysis, design, implementation, testing and debugging, deployment, maintenance and retirement. Several software life cycle models exist, each with its own preferences and specifications for when and how often software engineers should perform the various stages. Waterfall models perform each stage once in succession, whereas iterative models may repeat one or more stages several times throughout a product’s life cycle. The analysis stage of the software life cycle focuses on precisely defining the problem to be solved. When designing any system, one must certainly solve the problem right, but of equal importance, one must solve the right problem. Systems analysts collect the requirements that indicate the specific problem to solve. Our requirements document describes our simple ATM system in sufficient detail that you do not need to go through an extensive analysis stage—it has been done for you. To capture what a proposed system should do, developers often employ a technique known as use case modeling. This process identifies the use cases of the system, each of which represents a different capability that the system provides to its clients. For example, ATMs typically have several use cases, such as “View Account Balance,” “Withdraw Cash,” “Deposit Funds,” “Transfer Funds Between Accounts” and “Buy Postage Stamps.” The simplified ATM system we build in this case study requires only the first three use cases (Fig. 3.31). Each use case describes a typical scenario in which the user uses the system. You have already read descriptions of the ATM system’s use cases in the requirements document; the lists of steps required to perform each type of transaction (i.e., balance inquiry, withdrawal and deposit) actually described the three use cases of our ATM—“View Account Balance,” “Withdraw Cash” and “Deposit Funds.” Use Case Diagrams We now introduce the first of several UML diagrams in our ATM case study. We create a use case diagram to model the interactions between a system’s clients (in this case study,
3.10 Examining the ATM Requirements Document
87
View Account Balance
Withdraw Cash
User Deposit Funds
Fig. 3.31 | Use case diagram for the ATM system from the user’s perspective. bank customers) and the system. The goal is to show the kinds of interactions users have with a system without providing the details—these are shown in other UML diagrams (which we present throughout the case study). Use case diagrams are often accompanied by informal text that describes the use cases in more detail—like the text that appears in the requirements document. Use case diagrams are produced during the analysis stage of the software life cycle. In larger systems, use case diagrams are simple but indispensable tools that help system designers focus on satisfying the users’ needs. Figure 3.31 shows the use case diagram for our ATM system. The stick figure represents an actor, which defines the roles that an external entity—such as a person or another system—plays when interacting with the system. For our automated teller machine, the actor is a User who can view an account balance, withdraw cash and deposit funds using the ATM. The User is not an actual person, but instead comprises the roles that a real person—when playing the part of a User—can play while interacting with the ATM. Note that a use case diagram can include multiple actors. For example, the use case diagram for a real bank’s ATM system might also include an actor named Administrator who refills the cash dispenser each day. We identify the actor in our system by examining the requirements document, which states, “ATM users should be able to view their account balance, withdraw cash and deposit funds.” The actor in each of the three use cases is simply the User who interacts with the ATM. An external entity—a real person—plays the part of the User to perform financial transactions. Figure 3.31 shows one actor, whose name, User, appears below the actor in the diagram. The UML models each use case as an oval connected to an actor with a solid line. Software engineers (more precisely, systems designers) must analyze the requirements document or a set of use cases, and design the system before programmers implement it in a particular programming language. During the analysis stage, systems designers focus on understanding the requirements document to produce a high-level specification that describes what the system is supposed to do. The output of the design stage—a design specification—should specify how the system should be constructed to satisfy these requirements. In the next several Software Engineering Case Study sections, we perform the steps of a simple OOD process on the ATM system to produce a design specification
88
Chapter 3
Introduction to C# Applications
containing a collection of UML diagrams and supporting text. Recall that the UML is designed for use with any OOD process. Many such processes exist, the best known of which is the Rational Unified Process™ (RUP) developed by Rational Software Corporation (now a division of IBM). RUP is a rich process for designing “industrial strength” applications. For this case study, we present a simplified design process.
Designing the ATM System We now begin the design stage of our ATM system. A system is a set of components that interact to solve a problem. For example, to perform the ATM system’s designated tasks, our ATM system has a user interface (Fig. 3.28), contains software that executes financial transactions and interacts with a database of bank account information. System structure describes the system’s objects and their interrelationships. System behavior describes how the system changes as its objects interact with one another. Every system has both structure and behavior—designers must specify both. There are several distinct types of system structures and behaviors. For example, the interactions among objects in the system differ from those between the user and the system, yet both constitute a portion of the system behavior. The UML 2 specifies 13 diagram types for documenting system models. Each diagram type models a distinct characteristic of a system’s structure or behavior—six diagram types relate to system structure; the remaining seven relate to system behavior. We list here only the six types of diagrams used in our case study—one of which (the class diagram) models system structure; the remaining five model system behavior. We overview the remaining seven UML diagram types in Appendix K, UML 2: Additional Diagram Types. 1. Use case diagrams, such as the one in Fig. 3.31, model the interactions between a system and its external entities (actors) in terms of use cases (system capabilities, such as “View Account Balance,” “Withdraw Cash” and “Deposit Funds”). 2. Class diagrams, which you will study in Section 4.11, model the classes, or “building blocks,” used in a system. Each noun, or “thing,” described in the requirements document is a candidate to be a class in the system (e.g., “account,” “keypad”). Class diagrams help us specify the structural relationships between parts of the system. For example, the ATM system class diagram will, among other things, specify that the ATM is physically composed of a screen, a keypad, a cash dispenser and a deposit slot. 3. State machine diagrams, which you will study in Section 6.9, model the ways in which an object changes state. An object’s state is indicated by the values of all the object’s attributes at a given time. When an object changes state, it may subsequently behave differently in the system. For example, after validating a user’s PIN, the ATM transitions from the “user not authenticated” state to the “user authenticated” state, at which point the ATM allows the user to perform financial transactions (e.g., view account balance, withdraw cash, deposit funds). 4. Activity diagrams, which you will also study in Section 6.9, model an object’s activity—the object’s workflow (sequence of events) during program execution. An activity diagram models the actions the object performs and specifies the order in which it performs these actions. For example, an activity diagram shows that the ATM must obtain the balance of the user’s account (from the bank’s account information database) before the screen can display the balance to the user.
3.10 Examining the ATM Requirements Document
89
5. Communication diagrams (called collaboration diagrams in earlier versions of the UML) model the interactions among objects in a system, with an emphasis on what interactions occur. You will learn in Section 8.14 that these diagrams show which objects must interact to perform an ATM transaction. For example, the ATM must communicate with the bank’s account information database to retrieve an account balance. 6. Sequence diagrams also model the interactions among the objects in a system, but unlike communication diagrams, they emphasize when interactions occur. You will learn in Section 8.14 that these diagrams help show the order in which interactions occur in executing a financial transaction. For example, the screen prompts the user to enter a withdrawal amount before cash is dispensed. In Section 4.11, we continue designing our ATM system by identifying the classes from the requirements document. We accomplish this by extracting key nouns and noun phrases from the requirements document. Using these classes, we develop our first draft of the class diagram that models the structure of our ATM system.
Internet and Web Resources The following URLs provide information on object-oriented design with the UML. www-306.ibm.com/software/rational/uml/
Lists frequently asked questions about the UML, provided by IBM Rational. www.douglass.co.uk/documents/softdocwiz.com.UML.htm
Links to the Unified Modeling Language Dictionary, which defines all terms used in the UML. www.agilemodeling.com/essays/umlDiagrams.htm
Provides in-depth descriptions and tutorials on each of the 13 UML 2 diagram types. www-306.ibm.com/software/rational/offerings/design.html
IBM provides information about Rational software available for designing systems, and downloads of 30-day trial versions of several products, such as IBM Rational Rose® XDE (eXtended Development Environment) Developer. www.embarcadero.com/products/describe/index.html
Provides a 15-day trial license for the Embarcadero Technologies® UML modeling tool Describe.™ www.borland.com/together/index.html
Provides a free 30-day license to download a trial version of Borland® Together® ControlCenter™—a software development tool that supports the UML. www.ilogix.com/rhapsody/rhapsody.cfm
Provides a free 30-day license to download a trial version of I-Logix Rhapsody®—a UML 2-based model-driven development environment. argouml.tigris.org
Contains information and downloads for ArgoUML, a free open-source UML tool. www.objectsbydesign.com/books/booklist.html
Lists books on the UML and object-oriented design. www.objectsbydesign.com/tools/umltools_byCompany.html
Lists software tools that use the UML, such as IBM Rational Rose, Embarcadero Describe, Sparx Systems Enterprise Architect, I-Logix Rhapsody and Gentleware Poseidon for UML.
90
Chapter 3
Introduction to C# Applications
www.ootips.org/ood-principles.html
Provides answers to the question “What makes a good object-oriented design?” www.cetus-links.org/oo_uml.html
Introduces the UML and provides links to numerous UML resources.
Recommended Readings The following books provide information on object-oriented design with the UML. Ambler, S. The Elements of the UML 2.0 Style. New York: Cambridge University Press, 2005. Booch, G. Object-Oriented Analysis and Design with Applications, Third Edition. Boston: AddisonWesley, 2004. Eriksson, H., et al. UML 2 Toolkit. New York: John Wiley, 2003. Kruchten, P. The Rational Unified Process: An Introduction. Boston: Addison-Wesley, 2004. Larman, C. Applying UML and Patterns: An Introduction to Object-Oriented Analysis and Design, Second Edition. Upper Saddle River, NJ: Prentice Hall, 2002. Roques, P. UML in Practice: The Art of Modeling Software Systems Demonstrated Through Worked Examples and Solutions. New York: John Wiley, 2004. Rosenberg, D., and K. Scott. Applying Use Case Driven Object Modeling with UML: An Annotated e-Commerce Example. Reading, MA: Addison-Wesley, 2001. Rumbaugh, J., I. Jacobson, and G. Booch. The Complete UML Training Course. Upper Saddle River, NJ: Prentice Hall, 2000. Rumbaugh, J., I. Jacobson, and G. Booch. The Unified Modeling Language Reference Manual. Reading, MA: Addison-Wesley, 1999. Rumbaugh, J., I. Jacobson, and G. Booch. The Unified Software Development Process. Reading, MA: Addison-Wesley, 1999.
Software Engineering Case Study Self-Review Exercises 3.1 Suppose we enabled a user of our ATM system to transfer money between two bank accounts. Modify the use case diagram of Fig. 3.31 to reflect this change. 3.2 model the interactions among objects in a system with an emphasis on when these interactions occur. a) Class diagrams b) Sequence diagrams c) Communication diagrams d) Activity diagrams 3.3
Which of the following choices lists stages of a typical software life cycle in sequential order? a) design, analysis, implementation, testing b) design, analysis, testing, implementation c) analysis, design, testing, implementation d) analysis, design, implementation, testing
Answers to Software Engineering Case Study Self-Review Exercises 3.1 Figure 3.32 contains a use case diagram for a modified version of our ATM system that also allows users to transfer money between accounts. 3.2
b.
3.3
d.
3.11 Wrap-Up
91
View Account Balance
Withdraw Cash
Deposit Funds User Transfer Funds Between Accounts
Fig. 3.32 | Use case diagram for a modified version of our ATM system that also allows users to transfer money between accounts.
3.11 Wrap-Up You learned many important features of C# in this chapter, including displaying data on the screen in a command prompt, inputting data from the keyboard, performing calculations and making decisions. The applications presented here introduced you to basic programming concepts. As you will see in Chapter 4, C# applications typically contain just a few lines of code in method Main—these statements normally create the objects that perform the work of the application. In Chapter 4, you will learn how to implement your own classes and use objects of those classes in applications.
4 Introduction to Classes and Objects Nothing can have value without being an object of utility. —Karl Marx
OBJECTIVES In this chapter you will learn:
Your public servants serve you right. —Adlai E. Stevenson
I
What classes, objects, methods and instance variables are.
I
How to declare a class and use it to create an object.
I
How to implement a class’s behaviors as methods.
I
How to implement a class’s attributes as instance variables and properties.
—Amenemope
I
How to call an object’s methods to make the methods perform their tasks.
I
The differences between instance variables of a class and local variables of a method.
You will see something new. Two things. And I call them Thing One and Thing Two.
I
How to use a constructor to ensure that an object’s data is initialized when the object is created.
I
The differences between value types and reference types.
Knowing how to answer one who speaks, To reply to one who sends a message.
—Dr. Theodor Seuss Geisel
Outline
4.1 Introduction
93
4.1 4.2 4.3 4.4 4.5 4.6 4.7 4.8 4.9 4.10 4.11
Introduction Classes, Objects, Methods, Properties and Instance Variables Declaring a Class with a Method and Instantiating an Object of a Class Declaring a Method with a Parameter Instance Variables and Properties UML Class Diagram with a Property Software Engineering with Properties and set and get Accessors Value Types vs. Reference Types Initializing Objects with Constructors Floating-Point Numbers and Type decimal (Optional) Software Engineering Case Study: Identifying the Classes in the ATM Requirements Document 4.12 Wrap-Up
4.1 Introduction We introduced the basic terminology and concepts of object-oriented programming in Section 1.9. In Chapter 3, you began to use those concepts to create simple applications that displayed messages to the user, obtained information from the user, performed calculations and made decisions. One common feature of every application in Chapter 3 was that all the statements that performed tasks were located in method Main. Typically, the applications you develop in this book will consist of two or more classes, each containing one or more methods. If you become part of a development team in industry, you might work on applications that contain hundreds, or even thousands, of classes. In this chapter, we present a simple framework for organizing object-oriented applications in C#. First, we explain the concept of classes using a real-world example. Then we present five complete working applications to demonstrate how to create and use your own classes. The first four of these examples begin our case study on developing a grade-book class that instructors can use to maintain student test scores. This case study is enhanced over the next several chapters, culminating with the version presented in Chapter 8, Arrays. The last example in the chapter introduces the type decimal and uses it to declare monetary amounts in the context of a bank account class that maintains a customer’s balance.
4.2 Classes, Objects, Methods, Properties and Instance Variables Let’s begin with a simple analogy to help you understand classes and their contents. Suppose you want to drive a car and make it go faster by pressing down on its accelerator pedal. What must happen before you can do this? Well, before you can drive a car, someone has to design it. A car typically begins as engineering drawings, similar to the blueprints used to design a house. These engineering drawings include the design for an accelerator pedal to make the car go faster. The pedal “hides” the complex mechanisms that actually make the car go faster, just as the brake pedal “hides” the mechanisms that slow the car and the
94
Chapter 4
Introduction to Classes and Objects
steering wheel “hides” the mechanisms that turn the car. This enables people with little or no knowledge of how engines work to drive a car easily. Unfortunately, you cannot drive the engineering drawings of a car. Before you can drive a car, the car must be built from the engineering drawings that describe it. A completed car will have an actual accelerator pedal to make the car go faster, but even that’s not enough—the car will not accelerate on its own, so the driver must press the accelerator pedal. Now let’s use our car example to introduce the key programming concepts of this section. Performing a task in an application requires a method. The method describes the mechanisms that actually perform its tasks. The method hides from its user the complex tasks that it performs, just as the accelerator pedal of a car hides from the driver the complex mechanisms of making the car go faster. In C#, we begin by creating an application unit called a class to house (among other things) a method, just as a car’s engineering drawings house (among other things) the design of an accelerator pedal. In a class, you provide one or more methods that are designed to perform the class’s tasks. For example, a class that represents a bank account might contain one method to deposit money in an account, another to withdraw money from an account and a third to inquire what the current account balance is. Just as you cannot drive an engineering drawing of a car, you cannot “drive” a class. Just as someone has to build a car from its engineering drawings before you can actually drive a car, you must build an object of a class before you can get an application to perform the tasks the class describes. That is one reason C# is known as an object-oriented programming language. When you drive a car, pressing its gas pedal sends a message to the car to perform a task—make the car go faster. Similarly, you send messages to an object—each message is known as a method call and tells a method of the object to perform its task. Thus far, we have used the car analogy to introduce classes, objects and methods. In addition to the capabilities a car provides, it also has many attributes, such as its color, the number of doors, the amount of gas in its tank, its current speed and its total miles driven (i.e., its odometer reading). Like the car’s capabilities, these attributes are represented as part of a car’s design in its engineering diagrams. As you drive a car, these attributes are always associated with the car. Every car maintains its own attributes. For example, each car knows how much gas is in its own gas tank, but not how much is in the tanks of other cars. Similarly, an object has attributes that are carried with the object as it is used in an application. These attributes are specified as part of the object’s class. For example, a bank account object has a balance attribute that represents the amount of money in the account. Each bank account object knows the balance in the account it represents, but not the balances of the other accounts in the bank. Attributes are specified by the class’s instance variables. Notice that these attributes are not necessarily accessible directly. The car manufacturer does not want drivers to take apart the car’s engine to observe the amount of gas in its tank. Instead, the driver can check the meter on the dashboard. The bank does not want its customers to walk into the vault to count the amount of money in an account. Instead, the customers talk to a bank teller. Similarly, you do not need to have access to an object’s instance variables in order to use them. You can use the properties of an object. Properties contain get accessors for reading the values of variables, and set accessors for storing values into them.
4.3 Declaring a Class with a Method and Instantiating an Object of a Class
95
The remainder of this chapter presents examples that demonstrate the concepts we introduced here in the context of the car analogy. The first four examples, summarized below, incrementally build a GradeBook class: 1. The first example presents a GradeBook class with one method that simply displays a welcome message when it is called. We show how to create an object of that class and call the method so that it displays the welcome message. 2. The second example modifies the first by allowing the method to receive a course name as an “argument” and by displaying the name as part of the welcome message. 3. The third example shows how to store the course name in a GradeBook object. For this version of the class, we also show how to use properties to set the course name and obtain the course name. 4. The fourth example demonstrates how the data in a GradeBook object can be initialized when the object is created—the initialization is performed by the class’s constructor. The last example in the chapter presents an Account class that reinforces the concepts presented in the first four examples and introduces the decimal type—a decimal number can contain a decimal point, as in 0.0345, –7.23 and 100.7, and is used for precise calculations, especially those involving monetary values. For this purpose, we present an Account class that represents a bank account and maintains its decimal balance. The class contains a method to credit a deposit to the account, thus increasing the balance, and a property to retrieve the balance and ensure that all values assigned to the balance are non-negative. The class’s constructor initializes the balance of each Account object as the object is created. We create two Account objects and make deposits into each to show that each object maintains its own balance. The example also demonstrates how to input and display decimal numbers.
4.3 Declaring a Class with a Method and Instantiating an Object of a Class We begin with an example that consists of classes GradeBook (Fig. 4.1) and GradeBookTest (Fig. 4.2). Class GradeBook (declared in file GradeBook.cs) will be used to display a message on the screen (Fig. 4.2) welcoming the instructor to the grade-book application. Class GradeBookTest (declared in the file GradeBookTest.cs) is a testing class in which the Main method will create and use an object of class GradeBook. By convention, we declare classes GradeBook and GradeBookTest in separate files, such that each file’s name matches the name of the class it contains. To start, select File > New Project... to open the New Project dialog, then create a GradeBook Console Application. Delete all the code provided automatically by the IDE and replace it with the code in Fig. 4.1.
Class GradeBook The GradeBook class declaration (Fig. 4.1) contains a DisplayMessage method (lines 8– 11) that displays a message on the screen. Line 10 of the class displays the message. Recall that a class is like a blueprint—we need to make an object of this class and call its method to get line 10 to execute and display its message. (We do this in Fig. 4.2.)
96 1 2 3 4 5 6 7 8 9 10 11 12
Chapter 4
Introduction to Classes and Objects
// Fig. 4.1: GradeBook.cs // Class declaration with one method. using System; public class GradeBook { // display a welcome message to the GradeBook user public void DisplayMessage() { Console.WriteLine( "Welcome to the Grade Book!" ); } // end method DisplayMessage } // end class GradeBook
Fig. 4.1 | Class declaration with one method. The class declaration begins in line 5. The keyword public is an access modifier. For now, we simply declare every class public. Every class declaration contains keyword class followed by the class’s name. Every class’s body is enclosed in a pair of left and right braces ({ and }), as in lines 6 and 12 of class GradeBook. In Chapter 3, each class we declared had one method named Main. Class GradeBook also has one method—DisplayMessage (lines 8–11). Recall that Main is a special method that is always called automatically when you execute an application. Most methods do not get called automatically. As you will soon see, you must call method DisplayMessage to tell it to perform its task. The method declaration begins with keyword public to indicate that the method is “available to the public”—that is, it can be called from outside the class declaration’s body by methods of other classes. Keyword void—known as the method’s return type—indicates that this method will not return (i.e., give back) any information to its calling method when it completes its task. When a method that specifies a return type other than void is called and completes its task, the method returns a result to its calling method. For example, when you go to an automated teller machine (ATM) and request your account balance, you expect the ATM to give you back a value that represents your balance. If you have a method Square that returns the square of its argument, you would expect the statement int result = Square( 2 );
to return 4 from method Square and assign 4 to variable result. If you have a method Maximum that returns the largest of three integer arguments, you would expect the statement int biggest = Maximum( 27, 114, 51 );
to return the value 114 from method Maximum and assign the value to variable biggest. You have already used methods that return information—for example, in Chapter 3 you used Console method ReadLine to input a string typed by the user at the keyboard. When ReadLine inputs a value, it returns that value for use in the application. The name of the method, DisplayMessage, follows the return type (line 8). By convention, method names begin with an uppercase first letter, and all subsequent words in the name begin with a capital letter. The parentheses after the method name indicate that this is a method. An empty set of parentheses, as shown in line 8, indicates that this method does not require additional information to perform its task. Line 8 is commonly
4.3 Declaring a Class with a Method and Instantiating an Object of a Class
97
referred to as the method header. Every method’s body is delimited by left and right braces, as in lines 9 and 11. The body of a method contains statement(s) that perform the method’s task. In this case, the method contains one statement (line 10) that displays the message "Welcome to the Grade Book!", followed by a newline in the console window. After this statement executes, the method has completed its task. Next, we’d like to use class GradeBook in an application. As you learned in Chapter 3, method Main begins the execution of every application. Class GradeBook cannot begin an application because it does not contain Main. This was not a problem in Chapter 3, because every class you declared had a Main method. To fix this problem for the GradeBook, we must either declare a separate class that contains a Main method or place a Main method in class GradeBook. To help you prepare for the larger applications you will encounter later in this book and in industry, we use a separate class (GradeBookTest in this example) containing method Main to test each new class we create in this chapter.
Adding a Class to a Visual C# Project For each example in this chapter, you will add a class to your console application. To do this, right click the project name in the Solution Explorer and select Add > New Item… from the pop-up menu. In the Add New Item dialog that appears, select Code File and enter the name of your new file—in this case, GradeBookTest.cs. A new, blank file will be added to your project. Add the code from Fig. 4.2 to this file. Class GradeBookTest The GradeBookTest class declaration (Fig. 4.2) contains the Main method that controls our application’s execution. Any class that contains a Main method (as shown in line 7) can be used to execute an application. This class declaration begins in line 4 and ends in line 15. The class contains only a Main method, which is typical of many classes that simply begin an application’s execution. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
// Fig. 4.2: GradeBookTest.cs // Create a GradeBook object and call its DisplayMessage method. public class GradeBookTest { // Main method begins program execution public static void Main( string[] args ) { // create a GradeBook object and assign it to myGradeBook GradeBook myGradeBook = new GradeBook(); // call myGradeBook's DisplayMessage method myGradeBook.DisplayMessage(); } // end Main } // end class GradeBookTest
Welcome to the Grade Book!
Fig. 4.2 | Create a GradeBook object and call its DisplayMessage method.
98
Chapter 4
Introduction to Classes and Objects
Lines 7–14 declare method Main. A key part of enabling the method Main to begin the application’s execution is the static keyword (line 7), which indicates that Main is a static method. A static method is special because it can be called without first creating an object of the class (in this case, GradeBookTest) in which the method is declared. We explain static methods in Chapter 7, Methods: A Deeper Look. In this application, we’d like to call class GradeBook’s DisplayMessage method to display the welcome message in the console window. Typically, you cannot call a method that belongs to another class until you create an object of that class, as shown in line 10. We begin by declaring variable myGradeBook. Note that the variable’s type is GradeBook—the class we declared in Fig. 4.1. Each new class you create becomes a new type in C# that can be used to declare variables and create objects. New class types will be accessible to all classes in the same project. You can declare new class types as needed; this is one reason why C# is known as an extensible language. Variable myGradeBook (line 10) is initialized with the result of the object creation expression new GradeBook(). The new operator creates a new object of the class specified to the right of the keyword (i.e., GradeBook). The parentheses to the right of the GradeBook are required. As you will learn in Section 4.9, those parentheses in combination with a class name represent a call to a constructor, which is similar to a method, but is used only at the time an object is created to initialize the object’s data. In that section you will see that data can be placed in parentheses to specify initial values for the object’s data. For now, we simply leave the parentheses empty. We can now use myGradeBook to call its method DisplayMessage. Line 13 calls the method DisplayMessage (lines 8–11 of Fig. 4.1) using variable myGradeBook followed by a dot operator (.), the method name DisplayMessage and an empty set of parentheses. This call causes the DisplayMessage method to perform its task. This method call differs from the method calls in Chapter 3 that displayed information in a console window— each of those method calls provided arguments that specified the data to display. At the beginning of line 13, “myGradeBook.” indicates that Main should use the GradeBook object that was created on line 10. The empty parentheses in line 8 of Fig. 4.1 indicate that method DisplayMessage does not require additional information to perform its task. For this reason, the method call (line 13 of Fig. 4.2) specifies an empty set of parentheses after the method name to indicate that no arguments are being passed to method DisplayMessage. When method DisplayMessage completes its task, method Main continues executing at line 14. This is the end of method Main, so the application terminates.
UML Class Diagram for Class GradeBook Figure 4.3 presents a UML class diagram for class GradeBook of Fig. 4.1. Recall from Section 1.9 that the UML is a graphical language used by programmers to represent their ob-
GradeBook + DisplayMessage( )
Fig. 4.3 | UML class diagram indicating that class GradeBook has a public DisplayMessage
operation.
4.4 Declaring a Method with a Parameter
99
ject-oriented systems in a standardized manner. In the UML, each class is modeled in a class diagram as a rectangle with three compartments. The top compartment contains the name of the class centered horizontally in boldface type. The middle compartment contains the class’s attributes, which correspond to instance variables and properties in C#. In Fig. 4.3, the middle compartment is empty because the version of class GradeBook in Fig. 4.1 does not have any attributes. The bottom compartment contains the class’s operations, which correspond to methods in C#. The UML models operations by listing the operation name followed by a set of parentheses. Class GradeBook has one method, DisplayMessage, so the bottom compartment of Fig. 4.3 lists one operation with this name. Method DisplayMessage does not require additional information to perform its tasks, so there are empty parentheses following DisplayMessage in the class diagram, just as they appeared in the method’s declaration in line 8 of Fig. 4.1. The plus sign (+) in front of the operation name indicates that DisplayMessage is a public operation in the UML (i.e., a public method in C#). The plus sign is sometimes called the public visibility symbol. We will often use UML class diagrams to summarize a class’s attributes and operations.
4.4 Declaring a Method with a Parameter In our car analogy from Section 4.2, we discussed the fact that pressing a car’s gas pedal sends a message to the car to perform a task—make the car go faster. But how fast should the car accelerate? As you know, the farther down you press the pedal, the faster the car accelerates. So the message to the car actually includes both the task to be performed and additional information that helps the car perform the task. This additional information is known as a parameter—the value of the parameter helps the car determine how fast to accelerate. Similarly, a method can require one or more parameters that represent additional information it needs to perform its task. A method call supplies values—called arguments—for each of the method’s parameters. For example, the Console.WriteLine method requires an argument that specifies the data to be displayed in a console window. Similarly, to make a deposit into a bank account, a Deposit method specifies a parameter that represents the deposit amount. When the Deposit method is called, an argument value representing the deposit amount is assigned to the method’s parameter. The method then makes a deposit of that amount, by increasing the account’s balance. Our next example declares class GradeBook (Fig. 4.4) with a DisplayMessage method that displays the course name as part of the welcome message. (See the sample execution in Fig. 4.5.) The new DisplayMessage method requires a parameter that represents the course name to output. Before discussing the new features of class GradeBook, let’s see how the new class is used from the Main method of class GradeBookTest (Fig. 4.5). Line 12 creates an object of class GradeBook and assigns it to variable myGradeBook. Line 15 prompts the user to enter a course name. Line 16 reads the name from the user and assigns it to the variable nameOfCourse, using Console method ReadLine to perform the input. The user types the course name and presses Enter to submit the course name to the application. Note that pressing Enter inserts a newline character at the end of the characters typed by the user. Method ReadLine reads characters typed by the user until the newline character is encountered, then returns a string containing the characters up to, but not including, the newline. The newline character is discarded.
100 1 2 3 4 5 6 7 8 9 10 11 12 13
Chapter 4
Introduction to Classes and Objects
// Fig. 4.4: GradeBook.cs // Class declaration with a method that has a parameter. using System; public class GradeBook { // display a welcome message to the GradeBook user public void DisplayMessage( string courseName ) { Console.WriteLine( "Welcome to the grade book for\n{0}!", courseName ); } // end method DisplayMessage } // end class GradeBook
Fig. 4.4 | Class declaration with a method that has a parameter. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
// Fig. 4.5: GradeBookTest.cs // Create GradeBook object and pass a string to // its DisplayMessage method. using System; public class GradeBookTest { // Main method begins program execution public static void Main( string[] args ) { // create a GradeBook object and assign it to myGradeBook GradeBook myGradeBook = new GradeBook(); // prompt for and input course name Console.WriteLine( "Please enter the course name:" ); string nameOfCourse = Console.ReadLine(); // read a line of text Console.WriteLine(); // output a blank line // call myGradeBook's DisplayMessage method // and pass nameOfCourse as an argument myGradeBook.DisplayMessage( nameOfCourse ); } // end Main } // end class GradeBookTest
Please enter the course name: CS101 Introduction to C# Programming Welcome to the grade book for CS101 Introduction to C# Programming!
Fig. 4.5 | Create GradeBook object and pass a string to its DisplayMessage method. Line 21 calls myGradeBook’s DisplayMessage method. The variable nameOfCourse in parentheses is the argument that is passed to method DisplayMessage so that the method can perform its task. Variable nameOfCourse’s value in Main becomes the value of method DisplayMessage’s parameter courseName in line 8 of Fig. 4.4. When you execute this
4.4 Declaring a Method with a Parameter
101
application, notice that method DisplayMessage outputs the name you type as part of the welcome message (Fig. 4.5).
Software Engineering Observation 4.1 Normally, objects are created with new. One exception is a string literal that is contained in quotes, such as "hello". String literals are references to string objects that are implicitly created by C#. 4.1
More on Arguments and Parameters When you declare a method, you must specify in the method’s declaration whether the method requires data to perform its task. To do so, you place additional information in the method’s parameter list, which is located in the parentheses that follow the method name. The parameter list may contain any number of parameters, including none at all. Empty parentheses following the method name (as in Fig. 4.1, line 8) indicate that a method does not require any parameters. In Fig. 4.4, DisplayMessage’s parameter list (line 8) declares that the method requires one parameter. Each parameter must specify a type and an identifier. In this case, the type string and the identifier courseName indicate that method DisplayMessage requires a string to perform its task. At the time the method is called, the argument value in the call is assigned to the corresponding parameter (in this case, courseName) in the method header. Then, the method body uses the parameter courseName to access the value. Lines 10–11 of Fig. 4.4 display parameter courseName’s value, using the {0} format item in WriteLine’s first argument. Note that the parameter variable’s name (Fig. 4.4, line 8) can be the same or different from the argument variable’s name (Fig. 4.5, line 21). A method can specify multiple parameters by separating each parameter from the next with a comma. The number of arguments in a method call must match the number of parameters in the parameter list of the called method’s declaration. Also, the types of the arguments in the method call must be consistent with the types of the corresponding parameters in the method’s declaration. (As you will learn in subsequent chapters, an argument’s type and its corresponding parameter’s type are not always required to be identical.) In our example, the method call passes one argument of type string (nameOfCourse is declared as a string in line 16 of Fig. 4.5), and the method declaration specifies one parameter of type string (line 8 in Fig. 4.4). So the type of the argument in the method call exactly matches the type of the parameter in the method header.
Common Programming Error 4.1 A compilation error occurs if the number of arguments in a method call does not match the number of parameters in the method declaration. 4.1
Common Programming Error 4.2 A compilation error occurs if the types of the arguments in a method call are not consistent with the types of the corresponding parameters in the method declaration. 4.2
Updated UML Class Diagram for Class GradeBook The UML class diagram of Fig. 4.6 models class GradeBook of Fig. 4.4. Like Fig. 4.4, this GradeBook class contains public operation DisplayMessage. However, this version of DisplayMessage has a parameter. The UML models a parameter a bit differently from C#
102
Chapter 4
Introduction to Classes and Objects
by listing the parameter name, followed by a colon and the parameter type in the parentheses following the operation name. The UML has several data types that are similar to the C# types. For example, UML types String and Integer correspond to C# types string and int, respectively. Unfortunately, the UML does not provide types that correspond to every C# type. For this reason, and to avoid confusion between UML types and C# types, we use only C# types in our UML diagrams. Class Gradebook’s method DisplayMessage (Fig. 4.4) has a string parameter named courseName, so Fig. 4.6 lists courseName : string between the parentheses following DisplayMessage.
Notes on using Directives Notice the using directive in Fig. 4.5 (line 4). This indicates to the compiler that the application uses classes in the System namespace, like the Console class. Why do we need a using directive to use class Console, but not class GradeBook? There is a special relationship between classes that are compiled in the same project, like classes GradeBook and GradeBookTest. By default, such classes are considered to be in the same namespace. A using directive is not required when one class in a namespace uses another in the same namespace—such as when class GradeBookTest uses class GradeBook. You will see in Section 9.14 how to declare your own namespaces with the namespace keyword. For simplicity, our examples in this chapter do not declare a namespace. Any classes that are not explicitly placed in a namespace are implicitly placed in the so-called global namespace. Actually, the using directive in line 4 is not required if we always refer to class Console as System.Console, which includes the full namespace and class name. This is known as the class’s fully qualified class name. For example, line 15 could be written as System.Console.WriteLine( "Please enter the course name:" );
Most C# programmers consider using fully qualified names to be cumbersome, and instead prefer to use using directives. The code generated by the Visual C# Form Designer uses fully qualified names.
4.5 Instance Variables and Properties In Chapter 3, we declared all of an application’s variables in the application’s Main method. Variables declared in the body of a particular method are known as local variables and can be used only in that method. When a method terminates, the values of its local variables are lost. Recall from Section 4.2 that an object has attributes that are carried with the object as it is used in an application. Such attributes exist before a method is called on an object and after the method completes execution.
GradeBook + DisplayMessage( courseName : string )
Fig. 4.6 | UML class diagram indicating that class GradeBook has a public DisplayMessage operation with a courseName parameter of type string.
4.5 Instance Variables and Properties
103
Attributes are represented as variables in a class declaration. Such variables are called fields and are declared inside a class declaration but outside the bodies of the class’s method declarations. When each object of a class maintains its own copy of an attribute, the field that represents the attribute is also known as an instance variable—each object (instance) of the class has a separate instance of the variable in memory. [Note: In Chapter 9, Classes and Objects: A Deeper Look, we discuss another type of field called a static variable, where all objects of the same class share one copy of the variable.] A class normally consists of one or more properties that manipulate the attributes that belong to a particular object of the class. The example in this section demonstrates a GradeBook class that contains a courseName instance variable to represent a particular GradeBook object’s course name, and a CourseName property to manipulate courseName.
Class with an Instance Variable and a Property In our next application (Figs. 4.7–4.8), class GradeBook (Fig. 4.7) maintains the course name as an instance variable so that it can be used or modified at any time during an application’s execution. The class also contains one method—DisplayMessage (lines 24–30)—and one property—CourseName (line 11–21). Recall from Chapter 2 that properties are used to manipulate an object’s attributes. For example, in that chapter, we used a Label’s Text property to specify the text to display on the Label. In this example, we use a property in code rather than in the Properties window of the IDE. To do this, we first declare a property as a member of the GradeBook class. As you will soon see, the GradeBook’s CourseName property can be used to store a course name in a GradeBook (in instance variable courseName) or retrieve the GradeBook’s course name (from instance variable courseName). Method DisplayMessage— which now specifies no parameters—still displays a welcome message that includes the course name. However, the method now uses the CourseName property to obtain the course name from instance variable courseName. A typical instructor teaches more than one course, each with its own course name. Line 8 declares courseName as a variable of type string. Line 8 is a declaration for an instance variable because the variable is declared in the body of the class (lines 7–31) but outside the bodies of the class’s method (lines 24–30) and property (lines 11–21). Every instance (i.e., object) of class GradeBook contains one copy of each instance variable. For example, if there are two GradeBook objects, each object has its own copy of courseName (one per object). All the methods and properties of class GradeBook can directly manipulate its instance variable courseName, but it is considered good practice for methods of a class to use that class’s properties to manipulate instance variables (as we do in line 29 of method DisplayMessage). The software engineering reasons for this will soon become clear. GradeBook
1 2 3 4 5 6 7 8
// Fig. 4.7: GradeBook.cs // GradeBook class that contains a courseName instance variable, // and a property to get and set its value. using System; public class GradeBook { private string courseName; // course name for this GradeBook
Fig. 4.7 | public
GradeBook class that contains a private instance variable, courseName and a property to get and set its value. (Part 1 of 2.)
// property to get and set the course name public string CourseName { get { return courseName; } // end get set { courseName = value; } // end set } // end property CourseName // display a welcome message to the GradeBook user public void DisplayMessage() { // use property CourseName to get the // name of the course that this GradeBook represents Console.WriteLine( "Welcome to the grade book for\n{0}!", CourseName ); // display property CourseName } // end method DisplayMessage } // end class GradeBook
Fig. 4.7 | public
GradeBook class that contains a private instance variable, courseName and a property to get and set its value. (Part 2 of 2.)
Access Modifiers public and private Most instance variable declarations are preceded with the keyword private (as in line 8). Like public, keyword private is an access modifier. Variables or methods declared with access modifier private are accessible only to methods of the class in which they are declared. Thus, variable courseName can be used only in property CourseName and method DisplayMessage of class GradeBook.
Software Engineering Observation 4.2 Precede every field and method declaration with an access modifier. As a rule of thumb, instance variables should be declared private and methods and properties should be declared public. If the access modifier is omitted before a member of a class, the member is implicitly declared private by default. (We will see that it is appropriate to declare certain methods private, if they will be accessed only by other methods of the class.) 4.2
Good Programming Practice 4.1 We prefer to list the fields of a class first, so that, as you read the code, you see the names and types of the variables before you see them used in the methods of the class. It is possible to list the class’s fields anywhere in the class outside its method declarations, but scattering them can make code difficult to read. 4.1
Good Programming Practice 4.2 Placing a blank line between method and property declarations enhances application readability.
4.2
4.5 Instance Variables and Properties
105
Declaring instance variables with access modifier private is known as information hiding. When an application creates (instantiates) an object of class GradeBook, variable courseName is encapsulated (hidden) in the object and can be accessed only by methods and properties of the object’s class. In class GradeBook, the property CourseName manipulates the instance variable courseName.
Setting and Getting the Values of private Instance Variables How can we allow a program to manipulate a class’s private instance variables but ensure that they remain in a valid state? We need to provide controlled ways for programmers to “get” (i.e., retrieve) the value in an instance variable and “set” (i.e., modify) the value in an instance variable. For these purposes, programmers using languages other than C# normally use methods known as get and set methods. These methods typically are made public, and provide ways for the client to access or modify private data. Historically, these methods begin with the words “Get” and “Set”—in our class GradeBook, for example, if we were to use such methods they might be called GetCourseName and SetCourseName, respectively. Although you can define methods like GetCourseName and SetCourseName, C# properties provide a more elegant solution. Next, we show how to declare and use properties. Class with a Property The GradeBook class’s CourseName property declaration is located in lines 11–21 of Fig. 4.7. The property begins in line 11 with an access modifier (in this case, public), followed by the type that the property represents (string) and the property’s name (CourseName). Property names are normally capitalized. Properties contain accessors that handle the details of returning and modifying data. A property declaration can contain a get accessor, a set accessor or both. The get accessor (lines 13–16) enables a client to read the value of private instance variable courseName; the set accessor (lines 17–20) enables a client to modify courseName. After defining a property, you can use it like a variable in your code. For example, you can assign a value to a property using the = (assignment) operator. This executes the code in the property’s set accessor to set the value of the corresponding instance variable. Similarly, referencing the property to use its value (for example, to display it on the screen) executes the code in the property’s get accessor to obtain the corresponding instance variable’s value. We show how to use properties shortly. By convention, we name each property with the capitalized name of the instance variable that it manipulates (e.g., CourseName is the property that represents instance variable courseName)—C# is case sensitive, so these are distinct identifiers. GradeBook
and set Accessors Let us look more closely at property CourseName’s get and set accessors (Fig. 4.7). The get accessor (lines 13–16) begins with the identifier get and is delimited by braces. The accessor’s body contains a return statement, which consists of the keyword return followed by an expression. The expression’s value is returned to the client code that uses the property. In this example, the value of courseName is returned when the property CourseName is referenced. For example, the following statement get
string theCourseName = gradeBook.CourseName;
106
Chapter 4
Introduction to Classes and Objects
where gradeBook is an object of class GradeBook, executes property CourseName’s get accessor, which returns the value of instance variable courseName. That value is then stored in variable theCourseName. Note that property CourseName can be used as simply as if it were an instance variable. The property notation allows the client to think of the property as the underlying data. Again, the client cannot directly manipulate instance variable courseName because it is private. The set accessor (lines 17–20) begins with the identifier set and is delimited by braces. When the property CourseName appears in an assignment statement, as in gradeBook.CourseName = "CS100 Introduction to Computers";
the text "CS100 Introduction to Computers" is passed to an implicit parameter named value, and the set accessor executes. Notice that value is implicitly declared and initialized in the set accessor—it is a compilation error to declare a local variable value in this body. Line 19 stores this value in instance variable courseName. Note that set accessors do not return any data when they complete their tasks. The statements inside the property in lines 15 and 19 (Fig. 4.7) each access courseName even though it was declared outside the property. We can use instance variable courseName in the methods and properties of class GradeBook because courseName is an instance variable of the class. The order in which methods and properties are declared in a class does not determine when they are called at execution time, so you can declare method DisplayMessage (which uses property CourseName) before you declare property CourseName. Within the property itself, the get and set accessors can appear in any order, and either accessor can be omitted. In Chapter 9, we discuss how to omit either a set or get accessor to create so-called “read-only” and “write-only” properties, respectively.
Using Property CourseName in Method DisplayMessage Method DisplayMessage (lines 24–30 of Fig. 4.7) does not receive any parameters. Lines 28–29 output a welcome message that includes the value of instance variable courseName. We do not reference courseName directly. Instead, we access property CourseName (line 29), which executes the property’s get accessor, returning the value of courseName. GradeBookTest
Class That Demonstrates Class GradeBook
Class
GradeBookTest (Fig. 4.8) creates a GradeBook object and demonstrates property CourseName. Line 11 creates a GradeBook object and assigns it to local variable myGradeBook of type GradeBook. Lines 14–15 display the initial course name using the object’s CourseName property—this executes the property’s get accessor, which returns the value of courseName. Note that the first line of the output shows an empty name (marked by ''). Unlike
local variables, which are not automatically initialized, every field has a default initial value—a value provided by C# when you do not specify the initial value. Thus, fields are not required to be explicitly initialized before they are used in an application—unless they must be initialized to values other than their default values. The default value for an instance variable of type string (like courseName) is null. When you display a string variable that contains the value null, no text is displayed on the screen. We will discuss the significance of null in Section 4.8.
// Fig. 4.8: GradeBookTest.cs // Create and manipulate a GradeBook object. using System; public class GradeBookTest { // Main method begins program execution public static void Main( string[] args ) { // create a GradeBook object and assign it to myGradeBook GradeBook myGradeBook = new GradeBook(); // display initial value of CourseName Console.WriteLine( "Initial course name is: '{0}'\n", myGradeBook.CourseName ); // prompt for and read course name Console.WriteLine( "Please enter the course name:" ); string theName = Console.ReadLine(); // read a line of text myGradeBook.CourseName = theName; // set name using a property Console.WriteLine(); // output a blank line // display welcome message after specifying course name myGradeBook.DisplayMessage(); } // end Main } // end class GradeBookTest
Initial course name is: '' Please enter the course name: CS101 Introduction to C# Programming Welcome to the grade book for CS101 Introduction to C# Programming!
Fig. 4.8 | Create and manipulate a GradeBook object. Line 18 prompts the user to enter a course name. Local string variable theName (declared in line 19) is initialized with the course name entered by the user, which is returned by the call to ReadLine. Line 20 assigns theName to object myGradeBook’s CourseName property. When a value is assigned to CourseName, the value specified (in this case, theName) is assigned to implicit parameter value of CourseName’s set accessor (lines 17–20, Fig. 4.7). Then parameter value is assigned by the set accessor to instance variable courseName (line 19 of Fig. 4.7). Line 21 (Fig. 4.8) displays a blank line, then line 24 calls myGradeBook’s DisplayMessage method to display the welcome message containing the course name.
4.6 UML Class Diagram with a Property Figure 4.9 contains an updated UML class diagram for the version of class GradeBook in Fig. 4.7. We model properties in the UML as attributes—the property (in this case, CourseName) is listed as a public attribute—as indicated by the plus (+) sign—preceded by the word “property” in guillemets (« and »). Using descriptive words in guillemets (called
Fig. 4.9 | UML class diagram indicating that class GradeBook has a public CourseName property of type string and one public method.
stereotypes in the UML) helps distinguish properties from other attributes and operations. The UML indicates the type of the property by placing a colon and a type after the property name. The get and set accessors of the property are implied, so they are not listed in the UML diagram. Class GradeBook also contains one public method DisplayMessage, so the class diagram lists this operation in the third compartment. Recall that the plus (+) sign is the public visibility symbol. In the preceding section, you learned how to declare a property in C# code. You saw that we typically name a property the same as the instance variable it manipulates, but with a capital first letter (e.g., property CourseName manipulates instance variable courseName). A class diagram helps you design a class, so it is not required to show every implementation detail of the class. Since, an instance variable that is manipulated by a property is really an implementation detail of that property, our class diagram does not show the courseName instance variable. A programmer implementing the GradeBook class based on this class diagram would create the instance variable courseName as part of the implementation process (as we did in Fig. 4.7). In some cases, you may find it necessary to model the private instance variables of a class that are not properties. Like properties, instance variables are attributes of a class and are modeled in the middle compartment of a class diagram. The UML represents instance variables as attributes by listing the attribute name, followed by a colon and the attribute type. To indicate that an attribute is private, a class diagram would list the private visibility symbol—a minus sign (–)—before the attribute’s name. For example, the instance variable courseName in Fig. 4.7 would be modeled as “- courseName : string” to indicate that it is a private attribute of type string.
4.7 Software Engineering with Properties and set and get Accessors Using properties as described earlier in this chapter would seem to violate the notion of private data. Although providing a property with get and set accessors may appear to be the same as making its corresponding instance variable public, this is not the case. A public instance variable can be read or written by any property or method in the program. If an instance variable is private, the client code can access the instance variable only indirectly through the class’s non-private properties or methods. This allows the class to control the manner in which the data is set or returned. For example, get and set accessors can translate between the format of the data used by the client and the format stored in the private instance variable. Consider a Clock class that represents the time of day as a private int instance variable time, containing the number of seconds since midnight. Suppose the class provides a
4.8 Value Types vs. Reference Types
109
Time property of type string to manipulate this instance variable. Although get accessors typically return data exactly as it is stored in an object, they need not expose the data in this “raw” format. When a client refers to a Clock object’s Time property, the property’s get accessor could use instance variable time to determine the number of hours, minutes and seconds since midnight, then return the time as a string of the form "HH:MM:SS". Similarly, suppose a Clock object’s Time property is assigned a string of the form "HH:MM:SS". Using the string capabilities presented in Chapter 16 and the method Convert.ToInt32 presented in Section 3.6, the Time property’s set accessor could convert this string to an int number of seconds since midnight and store the result in the Clock object’s private instance variable time. The Time property’s set accessor can also provide data validation capabilities that scrutinize attempts to modify the instance variable’s value to ensure that the value it receives represents a valid time (e.g., "12:30:45" is valid but "42:85:70" is not). We demonstrate data validation in Section 4.10. So, although a property’s accessors enable clients to manipulate private data, they carefully control those manipulations, and the object’s private data remains safely encapsulated (i.e., hidden) in the object. This is not possible with public instance variables, which can easily be set by clients to invalid values. Properties of a class should also be used by the class’s own methods to manipulate the class’s private instance variables, even though the methods can directly access the private instance variables. Accessing an instance variable via a property’s accessors—as in the body of method DisplayMessage (Fig. 4.7, lines 28–29)—creates a more robust class that is easier to maintain and less likely to malfunction. If we decide to change the representation of instance variable courseName in some way, the declaration of method DisplayMessage does not require modification—only the bodies of property CourseName’s get and set accessors that directly manipulate the instance variable will need to change. For example, suppose we want to represent the course name as two separate instance variables—courseNumber (e.g., "CS101") and courseTitle (e.g., "Introduction to C# Programming"). The DisplayMessage method can still use property CourseName’s get accessor to obtain the full course name to display as part of the welcome message. In this case, the get accessor would need to build and return a string containing the courseNumber, followed by the courseTitle. Method DisplayMessage would continue to display the complete course title “CS101 Introduction to C# Programming,” because it is unaffected by the change to the class’s instance variables.
Software Engineering Observation 4.3 Accessing private data through set and get accessors not only protects the instance variables from receiving invalid values, but also hides the internal representation of the instance variables from that class’s clients. Thus, if representation of the data changes (often to reduce the amount of required storage or to improve performance), only the properties’ implementations need to change—the clients’ implementations need not change as long as the services provided by the properties are preserved. 4.3
4.8 Value Types vs. Reference Types Types in C# are divided into two categories—value types and reference types. C#’s simple types are all value types. A variable of a value type (such as int) simply contains a value of that type. For example, Fig. 4.10 shows an int variable named count that contains the value 7.
110
Chapter 4
Introduction to Classes and Objects
int count = 7; count
A variable (count) of a value type (int) contains a value (7) of that type
7
Fig. 4.10 | Value type variable. By contrast, a variable of a reference type (sometimes called a reference) contains the address of a location in memory where the data referred to by that variable is stored. Such a variable is said to refer to an object in the program. Line 11 of Fig. 4.8 creates a GradeBook object, places it in memory and stores the object’s memory address in reference variable myGradeBook of type GradeBook as shown in Fig. 4.11. Note that the GradeBook object is shown with its courseName instance variable. Reference type instance variables (such as myGradeBook in Fig. 4.11) are initialized by default to the value null. string is a reference type. For this reason, string variable courseName is shown in Fig. 4.11 with an empty box representing the null-valued variable in memory. A client of an object must use a reference to the object to invoke (i.e., call) the object’s methods and access the object’s properties. In Fig. 4.8, the statements in Main use variable myGradeBook, which contains the GradeBook object’s reference, to send messages to the GradeBook object. These messages are calls to methods (like DisplayMessage) or references to properties (like CourseName) that enable the program to interact with GradeBook objects. For example, the statement (in line 20 of Fig. 4.8) myGradeBook.CourseName = theName; // set name using a property
uses the reference myGradeBook to set the course name by assigning a value to property CourseName. This sends a message to the GradeBook object to invoke the CourseName property’s set accessor. The message includes as an argument the value "CS101 Introduction to C# Programming" that CourseName’s set accessor requires to perform its task. The set accessor uses this information to set the courseName instance variable. In Section 7.14, we discuss value types and reference types in detail.
GradeBook myGradeBook = new GradeBook(); myGradeBook
(The arrow represents the memory address of the GradeBook object)
GradeBook object courseName
Fig. 4.11 | Reference type variable.
A variable (myGradeBook) of a reference type (GradeBook) contains a reference (memory address) to an object of that type
4.9 Initializing Objects with Constructors
111
Software Engineering Observation 4.4 A variable’s declared type (e.g., int, double or GradeBook) indicates whether the variable is of a value or a reference type. If a variable’s type is not one of the thirteen simple types, or an enum or a struct type (which we discuss in Section 7.10 and Chapter 16, respectively), then it is a reference type. For example, Account account1 indicates that account1 is a variable that can refer to an Account object. 4.4
4.9 Initializing Objects with Constructors As mentioned in Section 4.5, when an object of class GradeBook (Fig. 4.7) is created, its instance variable courseName is initialized to null by default. What if you want to provide a course name when you create a GradeBook object? Each class you declare can provide a constructor that can be used to initialize an object of a class when the object is created. In fact, C# requires a constructor call for every object that is created. The new operator calls the class’s constructor to perform the initialization. The constructor call is indicated by the class name, followed by parentheses. For example, line 11 of Fig. 4.8 first uses new to create a GradeBook object. The empty parentheses after “new GradeBook” indicate a call without arguments to the class’s constructor. By default, the compiler provides a default constructor with no parameters in any class that does not explicitly include a constructor, so every class has a constructor. When you declare a class, you can provide your own constructor to specify custom initialization for objects of your class. For example, you might want to specify a course name for a GradeBook object when the object is created, as in GradeBook myGradeBook = new GradeBook( "CS101 Introduction to C# Programming" );
In this case, the argument "CS101 Introduction to C# Programming" is passed to the GradeBook object’s constructor and used to initialize the courseName. Each time you create a different GradeBook object, you can provide a different course name. The preceding statement requires that the class provide a constructor with a string parameter. Figure 4.12 contains a modified GradeBook class with such a constructor. 1 2 3 4 5 6 7 8 9 10 11 12 13 14
// Fig. 4.12: GradeBook.cs // GradeBook class with a constructor to initialize the course name. using System; public class GradeBook { private string courseName; // course name for this GradeBook // constructor initializes courseName with string supplied as argument public GradeBook( string name ) { CourseName = name; // initialize courseName using property } // end constructor
Fig. 4.12 |
GradeBook
class with a constructor to initialize the course name. (Part 1 of 2.)
// property to get and set the course name public string CourseName { get { return courseName; } // end get set { courseName = value; } // end set } // end property CourseName // display a welcome message to the GradeBook user public void DisplayMessage() { // use property CourseName to get the // name of the course that this GradeBook represents Console.WriteLine( "Welcome to the grade book for\n{0}!", CourseName ); } // end method DisplayMessage } // end class GradeBook
Fig. 4.12 |
GradeBook
class with a constructor to initialize the course name. (Part 2 of 2.)
Lines 10–13 declare the constructor for class GradeBook. A constructor must have the same name as its class. Like a method, a constructor specifies in its parameter list the data it requires to perform its task. Unlike a method, a constructor doesn’t specify a return type. When you create a new object (with new), you place this data in the parentheses that follow the class name. Line 10 indicates that class GradeBook’s constructor has a parameter called name of type string. In line 12 of the constructor’s body, the name passed to the constructor is assigned to instance variable courseName via the set accessor of property CourseName. Figure 4.13 demonstrates initializing GradeBook objects using this constructor. Lines 12–13 create and initialize a GradeBook object. The constructor of class GradeBook is called with the argument "CS101 Introduction to C# Programming" to initialize the course name. The object creation expression to the right of = in lines 12–13 returns a reference to the new object, which is assigned to variable gradeBook1. Lines 14–15 repeat this process for another GradeBook object, this time passing the argument "CS102 Data Structures in C#" to initialize the course name for gradeBook2. Lines 18–21 use each object’s CourseName property to obtain the course names and show that they were indeed initialized when the objects were created. In the introduction to Section 4.5, you learned that each instance (i.e., object) of a class contains its own copy of the class’s instance variables. The output confirms that each GradeBook maintains its own copy of instance variable courseName. Like methods, constructors also can take arguments. However, an important difference between constructors and methods is that constructors cannot return values—in fact, they cannot specify a return type (not even void). Normally, constructors are declared public. If a class does not include a constructor, the class’s instance variables are initialized to their default values. If you declare any constructors for a class, C# will not create a default constructor for that class.
// Fig. 4.13: GradeBookTest.cs // GradeBook constructor used to specify the course name at the // time each GradeBook object is created. using System; public class GradeBookTest { // Main method begins program execution public static void Main( string[] args ) { // create GradeBook object GradeBook gradeBook1 = new GradeBook( // invokes constructor "CS101 Introduction to C# Programming" ); GradeBook gradeBook2 = new GradeBook( // invokes constructor "CS102 Data Structures in C#" ); // display initial value of courseName for each GradeBook Console.WriteLine( "gradeBook1 course name is: {0}", gradeBook1.CourseName ); Console.WriteLine( "gradeBook2 course name is: {0}", gradeBook2.CourseName ); } // end Main } // end class GradeBookTest
gradeBook1 course name is: CS101 Introduction to C# Programming gradeBook2 course name is: CS102 Data Structures in C#
Fig. 4.13 |
constructor used to specify the course name at the time each GradeBook object is created. GradeBook
Error-Prevention Tip 4.1 Unless default initialization of your class’s instance variables is acceptable, provide a constructor to ensure that your class’s instance variables are properly initialized with meaningful values when each new object of your class is created. 4.1
Adding the Constructor to Class GradeBook’s UML Class Diagram The UML class diagram of Fig. 4.14 models class GradeBook of Fig. 4.12, which has a constructor that has a courseName parameter of type string. Like operations, the UML models constructors in the third compartment of a class in a class diagram. To distinguish a constructor from a class’s operations, the UML places the word “constructor” between guillemets (« and ») before the constructor’s name. It is customary to list constructors before other operations in the third compartment.
4.10 Floating-Point Numbers and Type decimal In our next application, we depart temporarily from our GradeBook case study to declare a class called Account that maintains the balance of a bank account. Most account balances are not whole numbers (e.g., 0, –22 and 1024). For this reason, class Account represents the account balance as a real number (i.e., a number with a decimal point, such as 7.33, 0.0975 or 1000.12345). C# provides three simple types for storing real numbers in memory—float,
Fig. 4.14 | UML class diagram indicating that class GradeBook has a constructor with a name
parameter of type string.
double, and decimal. Types float and double are called floating-point types. The primary difference between them and decimal is that decimal variables store a limited range of real
numbers precisely, whereas floating-point variables store only approximations of real numbers, but across a much greater range of values. Also, double variables can store numbers with larger magnitude and finer detail (i.e., more digits to the right of the decimal point—also known as the number’s precision) than float variables. A key application of type decimal is representing monetary amounts.
Real Number Precision and Memory Requirements Variables of type float represent single-precision floating-point numbers and have seven significant digits. Variables of type double represent double-precision floating-point numbers. These require twice as much memory as float variables and provide 15–16 significant digits—approximately double the precision of float variables. Furthermore, variables of type decimal require twice as much memory as double variables and provide 28– 29 significant digits. For the range of values required by most applications, variables of type float should suffice for approximations, but you can use double or decimal to “play it safe.” In some applications, even variables of type double and decimal will be inadequate—such applications are beyond the scope of this book. Most programmers represent floating-point numbers with type double. In fact, C# treats all real numbers you type in an application’s source code (such as 7.33 and 0.0975) as double values by default. Such values in the source code are known as floating-point literals. To type a decimal literal, you must type the letter “M” or “m” at the end of a real number (for example, 7.33M is a decimal literal rather than a double). Integer literals are implicitly converted into type float, double or decimal when they are assigned to a variable of one of these types. See Appendix L, Simple Types, for the ranges of values for floats, doubles, decimals and all the other simple types. Although floating-point numbers are not always 100% precise, they have numerous applications. For example, when we speak of a “normal” body temperature of 98.6, we do not need to be precise to a large number of digits. When we read the temperature on a thermometer as 98.6, it may actually be 98.5999473210643. Calling this number simply 98.6 is fine for most applications involving body temperatures. Due to the imprecise nature of floating-point numbers, type decimal is preferred over the floating-point types whenever the calculations need to be exact, as with monetary calculations. In cases where approximation is enough, double is preferred over type float because double variables can represent floating-point numbers more accurately. For this reason, we use type decimal throughout the book for dealing with monetary amounts and type double for other real numbers.
4.10 Floating-Point Numbers and Type decimal
115
Real numbers also arise as a result of division. In conventional arithmetic, for example, when we divide 10 by 3, the result is 3.3333333…, with the sequence of 3s repeating infinitely. The computer allocates only a fixed amount of space to hold such a value, so clearly the stored floating-point value can be only an approximation.
Common Programming Error 4.3 Using floating-point numbers in a manner that assumes they are represented precisely can lead to logic errors. 4.3
Class with an Instance Variable of Type decimal Our next application (Figs. 4.15–4.16) contains an oversimplified class named Account (Fig. 4.15) that maintains the balance of a bank account. A typical bank services many accounts, each with its own balance, so line 7 declares an instance variable named balance of type decimal. Variable balance is an instance variable because it is declared in the body of the class (lines 6–36) but outside the class’s method and property declarations (lines 10– 13, 16–19 and 22–35). Every instance (i.e., object) of class Account contains its own copy of balance. Class Account contains a constructor, a method, and a property. Since it is common for someone opening an account to place money in the account immediately, the constructor (lines 10–13) receives a parameter initialBalance of type decimal that represents the account’s starting balance. Line 12 assigns initialBalance to the property Balance, invoking Balance’s set accessor to initialize the instance variable balance. Method Credit (lines 16–19) does not return any data when it completes its task, so its return type is void. The method receives one parameter named amount—a decimal value that is added to the property Balance. Line 18 uses both the get and set accessors of Balance. The expression Balance + amount invokes property Balance’s get accessor to obtain the current value of instance variable balance, then adds amount to it. We then assign the result to instance variable balance by invoking the Balance property’s set accessor (thus replacing the prior balance value). Property Balance (lines 22–35) provides a get accessor, which allows clients of the class (i.e., other classes that use this class) to obtain the value of a particular Account object’s balance. The property has type decimal (line 22). Balance also provides an enhanced set accessor. In Section 4.5, we introduced properties whose set accessors allow clients of a class to modify the value of a private instance variable. In Fig. 4.7, class GradeBook defines property CourseName’s set accessor to assign the value received in its parameter value to instance variable courseName (line 19). This CourseName property does not ensure that courseName contains only valid data. The application of Figs. 4.15–4.16 enhances the set accessor of class Account’s property Balance to perform this validation (also known as validity checking). Line 32 (Fig. 4.15) ensures that value is non-negative. If the value is greater than or equal to 0, the amount stored in value is assigned to instance variable balance in line 33. Otherwise, balance is left unchanged. Account
Class to Use Class Account Class AccountTest (Fig. 4.16) creates two Account objects (lines 10–11) and initializes them respectively with 50.00M and -7.53M (the decimal literals representing the real numbers AccountTest
// Fig. 4.15: Account.cs // Account class with a constructor to // initialize instance variable balance. public class Account { private decimal balance; // instance variable that stores the balance // constructor public Account( decimal initialBalance ) { Balance = initialBalance; // set balance using property } // end Account constructor // credit (add) an amount to the account public void Credit( decimal amount ) { Balance = Balance + amount; // add amount to balance } // end method Credit // a property to get and set the account balance public decimal Balance { get { return balance; } // end get set { // validate that value is greater than or equal to 0; // if it is not, balance is left unchanged if ( value >= 0 ) balance = value; } // end set } // end property Balance } // end class Account
Fig. 4.15 |
Account
class with a constructor to initialize instance variable balance.
and -7.53). Note that the Account constructor (lines 10–13 of Fig. 4.15) references property Balance to initialize balance. In previous examples, the benefit of referencing the property in the constructor was not evident. Now, however, the constructor takes advantage of the validation provided by the set accessor of the Balance property. The constructor simply assigns a value to Balance rather than duplicating the set accessor’s validation code. When line 11 of Fig. 4.16 passes an initial balance of -7.53 to the Account constructor, the constructor passes this value to the set accessor of property Balance, where the actual initialization occurs. This value is less than 0, so the set accessor does not modify balance, leaving this instance variable with its default value of 0. Lines 14–17 in Fig. 4.16 output the balance in each Account by using the Account’s Balance property. When Balance is used for account1 (line 15), the value of account1’s balance is returned by the get accessor in line 26 of Fig. 4.15 and displayed by the Console.WriteLine statement (Fig. 4.16, lines 14–15). Similarly, when property Balance is 50.00
4.10 Floating-Point Numbers and Type decimal
117
called for account2 from line 17, the value of the account2’s balance is returned from line 26 of Fig. 4.15 and displayed by the Console.WriteLine statement (Fig. 4.16, lines 16– 17). Note that the balance of account2 is 0 because the constructor ensured that the account could not begin with a negative balance. The value is output by WriteLine with the format item {0:C}, which formats the account balance as a monetary amount. The : after the 0 indicates that the next character represents a format specifier, and the C format specifier after the : specifies a monetary amount (C is for currency). The cultural settings on the user’s machine determine the format for displaying monetary amounts. For example, in the United States, 50 displays as $50.00. In Germany, 50 displays as 50,00€. Figure 4.17 lists a few other format specifiers in addition to C. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
// Fig. 4.16: AccountTest.cs // Create and manipulate an Account object. using System; public class AccountTest { // Main method begins execution of C# application public static void Main( string[] args ) { Account account1 = new Account( 50.00M ); // create Account object Account account2 = new Account( -7.53M ); // create Account object // display initial balance of each object using a property Console.WriteLine( "account1 balance: {0:C}", account1.Balance ); // display Balance property Console.WriteLine( "account2 balance: {0:C}\n", account2.Balance ); // display Balance property decimal depositAmount; // deposit amount read from user // prompt and obtain user input Console.Write( "Enter deposit amount for account1: " ); depositAmount = Convert.ToDecimal( Console.ReadLine() ); Console.WriteLine( "adding {0:C} to account1 balance\n", depositAmount ); account1.Credit( depositAmount ); // add to account1 balance // display balances Console.WriteLine( "account1 balance: {0:C}", account1.Balance ); Console.WriteLine( "account2 balance: {0:C}\n", account2.Balance ); // prompt and obtain user input Console.Write( "Enter deposit amount for account2: " ); depositAmount = Convert.ToDecimal( Console.ReadLine() ); Console.WriteLine( "adding {0:C} to account2 balance\n", depositAmount ); account2.Credit( depositAmount ); // add to account2 balance
Fig. 4.16 | Create and manipulate an Account object. (Part 1 of 2.)
118 40 41 42 43 44 45
Chapter 4
Introduction to Classes and Objects
// display balances Console.WriteLine( "account1 balance: {0:C}", account1.Balance ); Console.WriteLine( "account2 balance: {0:C}", account2.Balance ); } // end Main } // end class AccountTest
account1 balance: $50.00 account2 balance: $0.00 Enter deposit amount for account1: 49.99 adding $49.99 to account1 balance account1 balance: $99.99 account2 balance: $0.00 Enter deposit amount for account2: 123.21 adding $123.21 to account2 balance account1 balance: $99.99 account2 balance: $123.21
Fig. 4.16 | Create and manipulate an Account object. (Part 2 of 2.)
Format Specifier
Description
C
or c
Formats the string as currency. Precedes the number with an appropriate currency symbol ($ in the US). Separates digits with an appropriate separator character (comma in the US) and sets the number of decimal places to two by default.
D
or d
Formats the string as a decimal. Displays number as an integer.
N
or n
Formats the string with commas and a default of two decimal places.
E
or e
Formats the number using scientific notation with a default of six decimal places.
F
or f
Formats the string with a fixed number of decimal places (two by default).
G
or g
General. Formats the number normally with decimal places or using scientific notation, depending on context. If a format item does not contain a format specifier, format G is assumed implicitly.
X
or x
Formats the string as hexadecimal.
Fig. 4.17
| string
format specifiers.
Line 19 declares local variable depositAmount to store each deposit amount entered by the user. Unlike the instance variable balance in class Account, the local variable depositAmount in Main is not initialized to 0 by default. However, this variable does not need to be initialized here because its value will be determined by the user’s input.
4.10 Floating-Point Numbers and Type decimal
119
Line 22 prompts the user to enter a deposit amount for account1. Line 23 obtains the input from the user by calling the Console class’s ReadLine method, and then passing the string entered by the user to the Convert class’s ToDecimal method, which returns the decimal value in this string. Lines 24–25 display the deposit amount. Line 26 calls object account1’s Credit method and supplies depositAmount as the method’s argument. When the method is called, the argument’s value is assigned to parameter amount of method Credit (lines 16–19 of Fig. 4.15), then method Credit adds that value to the balance (line 18 of Fig. 4.15). Lines 29–32 (Fig. 4.16) output the balances of both Accounts again to show that only account1’s balance changed. Line 35 prompts the user to enter a deposit amount for account2. Line 36 obtains the input from the user by calling Console class’s ReadLine method, and passing the return value to the Convert class’s ToDecimal method. Lines 37–38 display the deposit amount. Line 39 calls object account2’s Credit method and supplies depositAmount as the method’s argument, then method Credit adds that value to the balance. Finally, lines 42–43 output the balances of both Accounts again to show that only account2’s balance changed.
and get Accessors with Different Access Modifiers By default, the get and set accessors of a property have the same access as the property— for example, for a public property, the accessors are public. It is possible to declare the get and set accessors with different access modifiers. In this case, one of the accessors must implicitly have the same access as the property and the other must be declared with a more restrictive access modifier than the property. For example, in a public property, the get accessor might be public and the set accessor might be private. We demonstrate this feature in Section 9.6. set
Error-Prevention Tip 4.2 The benefits of data integrity are not automatic simply because instance variables are made private—you must provide appropriate validity checking and report the errors. 4.2
Error-Prevention Tip 4.3 accessors that set the values of private data should verify that the intended new values are proper; if they are not, the set accessors should leave the instance variables unchanged and generate an error. We demonstrate how to gracefully generate errors in Chapter 12, Exception Handling. set
4.3
UML Class Diagram for Class Account The UML class diagram in Fig. 4.18 models class Account of Fig. 4.15. The diagram models the Balance property as a UML attribute of type decimal (because the corresponding C# Account + «property» Balance : decimal + «constructor» Account( initialBalance : decimal ) + Credit( amount : decimal )
Fig. 4.18 | UML class diagram indicating that class Account has a public Balance property of type decimal, a constructor and a method.
120
Chapter 4
Introduction to Classes and Objects
property had type decimal). The diagram models class Account’s constructor with a parameter initialBalance of type decimal in the third compartment of the class. The diagram models operation Credit in the third compartment with an amount parameter of type decimal (because the corresponding method has an amount parameter of C# type decimal).
4.11 (Optional) Software Engineering Case Study: Identifying the Classes in the ATM Requirements Document Now we begin designing the ATM system that we introduced in Chapter 3. In this section, we identify the classes that are needed to build the ATM system by analyzing the nouns and noun phrases that appear in the requirements document. We introduce UML class diagrams to model the relationships between these classes. This is an important first step in defining the structure of our system.
Identifying the Classes in a System We begin our object-oriented design (OOD) process by identifying the classes required to build the ATM system. We will eventually describe these classes using UML class diagrams and implement these classes in C#. First, we review the requirements document of Section 3.10 and find key nouns and noun phrases to help us identify classes that comprise the ATM system. We may decide that some of these nouns and noun phrases are attributes of other classes in the system. We may also conclude that some of the nouns and noun phrases do not correspond to parts of the system and thus should not be modeled at all. Additional classes may become apparent to us as we proceed through the design process. Figure 4.19 lists the nouns and noun phrases in the requirements document. We create classes only for the nouns and noun phrases that have significance in the ATM system. We do not need to model “bank” as a class, because the bank is not a part of the ATM system—the bank simply wants us to build the ATM. “User” and “customer” also represent entities outside of the system—they are important because they interact with our ATM system, but we do not need to model them as classes in the ATM system. Recall that we modeled an ATM user (i.e., a bank customer) as the actor in the use case diagram of Fig. 3.31.
Nouns and noun phrases in the requirements document bank
money / funds
account number
ATM
screen
PIN
user
keypad
bank database
customer
cash dispenser
balance inquiry
transaction
$20 bill / cash
withdrawal
account
deposit slot
deposit
balance
deposit envelope
Fig. 4.19 | Nouns and noun phrases in the requirements document.
4.11 Identifying the Classes in the ATM Requirements Document
121
We do not model “$20 bill” or “deposit envelope” as classes. These are physical objects in the real world, but they are not part of what is being automated. We can adequately represent the presence of bills in the system using an attribute of the class that models the cash dispenser. (We assign attributes to classes in Section 5.12.) For example, the cash dispenser maintains a count of the number of bills it contains. The requirements document does not say anything about what the system should do with deposit envelopes after it receives them. We can assume that simply acknowledging the receipt of an envelope—an operation performed by the class that models the deposit slot—is sufficient to represent the presence of an envelope in the system. (We assign operations to classes in Section 7.15.) In our simplified ATM system, representing various amounts of “money,” including the “balance” of an account, as attributes of other classes seems most appropriate. Likewise, the nouns “account number” and “PIN” represent significant pieces of information in the ATM system. They are important attributes of a bank account. They do not, however, exhibit behaviors. Thus, we can most appropriately model them as attributes of an account class. Though the requirements document frequently describes a “transaction” in a general sense, we do not model the broad notion of a financial transaction at this time. Instead, we model the three types of transactions (i.e., “balance inquiry,” “withdrawal” and “deposit”) as individual classes. These classes possess specific attributes needed to execute the transactions they represent. For example, a withdrawal needs to know the amount of money the user wants to withdraw. A balance inquiry, however, does not require any additional data. Furthermore, the three transaction classes exhibit unique behaviors. A withdrawal involves dispensing cash to the user, whereas a deposit involves receiving a deposit envelope from the user. [Note: In Section 11.9, we “factor out” common features of all transactions into a general “transaction” class using the object-oriented concepts of abstract classes and inheritance.] We determine the classes for our system based on the remaining nouns and noun phrases from Fig. 4.19. Each of these refers to one or more of the following: • ATM •
screen
•
keypad
•
cash dispenser
•
deposit slot
•
account
•
bank database
•
balance inquiry
•
withdrawal
•
deposit
The elements of this list are likely to be classes we will need to implement our system, although it’s too early in our design process to claim that this list is complete. We can now model the classes in our system based on the list we have created. We capitalize class names in the design process—a UML convention—as we will do when we write the actual C# code that implements our design. If the name of a class contains more than one
122
Chapter 4
Introduction to Classes and Objects
word, we run the words together and capitalize each word (e.g., MultipleWordName). Using these conventions, we create classes ATM, Screen, Keypad, CashDispenser, DepositSlot, Account, BankDatabase, BalanceInquiry, Withdrawal and Deposit. We construct our system using all of these classes as building blocks. Before we begin building the system, however, we must gain a better understanding of how the classes relate to one another.
Modeling Classes The UML enables us to model, via class diagrams, the classes in the ATM system and their interrelationships. Figure 4.20 represents class ATM. In the UML, each class is modeled as a rectangle with three compartments. The top compartment contains the name of the class, centered horizontally and appearing in boldface. The middle compartment contains the class’s attributes. (We discuss attributes in Section 5.12 and Section 6.9.) The bottom compartment contains the class’s operations (discussed in Section 7.15). In Fig. 4.20, the middle and bottom compartments are empty, because we have not yet determined this class’s attributes and operations. Class diagrams also show the relationships between the classes of the system. Figure 4.21 shows how our classes ATM and Withdrawal relate to one another. For the moment, we choose to model only this subset of the ATM classes for simplicity. We present a more complete class diagram later in this section. Notice that the rectangles representing classes in this diagram are not subdivided into compartments. The UML allows the suppression of class attributes and operations in this manner, when appropriate, to create more readable diagrams. Such a diagram is said to be an elided diagram—one in which some information, such as the contents of the second and third compartments, is not modeled. We will place information in these compartments in Section 5.12 and Section 7.15. In Fig. 4.21, the solid line that connects the two classes represents an association—a relationship between classes. The numbers near each end of the line are multiplicity values, which indicate how many objects of each class participate in the association. In this case, following the line from one end to the other reveals that, at any given moment, one ATM object participates in an association with either zero or one Withdrawal objects—zero if the current user is not performing a transaction or has requested a different type of transaction, and one if the user has requested a withdrawal. The UML can model many types of multiplicity. Figure 4.22 explains the multiplicity types.
ATM
Fig. 4.20 | Representing a class in the UML using a class diagram.
ATM
1
0..1 Executes currentTransaction
Fig. 4.21 | Class diagram showing an association among classes.
Withdrawal
4.11 Identifying the Classes in the ATM Requirements Document
Symbol
Meaning
0
None
1
One
m
An integer value
0..1
Zero or one
m, n
m or n
m..n
At least m, but not more than n
*
Any nonnegative integer (zero or more)
0..*
Zero or more (identical to *)
1..*
One or more
123
Fig. 4.22 | Multiplicity types. An association can be named. For example, the word Executes above the line connecting classes ATM and Withdrawal in Fig. 4.21 indicates the name of that association. This part of the diagram reads “one object of class ATM executes zero or one objects of class Withdrawal.” Note that association names are directional, as indicated by the filled arrowhead—so it would be improper, for example, to read the preceding association from right to left as “zero or one objects of class Withdrawal execute one object of class ATM.” The word currentTransaction at the Withdrawal end of the association line in Fig. 4.21 is a role name, which identifies the role the Withdrawal object plays in its relationship with the ATM. A role name adds meaning to an association between classes by identifying the role a class plays in the context of an association. A class can play several roles in the same system. For example, in a college personnel system, a person may play the role of “professor” when relating to students. The same person may take on the role of “colleague” when participating in a relationship with another professor, and “coach” when coaching student athletes. In Fig. 4.21, the role name currentTransaction indicates that the Withdrawal object participating in the Executes association with an object of class ATM represents the transaction currently being processed by the ATM. In other contexts, a Withdrawal object may take on other roles (e.g., the previous transaction). Notice that we do not specify a role name for the ATM end of the Executes association. Role names are often omitted in class diagrams when the meaning of an association is clear without them. In addition to indicating simple relationships, associations can specify more complex relationships, such as objects of one class being composed of objects of other classes. Consider a real-world automated teller machine. What “pieces” does a manufacturer put together to build a working ATM? Our requirements document tells us that the ATM is composed of a screen, a keypad, a cash dispenser and a deposit slot. In Fig. 4.23, the solid diamonds attached to the association lines of class ATM indicate that class ATM has a composition relationship with classes Screen, Keypad, CashDispenser and DepositSlot. Composition implies a whole/part relationship. The class that has the composition symbol (the solid diamond) on its end of the association line is the whole (in this case, ATM), and the classes on the other end of the association lines are the parts—in this case, classes Screen, Keypad, CashDispenser and DepositSlot. The compositions in
124
Chapter 4
Introduction to Classes and Objects
Screen 1 1 DepositSlot
1
1
ATM
1
1
CashDispenser
1 1 Keypad
Fig. 4.23 | Class diagram showing composition relationships. Fig. 4.23 indicate that an object of class ATM is formed from one object of class Screen, one object of class CashDispenser, one object of class Keypad and one object of class DepositSlot—the ATM “has a” screen, a keypad, a cash dispenser and a deposit slot. The “hasa” relationship defines composition. (We will see in the Software Engineering Case Study section in Chapter 11 that the “is-a” relationship defines inheritance.) According to the UML specification, composition relationships have the following properties: 1. Only one class in the relationship can represent the whole (i.e., the diamond can be placed on only one end of the association line). For example, either the screen is part of the ATM or the ATM is part of the screen, but the screen and the ATM cannot both represent the whole in the relationship. 2. The parts in the composition relationship exist only as long as the whole, and the whole is responsible for the creation and destruction of its parts. For example, the act of constructing an ATM includes manufacturing its parts. Furthermore, if the ATM is destroyed, its screen, keypad, cash dispenser and deposit slot are also destroyed. 3. A part may belong to only one whole at a time, although the part may be removed and attached to another whole, which then assumes responsibility for the part. The solid diamonds in our class diagrams indicate composition relationships that fulfill these three properties. If a “has-a” relationship does not satisfy one or more of these criteria, the UML specifies that hollow diamonds be attached to the ends of association lines to indicate aggregation—a weaker form of composition. For example, a personal computer and a computer monitor participate in an aggregation relationship—the computer “has a” monitor, but the two parts can exist independently, and the same monitor can be attached to multiple computers at once, thus violating the second and third properties of composition. Figure 4.24 shows a class diagram for the ATM system. This diagram models most of the classes that we identified earlier in this section, as well as the associations between them that we can infer from the requirements document. [Note: Classes BalanceInquiry and Deposit participate in associations similar to those of class Withdrawal, so we have chosen
125
4.11 Identifying the Classes in the ATM Requirements Document
1 Keypad
1
1
1
CashDispenser
1
Screen
DepositSlot 1
1 1
1
1
ATM
1
0..1 Executes 1
0..1
0..1
0..1
Withdrawal 0..1
1 Authenticates user against 1 BankDatabase
Contains
1
Accesses/modifies an account balance through
1 0..*
Account
Fig. 4.24 | Class diagram for the ATM system model. to omit them from this diagram for simplicity. In Chapter 11, we expand our class diagram to include all the classes in the ATM system.] Figure 4.24 presents a graphical model of the structure of the ATM system. This class diagram includes classes BankDatabase and Account, and several associations that were not present in either Fig. 4.21 or Fig. 4.23. The class diagram shows that class ATM has a oneto-one relationship with class BankDatabase—one ATM object authenticates users against one BankDatabase object. In Fig. 4.24, we also model the fact that the bank’s database contains information about many accounts—one object of class BankDatabase participates in a composition relationship with zero or more objects of class Account. Recall from Fig. 4.22 that the multiplicity value 0..* at the Account end of the association between class BankDatabase and class Account indicates that zero or more objects of class Account take part in the association. Class BankDatabase has a one-to-many relationship with class Account— the BankDatabase can contain many Accounts. Similarly, class Account has a many-to-one relationship with class BankDatabase—there can be many Accounts in the BankDatabase. Recall from Fig. 4.22 that the multiplicity value * is identical to 0..*.] Figure 4.24 also indicates that if the user is performing a withdrawal, “one object of class Withdrawal accesses/modifies an account balance through one object of class BankDatabase.” We could have created an association directly between class Withdrawal and class Account. The requirements document, however, states that the “ATM must interact with the bank’s account information database” to perform transactions. A bank account contains sensitive information, and systems engineers must always consider the security of
126
Chapter 4
Introduction to Classes and Objects
personal data when designing a system. Thus, only the BankDatabase can access and manipulate an account directly. All other parts of the system must interact with the database to retrieve or update account information (e.g., an account balance). The class diagram in Fig. 4.24 also models associations between class Withdrawal and classes Screen, CashDispenser and Keypad. A withdrawal transaction includes prompting the user to choose a withdrawal amount and receiving numeric input. These actions require the use of the screen and the keypad, respectively. Dispensing cash to the user requires access to the cash dispenser. Classes BalanceInquiry and Deposit, though not shown in Fig. 4.24, take part in several associations with the other classes of the ATM system. Like class Withdrawal, each of these classes associates with classes ATM and BankDatabase. An object of class BalanceInquiry also associates with an object of class Screen to display the balance of an account to the user. Class Deposit associates with classes Screen, Keypad and DepositSlot. Like withdrawals, deposit transactions require use of the screen and the keypad to display prompts and receive inputs, respectively. To receive a deposit envelope, an object of class Deposit associates with an object of class DepositSlot. We have identified the classes in our ATM system, although we may discover others as we proceed with the design and implementation. In Section 5.12, we determine the attributes for each of these classes, and in Section 6.9, we use these attributes to examine how the system changes over time. In Section 7.15, we determine the operations of the classes in our system.
Software Engineering Case Study Self-Review Exercises 4.1 Suppose we have a class Car that represents a car. Think of some of the different pieces that a manufacturer would put together to produce a whole car. Create a class diagram (similar to Fig. 4.23) that models some of the composition relationships of class Car. 4.2 Suppose we have a class File that represents an electronic document in a stand-alone, nonnetworked computer represented by class Computer. What sort of association exists between class Computer and class File? a) Class Computer has a one-to-one relationship with class File. b) Class Computer has a many-to-one relationship with class File. c) Class Computer has a one-to-many relationship with class File. d) Class Computer has a many-to-many relationship with class File. 4.3 State whether the following statement is true or false. If false, explain why: A UML class diagram in which a class’s second and third compartments are not modeled is said to be an elided diagram. 4.4
Modify the class diagram of Fig. 4.24 to include class Deposit instead of class Withdrawal.
Answers to Software Engineering Case Study Self-Review Exercises 4.1 Figure 4.25 presents a class diagram that shows some of the composition relationships of a class Car. 4.2
c. In a computer network, this relationship could be many-to-many.
4.3
True.
Figure 4.26 presents a class diagram for the ATM including class Deposit instead of class (as in Fig. 4.24). Note that class Deposit does not associate with class CashDispenser, but does associate with class DepositSlot. 4.4
Withdrawal
4.12 Wrap-Up
127
Wheel 4 1 SteeringWheel
1
1
1
Car
5
SeatBelt
1 2 Windshield
Fig. 4.25 | Class diagram showing some composition relationships of a class Car.
1 Keypad
1
1
1
CashDispenser
1
Screen
DepositSlot 1
1 1
1
1
ATM
1
0..1 Executes 1
0..1
0..1
0..1
Deposit 0..1
1 Authenticates user against 1 BankDatabase
Contains
1
Accesses/modifies an account balance through
1 0..*
Account
Fig. 4.26 | Class diagram for the ATM system model including class Deposit.
4.12 Wrap-Up In this chapter, you learned the basic object-oriented concepts of classes, objects, methods, instance variables and properties—these will be used in most substantial C# applications you create. You learned how to declare instance variables of a class to maintain data for each object of the class, how to declare methods that operate on that data, and how to de-
128
Chapter 4
Introduction to Classes and Objects
clare properties to obtain and set that data. We demonstrated how to call a method to tell it to perform its task and how to pass information to methods as arguments. We discussed the difference between a local variable of a method and an instance variable of a class and that only instance variables are initialized automatically. You also learned how to use a class’s constructor to specify the initial values for an object’s instance variables. We discussed some of the differences between value types and reference types. You learned about the value types float, double and decimal for storing real numbers. Throughout the chapter, we showed how the UML can be used to create class diagrams that model the constructors, methods, properties and attributes of classes. You learned the value of declaring instance variables private, and using public properties to manipulate them. For example, we demonstrated how set accessors in properties can be used to validate an object’s data and ensure that the object is maintained in a consistent state. In the next chapter we begin our introduction to control statements, which specify the order in which an application’s actions are performed. You will use these in your methods to specify how they should perform their tasks.
5 Control Statements: Part 1 Let’s all move one place on. —Lewis Carroll
The wheel is come full circle. —William Shakespeare
How many apples fell on Newton’s head before he took the hint! —Robert Frost
All the evolution we know of proceeds from the vague to the definite. —Charles Sanders Peirce
OBJECTIVES In this chapter you will learn: I
To use the if and if…else selection statements to choose between alternative actions.
I
To use the while repetition statement to execute statements in an application repeatedly.
I
To use counter-controlled repetition and sentinelcontrolled repetition.
I
To use the increment, decrement and compound assignment operators.
Introduction Control Structures if Single-Selection Statement if…else Double-Selection Statement while Repetition Statement Formulating Algorithms: Counter-Controlled Repetition Formulating Algorithms: Sentinel-Controlled Repetition Formulating Algorithms: Nested Control Statements Compound Assignment Operators Increment and Decrement Operators Simple Types (Optional) Software Engineering Case Study: Identifying Class Attributes in the ATM System 5.13 Wrap-Up
5.1 Introduction In this chapter, we introduce C#’s if, if…else and while control statements. We devote a portion of the chapter (and Chapters 6 and 8) to further developing the GradeBook class introduced in Chapter 4. In particular, we add a method to the GradeBook class that uses control statements to calculate the average of a set of student grades. Another example demonstrates additional ways to combine control statements to solve a similar problem. We introduce C#’s compound assignment operators and explore its increment and decrement operators. Finally, we present an overview of C#’s simple types.
5.2 Control Structures Normally, statements in an application are executed one after the other in the order in which they are written. This process is called sequential execution. Various C# statements, which we will soon discuss, enable you to specify that the next statement to execute is not necessarily the next one in sequence. This is called transfer of control. During the 1960s, it became clear that the indiscriminate use of transfers of control was the root of much difficulty experienced by software development groups. The blame was pointed at the goto statement (used in most programming languages of the time), which allows programmers to specify a transfer of control to one of a very wide range of possible destinations in an application (creating what is often called “spaghetti code”). The notion of so-called structured programming became almost synonymous with “goto elimination.” We recommend that you avoid C#’s goto statement. The research of Bohm and Jacopini1 had demonstrated that applications could be written without goto statements. The challenge of the era for programmers was to shift their styles to “goto-less programming.” Not until the 1970s did programmers start taking 1.
Bohm, C., and G. Jacopini, “Flow Diagrams, Turing Machines, and Languages with Only Two Formation Rules,” Communications of the ACM, Vol. 9, No. 5, May 1966, pp. 336–371.
5.2 Control Structures
131
structured programming seriously. The results were impressive. Software development groups reported shorter development times, more frequent on-time delivery of systems and more frequent within-budget completion of software projects. The key to these successes was that structured applications were clearer, easier to debug and modify, and more likely to be bug free in the first place. Bohm and Jacopini’s work demonstrated that all applications could be written in terms of only three control structures—the sequence structure, the selection structure and the repetition structure. The term “control structures” comes from the field of computer science. When we introduce C#’s implementations of control structures, we will refer to them in the terminology of the C# Language Specification as “control statements.”
Sequence Structure in C# The sequence structure is built into C#. Unless directed otherwise, the computer executes C# statements one after the other in the order in which they are written—that is, in sequence. The UML activity diagram in Fig. 5.1 illustrates a typical sequence structure in which two calculations are performed in order. C# lets you have as many actions as you want in a sequence structure. As you will soon see, anywhere a single action may be placed, you may place several actions in sequence. An activity diagram models the workflow (also called the activity) of a portion of a software system. Such workflows may include a portion of an algorithm, such as the sequence structure in Fig. 5.1. Activity diagrams are composed of special-purpose symbols, such as action-state symbols (rectangles with their left and right sides replaced with arcs curving outward), diamonds and small circles. These symbols are connected by transition arrows, which represent the flow of the activity—that is, the order in which the actions should occur. Activity diagrams help you develop and represent algorithms. Activity diagrams clearly show how control structures operate. Consider the activity diagram for the sequence structure in Fig. 5.1. It contains two action states that represent actions to perform. Each action state contains an action expression—for example, “add grade to total” or “add 1 to counter”—that specifies a particular action to perform. Other actions might include calculations or input/output operations. The arrows in the activity diagram represent transitions, which indicate the order in which the actions represented by the action states occur. The portion of the application that implements the activities illustrated by the diagram in Fig. 5.1 first adds grade to total, then adds 1 to counter.
add grade to total
add 1 to counter
Fig. 5.1 | Sequence structure activity diagram.
Corresponding C# statement: total = total + grade;
The solid circle located at the top of the activity diagram represents the activity’s initial state—the beginning of the workflow before the application performs the modeled actions. The solid circle surrounded by a hollow circle that appears at the bottom of the diagram represents the final state—the end of the workflow after the application performs its actions. Figure 5.1 also includes rectangles with the upper-right corners folded over. These are UML notes (like comments in C#)—explanatory remarks that describe the purpose of symbols in the diagram. Figure 5.1 uses UML notes to show the C# code associated with each action state in the activity diagram. A dotted line connects each note with the element that the note describes. Activity diagrams normally do not show the C# code that implements the activity. We use notes for this purpose here to illustrate how the diagram relates to C# code. For more information on the UML, see our optional case study, which appears in the Software Engineering Case Study sections at the ends of Chapters 1, 3–9 and 11, and visit www.uml.org.
Selection Structures in C# C# has three types of selection structures, which from this point forward, we shall refer to as selection statements. These are discussed in this chapter and Chapter 6. The if statement either performs (selects) an action if a condition is true or skips the action if the condition is false. The if…else statement performs an action if a condition is true or performs a different action if the condition is false. The switch statement (Chapter 6) performs one of many different actions, depending on the value of an expression. The if statement is called a single-selection statement because it selects or ignores a single action (or, as we will soon see, a single group of actions). The if…else statement is called a double-selection statement because it selects between two different actions (or groups of actions). The switch statement is called a multiple-selection statement because it selects among many different actions (or groups of actions). Repetition Structures in C# C# provides four repetition structures, which from this point forward, we shall refer to as repetition statements (also called iteration statements or loops). Repetition statements enable applications to perform statements repeatedly, depending on the value of a loopcontinuation condition. The repetition statements are the while, do…while, for and foreach statements. (Chapter 6 presents the do…while and for statements. Chapter 8 discusses the foreach statement.) The while, for and foreach statements perform the action (or group of actions) in their bodies zero or more times—if the loop-continuation condition is initially false, the action (or group of actions) will not execute. The do…while statement performs the action (or group of actions) in its body one or more times. The words if, else, switch, while, do, for and foreach are C# keywords. Keywords cannot be used as identifiers, such as variable names. A complete list of C# keywords appears in Fig. 3.2. Summary of Control Statements in C# C# has only three kinds of structured control statements: the sequence statement, selection statement (three types) and repetition statement (four types). Every application is formed by combining as many sequence, selection and repetition statements as is appropriate for the algorithm the application implements. As with the sequence statement in Fig. 5.1, we
5.3 if Single-Selection Statement
133
can model each control statement as an activity diagram. Each diagram contains an initial state and a final state that represent a control statement’s entry point and exit point, respectively. Single-entry/single-exit control statements make it easy to build applications—the control statements are “attached” to one another by connecting the exit point of one to the entry point of the next. This procedure is similar to the way in which a child stacks building blocks, so we call it control-statement stacking. You will learn that there is only one other way in which control statements may be connected: control-statement nesting, in which a control statement appears inside another control statement. Thus, algorithms in C# applications are constructed from only three kinds of structured control statements, combined in only two ways. This is the essence of simplicity.
5.3 if Single-Selection Statement Applications use selection statements to choose among alternative courses of action. For example, suppose that the passing grade on an exam is 60. The statement if ( grade >= 60 ) Console.WriteLine( "Passed" );
determines whether the condition grade >= 60 is true or false. If the condition is true, "Passed" is printed, and the next statement in order is performed. If the condition is false, no printing occurs and the next statement in order is performed. Figure 5.2 illustrates the single-selection if statement. This activity diagram contains what is perhaps the most important symbol in an activity diagram—the diamond, or decision symbol, which indicates that a decision is to be made. The workflow will continue along a path determined by the symbol’s associated guard conditions, which can be true or false. Each transition arrow emerging from a decision symbol has a guard condition (specified in square brackets next to the transition arrow). If a guard condition is true, the workflow enters the action state to which the transition arrow points. In Fig. 5.2, if the grade is greater than or equal to 60, the application prints “Passed” then transitions to the final state of this activity. If the grade is less than 60, the application immediately transitions to the final state without displaying a message. The if statement is a single-entry/single-exit control statement. You will see that the activity diagrams for the remaining control statements also contain initial states, transition arrows, action states that indicate actions to perform and decision symbols (with associated guard conditions) that indicate decisions to be made, and final states.
[grade >= 60]
print “Passed”
[grade < 60]
Fig. 5.2 |
if
single-selection statement UML activity diagram.
134
Chapter 5
Control Statements: Part 1
5.4 if…else Double-Selection Statement The if single-selection statement performs an indicated action only when the condition is true; otherwise, the action is skipped. The if…else double-selection statement allows you to specify an action to perform when the condition is true and a different action when the condition is false. For example, the statement if ( grade >= 60 ) Console.WriteLine( "Passed" ); else Console.WriteLine( "Failed" );
prints "Passed" if the grade is greater than or equal to 60, but prints "Failed" if it is less than 60. In either case, after printing occurs, the next statement in sequence is performed. Figure 5.3 illustrates the flow of control in the if…else statement. Once again, the symbols in the UML activity diagram (besides the initial state, transition arrows and final state) represent action states and a decision.
Conditional Operator (?:) C# provides the conditional operator (?:), which can be used in place of an if…else statement. This is C#’s only ternary operator—this means that it takes three operands. Together, the operands and the ?: symbols form a conditional expression. The first operand (to the left of the ?) is a boolean expression (i.e., an expression that evaluates to a bool-type value—true or false), the second operand (between the ? and :) is the value of the conditional expression if the boolean expression is true and the third operand (to the right of the :) is the value of the conditional expression if the boolean expression is false. For example, the statement Console.WriteLine( grade >= 60 ? "Passed" : "Failed" );
prints the value of WriteLine’s conditional-expression argument. The conditional expression in this statement evaluates to the string "Passed" if the boolean expression grade >= 60 is true and evaluates to the string "Failed" if the boolean expression is false. Thus, this statement with the conditional operator performs essentially the same function as the if…else statement shown earlier in this section. You will see that conditional expressions can be used in some situations where if…else statements cannot.
print “Failed”
Fig. 5.3 |
if…else
[grade < 60]
[grade >= 60]
double-selection statement UML activity diagram.
print “Passed”
5.4 if…else Double-Selection Statement
135
Good Programming Practice 5.1 Conditional expressions are more difficult to read than if…else statements and should be used to replace only simple if…else statements that choose between two values. 5.1
Good Programming Practice 5.2 When a conditional expression is inside a larger expression, it’s good practice to parenthesize the conditional expression for clarity. Adding parentheses may also prevent operator precedence problems that could cause syntax errors. 5.2
Nested if…else Statements An application can test multiple cases by placing if…else statements inside other if…else statements to create nested if…else statements. For example, the following nested if…else statement prints A for exam grades greater than or equal to 90, B for grades in the range 80 to 89, C for grades in the range 70 to 79, D for grades in the range 60 to 69 and F for all other grades: if ( grade >= 90 ) Console.WriteLine( "A" ); else if ( grade >= 80 ) Console.WriteLine( "B" ); else if ( grade >= 70 ) Console.WriteLine( "C" ); else if ( grade >= 60 ) Console.WriteLine( "D" ); else Console.WriteLine( "F" );
If grade is greater than or equal to 90, the first four conditions will be true, but only the statement in the if-part of the first if…else statement will execute. After that statement executes, the else-part of the “outermost” if…else statement is skipped. Most C# programmers prefer to write the preceding if…else statement as if ( grade >= 90 ) Console.WriteLine( else if ( grade >= 80 Console.WriteLine( else if ( grade >= 70 Console.WriteLine( else if ( grade >= 60 Console.WriteLine( else Console.WriteLine(
"A" ) "B" ) "C" ) "D"
); ); ); );
"F" );
The two forms are identical except for the spacing and indentation, which the compiler ignores. The latter form is popular because it avoids deep indentation of the code to the right—such indentation often leaves little room on a line of code, forcing lines to be split and decreasing the readability of your code.
136
Chapter 5
Control Statements: Part 1
Dangling-else Problem The C# compiler always associates an else with the immediately preceding if unless told to do otherwise by the placement of braces ({ and }). This behavior can lead to what is referred to as the dangling-else problem. For example, if ( x > 5 ) if ( y > 5 ) Console.WriteLine( "x and y are > 5" ); else Console.WriteLine( "x is 5" is output. Otherwise, it appears that if x is not greater than 5, the else part of the if…else outputs the string "x is 5" ); else Console.WriteLine( "x is 5"—is displayed. However, if the second condition is false, the string "x is 5" ); } else Console.WriteLine( "x is = 0 ) ? value : 0 ); // validation } // end set } // end property WeeklySalary // calculate earnings; override abstract method Earnings in Employee public override decimal Earnings() { return WeeklySalary; } // end method Earnings // return string representation of SalariedEmployee object public override string ToString() { return string.Format( "salaried employee: {0}\n{1}: {2:C}", base.ToString(), "weekly salary", WeeklySalary ); } // end method ToString } // end class SalariedEmployee
Fig. 11.5 |
SalariedEmployee
class that extends Employee.
the base class’s ToString (line 37)—this is a nice example of code reuse. The string representation of a SalariedEmployee also contains the employee’s weekly salary, obtained by using the class’s WeeklySalary property.
11.5.3 Creating Concrete Derived Class HourlyEmployee Class HourlyEmployee (Fig. 11.6) also extends class Employee (line 3). The class includes a constructor (lines 9–15) that takes as arguments a first name, a last name, a social security number, an hourly wage and the number of hours worked. Lines 18–28 and 31–42 declare properties Wage and Hours for instance variables wage and hours, respectively. The set accessor in property Wage (lines 24–27) ensures that wage is non-negative, and the set accessor
// Fig. 11.6: HourlyEmployee.cs // HourlyEmployee class that extends Employee. public class HourlyEmployee : Employee { private decimal wage; // wage per hour private decimal hours; // hours worked for the week // five-parameter constructor public HourlyEmployee( string first, string last, string ssn, decimal hourlyWage, decimal hoursWorked ) : base( first, last, ssn ) { Wage = hourlyWage; // validate hourly wage via property Hours = hoursWorked; // validate hours worked via property } // end five-parameter HourlyEmployee constructor // property that gets and sets hourly employee's wage public decimal Wage { get { return wage; } // end get set { wage = ( value >= 0 ) ? value : 0; // validation } // end set } // end property Wage // property that gets and sets hourly employee's hours public decimal Hours { get { return hours; } // end get set { hours = ( ( value >= 0 ) && ( value = 0 ) ? value : 0; // validation } // end set } // end property GrossSales // calculate earnings; override abstract method Earnings in Employee public override decimal Earnings() { return CommissionRate * GrossSales; } // end method Earnings // return string representation of CommissionEmployee object public override string ToString() { return string.Format( "{0}: {1}\n{2}: {3:C}\n{4}: {5:F2}", "commission employee", base.ToString(), "gross sales", GrossSales, "commission rate", CommissionRate ); } // end method ToString } // end class CommissionEmployee
Fig. 11.7 |
CommissionEmployee
class that extends Employee. (Part 2 of 2.)
The CommissionEmployee’s constructor also passes the first name, last name and social security number to the Employee constructor (line 10) to initialize Employee’s private instance variables. Method ToString calls base class method ToString (line 53) to obtain the Employee-specific information (i.e., first name, last name and social security number).
11.5.5 Creating Indirect Concrete Derived Class BasePlusCommissionEmployee Class BasePlusCommissionEmployee (Fig. 11.8) extends class CommissionEmployee (line 3) and therefore is an indirect derived class of class Employee. Class BasePlusCommissionEmployee has a constructor (lines 8–13) that takes as arguments a first name, a last name, a social security number, a sales amount, a commission rate and a
11.5 Case Study: Payroll System Using Polymorphism
399
base salary. It then passes the first name, last name, social security number, sales amount and commission rate to the CommissionEmployee constructor (line 10) to initialize the base class’s private data members. BasePlusCommissionEmployee also contains property BaseSalary (lines 17–27) to manipulate instance variable baseSalary. Method Earnings (lines 30–33) calculates a BasePlusCommissionEmployee’s earnings. Note that line 32 in method Earnings calls base class CommissionEmployee’s Earnings method to calculate the commission-based portion of the employee’s earnings. This is another nice example of code reuse. BasePlusCommissionEmployee’s ToString method (lines 36–40) creates a string representation of a BasePlusCommissionEmployee that contains "base-salaried", followed by the string obtained by invoking base class CommissionEmployee’s ToString method (another example of code reuse), then the base salary. The result is a string beginning with "base-salaried commission employee", followed by the rest of the BasePlusCommissionEmployee’s information. Recall that CommissionEmployee’s ToString method obtains the employee’s first name, last name and social security number by invoking the ToString method of its base class (i.e., Employee)—yet another example of code reuse. Note that BasePlusCommissionEmployee’s ToString initiates a chain of method calls that span all three levels of the Employee hierarchy.
// Fig. 11.8: BasePlusCommissionEmployee.cs // BasePlusCommissionEmployee class that extends CommissionEmployee. public class BasePlusCommissionEmployee : CommissionEmployee { private decimal baseSalary; // base salary per week // six-parameter constructor public BasePlusCommissionEmployee( string first, string last, string ssn, decimal sales, decimal rate, decimal salary ) : base( first, last, ssn, sales, rate ) { BaseSalary = salary; // validate base salary via property } // end six-parameter BasePlusCommissionEmployee constructor // property that gets and sets // base-salaried commission employee's base salary public decimal BaseSalary { get { return baseSalary; } // end get set { baseSalary = ( value >= 0 ) ? value : 0; // validation } // end set } // end property BaseSalary
Fig. 11.8 | of 2.)
BasePlusCommissionEmployee
class that extends CommissionEmployee. (Part 1
400 29 30 31 32 33 34 35 36 37 38 39 40 41
Chapter 11
Polymorphism, Interfaces & Operator Overloading
// calculate earnings; override method Earnings in CommissionEmployee public override decimal Earnings() { return BaseSalary + base.Earnings(); } // end method Earnings // return string representation of BasePlusCommissionEmployee object public override string ToString() { return string.Format( "{0} {1}; {2}: {3:C}", "base-salaried", base.ToString(), "base salary", BaseSalary ); } // end method ToString } // end class BasePlusCommissionEmployee
Fig. 11.8 |
BasePlusCommissionEmployee
class that extends CommissionEmployee. (Part 2
of 2.)
11.5.6 Polymorphic Processing, Operator is and Downcasting To test our Employee hierarchy, the application in Fig. 11.9 creates an object of each of the four concrete classes SalariedEmployee, HourlyEmployee, CommissionEmployee and BasePlusCommissionEmployee. The application manipulates these objects, first via variables of each object’s own type, then polymorphically, using an array of Employee variables. While processing the objects polymorphically, the application increases the base salary of each BasePlusCommissionEmployee by 10% (this, of course, requires determining the ob1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
// Fig. 11.9: PayrollSystemTest.cs // Employee hierarchy test application. using System; public class PayrollSystemTest { public static void Main( string[] args ) { // create derived class objects SalariedEmployee salariedEmployee = new SalariedEmployee( "John", "Smith", "111-11-1111", 800.00M ); HourlyEmployee hourlyEmployee = new HourlyEmployee( "Karen", "Price", "222-22-2222", 16.75M, 40.0M ); CommissionEmployee commissionEmployee = new CommissionEmployee( "Sue", "Jones", "333-33-3333", 10000.00M, .06M ); BasePlusCommissionEmployee basePlusCommissionEmployee = new BasePlusCommissionEmployee( "Bob", "Lewis", "444-44-4444", 5000.00M, .04M, 300.00M );
commission employee: Sue Jones social security number: 333-33-3333 gross sales: $10,000.00 commission rate: 0.06 earned $600.00 base-salaried commission employee: Bob Lewis social security number: 444-44-4444 gross sales: $5,000.00 commission rate: 0.04; base salary: $300.00 new base salary with 10% increase is: $330.00 earned $530.00 Employee Employee Employee Employee
11.5 Case Study: Payroll System Using Polymorphism
403
ject’s type at execution time). Finally, the application polymorphically determines and outputs the type of each object in the Employee array. Lines 10–20 create objects of each of the four concrete Employee derived classes. Lines 24–32 output the string representation and earnings of each of these objects. Note that each object’s ToString method is called implicitly by Write when the object is output as a string with format items. Line 35 declares employees and assigns it an array of four Employee variables. Lines 38–41 assign a SalariedEmployee object, an HourlyEmployee object, a CommissionEmployee object and a BasePlusCommissionEmployee object to employees[ 0 ], employees[ 1 ], employees[ 2 ] and employees[ 3 ], respectively. Each assignment is allowed, because a SalariedEmployee is an Employee, an HourlyEmployee is an Employee, a CommissionEmployee is an Employee and a BasePlusCommissionEmployee is an Employee. Therefore, we can assign the references of SalariedEmployee, HourlyEmployee, CommissionEmployee and BasePlusCommissionEmployee objects to base class Employee variables, even though Employee is an abstract class. Lines 46–66 iterate through array employees and invoke methods ToString and Earnings with Employee variable currentEmployee, which is assigned the reference to a different Employee in the array during each iteration. The output illustrates that the appropriate methods for each class are indeed invoked. All calls to method’s ToString and Earnings are resolved at execution time, based on the type of the object to which currentEmployee refers. This process is known as dynamic binding or late binding. For example, line 48 implicitly invokes method ToString of the object to which currentEmployee refers. As a result of dynamic binding, the CLR decides which class’s ToString method to call at execution time rather than at compile time. Note that only the methods of class Employee can be called via an Employee variable—and Employee, of course, includes the methods of class object, such as ToString. (Section 10.7 discussed the set of methods that all classes inherit from class object.) A base class reference can be used to invoke only methods of the base class. We perform special processing on BasePlusCommissionEmployee objects—as we encounter them, we increase their base salary by 10%. When processing objects polymorphically, we typically do not need to worry about the “specifics,” but to adjust the base salary, we do have to determine the specific type of each Employee object at execution time. Line 51 uses the is operator to determine whether a particular Employee object’s type is BasePlusCommissionEmployee. The condition in line 51 is true if the object referenced by currentEmployee is a BasePlusCommissionEmployee. This would also be true for any object of a BasePlusCommissionEmployee derived class (if there were any) because of the is-a relationship a derived class has with its base class. Lines 55–56 downcast currentEmployee from type Employee to type BasePlusCommissionEmployee—this cast is allowed only if the object has an is-a relationship with BasePlusCommissionEmployee. The condition at line 51 ensures that this is the case. This cast is required if we are to use derived class BasePlusCommissionEmployee’s BaseSalary property on the current Employee object—attempting to invoke a derived-class-only method directly on a base class reference is a compilation error.
Common Programming Error 11.3 Assigning a base class variable to a derived class variable (without an explicit downcast) is a compilation error. 11.3
404
Chapter 11
Polymorphism, Interfaces & Operator Overloading
Software Engineering Observation 11.4 If at execution time the reference to a derived class object has been assigned to a variable of one of its direct or indirect base classes, it is acceptable to cast the reference stored in that base class variable back to a reference of the derived class type. Before performing such a cast, use the is operator to ensure that the object is indeed an object of an appropriate derived class type. 11.4
Common Programming Error 11.4 When downcasting an object, an InvalidCastException (of namespace System) occurs if at execution time the object does not have an is-a relationship with the type specified in the cast operator. An object can be cast only to its own type or to the type of one of its base classes. 11.4
If the is expression in line 51 is true, the if statement (lines 51–62) performs the special processing required for the BasePlusCommissionEmployee object. Using BasePlusCommissionEmployee variable employee, line 58 uses the derived-class-only property BaseSalary to retrieve and update the employee’s base salary with the 10% raise. Lines 64–65 invoke method Earnings on currentEmployee, which calls the appropriate derived class object’s Earnings method polymorphically. Note that obtaining the earnings of the SalariedEmployee, HourlyEmployee and CommissionEmployee polymorphically in lines 64–65 produces the same result as obtaining these employees’ earnings individually in lines 24–32. However, the earnings amount obtained for the BasePlusCommissionEmployee in lines 64–65 is higher than that obtained in lines 30–32, due to the 10% increase in its base salary. Lines 69–71 display each employee’s type as a string. Every object in C# knows its own class and can access this information through method GetType, which all classes inherit from class object. Method GetType returns an object of class Type (of namespace System), which contains information about the object’s type, including its class name, the names of its public methods, and the name of its base class. Line 71 invokes method GetType on the object to get its runtime class (i.e., a Type object that represents the object’s type). Then method ToString is implicitly invoked on the object returned by GetType. The Type class’s ToString method returns the class name. In the previous example, we avoid several compilation errors by downcasting an Employee variable to a BasePlusCommissionEmployee variable in lines 55–56. If we remove the cast operator ( BasePlusCommissionEmployee ) from line 56 and attempt to assign Employee variable currentEmployee directly to BasePlusCommissionEmployee variable employee, we receive a “Cannot implicitly convert type” compilation error. This error indicates that the attempt to assign the reference of base class object commissionEmployee to derived class variable basePlusCommissionEmployee is not allowed without an appropriate cast operator. The compiler prevents this assignment because a CommissionEmployee is not a BasePlusCommissionEmployee—again, the is-a relationship applies only between the derived class and its base classes, not vice versa. Similarly, if lines 58 and 61 use base class variable currentEmployee, rather than derived class variable employee, to use derived-class-only property BaseSalary, we receive an “‘Employee’ does not contain a definition for ‘BaseSalary’” compilation error on each of these lines. Attempting to invoke derived-class-only methods on a base class reference is not allowed. While lines 58 and 61 execute only if is in line 51 returns true to indicate that currentEmployee has been assigned a reference to a BasePlusCommissionEmployee object, we cannot attempt to use derived class BasePlusCommissionEmployee
11.6 sealed Methods and Classes
405
property BaseSalary with base class Employee reference currentEmployee. The compiler would generate errors in lines 58 and 61, because BaseSalary is not a base class member and cannot be used with a base class variable. Although the actual method that is called depends on the object’s type at execution time, a variable can be used to invoke only those methods that are members of that variable’s type, which the compiler verifies. Using a base class Employee variable, we can invoke only methods and properties found in class Employee—methods Earnings and ToString, and properties FirstName, LastName and SocialSecurityNumber.
11.5.7 Summary of the Allowed Assignments Between Base Class and Derived Class Variables Now that you have seen a complete application that processes diverse derived class objects polymorphically, we summarize what you can and cannot do with base class and derived class objects and variables. Although a derived class object also is a base class object, the two objects are nevertheless different. As discussed previously, derived class objects can be treated as if they are base class objects. However, the derived class can have additional derived-class-only members. For this reason, assigning a base class reference to a derived class variable is not allowed without an explicit cast—such an assignment would leave the derived class members undefined for a base class object. We have discussed four ways to assign base class and derived class references to variables of base class and derived class types: 1. Assigning a base class reference to a base class variable is straightforward. 2. Assigning a derived class reference to a derived class variable is straightforward. 3. Assigning a derived class reference to a base class variable is safe, because the derived class object is an object of its base class. However, this reference can be used to refer only to base class members. If this code refers to derived-class-only members through the base class variable, the compiler reports errors. 4. Attempting to assign a base class reference to a derived class variable is a compilation error. To avoid this error, the base class reference must be cast to a derived class type explicitly. At execution time, if the object to which the reference refers is not a derived class object, an exception will occur. (For more on exception handling, see Chapter 12, Exception Handling.) The is operator can be used to ensure that such a cast is performed only if the object is a derived class object.
11.6 sealed Methods and Classes We saw in Section 10.4 that only methods declared virtual, override or abstract can be overridden in derived classes. A method declared sealed in a base class cannot be overridden in a derived class. Methods that are declared private are implicitly sealed, because it is impossible to override them in a derived class (though the derived class can declare a new method with the same signature as the private method in the base class). Methods that are declared static also are implicitly sealed, because static methods cannot be overridden either. A derived class method declared both override and sealed can override a base class method, but cannot be overridden in derived classes further down the inheritance hierarchy.
406
Chapter 11
Polymorphism, Interfaces & Operator Overloading
A sealed method’s declaration can never change, so all derived classes use the same method implementation, and calls to sealed methods are resolved at compile time—this is known as static binding. Since the compiler knows that sealed methods cannot be overridden, it can often optimize code by removing calls to sealed methods and replacing them with the expanded code of their declarations at each method-call location—a technique known as inlining the code.
Performance Tip 11.1 The compiler can decide to inline a sealed method call and will do so for small, simple sealed methods. Inlining does not violate encapsulation or information hiding, but does improve performance because it eliminates the overhead of making a method call. 11.1
A class that is declared sealed cannot be a base class (i.e., a class cannot extend a sealed class). All methods in a sealed class are implicitly sealed. Class string is a sealed class. This class cannot be extended, so applications that use strings can rely on the functionality of string objects as specified in the FCL.
Common Programming Error 11.5 Attempting to declare a derived class of a sealed class is a compilation error.
11.5
Software Engineering Observation 11.5 In the FCL, the vast majority of classes are not declared sealed. This enables inheritance and polymorphism—the fundamental capabilities of object-oriented programming. 11.5
11.7 Case Study: Creating and Using Interfaces Our next example (Fig. 11.11–Fig. 11.15) reexamines the payroll system of Section 11.5. Suppose that the company involved wishes to perform several accounting operations in a single accounts-payable application—in addition to calculating the payroll earnings that must be paid to each employee, the company must also calculate the payment due on each of several invoices (i.e., bills for goods purchased). Though applied to unrelated things (i.e., employees and invoices), both operations have to do with calculating some kind of payment amount. For an employee, the payment refers to the employee’s earnings. For an invoice, the payment refers to the total cost of the goods listed on the invoice. Can we calculate such different things as the payments due for employees and invoices polymorphically in a single application? Does C# offer a capability that requires that unrelated classes implement a set of common methods (e.g., a method that calculates a payment amount)? C# interfaces offer exactly this capability. Interfaces define and standardize the ways in which people and systems can interact with one another. For example, the controls on a radio serve as an interface between a radio’s users and its internal components. The controls allow users to perform a limited set of operations (e.g., changing the station, adjusting the volume, choosing between AM and FM), and different radios may implement the controls in different ways (e.g., using push buttons, dials, voice commands). The interface specifies what operations a radio must permit users to perform but does not specify how the operations are performed. Similarly, the interface between a driver and a car with a manual transmission includes the
11.7 Case Study: Creating and Using Interfaces
407
steering wheel, the gear shift, the clutch pedal, the gas pedal and the brake pedal. This same interface is found in nearly all manual-transmission cars, enabling someone who knows how to drive one particular manual-transmission car to drive just about any manual transmission car. The components of each individual car may look a bit different, but the general purpose is the same—to allow people to drive the car. Software objects also communicate via interfaces. A C# interface describes a set of methods that can be called on an object, to tell the object to perform some task or return some piece of information, for example. The next example introduces an interface named IPayable that describes the functionality of any object that must be capable of being paid and thus must offer a method to determine the proper payment amount due. An interface declaration begins with the keyword interface and can contain only abstract methods, properties, indexers and events (events are discussed in Chapter 13, Graphical User Interface Concepts: Part 1.) All interface members are implicitly declared both public and abstract. In addition, each interface can extend one or more other interfaces to create a more elaborate interface that other classes can implement.
Common Programming Error 11.6 It is a compilation error to declare an interface member public or abstract explicitly, because they are redundant in interface member declarations. It is also a compilation error to specify any implementation details, such as concrete method declarations, in an interface. 11.6
To use an interface, a class must specify that it implements the interface by listing the interface after the colon (:) in the class declaration. Note that this is the same syntax used to indicate inheritance from a base class. A concrete class implementing the interface must declare each member of the interface with the signature specified in the interface declaration. A class that implements an interface but does not implement all the interface’s members is an abstract class—it must be declared abstract and must contain an abstract declaration for each unimplemented member of the interface. Implementing an interface is like signing a contract with the compiler that states, “I will provide an implementation for all the members specified by the interface, or I will declare them abstract.”
Common Programming Error 11.7 Failing to declare any member of an interface in a class that implements the interface results in a compilation error. 11.7
An interface is typically used when disparate (i.e., unrelated) classes need to share common methods. This allows objects of unrelated classes to be processed polymorphically—objects of classes that implement the same interface can respond to the same method calls. Programmers can create an interface that describes the desired functionality, then implement this interface in any classes requiring that functionality. For example, in the accounts-payable application developed in this section, we implement interface IPayable in any class that must be able to calculate a payment amount (e.g., Employee, Invoice). An interface often is used in place of an abstract class when there is no default implementation to inherit—that is, no fields and no default method implementations. Like public abstract classes, interfaces are typically public types, so they are normally declared in files by themselves with the same name as the interface and the .cs filename extension.
408
Chapter 11
Polymorphism, Interfaces & Operator Overloading
11.7.1 Developing an IPayable Hierarchy To build an application that can determine payments for employees and invoices alike, we first create an interface named IPayable. Interface IPayable contains method GetPaymentAmount that returns a decimal amount that must be paid for an object of any class that implements the interface. Method GetPaymentAmount is a general purpose version of method Earnings of the Employee hierarchy—method Earnings calculates a payment amount specifically for an Employee, while GetPaymentAmount can be applied to a broad range of unrelated objects. After declaring interface IPayable, we introduce class Invoice, which implements interface IPayable. We then modify class Employee such that it also implements interface IPayable. Finally, we update Employee derived class SalariedEmployee to “fit” into the IPayable hierarchy (i.e., rename SalariedEmployee method Earnings as GetPaymentAmount).
Good Programming Practice 11.1 By convention, the name of an interface begins with "I". This helps distinguish interfaces from classes, improving code readability. 11.1
Good Programming Practice 11.2 When declaring a method in an interface, choose a method name that describes the method’s purpose in a general manner, because the method may be implemented by a broad range of unrelated classes. 11.2
Classes Invoice and Employee both represent things for which the company must be able to calculate a payment amount. Both classes implement IPayable, so an application can invoke method GetPaymentAmount on Invoice objects and Employee objects alike. This enables the polymorphic processing of Invoices and Employees required for our company’s accounts-payable application. The UML class diagram in Fig. 11.10 shows the interface and class hierarchy used in our accounts-payable application. The hierarchy begins with interface IPayable. The UML distinguishes an interface from a class by placing the word “interface” in guillemets (« and ») above the interface name. The UML expresses the relationship between a class and an interface through a realization. A class is said to “realize,” or implement, an interface. A class diagram models a realization as a dashed arrow with a hollow arrowhead pointing from the implementing class to the interface. The diagram in Fig. 11.10 indicates that classes Invoice and Employee each realize (i.e., implement) interface IPayable. Note that as in the class diagram of Fig. 11.2, class Employee appears in italics, indicating that it is an abstract class. Concrete class SalariedEmployee extends Employee and inherits its base class’s realization relationship with interface IPayable.
11.7.2 Declaring Interface IPayable The declaration of interface IPayable begins in Fig. 11.11 at line 3. Interface IPayable contains public abstract method GetPaymentAmount (line 5). Note that the method cannot be explicitly declared public or abstract. Interface IPayable has only one method, but interfaces can have any number of members. In addition, method GetPaymentAmount has no parameters, but interface methods can have parameters.
11.7 Case Study: Creating and Using Interfaces
409
«interface» IPayable
Invoice
Employee
SalariedEmployee
Fig. 11.10 | 1 2 3 4 5 6
IPayable
interface and class hierarchy UML class diagram.
// Fig. 11.11: IPayable.cs // IPayable interface declaration. public interface IPayable { decimal GetPaymentAmount(); // calculate payment; no implementation } // end interface IPayable
Fig. 11.11 |
IPayable
interface declaration.
11.7.3 Creating Class Invoice We now create class Invoice (Fig. 11.12) to represent a simple invoice that contains billing information for one kind of part. The class declares private instance variables partNumber, partDescription, quantity and pricePerItem (lines 5–8) that indicate the part number, the description of the part, the quantity of the part ordered and the price per item. Class Invoice also contains a constructor (lines 11–18), properties (lines 21–70) that manipulate the class’s instance variables and a ToString method (lines 73–79) that returns a string representation of an Invoice object. Note that the set accessors of properties Quantity (lines 53–56) and PricePerItem (lines 66–69) ensure that quantity and pricePerItem are assigned only non-negative values. Line 3 of Fig. 11.12 indicates that class Invoice implements interface IPayable. Like all classes, class Invoice also implicitly extends object. C# does not allow derived classes to inherit from more than one base class, but it does allow a class to inherit from a base class and implement any number of interfaces. All objects of a class that implement multiple interfaces have the is-a relationship with each implemented interface type. To implement more than one interface, use a comma-separated list of interface names after the colon (:) in the class declaration, as in: public class ClassName : BaseClassName, FirstInterface, SecondInterface, …
When a class inherits from a base class and implements one or more interfaces, the class declaration must list the base class name before any interface names. Class Invoice implements the one method in interface IPayable—method GetPaymentAmount is declared in lines 82–85. The method calculates the amount required
// Fig. 11.12: Invoice.cs // Invoice class implements IPayable. public class Invoice : IPayable { private string partNumber; private string partDescription; private int quantity; private decimal pricePerItem; // four-parameter constructor public Invoice( string part, string description, int count, decimal price ) { PartNumber = part; PartDescription = description; Quantity = count; // validate quantity via property PricePerItem = price; // validate price per item via property } // end four-parameter Invoice constructor // property that gets and sets the part number on the invoice public string PartNumber { get { return partNumber; } // end get set { partNumber = value; // should validate } // end set } // end property PartNumber // property that gets and sets the part description on the invoice public string PartDescription { get { return partDescription; } // end get set { partDescription = value; // should validate } // end set } // end property PartDescription // property that gets and sets the quantity on the invoice public int Quantity { get { return quantity; } // end get
set { quantity = ( value < 0 ) ? 0 : value; // validate quantity } // end set } // end property Quantity // property that gets and sets the price per item public decimal PricePerItem { get { return pricePerItem; } // end get set { pricePerItem = ( value < 0 ) ? 0 : value; // validate price } // end set } // end property PricePerItem // return string representation of Invoice object public override string ToString() { return string.Format( "{0}: \n{1}: {2} ({3}) \n{4}: {5} \n{6}: {7:C}", "invoice", "part number", PartNumber, PartDescription, "quantity", Quantity, "price per item", PricePerItem ); } // end method ToString // method required to carry out contract with interface IPayable public decimal GetPaymentAmount() { return Quantity * PricePerItem; // calculate total cost } // end method GetPaymentAmount } // end class Invoice
Fig. 11.12 |
Invoice
class implements IPayable. (Part 2 of 2.)
to pay the invoice. The method multiplies the values of quantity and pricePerItem (obtained through the appropriate properties) and returns the result (line 84). This method satisfies the implementation requirement for the method in interface IPayable— we have fulfilled the interface contract with the compiler.
11.7.4 Modifying Class Employee to Implement Interface IPayable We now modify class Employee to implement interface IPayable. Figure 11.13 contains the modified Employee class. This class declaration is identical to that of Fig. 11.4 with two exceptions. First, line 3 of Fig. 11.13 indicates that class Employee now implements interface IPayable. Second, since Employee now implements interface IPayable, we must rename Earnings to GetPaymentAmount throughout the Employee hierarchy. As with method Earnings in the version of class Employee in Fig. 11.4, however, it does not make sense to implement method GetPaymentAmount in class Employee, because we cannot calculate the earnings payment owed to a general Employee—first, we must know the specific type of Em-
412
Chapter 11
Polymorphism, Interfaces & Operator Overloading
ployee.
In Fig. 11.4, we declared method Earnings as abstract for this reason, and as a result, class Employee had to be declared abstract. This forced each Employee derived class to override Earnings with a concrete implementation. In Fig. 11.13, we handle this situation the same way. Recall that when a class implements an interface, the class makes a contract with the compiler stating that the class either will implement each of the methods in the interface or will declare them abstract. If the latter option is chosen, we must also declare the class abstract. As we discussed in Section 11.4, any concrete derived class of the abstract class must implement the abstract 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
// Fig. 11.13: Employee.cs // Employee abstract base class. public abstract class Employee : IPayable { private string firstName; private string lastName; private string socialSecurityNumber; // three-parameter constructor public Employee( string first, string last, string ssn ) { firstName = first; lastName = last; socialSecurityNumber = ssn; } // end three-parameter Employee constructor // read-only property that gets employee's first name public string FirstName { get { return firstName; } // end get } // end property FirstName // read-only property that gets employee's last name public string LastName { get { return lastName; } // end get } // end property LastName // read-only property that gets employee's social security number public string SocialSecurityNumber { get { return socialSecurityNumber; } // end get } // end property SocialSecurityNumber
Fig. 11.13 |
Employee
abstract base class. (Part 1 of 2.)
11.7 Case Study: Creating and Using Interfaces
43 44 45 46 47 48 49 50 51 52 53 54
413
// return string representation of Employee object public override string ToString() { return string.Format( "{0} {1}\nsocial security number: {2}", FirstName, LastName, SocialSecurityNumber ); } // end method ToString // Note: We do not implement IPayable method GetPaymentAmount here so // this class must be declared abstract to avoid a compilation error. public abstract decimal GetPaymentAmount(); } // end abstract class Employee
Fig. 11.13 |
Employee
abstract base class. (Part 2 of 2.)
methods of the base class. If the derived class does not do so, it too must be declared abstract. As indicated by the comments in lines 51–52, class Employee of Fig. 11.13 does not implement method GetPaymentAmount, so the class is declared abstract.
11.7.5 Modifying Class SalariedEmployee for Use in the IPayable Hierarchy Figure 11.14 contains a modified version of class SalariedEmployee that extends Employee and implements method GetPaymentAmount. This version of SalariedEmployee is identical 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
// Fig. 11.14: SalariedEmployee.cs // SalariedEmployee class that extends Employee. public class SalariedEmployee : Employee { private decimal weeklySalary; // four-parameter constructor public SalariedEmployee( string first, string last, string ssn, decimal salary ) : base( first, last, ssn ) { WeeklySalary = salary; // validate salary via property } // end four-parameter SalariedEmployee constructor // property that gets and sets salaried employee's salary public decimal WeeklySalary { get { return weeklySalary; } // end get set { weeklySalary = value < 0 ? 0 : value; // validation } // end set } // end property WeeklySalary
Fig. 11.14 |
SalariedEmployee
class that extends Employee. (Part 1 of 2.)
414 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
Chapter 11
Polymorphism, Interfaces & Operator Overloading
// calculate earnings; implement interface IPayable method // that was abstract in base class Employee public override decimal GetPaymentAmount() { return WeeklySalary; } // end method GetPaymentAmount // return string representation of SalariedEmployee object public override string ToString() { return string.Format( "salaried employee: {0}\n{1}: {2:C}", base.ToString(), "weekly salary", WeeklySalary ); } // end method ToString } // end class SalariedEmployee
Fig. 11.14 |
SalariedEmployee
class that extends Employee. (Part 2 of 2.)
to that of Fig. 11.5 with the exception that the version here implements method GetPaymentAmount (lines 29–32) instead of method Earnings. The two methods contain the same functionality but have different names. Recall that the IPayable version of the method has a more general name to be applicable to possibly disparate classes. The remaining Employee derived classes (e.g., HourlyEmployee, CommissionEmployee and BasePlusCommissionEmployee) also must be modified to contain method GetPaymentAmount in place of Earnings to reflect the fact that Employee now implements IPayable. We leave these modifications as an exercise and use only SalariedEmployee in our test application in this section. When a class implements an interface, the same is-a relationship provided by inheritance applies. For example, class Employee implements IPayable, so we can say that an Employee is an IPayable, as are any classes that extend Employee. SalariedEmployee objects, for instance, are IPayable objects. As with inheritance relationships, an object of a class that implements an interface may be thought of as an object of the interface type. Objects of any classes derived from the class that implements the interface can also be thought of as objects of the interface type. Thus, just as we can assign the reference of a SalariedEmployee object to a base class Employee variable, we can assign the reference of a SalariedEmployee object to an interface IPayable variable. Invoice implements IPayable, so an Invoice object also is an IPayable object, and we can assign the reference of an Invoice object to an IPayable variable.
Software Engineering Observation 11.6 Inheritance and interfaces are similar in their implementation of the is-a relationship. An object of a class that implements an interface may be thought of as an object of that interface type. An object of any derived classes of a class that implements an interface also can be thought of as an object of the interface type. 11.6
Software Engineering Observation 11.7 The is-a relationship that exists between base classes and derived classes, and between interfaces and the classes that implement them, holds when passing an object to a method. When a method parameter receives a variable of a base class or interface type, the method polymorphically processes the object received as an argument. 11.7
11.7 Case Study: Creating and Using Interfaces
415
11.7.6 Using Interface IPayable to Process Invoices and Employees Polymorphically PayableInterfaceTest (Fig. 11.15) illustrates that interface IPayable can be used to process a set of Invoices and Employees polymorphically in a single application. Line 10 declares payableObjects and assigns it an array of four IPayable variables. Lines 13–14 assign the references of Invoice objects to the first two elements of payableObjects. Lines 15–18 assign the references of SalariedEmployee objects to the remaining two elements of payableObjects. These assignments are allowed because an Invoice is an IPayable, a SalariedEmployee is an Employee and an Employee is an IPayable. Lines 24–29 use a foreach statement to process each IPayable object in payableObjects polymorphically, printing the object as a string, along with the payment due. Note that line 27 implicitly invokes method ToString off an IPayable interface reference, even though ToString is not declared in interface IPayable—all references (including those of interface types) refer to objects that extend object and therefore have a ToString method. Line 28 invokes IPayable method GetPaymentAmount to obtain the payment amount for each object in payableObjects, regardless of the actual type of the object. The output reveals that the
method calls in lines 27–28 invoke the appropriate class’s implementation of methods ToString and GetPaymentAmount. For instance, when currentEmployee refers to an Invoice during the first iteration of the foreach loop, class Invoice’s ToString and GetPaymentAmount methods execute.
Software Engineering Observation 11.8 All methods of class object can be called by using a reference of an interface type—the reference refers to an object, and all objects inherit the methods of class object. 11.8
// Fig. 11.15: PayableInterfaceTest.cs // Tests interface IPayable with disparate classes. using System; public class PayableInterfaceTest { public static void Main( string[] args ) { // create four-element IPayable array IPayable[] payableObjects = new IPayable[ 4 ]; // populate array payableObjects[ 0 payableObjects[ 1 payableObjects[ 2 "111-11-1111", payableObjects[ 3 "888-88-8888",
with objects that implement IPayable ] = new Invoice( "01234", "seat", 2, 375.00M ); ] = new Invoice( "56789", "tire", 4, 79.95M ); ] = new SalariedEmployee( "John", "Smith", 800.00M ); ] = new SalariedEmployee( "Lisa", "Barnes", 1200.00M );
Console.WriteLine( "Invoices and Employees processed polymorphically:\n" );
Fig. 11.15 | Tests interface IPayable with disparate classes. (Part 1 of 2.)
416 23 24 25 26 27 28 29 30 31
Chapter 11
Polymorphism, Interfaces & Operator Overloading
// generically process each element in array payableObjects foreach ( IPayable currentPayable in payableObjects ) { // output currentPayable and its appropriate payment amount Console.WriteLine( "{0} \n{1}: {2:C}\n", currentPayable, "payment due", currentPayable.GetPaymentAmount() ); } // end foreach } // end Main } // end class PayableInterfaceTest
Invoices and Employees processed polymorphically: invoice: part number: 01234 (seat) quantity: 2 price per item: $375.00 payment due: $750.00 invoice: part number: 56789 (tire) quantity: 4 price per item: $79.95 payment due: $319.80 salaried employee: John Smith social security number: 111-11-1111 weekly salary: $800.00 payment due: $800.00 salaried employee: Lisa Barnes social security number: 888-88-8888 weekly salary: $1,200.00 payment due: $1,200.00
Fig. 11.15 | Tests interface IPayable with disparate classes. (Part 2 of 2.)
11.7.7 Common Interfaces of the .NET Framework Class Library In this section, we overview several common interfaces in the .NET Framework Class Library. These interfaces are implemented and used in the same manner as those you create (e.g., interface IPayable in Section 11.7.2). The FCL’s interfaces enable you to extend many important aspects of C# with your own classes. Figure 11.16 overviews several commonly used FCL interfaces.
11.8 Operator Overloading Manipulations on class objects are accomplished by sending messages (in the form of method calls) to the objects. This method-call notation is cumbersome for certain kinds of classes, especially mathematical classes. For these classes, it would be convenient to use C#’s rich set of built-in operators to specify object manipulations. In this section, we show how to enable these operators to work with class objects—via a process called operator overloading.
11.8 Operator Overloading
417
Interface
Description
IComparable
As you learned in Chapter 3, C# contains several comparison operators (e.g., =, ==, !=) that allow you to compare simple-type values. In Section 11.8 you will see that these operators can be defined to compare two objects. Interface IComparable can also be used to allow objects of a class that implements the interface to be compared to one another. The interface contains one method, CompareTo, that compares the object that calls the method to the object passed as an argument to the method. Classes must implement CompareTo to return a value indicating whether the object on which it is invoked is less than (negative integer return value), equal to (0 return value) or greater than (positive integer return value) the object passed as an argument, using any criteria specified by the programmer. For example, if class Employee implements IComparable, its CompareTo method could compare Employee objects by their earnings amounts. Interface IComparable is commonly used for ordering objects in a collection such as an array. We use IComparable in Chapter 25, Generics, and Chapter 26, Collections.
IComponent
Implemented by any class that represents a component, including Graphical User Interface (GUI) controls (such as buttons or labels). Interface IComponent defines the behaviors that components must implement. We discuss IComponent and many GUI controls that implement this interface in Chapter 13, Graphical User Interface Concepts: Part 1, and Chapter 14, Graphical User Interface Concepts: Part 2.
IDisposable
Implemented by classes that must provide an explicit mechanism for releasing resources. Some resources can be used by only one program at a time. In addition, some resources, such as files on disk, are unmanaged resources that, unlike memory, cannot be released by the garbage collector. Classes that implement interface IDisposable provide a Dispose method that can be called to explicitly release resources. We discuss IDisposable briefly in Chapter 12, Exception Handling. You can learn more about this interface at msdn2.microsoft.com/en-us/library/aax125c9. The MSDN article Implementing a Dispose Method at msdn2.microsoft.com/en-us/library/fs2xkftw discusses the proper implementation of this interface in your classes.
IEnumerator
Used for iterating through the elements of a collection (such as an array) one element at a time. Interface IEnumerator contains method MoveNext to move to the next element in a collection, method Reset to move to the position before the first element and property Current to return the object at the current location. We use IEnumerator in Chapter 26, Collections.
Fig. 11.16 | Common interfaces of the .NET Framework Class Library.
418
Chapter 11
Polymorphism, Interfaces & Operator Overloading
Software Engineering Observation 11.9 Use operator overloading when it makes an application clearer than accomplishing the same operations with explicit method calls. 11.9
C# enables you to overload most operators to make them sensitive to the context in which they are used. Some operators are overloaded frequently, especially various arithmetic operators, such as + and -. The job performed by overloaded operators also can be performed by explicit method calls, but operator notation often is more natural. Figures 11.17–11.18 provide an example of using operator overloading with a ComplexNumber class. Class ComplexNumber (Fig. 11.17) overloads the plus (+), minus (-) and multiplication (*) operators to enable programs to add, subtract and multiply instances of class ComplexNumber using common mathematical notation. Lines 8–9 declare instance variables for the real and imaginary parts of the complex number. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
// Fig. 11.17: ComplexNumber.cs // Class that overloads operators for adding, subtracting // and multiplying complex numbers. using System; public class ComplexNumber { private double real; // real component of the complex number private double imaginary; // imaginary component of the complex number // constructor public ComplexNumber( double a, double b ) { real = a; imaginary = b; } // end constructor // return string representation of ComplexNumber public override string ToString() { return string.Format( "({0} {1} {2}i)", Real, ( Imaginary < 0 ? "-" : "+" ), Math.Abs( Imaginary ) ); } // end method ToString // read-only property that gets the real component public double Real { get { return real; } // end get } // end property Real
Fig. 11.17 | Class that overloads operators for adding, subtracting and multiplying complex numbers. (Part 1 of 2.)
// read-only property that gets the imaginary component public double Imaginary { get { return imaginary; } // end get } // end property Imaginary // overload the addition operator public static ComplexNumber operator+( ComplexNumber x, ComplexNumber y ) { return new ComplexNumber( x.Real + y.Real, x.Imaginary + y.Imaginary ); } // end operator + // overload the subtraction operator public static ComplexNumber operator-( ComplexNumber x, ComplexNumber y ) { return new ComplexNumber( x.Real - y.Real, x.Imaginary - y.Imaginary ); } // end operator // overload the multiplication operator public static ComplexNumber operator*( ComplexNumber x, ComplexNumber y ) { return new ComplexNumber( x.Real * y.Real - x.Imaginary * y.Imaginary, x.Real * y.Imaginary + y.Real * x.Imaginary ); } // end operator * } // end class ComplexNumber
Fig. 11.17 | Class that overloads operators for adding, subtracting and multiplying complex numbers. (Part 2 of 2.)
Lines 44–49 overload the plus operator (+) to perform addition of ComplexNumbers. Keyword operator, followed by an operator symbol, indicates that a method overloads the specified operator. Methods that overload binary operators must take two arguments. The first argument is the left operand, and the second argument is the right operand. Class ComplexNumber’s overloaded plus operator takes two ComplexNumber references as arguments and returns a ComplexNumber that represents the sum of the arguments. Note that this method is marked public and static, which is required for overloaded operators. The body of the method (lines 47–48) performs the addition and returns the result as a new ComplexNumber. Notice that we do not modify the contents of either of the original operands passed as arguments x and y. This matches our intuitive sense of how this operator should behave— adding two numbers does not modify either of the original numbers. Lines 52–66 provide similar overloaded operators for subtracting and multiplying ComplexNumbers.
420
Chapter 11
Polymorphism, Interfaces & Operator Overloading
Software Engineering Observation 11.10 Overload operators to perform the same function or similar functions on class objects as the operators perform on objects of simple types. Avoid non-intuitive use of operators. 11.10
Software Engineering Observation 11.11 At least one argument of an overloaded operator method must be a reference to an object of the class in which the operator is overloaded. This prevents programmers from changing how operators work on simple types. 11.11
Class ComplexTest (Fig. 11.18) demonstrates the overloaded operators for adding, subtracting and multiplying ComplexNumbers. Lines 14–27 prompt the user to enter two complex numbers, then use this input to create two ComplexNumbers and assign them to variables x and y.
// Fig 11.18: OperatorOverloading.cs // Overloading operators for complex numbers. using System; public class ComplexTest { public static void Main( string[] args ) { // declare two variables to store complex numbers // to be entered by user ComplexNumber x, y; // prompt the user to enter the first complex number Console.Write( "Enter the real part of complex number x: " ); double realPart = Convert.ToDouble( Console.ReadLine() ); Console.Write( "Enter the imaginary part of complex number x: " ); double imaginaryPart = Convert.ToDouble( Console.ReadLine() ); x = new ComplexNumber( realPart, imaginaryPart ); // prompt the user to enter the second complex number Console.Write( "\nEnter the real part of complex number y: " ); realPart = Convert.ToDouble( Console.ReadLine() ); Console.Write( "Enter the imaginary part of complex number y: " ); imaginaryPart = Convert.ToDouble( Console.ReadLine() ); y = new ComplexNumber( realPart, imaginaryPart ); // display the results of Console.WriteLine(); Console.WriteLine( "{0} + Console.WriteLine( "{0} Console.WriteLine( "{0} * } // end method Main } // end class ComplexTest
calculations with x and y {1} = {2}", x, y, x + y ); {1} = {2}", x, y, x - y ); {1} = {2}", x, y, x * y );
Fig. 11.18 | Overloading operators for complex numbers. (Part 1 of 2.)
11.9 Incorporating Inheritance and Polymorphism into the ATM System
421
Enter the real part of complex number x: 2 Enter the imaginary part of complex number x: 4 Enter the real part of complex number y: 4 Enter the imaginary part of complex number y: -2 (2 + 4i) + (4 - 2i) = (6 + 2i) (2 + 4i) - (4 - 2i) = (-2 + 6i) (2 + 4i) * (4 - 2i) = (16 + 12i)
Fig. 11.18 | Overloading operators for complex numbers. (Part 2 of 2.) Lines 31–33 add, subtract and multiply x and y with the overloaded operators, then output the results. In line 31, we perform the addition by using the plus operator with ComplexNumber operands x and y. Without operator overloading, the expression x + y would not make sense—the compiler would not know how two objects should be added. This expression makes sense here because we’ve defined the plus operator for two ComplexNumbers in lines 44–49 of Fig. 11.17. When the two ComplexNumbers are “added” in line 31 of Fig. 11.18, this invokes the operator+ declaration, passing the left operand as the first argument and the right operand as the second argument. When we use the subtraction and multiplication operators in lines 32–33, their respective overloaded operator declarations are invoked similarly. Notice that the result of each calculation is a reference to a new ComplexNumber object. When this new object is passed to the Console class’s WriteLine method, its ToString method (lines 19–23 of Fig. 11.17) is implicitly invoked. We do not need to assign an object to a reference-type variable to invoke its ToString method. Line 31 of Fig. 11.18 could be rewritten to explicitly invoke the ToString method of the object created by the overloaded plus operator, as in: Console.WriteLine( "{0} + {1} = {2}", x, y, ( x + y ).ToString() );
11.9 (Optional) Software Engineering Case Study: Incorporating Inheritance and Polymorphism into the ATM System We now revisit our ATM system design to see how it might benefit from inheritance and polymorphism. To apply inheritance, we first look for commonality among classes in the system. We create an inheritance hierarchy to model similar classes in an elegant and efficient manner that enables us to process objects of these classes polymorphically. We then modify our class diagram to incorporate the new inheritance relationships. Finally, we demonstrate how to translate the inheritance aspects of our updated design into C# code. In Section 4.11, we encountered the problem of representing a financial transaction in the system. Rather than create one class to represent all transaction types, we created three distinct transaction classes—BalanceInquiry, Withdrawal and Deposit—to represent the transactions that the ATM system can perform. The class diagram of Fig. 11.19 shows the attributes and operations of these classes. Note that they have one private attribute (accountNumber) and one public operation (Execute) in common. Each class requires attribute accountNumber to specify the account to which the transaction applies.
422
Chapter 11
Polymorphism, Interfaces & Operator Overloading
BalanceInquiry – accountNumber : int + Execute() Withdrawal
Deposit
– accountNumber : int – amount : decimal
– accountNumber : int – amount : decimal
+ Execute()
+ Execute()
Fig. 11.19 | Attributes and operations of classes BalanceInquiry, Withdrawal and Deposit.
Each class contains operation Execute, which the ATM invokes to perform the transaction. Clearly, BalanceInquiry, Withdrawal and Deposit represent types of transactions. Figure 11.19 reveals commonality among the transaction classes, so using inheritance to factor out the common features seems appropriate for designing these classes. We place the common functionality in base class Transaction and derive classes BalanceInquiry, Withdrawal and Deposit from Transaction (Fig. 11.20). The UML specifies a relationship called a generalization to model inheritance. Figure 11.20 is the class diagram that models the inheritance relationship between base class Transaction and its three derived classes. The arrows with triangular hollow arrowheads indicate that classes BalanceInquiry, Withdrawal and Deposit are derived from class Transaction by inheritance. Class Transaction is said to be a generalization of its derived classes. The derived classes are said to be specializations of class Transaction. As Fig. 11.19 shows, classes BalanceInquiry, Withdrawal and Deposit share private int attribute accountNumber. We’d like to factor out this common attribute and place it in the base class Transaction. However, recall that a base class’s private attributes are not accessible in derived classes. The derived classes of Transaction require access to attribute
Transaction + «property» AccountNumber : int {readOnly} + Execute()
BalanceInquiry + Execute()
Withdrawal
Deposit
– amount : decimal
– amount : decimal
+ Execute()
+ Execute()
Fig. 11.20 | Class diagram modeling the generalization (i.e., inheritance) relationship between the base class Transaction and its derived classes BalanceInquiry, Withdrawal and Deposit.
11.9 Incorporating Inheritance and Polymorphism into the ATM System
423
so that they can specify which Account to process in the BankDatabase. As you learned in Chapter 10, a derived class can access only the public, protected and protected internal members of its base class. However, the derived classes in this case do not need to modify attribute accountNumber—they need only to access its value. For this reason, we have chosen to replace private attribute accountNumber in our model with the public read-only property AccountNumber. Since this is a read-only property, it provides only a get accessor to access the account number. Each derived class inherits this property, enabling the derived class to access its account number as needed to execute a transaction. We no longer list accountNumber in the second compartment of each derived class, because the three derived classes inherit property AccountNumber from Transaction. According to Fig. 11.19, classes BalanceInquiry, Withdrawal and Deposit also share operation Execute, so base class Transaction should contain public operation Execute. However, it does not make sense to implement Execute in class Transaction, because the functionality that this operation provides depends on the specific type of the actual transaction. We therefore declare Execute as an abstract operation in base class Transaction —it will become an abstract method in the C# implementation. This makes Transaction an abstract class and forces any class derived from Transaction that must be a concrete class (i.e., BalanceInquiry, Withdrawal and Deposit) to implement the operation Execute to make the derived class concrete. The UML requires that we place abstract class names and abstract operations in italics. Thus, in Fig. 11.20, Transaction and Execute appear in italics for the Transaction class; Execute is not italicized in derived classes BalanceInquiry, Withdrawal and Deposit. Each derived class overrides base class Transaction’s Execute operation with an appropriate concrete implementation. Note that Fig. 11.20 includes operation Execute in the third compartment of classes BalanceInquiry, Withdrawal and Deposit, because each class has a different concrete implementation of the overridden operation. As you learned in this chapter, a derived class can inherit interface and implementation from a base class. Compared to a hierarchy designed for implementation inheritance, one designed for interface inheritance tends to have its functionality lower in the hierarchy—a base class signifies one or more operations that should be defined by each class in the hierarchy, but the individual derived classes provide their own implementations of the operation(s). The inheritance hierarchy designed for the ATM system takes advantage of this type of inheritance, which provides the ATM with an elegant way to execute all transactions “in the general” (i.e., polymorphically). Each class derived from Transaction inherits some implementation details (e.g., property AccountNumber), but the primary benefit of incorporating inheritance into our system is that the derived classes share a common interface (e.g., abstract operation Execute). The ATM can aim a Transaction reference at any transaction, and when the ATM invokes the operation Execute through this reference, the version of Execute specific to that transaction runs (polymorphically) automatically (due to polymorphism). For example, suppose a user chooses to perform a balance inquiry. The ATM aims a Transaction reference at a new object of class BalanceInquiry, which the C# compiler allows because a BalanceInquiry is a Transaction. When the ATM uses this reference to invoke Execute, BalanceInquiry’s version of Execute is called (polymorphically). This polymorphic approach also makes the system easily extensible. Should we wish to create a new transaction type (e.g., funds transfer or bill payment), we would simply accountNumber
424
Chapter 11
Polymorphism, Interfaces & Operator Overloading
create an additional Transaction derived class that overrides the Execute operation with a version appropriate for the new transaction type. We would need to make only minimal changes to the system code to allow users to choose the new transaction type from the main menu and for the ATM to instantiate and execute objects of the new derived class. The ATM could execute transactions of the new type using the current code, because it executes all transactions identically (through polymorphism). As you learned earlier in the chapter, an abstract class like Transaction is one for which the programmer never intends to (and, in fact, cannot) instantiate objects. An abstract class simply declares common attributes and behaviors for its derived classes in an inheritance hierarchy. Class Transaction defines the concept of what it means to be a transaction that has an account number and can be executed. You may wonder why we bother to include abstract operation Execute in class Transaction if Execute lacks a concrete implementation. Conceptually, we include this operation because it is the defining behavior of all transactions—executing. Technically, we must include operation Execute in base class Transaction so that the ATM (or any other class) can invoke each derived class’s overridden version of this operation polymorphically via a Transaction reference. Derived classes BalanceInquiry, Withdrawal and Deposit inherit property AccountNumber from base class Transaction, but classes Withdrawal and Deposit contain the additional attribute amount that distinguishes them from class BalanceInquiry. Classes Withdrawal and Deposit require this additional attribute to store the amount of money that the user wishes to withdraw or deposit. Class BalanceInquiry has no need for such an attribute and requires only an account number to execute. Even though two of the three Transaction derived classes share the attribute amount, we do not place it in base class Transaction—we place only features common to all the derived classes in the base class, so derived classes do not inherit unnecessary attributes (and operations). Figure 11.21 presents an updated class diagram of our model that incorporates inheritance and introduces abstract base class Transaction. We model an association between class ATM and class Transaction to show that the ATM, at any given moment, either is executing a transaction or is not (i.e., zero or one objects of type Transaction exist in the system at a time). Because a Withdrawal is a type of Transaction, we no longer draw an association line directly between class ATM and class Withdrawal—derived class Withdrawal inherits base class Transaction’s association with class ATM. Derived classes BalanceInquiry and Deposit also inherit this association, which replaces the previously omitted associations between classes BalanceInquiry and Deposit, and class ATM. Note again the use of triangular hollow arrowheads to indicate the specializations (i.e., derived classes) of class Transaction, as indicated in Fig. 11.20. We also add an association between class Transaction and the BankDatabase (Fig. 11.21). All Transactions require a reference to the BankDatabase so that they can access and modify account information. Each Transaction derived class inherits this reference, so we no longer model the association between class Withdrawal and the BankDatabase. Note that the association between class Transaction and the BankDatabase replaces the previously omitted associations between classes BalanceInquiry and Deposit, and the BankDatabase. We include an association between class Transaction and the Screen because all Transactions display output to the user via the Screen. Each derived class inherits this association. Therefore, we no longer include the association previously modeled between
11.9 Incorporating Inheritance and Polymorphism into the ATM System
1
425
1 Keypad
1
1
CashDispenser
1
1 Screen
DepositSlot 1
0..1
1
Withdrawal
1 1
1
1
ATM
1
0..1
0..1 Executes 1
0..1
0..1
Transaction
0..1
Deposit
0..1
1 Authenticates user against 1
BalanceInquiry
1 BankDatabase
Contains
Accesses/modifies an account balance through
1 0..1
Account
Fig. 11.21 | Class diagram of the ATM system (incorporating inheritance). Note that abstract class name Transaction appears in italics. Withdrawal and the Screen. Class Withdrawal still participates in associations with the CashDispenser and the Keypad, however—these associations apply to derived class Withdrawal but not to derived classes BalanceInquiry and Deposit, so we do not move these associations to base class Transaction. Our class diagram incorporating inheritance (Fig. 11.21) also models classes Deposit and BalanceInquiry. We show associations between Deposit and both the DepositSlot and the Keypad. Note that class BalanceInquiry takes part in only those associations inherited from class Transaction—a BalanceInquiry interacts only with the BankDatabase and the Screen.
The class diagram of Fig. 9.23 showed attributes, properties and operations with visibility markers. Now we present a modified class diagram in Fig. 11.22 that includes abstract base class Transaction. This abbreviated diagram does not show inheritance relationships (these appear in Fig. 11.21), but instead shows the attributes and operations after we have employed inheritance in our system. Note that abstract class name Transaction and abstract operation name Execute in class Transaction appear in italics. To save space, as we did in Fig. 5.16, we do not include those attributes shown by associations in Fig. 11.22—we do, however, include them in the C# implementation in Appendix J. We also omit all operation parameters, as we did in Fig. 9.23—incorporating inheritance does not affect the parameters already modeled in Figs. 7.22–7.24.
426
Chapter 11
Polymorphism, Interfaces & Operator Overloading
ATM – userAuthenticated : bool = false
Transaction + «property» AccountNumber : int {readOnly} + Execute()
Fig. 11.22 | Class diagram after incorporating inheritance into the system.
Software Engineering Observation 11.12 A complete class diagram shows all the associations among classes, and all the attributes and operations for each class. When the number of class attributes, operations and associations is substantial (as in Fig. 11.21 and Fig. 11.22), a good practice that promotes readability is to divide this information between two class diagrams—one focusing on associations and the other on attributes and operations. When examining classes modeled in this fashion, it is crucial to consider both class diagrams to get a complete picture of the classes. For example, one must refer to Fig. 11.21 to observe the inheritance relationship between Transaction and its derived classes; that relationship is omitted from Fig. 11.22. 11.12
Implementing the ATM System Design Incorporating Inheritance In Section 9.17, we began implementing the ATM system design in C# code. We now modify our implementation to incorporate inheritance, using class Withdrawal as an example.
11.9 Incorporating Inheritance and Polymorphism into the ATM System
427
1. If a class A is a generalization of class B, then class B is derived from (and is a specialization of) class A. For example, abstract base class Transaction is a generalization of class Withdrawal. Thus, class Withdrawal is derived from (and is a specialization of) class Transaction. Figure 11.23 contains the shell of class Withdrawal, in which the class definition indicates the inheritance relationship between Withdrawal and Transaction (line 3). 2. If class A is an abstract class and class B is derived from class A, then class B must implement the abstract operations of class A if class B is to be a concrete class. For example, class Transaction contains abstract operation Execute, so class Withdrawal must implement this operation if we want to instantiate Withdrawal objects. Figure 11.24 contains the portions of the C# code for class Withdrawal that can be inferred from Fig. 11.21 and Fig. 11.22. Class Withdrawal inherits property AccountNumber from base class Transaction, so Withdrawal does not declare this property. Class Withdrawal also inherits references to the Screen and the BankDatabase from class Transaction, so we do not include these references in our code. Figure 11.22 specifies attribute amount and operation Execute for class Withdrawal. Line 6 of Fig. 11.24 declares an instance variable for attribute amount. Lines 17–20 declare the shell of a method for operation Execute. Recall that derived class Withdrawal must provide a concrete implementation of the abstract method Execute from base class Transaction. The keypad and cashDispenser references (lines 7–8) are instance variables whose need is apparent from class Withdrawal’s associations in Fig. 11.21—in the C# implementation of this class in Appendix J, a constructor initializes these references to actual objects. 1 2 3 4 5 6
// Fig 11.23: Withdrawal.cs // Class Withdrawal represents an ATM withdrawal transaction. public class Withdrawal : Transaction { // code for members of class Withdrawal } // end class Withdrawal
Fig. 11.23 | C# code for shell of class Withdrawal. 1 2 3 4 5 6 7 8 9 10 11 12 13 14
// Fig 11.24: Withdrawal.cs // Class Withdrawal represents an ATM withdrawal transaction. public class Withdrawal : Transaction { // attributes private decimal amount; // amount to withdraw private Keypad keypad; // reference to keypad private CashDispenser cashDispenser; // reference to cash dispenser // parameterless constructor public Withdrawal() { // constructor body code } // end constructor
Fig. 11.24 | C# code for class Withdrawal based on Fig. 11.21 and Fig. 11.22. (Part 1 of 2.)
428 15 16 17 18 19 20 21
Chapter 11
Polymorphism, Interfaces & Operator Overloading
// method that overrides Execute public override void Execute() { // Execute method body code } // end method Execute } // end class Withdrawal
Fig. 11.24 | C# code for class Withdrawal based on Fig. 11.21 and Fig. 11.22. (Part 2 of 2.) We discuss the polymorphic processing of Transactions in Section J.2 of the ATM implementation. Class ATM performs the actual polymorphic call to method Execute at line 99 of Fig. J.1.
ATM Case Study Wrap-Up This concludes our object-oriented design of the ATM system. A complete C# implementation of the ATM system in 655 lines of code appears in Appendix J. This working implementation uses most of the key object-oriented programming concepts that we have covered to this point in the book, including classes, objects, encapsulation, visibility, composition, inheritance and polymorphism. The code is abundantly commented and conforms to the coding practices you’ve learned so far. Mastering this code is a wonderful capstone experience for you after studying the nine Software Engineering Case Study sections in Chapters 1, 3–9 and 11. Software Engineering Case Study Self-Review Exercises 11.1
The UML uses an arrow with a a) solid filled arrowhead b) triangular hollow arrowhead c) diamond-shaped hollow arrowhead d) stick arrowhead
to indicate a generalization relationship.
11.2 State whether the following statement is true or false, and if false, explain why: The UML requires that we underline abstract class names and abstract operation names. 11.3 Write C# code to begin implementing the design for class Transaction specified in Fig. 11.21 and Fig. 11.22. Be sure to include private references based on class Transaction’s associations. Also, be sure to include properties with public get accessors for any of the private instance variables that the derived classes must access to perform their tasks.
Answers to Software Engineering Case Study Self-Review Exercises 11.1
b.
11.2
False. The UML requires that we italicize abstract class names and operation names.
11.3 The design for class Transaction yields the code in Fig. 11.25. In the implementation in Appendix J, a constructor initializes private instance variables userScreen and database to actual objects, and read-only properties UserScreen and Database access these instance variables. These properties allow classes derived from Transaction to access the ATM’s screen and interact with the bank’s database. Note that we chose the names of the UserScreen and Database properties for clarity—we wanted to avoid property names that are the same as the class names Screen and BankDatabase, which can be confusing.
// Fig 11.25: Transaction.cs // Abstract base class Transaction represents an ATM transaction. public abstract class Transaction { private int accountNumber; // indicates account involved private Screen userScreen; // ATM's screen private BankDatabase database; // account info database // parameterless constructor public Transaction() { // constructor body code } // end constructor // read-only property that gets the account number public int AccountNumber { get { return accountNumber; } // end get } // end property AccountNumber // read-only property that gets the screen reference public Screen UserScreen { get { return userScreen; } // end get } // end property UserScreen // read-only property that gets the bank database reference public BankDatabase Database { get { return database; } // end get } // end property Database // perform the transaction (overridden by each derived class) public abstract void Execute(); } // end class Transaction
Fig. 11.25 | C# code for class Transaction based on Fig. 11.21 and Fig. 11.22.
11.10 Wrap-Up This chapter introduced polymorphism—the ability to process objects that share the same base class in a class hierarchy as if they are all objects of the base class. The chapter discussed how polymorphism makes systems extensible and maintainable, then demonstrated how to use overridden methods to effect polymorphic behavior. We introduced the notion
430
Chapter 11
Polymorphism, Interfaces & Operator Overloading
of an abstract class, which allows you to provide an appropriate base class from which other classes can inherit. You learned that an abstract class can declare abstract methods that each derived class must implement to become a concrete class, and that an application can use variables of an abstract class to invoke derived class implementations of abstract methods polymorphically. You also learned how to determine an object’s type at execution time. We showed how to create sealed methods and classes. The chapter discussed declaring and implementing an interface as another way to achieve polymorphic behavior, often among objects of different classes. Finally, you learned how to define the behavior of the built-in operators on objects of your own classes with operator overloading. You should now be familiar with classes, objects, encapsulation, inheritance, interfaces and polymorphism—the most essential aspects of object-oriented programming. In the next chapter, we take a deeper look at how to use exception handling to deal with errors during execution time.
12 It is common sense to take a method and try it. If it fails, admit it frankly and try another. But above all, try something.
Exception Handling
—Franklin Delano Roosevelt
O! throw away the worser part of it, And live the purer with the other half. —William Shakespeare
If they’re running and they don’t look where they’re going I have to come out from somewhere and catch them.
OBJECTIVES In this chapter you will learn: I
What exceptions are and how they are handled.
I
When to use exception handling.
I
To use try blocks to delimit code in which exceptions might occur.
I
To throw exceptions to indicate a problem.
I
To use catch blocks to specify exception handlers.
I
To use the finally block to release resources.
I
The .NET exception class hierarchy.
I
Exception
I
To create user-defined exceptions.
—J. D. Salinger
And oftentimes excusing of a fault Doth make the fault the worse by the excuse. —William Shakespeare
O infinite virtue! com’st thou smiling from the world’s great snare uncaught? —William Shakespeare
properties.
Outline
432 12.1 12.2 12.3 12.4
12.5
12.6 12.7 12.8 12.9
Chapter 12
Exception Handling
Introduction Exception Handling Overview Example: Divide by Zero Without Exception Handling Example: Handling DivideByZeroExceptions and FormatExceptions 12.4.1 Enclosing Code in a try Block 12.4.2 Catching Exceptions 12.4.3 Uncaught Exceptions 12.4.4 Termination Model of Exception Handling 12.4.5 Flow of Control When Exceptions Occur .NET Exception Hierarchy 12.5.1 Classes ApplicationException and SystemException 12.5.2 Determining Which Exceptions a Method Throws finally Block Exception Properties User-Defined Exception Classes Wrap-Up
12.1 Introduction In this chapter, we introduce exception handling. An exception is an indication of a problem that occurs during a program’s execution. The name “exception” comes from the fact that, although the problem can occur, it occurs infrequently. If the “rule” is that a statement normally executes correctly, then the occurrence of a problem represents the “exception to the rule.” Exception handling enables programmers to create applications that can resolve (or handle) exceptions. In many cases, handling an exception allows a program to continue executing as if no problems were encountered. However, more severe problems may prevent a program from continuing normal execution, instead requiring the program to notify the user of the problem, then terminate in a controlled manner. The features presented in this chapter enable programmers to write clear, robust and more fault-tolerant programs (i.e., programs that are able to deal with problems that may arise and continue executing). The style and details of C# exception handling are based in part on the work of Andrew Koenig and Bjarne Stroustrup. “Best practices” for exception handling in Visual C# 2005 are specified in the Visual Studio documentation.1
Error-Prevention Tip 12.1 Exception handling helps improve a program’s fault tolerance.
12.1
This chapter begins with an overview of exception-handling concepts and demonstrations of basic exception-handling techniques. The chapter also overviews .NET’s excep1.
“Best Practices for Handling Exceptions [C#],” .NET Framework Developer’s Guide, Visual Studio .NET Online Help. Available at msdn2.microsoft.com/library/seyhszts(en-us,vs.80).aspx.
12.2 Exception Handling Overview
433
tion-handling class hierarchy. Programs typically request and release resources (such as files on disk) during program execution. Often, the supply of these resources is limited, or the resources can be used by only one program at a time. We demonstrate a part of the exception-handling mechanism that enables a program to use a resource, then guarantee that the resource will be released for use by other programs, even if an exception occurs. The chapter demonstrates several properties of class System.Exception (the base class of all exception classes) and discusses how you can create and use your own exception classes.
12.2 Exception Handling Overview Programs frequently test conditions to determine how program execution should proceed. Consider the following pseudocode: Perform a task If the preceding task did not execute correctly Perform error processing Perform next task If the preceding task did not execute correctly Perform error processing … In this pseudocode, we begin by performing a task; then we test whether that task executed correctly. If not, we perform error processing. Otherwise, we continue with the next task. Although this form of error handling works, intermixing program logic with error-handling logic can make programs difficult to read, modify, maintain and debug—especially in large applications. Exception handling enables programmers to remove error-handling code from the “main line” of the program’s execution, improving program clarity and enhancing modifiability. Programmers can decide to handle any exceptions they choose—all exceptions, all exceptions of a certain type or all exceptions of a group of related types (i.e., exception types that are related through an inheritance hierarchy). Such flexibility reduces the likelihood that errors will be overlooked, thus making programs more robust. With programming languages that do not support exception handling, programmers often delay writing error-processing code and sometimes forget to include it. This results in less robust software products. C# enables programmers to deal with exception handling easily from the beginning of a project.
12.3 Example: Divide by Zero Without Exception Handling First we demonstrate what happens when errors arise in a console application that does not use exception handling. Figure 12.1 inputs two integers from the user, then divides the first integer by the second using integer division to obtain an int result. In this example, we will see that an exception is thrown (i.e., an exception occurs) when a method detects a problem and is unable to handle it.
// Fig. 12.1: DivideByZeroNoExceptionHandling.cs // An application that attempts to divide by zero. using System; class DivideByZeroNoExceptionHandling { static void Main() { // get numerator and denominator Console.Write( "Please enter an integer numerator: " ); int numerator = Convert.ToInt32( Console.ReadLine() ); Console.Write( "Please enter an integer denominator: " ); int denominator = Convert.ToInt32( Console.ReadLine() ); // divide the two integers, then display the result int result = numerator / denominator; Console.WriteLine( "\nResult: {0:D} / {1:D} = {2:D}", numerator, denominator, result ); } // end Main } // end class DivideByZeroNoExceptionHandling
Please enter an integer numerator: 100 Please enter an integer denominator: 7 Result: 100 / 7 = 14
Please enter an integer numerator: 100 Please enter an integer denominator: 0 Unhandled Exception: System.DivideByZeroException: Attempted to divide by zero. at DivideByZeroNoExceptionHandling.Main() in C:\examples\ch12\Fig12_01\DivideByZeroNoExceptionHandling\ DivideByZeroNoExceptionHandling.cs:line 16
Please enter an integer numerator: 100 Please enter an integer denominator: hello Unhandled Exception: System.FormatException: Input string was not in a correct format. at System.Number.StringToNumber(String str, NumberStyles options, NumberBuffer& number, NumberFormatInfo info, Boolean parseDecimal) at System.Number.ParseInt32(String s, NumberStyles style, NumberFormatInfo info) at System.Convert.ToInt32(String value) at DivideByZeroNoExceptionHandling.Main() in C:\examples\ch12\Fig12_01\DivideByZeroNoExceptionHandling\ DivideByZeroNoExceptionHandling.cs:line 13
Fig. 12.1 | Integer division without exception handling.
12.3 Example: Divide by Zero Without Exception Handling
435
Running the Application In most of the examples we have created so far, the application appears to run the same with or without debugging. As we discuss shortly, the example in Fig. 12.1 might cause errors, depending on the user’s input. If you run this application using the Debug > Start Debugging menu option, the program pauses at the line where an exception occurs and displays the Exception Assistant, allowing you to analyze the current state of the program and debug it. We discuss the Exception Assistant in Section 12.4.3. We discuss debugging in detail in Appendix C. In this example, we do not wish to debug the application; we simply want to see what happens when errors arise. For this reason, we execute this application from a Command Prompt window. Select Start > All Programs > Accessories > Command Prompt to open a Command Prompt window, then use the cd command to change to the application’s Debug directory. For example, if this application resides in the directory C:\examples\ ch12\Fig12_01\DivideByZeroNoExceptionHandling on your system, you would type cd /d C:\examples\ch12\Fig12_01\DivideByZeroNoExceptionHandling \bin\Debug
in the Command Prompt, then press Enter to change to the application’s Debug directory. To execute the application, type DivideByZeroNoExceptionHandling.exe
in the Command Prompt, then press Enter. If an error arises during execution, a dialog is displayed indicating that the application has encountered a problem and needs to close. The dialog also asks whether you’d like to send information about this error to Microsoft. Since we are creating this error for demonstration purposes, you should click Don’t Send. [Note: On some systems a Just-In-Time Debugging dialog is displayed instead. If this occurs, simply click the No button to dismiss the dialog.] At this point, an error message describing the problem is displayed in the Command Prompt. We formatted the error messages in Fig. 12.1 for readability. [Note: Selecting Debug > Start Without Debugging (or F5) to run the application from Visual Studio executes the application’s so-called release version. The error messages produced by this version of the application may differ from those shown in Fig. 12.1 due to optimizations that the compiler performs to create an application’s release version.]
Analyzing the Results The first sample execution in Fig. 12.1 shows a successful division. In the second sample execution, the user enters 0 as the denominator. Note that several lines of information are displayed in response to the invalid input. This information—known as a stack trace— includes the exception name (System.DivideByZeroException) in a descriptive message indicating the problem that occurred and the path of execution that led to the exception, method by method. This information helps you debug a program. The first line of the error message specifies that a DivideByZeroException has occurred. When division by zero in integer arithmetic occurs, the CLR throws a DivideByZeroException (namespace System). The text after the name of the exception, “Attempted to divide by zero,” indicates that this exception occurred as a result of an attempt to divide by zero. Division by zero is not allowed in integer arithmetic. [Note: Division by zero with floating-point values is al-
436
Chapter 12
Exception Handling
lowed. Such a calculation results in the value infinity, which is represented by either constant Double.PositiveInfinity or constant Double.NegativeInfinity, depending on whether the numerator is positive or negative. These values are displayed as Infinity or -Infinity. If both the numerator and denominator are zero, the result of the calculation is the constant Double.NaN (“not a number”), which is returned when a calculation’s result is undefined.] Each “at” line in the stack trace indicates a line of code in the particular method that was executing when the exception occurred. The “at” line contains the namespace, class name and method name in which the exception occurred (DivideByZeroNoExceptionHandling.Main), the location and name of the file in which the code resides (C:\examples\ch12\Fig12_01\DivideByZeroNoExceptionHandling\DivideByZeroNoException Handling.cs:line 16) and the line of code where the exception occurred. In this case, the stack trace indicates that the DivideByZeroException occurred when the program was executing line 16 of method Main. The first "at" line in the stack trace indicates the excep-
tion’s throw point—the initial point at which the exception occurred (i.e., line 16 in Main). This information makes it easy for the programmer to see where the exception originated, and what method calls were made to get to that point in the program. Now, let’s look at a more detailed stack trace. In the third sample execution, the user enters the string "hello" as the denominator. This causes a FormatException, and another stack trace is displayed. Our earlier examples that read numeric values from the user assumed that the user would input an integer value. However, a user could erroneously input a noninteger value. A FormatException (namespace System) occurs, for example, when Convert method ToInt32 receives a string that does not represent a valid integer. Starting from the last "at" line in the stack trace, we see that the exception was detected in line 13 of method Main. The stack trace also shows the other methods that led to the exception being thrown—Convert.ToInt32, Number.ParseInt32 and Number.StringToNumber. To perform its task, Convert.ToInt32 calls method Number.ParseInt32, which in turn calls Number.StringToNumber. The throw point occurs in Number.StringToNumber, as indicated by the first "at" line in the stack trace. Note that in the sample executions in Fig. 12.1, the program also terminates when exceptions occur and stack traces are displayed. This does not always happen—sometimes a program may continue executing even though an exception has occurred and a stack trace has been printed. In such cases, the application may produce incorrect results. The next section demonstrates how to handle exceptions to enable the program to run to normal completion.
12.4 Example: Handling DivideByZeroExceptions and FormatExceptions Let us consider a simple example of exception handling. The application in Fig. 12.2 uses exception handling to process any DivideByZeroExceptions and FormatExceptions that might arise. The application displays two TextBoxes in which the user can type integers. When the user presses Click To Divide, the program invokes event handler DivideButton_Click (lines 17–48), which obtains the user’s input, converts the input values to type int and divides the first number (numerator) by the second number (denominator). Assuming that the user provides integers as input and does not specify 0 as the
12.4 Handling DivideByZeroExceptions and FormatExceptions
// Fig. 12.2: DivideByZeroTest.cs // Exception handlers for FormatException and DivideByZeroException. using System; using System.Windows.Forms; namespace DivideByZeroTest { public partial class DivideByZeroTestForm : Form { public DivideByZeroTestForm() { InitializeComponent(); } // end constructor // obtain 2 integers from the user // and divide numerator by denominator private void DivideButton_Click( object sender, EventArgs e ) { OutputLabel.Text = ""; // clear Label OutputLabel // retrieve user input and calculate quotient try { // Convert.ToInt32 generates FormatException // if argument is not an integer int numerator = Convert.ToInt32( NumeratorTextBox.Text ); int denominator = Convert.ToInt32( DenominatorTextBox.Text ); // division generates DivideByZeroException // if denominator is 0 int result = numerator / denominator; // display result in OutputLabel OutputLabel.Text = result.ToString(); } // end try catch ( FormatException ) { MessageBox.Show( "You must enter two integers.", "Invalid Number Format", MessageBoxButtons.OK, MessageBoxIcon.Error ); } // end catch catch ( DivideByZeroException divideByZeroExceptionParameter ) { MessageBox.Show( divideByZeroExceptionParameter.Message, "Attempted to Divide by Zero", MessageBoxButtons.OK, MessageBoxIcon.Error ); } // end catch } // end method DivideButton_Click } // end class DivideByZeroTestForm } // end namespace DivideByZeroTest
Fig. 12.2 | Exception handlers for FormatException and DivideByZeroException. (Part 1 of 2.)
438
Chapter 12
Exception Handling
(a)
(b)
(c)
(d)
(e)
Fig. 12.2 | Exception handlers for FormatException and DivideByZeroException. (Part 2 of 2.)
denominator for the division, DivideButton_Click displays the division result in OutputLabel. However, if the user inputs a noninteger value or supplies 0 as the denominator, an exception occurs. This program demonstrates how to catch and handle (i.e., deal with) such exceptions—in this case, displaying an error message and allowing the user to enter another set of values. Before we discuss the details of the program, let’s consider the sample output windows in Fig. 12.2. The window in Fig. 12.2(a) shows a successful calculation, in which the user enters the numerator 100 and the denominator 7. Note that the result (14) is an int, because integer division always yields an int result. The next two windows, Fig. 12.2(b) and Fig. 12.2(c), demonstrate the result of an attempt to divide by zero. In integer arithmetic, the CLR tests for division by zero and generates a DivideByZeroException if the denominator is zero. The program detects the exception and displays the error message dialog in Fig. 12.2(c) indicating the attempt to divide by zero. The last two output windows, Fig. 12.2(d) and Fig. 12.2(e), depict the result of inputting a non-int value—in this case, the user enters “hello” in the second TextBox, as shown in Fig. 12.2(d). When the user clicks Click To Divide, the program attempts to convert the input strings into int values using method Convert.ToInt32 (lines 26–27). If an argument passed to Convert.ToInt32 cannot be converted to an int value, the method throws a FormatException. The program catches the exception and displays the error message dialog in
12.4 Handling DivideByZeroExceptions and FormatExceptions
439
Fig. 12.2(e) indicating that the user must enter two ints. Notice that we did not include a parameter name for the catch at line 36. In the catch’s block, we do not use any information from the FormatException object. Omitting the parameter name prevents the compiler from issuing a warning which indicates that we declared a variable, but did not use it in the catch block.
12.4.1 Enclosing Code in a try Block Now we consider the user interactions and flow of control that yield the results shown in the sample output windows. The user inputs values into the TextBoxes that represent the numerator and denominator, then presses Click To Divide. At this point, the program invokes method DivideButton_Click. Line 19 assigns the empty string to OutputLabel to clear any prior result in preparation for a new calculation. Lines 22–35 define a try block enclosing the code that might throw exceptions, as well as the code that is skipped when an exception occurs. For example, the program should not display a new result in OutputLabel (line 34) unless the calculation in line 31 completes successfully. The two statements that read the ints from the TextBoxes (lines 26–27) call method Convert.ToInt32 to convert strings to int values. This method throws a FormatException if it cannot convert its string argument to an int. If lines 26–27 convert the values properly (i.e., no exceptions occur), then line 31 divides the numerator by the denominator and assigns the result to variable result. If denominator is 0, line 31 causes the CLR to throw a DivideByZeroException. If line 31 does not cause an exception to be thrown, then line 34 displays the result of the division.
12.4.2 Catching Exceptions Exception-handling code appears in a catch block. In general, when an exception occurs in a try block, a corresponding catch block catches the exception and handles it. The try block in this example is followed by two catch blocks—one that handles a FormatException (lines 36–41) and one that handles a DivideByZeroException (lines 42–47). A catch block specifies an exception parameter representing the exception that the catch block can handle. The catch block can use the parameter’s identifier (which is chosen by the programmer) to interact with a caught exception object. If there is no need to use the exception object in the catch block, the exception parameter’s identifier can be omitted. The type of the catch’s parameter is the type of the exception that the catch block handles. Optionally, programmers can include a catch block that does not specify an exception type or an identifier—such a catch block (known as a general catch clause) catches all exception types. At least one catch block and/or a finally block (discussed in Section 12.6) must immediately follow a try block. In Fig. 12.2, the first catch block catches FormatExceptions (thrown by method Convert.ToInt32), and the second catch block catches DivideByZeroExceptions (thrown by the CLR). If an exception occurs, the program executes only the first matching catch block. Both exception handlers in this example display an error message dialog. After either catch block terminates, program control continues with the first statement after the last catch block (the end of the method, in this example). We will soon take a deeper look at how this flow of control works in exception handling.
440
Chapter 12
Exception Handling
12.4.3 Uncaught Exceptions An uncaught exception is an exception for which there is no matching catch block. You saw the results of uncaught exceptions in the second and third outputs of Fig. 12.1. Recall that when exceptions occur in that example, the application terminates early (after displaying the exception’s stack trace). The result of an uncaught exception depends on how you execute the program—Fig. 12.1 demonstrated the results of an uncaught exception when an application is executed in a Command Prompt. If you run the application from Visual Studio with debugging and the runtime environment detects an uncaught exception, the application pauses, and a window called the Exception Assistant appears indicating where the exception occurred, the type of the exception and links to helpful information on handling the exception. Figure 12.3 shows the Exception Assistant that is displayed if the user attempts to divide by zero in the application of Fig. 12.1.
12.4.4 Termination Model of Exception Handling When a method called in a program or the CLR detects a problem, the method or the CLR throws an exception. Recall that the point in the program at which an exception occurs is called the throw point—this is an important location for debugging purposes (as we demonstrate in Section 12.7). If an exception occurs in a try block (such as a FormatException being thrown as a result of the code in line 27 in Fig. 12.2), the try block terminates immediately, and program control transfers to the first of the following catch blocks in which the exception parameter’s type matches the type of the thrown exception. In Fig. 12.2, the first catch block catches FormatExceptions (which occur if input of an invalid type is entered); the second catch block catches DivideByZeroExceptions (which occur if an attempt is made to divide by zero). After the exception is handled, program control does not return to the throw point because the try block has expired (which also causes any of its local variables to go out of scope). Rather, control resumes after the last catch block. This is known as the termination model of exception handling. [Note: Some languages use the resumption model of exception handling, in which after an exception is handled, control resumes just after the throw point.]
Throw point
Fig. 12.3 | Exception Assistant.
Exception Assistant
12.4 Handling DivideByZeroExceptions and FormatExceptions
441
Common Programming Error 12.1 Logic errors can occur if you assume that after an exception is handled, control will return to the first statement after the throw point. 12.1
If no exceptions occur in the try block, the program of Fig. 12.2 successfully completes the try block by ignoring the catch blocks in lines 36–41 and 42–47, and passing line 47. Then the program executes the first statement following the try and catch blocks. In this example, the program reaches the end of event handler DivideButton_Click (line 48), so the method terminates, and the program awaits the next user interaction. The try block and its corresponding catch and finally blocks together form a try statement. It is important not to confuse the terms “try block” and “try statement”—the term “try block” refers to the block of code following the keyword try (but before any catch or finally blocks), while the term “try statement” includes all the code from the opening try keyword to the end of the last catch or finally block. This includes the try block, as well as any associated catch blocks and finally block. As with any other block of code, when a try block terminates, local variables defined in the block go out of scope. If a try block terminates due to an exception, the CLR searches for the first catch block that can process the type of exception that occurred. The CLR locates the matching catch by comparing the type of the thrown exception to each catch’s parameter type. A match occurs if the types are identical or if the thrown exception’s type is a derived class of the catch’s parameter type. Once an exception is matched to a catch block, the code in that block executes and the other catch blocks in the try statement are ignored.
12.4.5 Flow of Control When Exceptions Occur In the sample output of Fig. 12.2(b), the user inputs hello as the denominator. When line 27 executes, Convert.ToInt32 cannot convert this string to an int, so Convert.ToInt32 throws a FormatException object to indicate that the method was unable to convert the string to an int. When the exception occurs, the try block expires (terminates). Next, the CLR attempts to locate a matching catch block. A match occurs with the catch block in line 36, so the exception handler executes and the program ignores all other exception handlers following the try block.
Common Programming Error 12.2 Specifying a comma-separated list of parameters in a catch block is a syntax error. A catch block can have at most one parameter. 12.2
In the sample output of Fig. 12.2(d), the user inputs 0 as the denominator. When the division in line 31 executes, a DivideByZeroException occurs. Once again, the try block terminates, and the program attempts to locate a matching catch block. In this case, the first catch block does not match—the exception type in the catch-handler declaration is not the same as the type of the thrown exception, and FormatException is not a base class of DivideByZeroException. Therefore the program continues to search for a matching catch block, which it finds in line 42. Line 44 displays the value of property Message of class Exception, which contains the error message. Note that our program never “sets” this error message attribute. This is done by the CLR when it creates the exception object.
442
Chapter 12
Exception Handling
12.5 .NET Exception Hierarchy In C#, the exception-handling mechanism allows only objects of class Exception (namespace System) and its derived classes to be thrown and caught. Note, however, that C# programs may interact with software components written in other .NET languages (such as C++) that do not restrict exception types. The general catch clause can be used to catch such exceptions. This section overviews several of the .NET Framework’s exception classes and focuses exclusively on exceptions that derived from class Exception. In addition, we discuss how to determine whether a particular method throws exceptions.
12.5.1 Classes ApplicationException and SystemException Class Exception of namespace System is the base class of the .NET Framework exception class hierarchy. Two of the most important classes derived from Exception are ApplicationException and SystemException. ApplicationException is a base class that programmers can extend to create exception classes that are specific to their applications. We show how to create user-defined exception classes in Section 12.8. Programs can recover from most ApplicationExceptions and continue execution. The CLR generates SystemExceptions, which can occur at any point during program execution. Many of these exceptions can be avoided if applications are coded properly. For example, if a program attempts to access an out-of-range array index, the CLR throws an exception of type IndexOutOfRangeException (a derived class of SystemException). Similarly, an exception occurs when a program uses an object reference to manipulate an object that does not yet exist (i.e., the reference has a value of null). Attempting to use a null reference causes a NullReferenceException (another derived class of SystemException). You saw earlier in this chapter that a DivideByZeroException occurs in integer division when a program attempts to divide by zero. Other SystemException types thrown by the CLR include OutOfMemoryException, StackOverflowException and ExecutionEngineException. These are thrown when the something goes wrong that causes the CLR to become unstable. In some cases, such exceptions cannot even be caught. In general, it is best to simply log such exceptions then terminate your application. A benefit of the exception class hierarchy is that a catch block can catch exceptions of a particular type or—because of the is-a relationship of inheritance—can use a base-class type to catch exceptions in a hierarchy of related exception types. For example, Section 12.4.2 discussed the catch block with no parameter, which catches exceptions of all types (including those that are not derived from Exception). A catch block that specifies a parameter of type Exception can catch all exceptions that derive from Exception, because Exception is the base class of all exception classes. The advantage of this approach is that the exception handler can access the caught exception’s information via the parameter in the catch. We demonstrated accessing the information in an exception in line 44 of Fig. 12.2. We’ll say more about accessing exception information in Section 12.7. Using inheritance with exceptions enables an catch block to catch related exceptions using a concise notation. A set of exception handlers could catch each derived-class exception type individually, but catching the base-class exception type is more concise. However, this technique makes sense only if the handling behavior is the same for a base class and all derived classes. Otherwise, catch each derived-class exception individually.
12.6 finally Block
443
Common Programming Error 12.3 It is a compilation error if a catch block that catches a base-class exception is placed before a catch block for any of that class’s derived-class types. If this were allowed, the base-class catch block would catch all base-class and derived-class exceptions, so the derived-class exception handler would never execute. 12.3
12.5.2 Determining Which Exceptions a Method Throws How do we determine that an exception might occur in a program? For methods contained in the .NET Framework classes, read the detailed descriptions of the methods in the online documentation. If a method throws an exception its description contains a section called Exceptions that specifies the types of exceptions the method throws and briefly describes possible causes for the exceptions. For example, search for “Convert.ToInt32 method” in the Index of the Visual Studio online documentation (use the .NET Framework filter). Select the document entitled Convert.ToInt32 Method (System). In the document that describes the method, click the link Convert.ToInt32(String). In the document that appears, the Exceptions section (near the bottom of the document) indicates that method Convert.ToInt32 throws two exception types—FormatException and OverflowException—and describes the reason why each might occur.
Software Engineering Observation 12.1 If a method throws exceptions, statements that invoke the method directly or indirectly should be placed in try blocks, and those exceptions should be caught and handled. 12.0
It is more difficult to determine when the CLR throws exceptions. Such information appears in the C# Language Specification. This document defines C#’s syntax and specifies cases in which exceptions are thrown. Figure 12.2 demonstrated that the CLR throws a DivideByZeroException in integer arithmetic when a program attempts to divide by zero. Section 7.7.2 of the language specification (14.7.2 in the ECMA version) discusses the division operator and when DivideByZeroExceptions occur.
12.6 finally Block Programs frequently request and release resources dynamically (i.e., at execution time). For example, a program that reads a file from disk first makes a file-open request (as we’ll see in Chapter 18, Files and Streams). If that request succeeds, the program reads the contents of the file. Operating systems typically prevent more than one program from manipulating a file at once. Therefore, when a program finishes processing a file, the program should close the file (i.e., release the resource) so other programs can use it. If the file is not closed, a resource leak occurs. In such a case, the file resource is not available to other programs, possibly because a program using the file has not closed it. In programming languages such as C and C++, in which the programmer (not the language) is responsible for dynamic memory management, the most common type of resource leak is a memory leak. A memory leak occurs when a program allocates memory (as C# programmers do via keyword new), but does not deallocate the memory when it is no longer needed. Normally, this is not an issue in C#, because the CLR performs garbage collection of memory that is no longer needed by an executing program (Section 9.10). However, other kinds of resource leaks (such as unclosed files) can occur.
444
Chapter 12
Exception Handling
Error-Prevention Tip 12.2 The CLR does not completely eliminate memory leaks. The CLR will not garbage collect an object until the program contains no more references to that object. Thus, memory leaks can occur if programmers inadvertently keep references to unwanted objects. 12.2
Moving Resource Release Code to a finally Block Typically, exceptions occur when processing resources that require explicit release. For example, a program that processes a file might receive IOExceptions during the processing. For this reason, file processing code normally appears in a try block. Regardless of whether a program experiences exceptions while processing a file, the program should close the file when it is no longer needed. Suppose a program places all resource request and resource release code in a try block. If no exceptions occur, the try block executes normally and releases the resources after using them. However, if an exception occurs, the try block may expire before the resource-release code can execute. We could duplicate all the resource release code in each of the catch blocks, but this would make the code more difficult to modify and maintain. We could also place the resource release code after the try statement; however, if the try block terminates due to a return statement, code following the try statement would never execute. To address these problems, C#’s exception handling mechanism provides the finally block, which is guaranteed to execute regardless of whether the try block executes successfully or an exception occurs. This makes the finally block an ideal location in which to place resource-release code for resources that are acquired and manipulated in the corresponding try block. If the try block executes successfully, the finally block executes immediately after the try block terminates. If an exception occurs in the try block, the finally block executes immediately after a catch block completes. If the exception is not caught by a catch block associated with the try block, or if a catch block associated with the try block throws an exception itself, the finally block executes before the exception is processed by the next enclosing try block (if there is one). By placing the resource release code in a finally block, we ensure that even if the program terminates due to an uncaught exception, the resource will be deallocated. Note that local variables in a try block cannot be accessed in the corresponding finally block. For this reason, variables that must be accessed in both a try block and its corresponding finally block should be declared before the try block.
Error-Prevention Tip 12.3 A finally block typically contains code to release resources acquired in the corresponding try block, which makes the finally block an effective mechanism for eliminating resource leaks.
12.3
Performance Tip 12.1 As a rule, resources should be released as soon as they are no longer needed in a program. This makes them available for reuse promptly. 12.1
If one or more catch blocks follow a try block, the finally block is optional. However, if no catch blocks follow a try block, a finally block must appear immediately after the try block. If any catch blocks follow a try block, the finally block (if there is one) appears after the last catch block. Only whitespace and comments can separate the blocks in a try statement.
12.6 finally Block
445
Common Programming Error 12.4 Placing the finally block before a catch block is a syntax error.
12.4
Demonstrating the finally Block The application in Fig. 12.4 demonstrates that the finally block always executes, regardless of whether an exception occurs in the corresponding try block. The program consists of method Main (lines 8–47) and four other methods that Main invokes to demonstrate finally. These methods are DoesNotThrowException (lines 50–67), ThrowExceptionWithCatch (lines 70–89), ThrowExceptionWithoutCatch (lines 92–108) and ThrowExceptionCatchRethrow (lines 111–136).
// Fig. 12.4: UsingExceptions.cs // Using finally blocks. // Demonstrate that finally always executes. using System; class UsingExceptions { static void Main() { // Case 1: No exceptions occur in called method Console.WriteLine( "Calling DoesNotThrowException" ); DoesNotThrowException();
Fig. 12.4 | of 4.)
// Case 2: Exception occurs and is caught in called method Console.WriteLine( "\nCalling ThrowExceptionWithCatch" ); ThrowExceptionWithCatch(); // Case 3: Exception occurs, but is not caught in called method // because there is no catch block. Console.WriteLine( "\nCalling ThrowExceptionWithoutCatch" ); // call ThrowExceptionWithoutCatch try { ThrowExceptionWithoutCatch(); } // end try catch { Console.WriteLine( "Caught exception from " + "ThrowExceptionWithoutCatch in Main" ); } // end catch // Case 4: Exception occurs and is caught in called method, // then rethrown to caller. Console.WriteLine( "\nCalling ThrowExceptionCatchRethrow" ); finally
blocks always execute, regardless of whether an exception occurs. (Part 1
// call ThrowExceptionCatchRethrow try { ThrowExceptionCatchRethrow(); } // end try catch { Console.WriteLine( "Caught exception from " + "ThrowExceptionCatchRethrow in Main" ); } // end catch } // end method Main // no exceptions thrown static void DoesNotThrowException() { // try block does not throw any exceptions try { Console.WriteLine( "In DoesNotThrowException" ); } // end try catch { Console.WriteLine( "This catch never executes" ); } // end catch finally { Console.WriteLine( "finally executed in DoesNotThrowException" ); } // end finally Console.WriteLine( "End of DoesNotThrowException" ); } // end method DoesNotThrowException // throws exception and catches it locally static void ThrowExceptionWithCatch() { // try block throws exception try { Console.WriteLine( "In ThrowExceptionWithCatch" ); throw new Exception( "Exception in ThrowExceptionWithCatch" ); } // end try catch ( Exception exceptionParameter ) { Console.WriteLine( "Message: " + exceptionParameter.Message ); } // end catch finally { Console.WriteLine( "finally executed in ThrowExceptionWithCatch" ); } // end finally
Fig. 12.4 | of 4.)
Exception Handling
finally
blocks always execute, regardless of whether an exception occurs. (Part 2
12.6 finally Block
447
87 88 Console.WriteLine( "End of ThrowExceptionWithCatch" ); 89 } // end method ThrowExceptionWithCatch 90 91 // throws exception and does not catch it locally 92 static void ThrowExceptionWithoutCatch() 93 { 94 // throw exception, but do not catch it 95 try 96 { 97 Console.WriteLine( "In ThrowExceptionWithoutCatch" ); throw new Exception( "Exception in ThrowExceptionWithoutCatch" ); 98 99 } // end try finally 100 { 101 Console.WriteLine( "finally executed in " + 102 "ThrowExceptionWithoutCatch" ); 103 } // end finally 104 105 106 // unreachable code; logic error 107 Console.WriteLine( "End of ThrowExceptionWithoutCatch" ); 108 } // end method ThrowExceptionWithoutCatch 109 110 // throws exception, catches it and rethrows it 111 static void ThrowExceptionCatchRethrow() 112 { 113 // try block throws exception 114 try 115 { 116 Console.WriteLine( "In ThrowExceptionCatchRethrow" ); throw new Exception( "Exception in ThrowExceptionCatchRethrow" ); 117 118 } // end try 119 catch ( Exception exceptionParameter ) 120 { 121 Console.WriteLine( "Message: " + exceptionParameter.Message ); 122 // rethrow exception for further processing 123 throw; 124 125 126 // unreachable code; logic error 127 } // end catch finally 128 { 129 Console.WriteLine( "finally executed in " + 130 "ThrowExceptionCatchRethrow" ); 131 } // end finally 132 133 134 // any code placed here is never reached 135 Console.WriteLine( "End of ThrowExceptionCatchRethrow" ); 136 } // end method ThrowExceptionCatchRethrow 137 } // end class UsingExceptions
Fig. 12.4 | of 4.)
finally
blocks always execute, regardless of whether an exception occurs. (Part 3
448
Chapter 12
Exception Handling
Calling DoesNotThrowException In DoesNotThrowException finally executed in DoesNotThrowException End of DoesNotThrowException Calling ThrowExceptionWithCatch In ThrowExceptionWithCatch Message: Exception in ThrowExceptionWithCatch finally executed in ThrowExceptionWithCatch End of ThrowExceptionWithCatch Calling ThrowExceptionWithoutCatch In ThrowExceptionWithoutCatch finally executed in ThrowExceptionWithoutCatch Caught exception from ThrowExceptionWithoutCatch in Main Calling ThrowExceptionCatchRethrow In ThrowExceptionCatchRethrow Message: Exception in ThrowExceptionCatchRethrow finally executed in ThrowExceptionCatchRethrow Caught exception from ThrowExceptionCatchRethrow in Main
Fig. 12.4 |
finally
blocks always execute, regardless of whether an exception occurs. (Part 4
of 4.)
Line 12 of Main invokes method DoesNotThrowException. The try block for this method outputs a message (line 55). Because the try block does not throw any exceptions, program control ignores the catch block (lines 57–60) and executes the finally block (lines 61–64), which outputs a message. At this point, program control continues with the first statement after the close of the finally block (line 66) which outputs a message indicating that the end of the method has been reached. Then, program control returns to Main.
Throwing Exceptions Using the throw Statement Line 16 of Main invokes method ThrowExceptionWithCatch (lines 70–89), which begins in its try block (lines 73–77) by outputting a message. Next, the try block creates an Exception object and uses a throw statement to throw the exception object (line 76). Executing the throw statement indicates that an exception has occurred. So far you have only caught exceptions thrown by called methods. You can throw exceptions by using the throw statement. Just as with exceptions thrown by the FCL’s methods and the CLR, this indicates to client applications that an error has occurred. A throw statement specifies an object to be thrown. The operand of a throw statement can be of type Exception or of any type derived from class Exception.
Common Programming Error 12.5 It is a compilation error if the argument of a throw—an exception object—is not of class Exception or one of its derived classes. 12.5
The string passed to the constructor becomes the exception object’s error message. When a throw statement in a try block executes, the try block expires immediately, and
12.6 finally Block
449
program control continues with the first matching catch block (lines 78–81) following the try block. In this example, the type thrown (Exception) matches the type specified in the catch, so line 80 outputs a message indicating the exception that occurred. Then, the finally block (lines 82–86) executes and outputs a message. At this point, program control continues with the first statement after the close of the finally block (line 88), which outputs a message indicating that the end of the method has been reached. Program control then returns to Main. In line 80, note that we use the exception object’s Message property to retrieve the error message associated with the exception (i.e., the message passed to the Exception constructor). Section 12.7 discusses several properties of class Exception. Lines 23–31 of Main define a try statement in which Main invokes method ThrowExceptionWithoutCatch (lines 92–108). The try block enables Main to catch any exceptions thrown by ThrowExceptionWithoutCatch. The try block in lines 95–99 of ThrowExceptionWithoutCatch begins by outputting a message. Next, the try block throws an Exception (line 98) and expires immediately. Normally, program control would continue at the first catch following this try block. However, this try block does not have any catch blocks. Therefore, the exception is not caught in method ThrowExceptionWithoutCatch. Program control proceeds to the finally block (lines 100–104), which outputs a message. At this point, program control returns to Main—any statements appearing after the finally block (e.g., line 107) do not execute. In this example, such statements could cause logic errors, because the exception thrown in line 98 is not caught. In Main, the catch block in lines 27–31 catches the exception and displays a message indicating that the exception was caught in Main.
Rethrowing Exceptions Lines 38–46 of Main define a try statement in which Main invokes method ThrowExceptionCatchRethrow (lines 111–136). The try statement enables Main to catch any exceptions thrown by ThrowExceptionCatchRethrow. The try statement in lines 114– 132 of ThrowExceptionCatchRethrow begins by outputting a message. Next, the try block throws an Exception (line 117). The try block expires immediately, and program control continues at the first catch (lines 119–127) following the try block. In this example, the type thrown (Exception) matches the type specified in the catch, so line 121 outputs a message indicating where the exception occurred. Line 124 uses the throw statement to rethrow the exception. This indicates that the catch block performed partial processing of the exception and now is passing the exception back to the calling method (in this case, Main) for further processing. You can also rethrow an exception with a version of the throw statement which takes an operand that is the reference to the exception that was caught. It is important to note, however, that this form of throw statement resets the throw point, so the original throw point’s stack trace information is lost. Section 12.7 demonstrates using a throw statement with an operand from a catch block. In that section, you will see that after an exception is caught, you can create and throw a different type of exception object from the catch block and you can include the original exception as part of the new exception object. Class library designers often do this to customize the exception types thrown from methods in their class libraries or to provide additional debugging information. The exception handling in method ThrowExceptionCatchRethrow does not complete, because the program cannot run code in the catch block placed after the invocation
450
Chapter 12
Exception Handling
of the throw statement in line 124. Therefore, method ThrowExceptionCatchRethrow terminates and returns control to Main. Once again, the finally block (lines 128–132) executes and outputs a message before control returns to Main. When control returns to Main, the catch block in lines 42–46 catches the exception and displays a message indicating that the exception was caught. Then the program terminates.
Returning After a finally Block Note that the next statement to execute after a finally block terminates depends on the exception-handling state. If the try block successfully completes, or if a catch block catches and handles an exception, the program continues its execution with the next statement after the finally block. However, if an exception is not caught, or if a catch block rethrows an exception, program control continues in the next enclosing try block. The enclosing try could be in the calling method or in one of its callers. It also is possible to nest a try statement in a try block; in such a case, the outer try statement’s catch blocks would process any exceptions that were not caught in the inner try statement. If a try block executes and has a corresponding finally block, the finally block executes even if the try block terminates due to a return statement. The return occurs after the execution of the finally block.
Common Programming Error 12.6 Throwing an exception from a finally block can be dangerous. If an uncaught exception is awaiting processing when the finally block executes, and the finally block throws a new exception that is not caught in the finally block, the first exception is lost, and the new exception is passed to the next enclosing try block. 12.6
Error-Prevention Tip 12.4 When placing code that can throw an exception in a finally block, always enclose the code in a try statement that catches the appropriate exception types. This prevents the loss of any uncaught and rethrown exceptions that occur before the finally block executes. 12.4
Software Engineering Observation 12.2 Do not place try blocks around every statement that might throw an exception, because this can make programs difficult to read. It is better to place one try block around a significant portion of code, and follow this try block with catch blocks that handle each of the possible exceptions. Then follow the catch blocks with a single finally block. Separate try blocks should be used when it is important to distinguish between multiple statements that can throw the same exception type. 12.2
using
Recall from earlier in this section that resource-release code should be placed in a finally block to ensure that a resource is released, regardless of whether there were exceptions when the resource was used in the corresponding try block. An alternative notation—the using statement (not to be confused with the using directive for using namespaces)—simplifies writing code in which you obtain a resource, use the resource in a try block and release the resource in a corresponding finally block. For example, a file processing application (Chapter 18) could process a file with a using statement to ensure that the file is closed properly when it is no longer needed. The resource must be an object that imple-
12.7 Exception Properties
451
ments the IDisposable interface and therefore has a Dispose method. The general form of a using statement would be using ( ExampleObject exampleObject = new ExampleObject() ) { exampleObject.SomeMethod(); }
where ExampleObject is a class that implements the IDisposable interface. This code creates an object of type ExampleObject and uses it in a statement, then calls its Dispose method to release any resources used by the object. The using statement implicitly places the code in its body in a try block with a corresponding finally block that calls the object’s Dispose method. For instance, the preceding code is equivalent to { ExampleObject exampleObject = new ExampleObject(); try { exampleObject.SomeMethod(); } finally { if ( exampleObject != null ) ( ( IDisposable ) exampleObject ).Dispose(); } }
Note that the if statement ensures that exampleObject still references an object; otherwise, a NullReferenceException might occur. You can read more about the using statement in the C# Language Specification Section 8.13 (Section 15.13 in the ECMA version).
12.7 Exception Properties As we discussed in Section 12.5, exception types derive from class Exception, which has several properties. These frequently are used to formulate error messages indicating a caught exception. Two important properties are Message and StackTrace. Property Message stores the error message associated with an Exception object. This message can be a default message associated with the exception type or a customized message passed to an Exception object’s constructor when the Exception object is thrown. Property StackTrace contains a string that represents the method-call stack. Recall that the runtime environment at all times keeps a list of open method calls that have been made but have not yet returned. The StackTrace represents the series of methods that have not finished processing at the time the exception occurs.
Error-Prevention Tip 12.5 A stack trace shows the complete method-call stack at the time an exception occurred. This enables the programmer to view the series of method calls that led to the exception. Information in the stack trace includes the names of the methods on the call stack at the time of the exception, the names of the classes in which the methods are defined and the names of the namespaces in which the classes are defined. If the program database (PDB) file that contains the debugging
452
Chapter 12
Exception Handling
information for the method is available, the stack trace also includes line numbers; the first line number indicates the throw point, and subsequent line numbers indicate the locations from which the methods in the stack trace were called. PDB files are created by the IDE to maintain the debugging information for your projects. 12.5
Property InnerException Another property used frequently by class-library programmers is InnerException. Typically, class library programmers “wrap” exception objects caught in their code so that they then can throw new exception types that are specific to their libraries. For example, a programmer implementing an accounting system might have some account-number processing code in which account numbers are input as strings but represented as ints in the code. Recall that a program can convert strings to int values with Convert.ToInt32, which throws a FormatException when it encounters an invalid number format. When an invalid account number format occurs, the accounting system programmer might wish to employ a different error message than the default message supplied by FormatException or might wish to indicate a new exception type, such as InvalidAccountNumberFormatException. In such cases, the programmer would provide code to catch the FormatException, then create an appropriate type of Exception object in the catch block and pass the original exception as one of the constructor arguments. The original exception object becomes the InnerException of the new exception object. When an InvalidAccountNumberFormatException occurs in code that uses the accounting system library, the catch block that catches the exception can obtain a reference to the original exception via property InnerException. Thus the exception indicates both that the user specified an invalid account number and that the problem was an invalid number format. If the InnerException property is null, this indicates that the exception was not caused by another exception. Other Exception Properties Class Exception provides other properties, including HelpLink, Source and TargetSite. Property HelpLink specifies the location of the help file that describes the problem that occurred. This property is null if no such file exists. Property Source specifies the name of the application where the exception occurred. Property TargetSite specifies the method where the exception originated. Demonstrating Exception Properties and Stack Unwinding Our next example (Fig. 12.5) demonstrates properties Message, StackTrace and InnerException, and method ToString, of class Exception. In addition, the example introduces stack unwinding—when an exception is thrown but not caught in a particular scope, the method-call stack is “unwound,” and an attempt is made to catch the exception in the 1 2 3 4 5
// Fig. 12.5: Properties.cs // Stack unwinding and Exception class properties. // Demonstrates using properties Message, StackTrace and InnerException. using System;
class Properties { static void Main() { // call Method1; any Exception generated is caught // in the catch block that follows try { Method1(); } // end try catch ( Exception exceptionParameter ) { // output the string representation of the Exception, then output // properties InnerException, Message and StackTrace Console.WriteLine( "exceptionParameter.ToString: \n{0}\n", exceptionParameter.ToString() ); Console.WriteLine( "exceptionParameter.Message: \n{0}\n", exceptionParameter.Message ); Console.WriteLine( "exceptionParameter.StackTrace: \n{0}\n", exceptionParameter.StackTrace ); Console.WriteLine( "exceptionParameter.InnerException: \n{0}\n", exceptionParameter.InnerException.ToString() ); } // end catch } // end method Main // calls Method2 static void Method1() { Method2(); } // end method Method1 // calls Method3 static void Method2() { Method3(); } // end method Method2 // throws an Exception containing an InnerException static void Method3() { // attempt to convert string to int try { Convert.ToInt32( "Not an integer" ); } // end try catch ( FormatException formatExceptionParameter ) { // wrap FormatException in new Exception throw new Exception( "Exception occurred in Method3", formatExceptionParameter ); } // end catch } // end method Method3 } // end class Properties
Fig. 12.5 |
Exception
properties and stack unwinding. (Part 2 of 3.)
454
Chapter 12
Exception Handling
exceptionParameter.ToString: System.Exception: Exception occurred in Method3 ---> System.FormatException: Input string was not in a correct format. at System.Number.StringToNumber(String str, NumberStyles options, NumberBuffer& number, NumberFormatInfo info, Boolean parseDecimal) at System.Number.ParseInt32(String s, NumberStyles style, NumberFormatInfo info) at System.Convert.ToInt32(String value) at Properties.Method3() in C:\examples\ch12\Fig12_04\Properties\ Properties.cs:line 49 --- End of inner exception stack trace --at Properties.Method3() in C:\examples\ch12\Fig12_04\Properties\ Properties.cs:line 54 at Properties.Method2() in C:\examples\ch12\Fig12_04\Properties\ Properties.cs:line 40 at Properties.Method1() in C:\examples\ch12\Fig12_04\Properties\ Properties.cs:line 34 at Properties.Main() in C:\examples\ch12\Fig12_04\Properties\ Properties.cs:line 14 exceptionParameter.Message: Exception occurred in Method3 exceptionParameter.StackTrace: at Properties.Method3() in C:\examples\ch12\Fig12_04\Properties\ Properties.cs:line 54 at Properties.Method2() in C:\examples\ch12\Fig12_04\Properties\ Properties.cs:line 40 at Properties.Method1() in C:\examples\ch12\Fig12_04\Properties\ Properties.cs:line 34 at Properties.Main() in C:\examples\ch12\Fig12_04\Properties\ Properties.cs:line 14 exceptionParameter.InnerException: System.FormatException: Input string was not in a correct format. at System.Number.StringToNumber(String str, NumberStyles options, NumberBuffer& number, NumberFormatInfo info, Boolean parseDecimal) at System.Number.ParseInt32(String s, NumberStyles style, NumberFormatInfo info) at System.Convert.ToInt32(String value) at Properties.Method3() in C:\examples\ch12\Fig12_04\Properties\ Properties.cs:line 49
Fig. 12.5 |
Exception
properties and stack unwinding. (Part 3 of 3.)
next outer try block. We keep track of the methods on the call stack as we discuss property StackTrace and the stack-unwinding mechanism. To see the proper stack trace, you should execute this program using steps similar to those presented in Section 12.3. Program execution begins with Main, which becomes the first method on the method call stack. Line 14 of the try block in Main invokes Method1 (declared in lines 32–35), which becomes the second method on the stack. If Method1 throws an exception, the catch block in lines 16–28 handles the exception and outputs information about the exception that occurred. Line 34 of Method1 invokes Method2 (lines 38–41), which becomes the third method on the stack. Then line 40 of Method2 invokes Method3 (lines 44–57), which becomes the fourth method on the stack.
12.7 Exception Properties
455
At this point, the method-call stack (from top to bottom) for the program is: Method3 Method2 Method1 Main
The method called most recently (Method3) appears at the top of the stack; the first method called (Main) appears at the bottom. The try statement (lines 47–56) in Method3 invokes method Convert.ToInt32 (line 49), which attempts to convert a string to an int. At this point, Convert.ToInt32 becomes the fifth and final method on the call stack.
Throwing an Exception with an InnerException Because the argument to Convert.ToInt32 is not in int format, line 49 throws a FormatException that is caught in line 51 of Method3. The exception terminates the call to Convert.ToInt32, so the method is removed (or unwound) from the method-call stack. The catch block in Method3 then creates and throws an Exception object. The first argument to the Exception constructor is the custom error message for our example, “Exception occurred in Method3.” The second argument is the InnerException—the FormatException that was caught. The StackTrace for this new exception object reflects the point at which the exception was thrown (lines 54–55). Now Method3 terminates, because the exception thrown in the catch block is not caught in the method body. Thus, control returns to the statement that invoked Method3 in the prior method in the call stack (Method2). This removes, or unwinds, Method3 from the method-call stack. When control returns to line 40 in Method2, the CLR determines that line 40 is not in a try block. Therefore the exception cannot be caught in Method2, and Method2 terminates. This unwinds Method2 from the call stack and returns control to line 28 in Method1. Here again, line 34 is not in a try block, so Method1 cannot catch the exception. The method terminates and is unwound from the call stack, returning control to line 14 in Main, which is located in a try block. The try block in Main expires and the catch block (lines 16–28) catches the exception. The catch block uses method ToString and properties Message, StackTrace and InnerException to create the output. Note that stack unwinding continues until a catch block catches the exception or the program terminates. Displaying Information About the Exception The first block of output (which we reformatted for readability) in Fig. 12.5 contains the exception’s string representation, which is returned from method ToString. The string begins with the name of the exception class followed by the Message property value. The next four items present the stack trace of the InnerException object. The remainder of the block of output shows the StackTrace for the exception thrown in Method3. Note that the StackTrace represents the state of the method-call stack at the throw point of the exception, rather than at the point where the exception eventually is caught. Each StackTrace line that begins with “at” represents a method on the call stack. These lines indicate the method in which the exception occurred, the file in which the method resides and the line number of the throw point in the file. Note that the inner-exception information includes the inner exception stack trace.
456
Chapter 12
Exception Handling
Error-Prevention Tip 12.6 When catching and rethrowing an exception, provide additional debugging information in the rethrown exception. To do so, create an Exception object containing more specific debugging information, then pass the original caught exception to the new exception object’s constructor to initialize the InnerException property. 12.0
The next block of output (two lines) simply displays the Message property’s value (Exception occurred in Method3) of the exception thrown in Method3. The third block of output displays the StackTrace property of the exception thrown in Method3. Note that this StackTrace property contains the stack trace starting from line 54 in Method3, because that is the point at which the Exception object was created and thrown. The stack trace always begins from the exception’s throw point. Finally, the last block of output displays the string representation of the InnerException property, which includes the namespace and class name of the exception object, as well as its Message and StackTrace properties.
12.8 User-Defined Exception Classes In many cases, you can use existing exception classes from the .NET Framework Class Library to indicate exceptions that occur in your programs. However, in some cases, you might wish to create new exception classes specific to the problems that occur in your programs. User-defined exception classes should derive directly or indirectly from class ApplicationException of namespace System.
Good Programming Practice 12.1 Associating each type of malfunction with an appropriately named exception class improves program clarity. 12.1
Software Engineering Observation 12.3 Before creating a user-defined exception class, investigate the existing exceptions in the .NET Framework Class Library to determine whether an appropriate exception type already exists. 12.3
Figures 12.6 and 12.7 demonstrate a user-defined exception class. Class Negative12.6) is a user-defined exception class representing exceptions that occur when a program performs an illegal operation on a negative number, such as attempting to calculate its square root. According to “Best Practices for Handling Exceptions [C#],” user-defined exceptions should extend class ApplicationException, have a class name that ends with “Exception” and define three constructors: a parameterless constructor; a constructor that receives a string argument (the error message); and a constructor that receives a string argument and an Exception argument (the error message and the inner exception object). Defining these three constructors makes your exception class more flexible, allowing other programmers to easily use and extend it. NegativeNumberExceptions most frequently occur during arithmetic operations, so it seems logical to derive class NegativeNumberException from class ArithmeticException. However, class ArithmeticException derives from class SystemException— the category of exceptions thrown by the CLR. Recall that user-defined exception classes should inherit from ApplicationException rather than SystemException. NumberException (Fig.
// Fig. 12.6: NegativeNumberException.cs // NegativeNumberException represents exceptions caused by // illegal operations performed on negative numbers. using System; namespace SquareRootTest { class NegativeNumberException : ApplicationException { // default constructor public NegativeNumberException() : base( "Illegal operation for a negative number" ) { // empty body } // end default constructor // constructor for customizing error message public NegativeNumberException( string messageValue ) : base( messageValue ) { // empty body } // end one-argument constructor // constructor for customizing the exception's error // message and specifying the InnerException object public NegativeNumberException( string messageValue, Exception inner ) : base( messageValue, inner ) { // empty body } // end two-argument constructor } // end class NegativeNumberException } // end namespace SquareRootTest
Fig. 12.6
| ApplicationException
derived class thrown when a program performs an illegal
operation on a negative number.
Class SquareRootForm (Fig. 12.7) demonstrates our user-defined exception class. The application enables the user to input a numeric value, then invokes method SquareRoot (lines 17–25) to calculate the square root of that value. To perform this calculation, SquareRoot invokes class Math’s Sqrt method, which receives a double value as its argument. Normally, if the argument is negative, method Sqrt returns NaN. In this program, we would like to prevent the user from calculating the square root of a negative number. If the numeric value that the user enters is negative, method SquareRoot throws a NegativeNumberException (lines 21–22). Otherwise, SquareRoot invokes class Math’s method Sqrt to compute the square root (line 24). When the user inputs a value and clicks the Square Root button, the program invokes event handler SquareRootButton_Click (lines 28–53). The try statement (lines 33–52) attempts to invoke SquareRoot using the value input by the user. If the user input is not a valid number, a FormatException occurs, and the catch block in lines 40–45 processes the exception. If the user inputs a negative number, method SquareRoot throws a Nega-
458
Chapter 12
tiveNumberException
Exception Handling (lines 21–22); the catch block in lines 46–52 catches and handles
// Fig. 12.7: SquareRootTest.cs // Demonstrating a user-defined exception class. using System; using System.Windows.Forms; namespace SquareRootTest { public partial class SquareRootForm : Form { public SquareRootForm() { InitializeComponent(); } // end constructor
Fig. 12.7
// computes square root of parameter; throws // NegativeNumberException if parameter is negative public double SquareRoot( double value ) { // if negative operand, throw NegativeNumberException if ( value < 0 ) throw new NegativeNumberException( "Square root of negative number not permitted" ); else return Math.Sqrt( value ); // compute square root } // end method SquareRoot // obtain user input, convert to double, calculate square root private void SquareRootButton_Click( object sender, EventArgs e ) { OutputLabel.Text = ""; // clear OutputLabel // catch any NegativeNumberException thrown try { double result = SquareRoot( Convert.ToDouble( InputTextBox.Text ) ); OutputLabel.Text = result.ToString(); } // end try catch ( FormatException formatExceptionParameter ) { MessageBox.Show( formatExceptionParameter.Message, "Invalid Number Format", MessageBoxButtons.OK, MessageBoxIcon.Error ); } // end catch catch ( NegativeNumberException negativeNumberExceptionParameter ) { | SquareRootForm
square root. (Part 1 of 2.)
class throws an exception if an error occurs when calculating the
12.9 Wrap-Up
49 50 51 52 53 54 55
459
MessageBox.Show( negativeNumberExceptionParameter.Message, "Invalid Operation", MessageBoxButtons.OK, MessageBoxIcon.Error ); } // end catch } // end method SquareRootButton_Click } // end class SquareRootForm } // end namespace SquareRootTest (a)
(b)
(c)
(d)
Fig. 12.7
| SquareRootForm
class throws an exception if an error occurs when calculating the
square root. (Part 2 of 2.)
12.9 Wrap-Up In this chapter, you learned how to use exception handling to deal with errors in an application. We demonstrated that exception handling enables you to remove error-handling code from the “main line” of the program’s execution. You saw exception handling in the context of a divide-by-zero example. You learned how to use try blocks to enclose code that may throw an exception, and how to use catch blocks to deal with exceptions that may arise. We discussed the termination model of exception handling, in which after an exception is handled, program control does not return to the throw point. We also discussed several important classes of the .NET Exception hierarchy, including ApplicationException (from which user-defined exception classes are derived) and SystemException. Next you learned how to use the finally block to release resources whether or not an exception occurs, and how to throw and rethrow exceptions with the throw statement. We also discussed how the using statement can be used to automate the process of releasing a resource. You then learned how to obtain information about an exception using Exception properties Message, StackTrace and InnerException, and method ToString. You learned how to create your own exception classes. In the next two chapters, we present an in-depth treatment of graphical user interfaces. In these chapters and throughout the rest of the book, we use exception handling to make our examples more robust, while still demonstrating new features of the language.
13 Graphical User Interface Concepts: Part 1 … the wisest prophets make sure of the event first. —Horace Walpole
OBJECTIVES In this chapter you will learn: I
Design principles of graphical user interfaces (GUIs).
I
How to create graphical user interfaces.
I
How to process events that are generated by user interactions with GUI controls.
I
The namespaces that contain the classes for graphical user interface controls and event handling.
I
How to create and manipulate Button, Label, RadioButton, CheckBox, TextBox, Panel and NumericUpDown controls.
I
How to add descriptive ToolTips to GUI controls.
I
How to process mouse and keyboard events.
...The user should feel in control of the computer; not the other way around. This is achieved in applications that embody three qualities: responsiveness, permissiveness, and consistency. —Apple Computer, Inc. 1985
All the better to see you with my dear. —The Big Bad Wolf to Little Red Riding Hood
Outline
13.1 Introduction
461
13.1 Introduction 13.2 Windows Forms 13.3 Event Handling 13.3.1 A Simple Event-Driven GUI 13.3.2 Another Look at the Visual Studio Generated Code 13.3.3 Delegates and the Event-Handling Mechanism 13.3.4 Other Ways to Create Event Handlers 13.3.5 Locating Event Information 13.4 Control Properties and Layout 13.5 Labels, TextBoxes and Buttons 13.6 GroupBoxes and Panels 13.7 CheckBoxes and RadioButtons 13.8 PictureBoxes 13.9 ToolTips 13.10 NumericUpDown Control 13.11 Mouse-Event Handling 13.12 Keyboard-Event Handling 13.13 Wrap-Up
13.1 Introduction A graphical user interface (GUI) allows a user to interact visually with a program. A GUI (pronounced “GOO-ee”) gives a program a distinctive “look” and “feel.” Providing different applications with a consistent set of intuitive user-interface components enables users to become productive with each application faster.
Look-and-Feel Observation 13.1 Consistent user interfaces enable a user to learn new applications more quickly because the applications have the same “look” and “feel.” 13.1
As an example of a GUI, consider Fig. 13.1, which shows an Internet Explorer Web browser window containing various GUI controls. Near the top of the window, there is a menu bar containing the menus File, Edit, View, Favorites, Tools and Help. Below the menu bar is a set of buttons, each of which has a defined task in Internet Explorer, such as going back to the previously viewed Web page, printing the current page or refreshing the page. Below these buttons lies a combobox, in which users can type the locations of Web sites that they wish to visit. To the left of the combobox is a label (Address) that indicates the combobox’s purpose (in this case, entering the location of a Web site). Scrollbars are located at the right side and bottom of the window. Usually, scrollbars appear when a window contains more information than can be displayed in the window’s viewable area. Scrollbars enable a user to view different portions of the window’s contents. These controls form a user-friendly interface through which the user interacts with the Internet Explorer Web browser.
462
Label
Chapter 13
Button
Graphical User Interface Concepts: Part 1
Menu
Title bar
Menu bar
Combobox
Scrollbars
Fig. 13.1 | GUI controls in an Internet Explorer window. GUIs are built from GUI controls (which are sometimes called components or widgets—short for window gadgets). GUI controls are objects that can display information on the screen or enable users to interact with an application via the mouse, keyboard or some other form of input (such as voice commands). Several common GUI controls are listed in Fig. 13.2—in the sections that follow and in Chapter 14, we discuss each of these in detail. Chapter 14 also explores the features and properties of additional GUI controls. Control
Description
Label
Displays images or uneditable text.
TextBox
Enables the user to enter data via the keyboard. It can also be used to display editable or uneditable text.
Button
Triggers an event when clicked with the mouse.
CheckBox
Specifies an option that can be selected (checked) or unselected (not checked).
ComboBox
Provides a drop-down list of items from which the user can make a selection either by clicking an item in the list or by typing in a box.
Fig. 13.2 | Some basic GUI controls. (Part 1 of 2.)
13.2 Windows Forms
463
Control
Description
ListBox
Provides a list of items from which the user can make a selection by clicking an item in the list. Multiple elements can be selected.
Panel
A container in which controls can be placed and organized.
NumericUpDown
Enables the user to select from a range of input values.
Fig. 13.2 | Some basic GUI controls. (Part 2 of 2.)
13.2 Windows Forms Windows Forms are used to create the GUIs for programs. A Form is a graphical element that appears on your computer’s desktop; it can be a dialog, a window or an MDI window (multiple document interface window)—discussed in Chapter 14, Graphical User Interface Concepts: Part 2. A component is an instance of a class that implements the IComponent interface, which defines the behaviors that components must implement, such as how the component is loaded. A control, such as a Button or Label, has a graphical representation at runtime. Some components lack graphical representations (e.g., class Timer of namespace System.Windows.Forms—see Chapter 14). Such, components are not visible at run time. Figure 13.3 displays the Windows Forms controls and components from the C# Toolbox. The controls and components are organized into categories by functionality. Selecting the category All Windows Forms at the top of the Toolbox allows you to view all the controls and components from the other tabs in one list (as shown in Fig. 13.3). In this chapter and the next, we discuss many of these controls and components. To add a control or component to a Form, select that control component or from the Toolbox and drag it on the Form. To deselect a control or component, select the Pointer item in the Toolbox (the icon at the top of the list). When the Pointer item is selected, you cannot accidentally add a new control to the Form. When there are several windows on the screen, the active window is the frontmost and has a highlighted title bar—typically darker blue than the other windows on the screen. A window becomes the active window when the user clicks somewhere inside it. The active window is said to “have the focus.” For example, in Visual Studio the active window is the Toolbox when you are selecting an item from it, or the Properties window when you are editing a control’s properties. A Form is a container for controls and components. When you drag a control or component from the Toolbox on the Form, Visual Studio generates code that instantiates the object and sets its basic properties. This code is updated when the control or component’s properties are modified in the IDE. If a control or component is removed from the Form, the generated code for that control is deleted. The generated code is placed by the IDE in a separate file using partial classes. Although we could write this code ourselves, it is much easier to create and modify controls and components using the Toolbox and Properties windows and allow Visual Studio to handle the details. We introduced visual programming concepts in Chapter 2. In this chapter and the next, we use visual programming to build more substantial GUIs.
464
Chapter 13
Graphical User Interface Concepts: Part 1
Display all controls and components
Categories that organize controls and components by functionality
Fig. 13.3 | Components and controls for Windows Forms. Each control or component we present in this chapter is located in namespace System.Windows.Forms. To create a Windows application, you generally create a Windows Form, set its properties, add controls to the Form, set their properties and implement
event handlers (methods) that respond to events generated by the controls. Figure 13.4 lists common Form properties, methods and events. properties, methods and events Form
Description
Common Properties AcceptButton
Button
AutoScroll
Boolean
CancelButton
Button
FormBorderStyle
Border style for the Form (e.g., none, single, three-dimensional).
Fig. 13.4
|
that is clicked when Enter is pressed. value that allows or disallows scrollbars when needed.
that is clicked when the Escape key is pressed.
Common Form properties, methods and events. (Part 1 of 2.)
13.3 Event Handling
properties, methods and events
465
Form
Description
Font
Font of text displayed on the Form, and the default font for controls added to the Form.
Text
Text in the Form’s title bar.
Common Methods Close
Closes a Form and releases all resources, such as the memory used for the Form’s controls and components. A closed Form cannot be reopened.
Hide
Hides a Form, but does not destroy the Form or release its resources.
Show
Displays a hidden Form.
Common Event Load
Fig. 13.4
Occurs before a Form is displayed to the user. The handler for this event is displayed in the Visual Studio editor when you double click the Form in the Visual Studio designer. |
Common Form properties, methods and events. (Part 2 of 2.)
When we create controls and event handlers, Visual Studio generates much of the GUI-related code. In visual programming, the IDE maintains GUI-related code and you write the bodies of the event handlers to indicate what actions the program should take when particular events occur.
13.3 Event Handling Normally, a user interacts with an application’s GUI to indicate the tasks that the application should perform. For example, when you write an e-mail in an e-mail application, clicking the Send button tells the application to send the e-mail to the specified e-mail addresses. GUIs are event driven. When the user interacts with a GUI component, the interaction—known as an event—drives the program to perform a task. Common events (user interactions) that might cause an application to perform a task include clicking a Button, typing in a TextBox, selecting an item from a menu, closing a window and moving the mouse. A method that performs a task in response to an event is called an event handler, and the overall process of responding to events is known as event handling.
13.3.1 A Simple Event-Driven GUI The Form in the application of Fig. 13.5 contains a Button that a user can click to display a MessageBox. You have already created several GUI examples that execute an event handler in response to clicking a Button. In this example, we discuss Visual Studio’s auto-generated code in more depth. Using the techniques presented earlier in the book, create a Form containing a Button. First, create a new Windows application and add a Button to the Form. In the Properties
// Fig. 13.5: SimpleEventExampleForm.cs // Using Visual Studio to create event handlers. using System; using System.Windows.Forms; // Form that shows a simple event handler public partial class SimpleEventExampleForm : Form { // default constructor public SimpleEventExampleForm() { InitializeComponent(); } // end constructor // handles click event of Button clickButton private void clickButton_Click( object sender, EventArgs e ) { MessageBox.Show( "Button was clicked." ); } // end method clickButton_Click } // end class SimpleEventExampleForm
Fig. 13.5 | Simple event-handling example using visual programming. window for the Button, set the (Name) property to clickButton and the Text property to Click Me. You’ll notice that we use a convention in which each variable name we create for a control ends with the control’s type. For example, in the variable name clickButton, “Button” is the control’s type. When the user clicks the Button in this example, we’ want the application to respond by displaying a MessageBox. To do this, you must create an event handler for the Button’s Click event. You can create this event handler by double clicking the Button on the Form, which declares the following empty event handler in the program code: private void clickButton_Click( object sender, EventArgs e ) { } // end method clickButton_Click
By convention, C# names the event-handler method as controlName_eventName (e.g., clickButton_Click). The clickButton_Click event handler executes when the user clicks the clickButton control. Each event handler receives two parameters when it is called. The first—an object reference named sender—is a reference to the object that generated the event. The second is a reference to an event arguments object of type EventArgs (or one of its derived classes), which is typically named e. This object contains additional information about the event that occurred. EventArgs is the base class of all classes that represent event information.
13.3 Event Handling
467
Software Engineering Observation 13.1 You should not expect return values from event handlers—event handlers are designed to execute code based on an action and return control to the main program. 13.1
Good Programming Practice 13.1 Use the event-handler naming convention controlName_eventName, so method names are meaningful. Such names tell users what event a method handles for what control. This convention is not required, but it makes your code easier to read, understand, modify and maintain. 13.1
To display a MessageBox in response to the event, insert the statement MessageBox.Show( "Button was clicked." );
in the event handler’s body. The resulting event handler appears in lines 16–19 of Fig. 13.5. When you execute the application and click the Button, a MessageBox appears displaying the text "Button was clicked".
13.3.2 Another Look at the Visual Studio Generated Code Visual Studio generates the code that creates and initializes the GUI that you build in the GUI design window. This auto-generated code is placed in the Designer.cs file of the Form (SimpleEventExampleForm.Designer.cs in this example). You can open this file by expanding the node for the file you are currently working in (SimpleEventExampleForm.cs) and double clicking the file name that ends with Designer.cs. Figs. 13.6–13.7 show this file’s contents. The IDE collapses the code in lines 21–53 of Fig. 13.7 by default. Now that you have studied classes and objects in detail, this code will be easier to understand. Since this code is created and maintained by Visual Studio, you generally don’t need to look at it. In fact, you do not need to understand most of the code shown here to build GUI applications. However, we now take a closer look to help you understand how GUI applications work.
Fig. 13.6 | First half of the Visual Studio generated code file.
468
Chapter 13
Graphical User Interface Concepts: Part 1
Fig. 13.7 | Second half of the Visual Studio generated code file. The auto-generated code that defines the GUI is actually part of the Form’s class—in this case, SimpleEventExampleForm. Line 1 of Fig. 13.6 uses the partial modifier, which allows this class to be split among multiple files. Line 55 contains the declaration of the Button control clickButton that we created in Design mode. Note that the control is declared as an instance variable of class SimpleEventExampleForm. By default, all variable declarations for controls created through C#’s design window have a private access modifier. The code also includes the Dispose method for releasing resources (lines 12–19) and method InitializeComponent (lines 27–51), which contains the code that creates the Button, then sets some of the Button’s and the Form’s properties. The property values correspond to the values set in the Properties window for each control. Note that Visual Studio adds comments to the code that it generates, as in lines 31–33. Line 39 was generated when we created the event handler for the Button’s Click event. Method InitializeComponent is called when the Form is created, and establishes such properties as the Form title, the Form size, control sizes and text. Visual Studio also uses the code in this method to create the GUI you see in design view. Changing the code in InitializeComponent may prevent Visual Studio from displaying the GUI properly.
Error-Prevention Tip 13.1 The code generated by building a GUI in Design mode is not meant to be modified directly, and doing so can result in an application that functions incorrectly. You should modify control properties through the Properties window. 13.1
13.3 Event Handling
469
13.3.3 Delegates and the Event-Handling Mechanism The control that generates an event is known as the event sender. An event-handling method—known as the event receiver—responds to a particular event that a control generates. When the event occurs, the event sender calls its event receiver to perform a task (i.e., to “handle the event”). The .NET event-handling mechanism allows you to choose your own names for event-handling methods. However, each event-handling method must declare the proper parameters to receive information about the event that it handles. Since you can choose your own method names, an event sender such as a Button cannot know in advance which method will respond to its events. So, we need a mechanism to indicate which method is the event receiver for an event.
Delegates Event handlers are connected to a control’s events via special objects called delegates. A delegate object holds a reference to a method with a signature that is specified by the delegate type’s declaration. GUI controls have predefined delegates that correspond to every event they can generate. For example, the delegate for a Button’s Click event is of type EventHandler (namespace System). If you look at this type in the online help documentation, you will see that it is declared as follows: public delegate void EventHandler( object sender, EventArgs e );
This uses the delegate keyword to declare a delegate type named EventHandler, which can hold references to methods that return void and receive two parameters—one of type object (the event sender) and one of type EventArgs. If you compare the delegate declaration with clickButton_Click’s header (Fig. 13.5, line 16), you will see that this event handler indeed meets the requirements of the EventHandler delegate. Note that the preceding declaration actually creates an entire class for you. The details of this special class’s declaration are handled by the compiler.
Indicating the Method that a Delegate Should Call An event sender calls a delegate object like a method. Since each event handler is declared as a delegate, the event sender can simply call the appropriate delegate when an event occurs—a Button calls its EventHandler delegate in response to a click. The delegate’s job is to invoke the appropriate method. To enable the clickButton_Click method to be called, Visual Studio assigns clickButton_Click to the delegate, as shown in line 39 of Fig. 13.7. This code is added by Visual Studio when you double click the Button control in Design mode. The expression new System.EventHandler(this.clickButton_Click);
creates an EventHandler delegate object and initializes it with the clickButton_Click method. Line 39 uses the += operator to add the delegate to the Button’s Click event. This indicates that clickButton_Click will respond when a user clicks the Button. Note that the += operator is overloaded by the delegate class that is created by the compiler. You can actually specify that several different methods should be invoked in response to an event by adding other delegates to the Button’s Click event with statements similar to line 39 of Fig. 13.7. Event delegates are multicast—they represent a set of delegate objects that all have the same signature. Multicast delegates enable several methods to be
470
Chapter 13
Graphical User Interface Concepts: Part 1
called in response to a single event. When an event occurs, the event sender calls every method referenced by the multicast delegate. This is known as event multicasting. Event delegates derive from class MulticastDelegate, which derives from class Delegate (both from namespace System).
13.3.4 Other Ways to Create Event Handlers In all the GUI applications you have created so far, you double clicked a control on the Form to create an event handler for that control. This technique creates an event handler for a control’s default event—the event that is most frequently used with that control. Typically, controls can generate many different types of events, and each type can have its own event handler. For instance, you already created Click event handlers for Buttons by double clicking a Button in design view (Click is the default event for a Button). However your application can also provide an event handler for a Button’s MouseHover event, which occurs when the mouse pointer remains positioned over the Button. We now discuss how to create an event handler for an event that is not a control’s default event.
Using the Properties Window to Create Event Handlers You can create additional event handlers through the Properties window. If you select a control on the Form, then click the Events icon (the lightning bolt icon in Fig. 13.8) in the Properties window, all the events for that control are listed in the window. You can double click an event’s name to display the event handler in the editor, if the event handler already exists, or to create the event handler. You can also select an event, then use the drop-down list to its right to choose an existing method that should be used as the event handler for that event. The methods that appear in this drop-down list are the class’s methods that have the proper signature to be an event handler for the selected event. You can return to viewing the properties of a control by selecting the Properties icon (Fig. 13.8). Properties icon
Events icon
Selected event
Fig. 13.8 | Viewing events for a Button control in the Properties window.
13.3 Event Handling
471
13.3.5 Locating Event Information Read the Visual Studio documentation to learn about the different events raised by a control. To do this, select Help > Index. In the window that appears, select .NET Framework in the Filtered by drop-down list and enter the name of the control’s class in the Index window. To ensure that you are selecting the proper class, enter the fully qualified class name as shown in Fig. 13.9 for class System.Windows.Forms.Button. Once you select a control’s class in the documentation, a list of all the class’s members is displayed. This list includes the events that the class can generate. In Fig. 13.9, we scrolled to class Button’s events. Click the name of an event to view its description and examples of its use (Fig. 13.10). Notice that the Click event is listed as a member of class Control, because class Button’s Click event is inherited from class Control. Class name
List of events
Fig. 13.9 | List of Button events.
Event name Event type Event argument class
Fig. 13.10 |
Click
event details.
472
Chapter 13
Graphical User Interface Concepts: Part 1
13.4 Control Properties and Layout This section overviews properties that are common to many controls. Controls derive from class Control (namespace System.Windows.Forms). Figure 13.11 lists some of class Control’s properties and methods. The properties shown here can be set for many controls. For example, the Text property specifies the text that appears on a control. The location of this text varies depending on the control. In a Windows Form, the text appears in the title bar, but the text of a Button appears on its face. The Focus method transfers the focus to a control and makes it the active control. When you press the Tab key in an executing Windows application, controls receive the focus in the order specified by their TabIndex property. This property is set by Visual Studio based on the order in which controls are added to a Form, but you can change the tabbing order. TabIndex is helpful for users who enter information in many controls, such Class Control properties and methods
Description
Common Properties BackColor
The control’s background color.
BackgroundImage
The control’s background image.
Enabled
Specifies whether the control is enabled (i.e., if the user can interact with it). Typically, portions of a disabled control appear “grayed out” as a visual indication to the user that the control is disabled.
Focused
Indicates whether the control has the focus.
Font
The Font used to display the control’s text.
ForeColor
The control’s foreground color. This usually determines the color of the text in the Text property.
TabIndex
The tab order of the control. When the Tab key is pressed, the focus transfers between controls based on the tab order. You can set this order.
TabStop
If true, then a user can give focus to this control via the Tab key.
Text
The text associated with the control. The location and appearance of the text vary depending on the type of control.
Visible
Indicates whether the control is visible.
Common Methods Focus
Acquires the focus.
Hide
Hides the control (sets the Visible property to false).
Show
Shows the control (sets the Visible property to true).
Fig. 13.11 | Class Control properties and methods.
13.4 Control Properties and Layout
473
as a set of TextBoxes that represent a user’s name, address and telephone number. The user can enter information, then quickly select the next control by pressing the Tab key. The Enabled property indicates whether the user can interact with a control to generate an event. Often, if a control is disabled, it is because an option is unavailable to the user at that time. For example, text editor applications often disable the “paste” command until the user copies some text. In most cases, a disabled control’s text appears in gray (rather than in black). You can also hide a control from the user without disabling the control by setting the Visible property to false or by calling method Hide. In each case, the control still exists but is not visible on the Form. You can use anchoring and docking to specify the layout of controls inside a container (such as a Form). Anchoring causes controls to remain at a fixed distance from the sides of the container even when the container is resized. Anchoring enhances the user experience. For example, if the user expects a control to appear in a particular corner of the application, anchoring ensures that the control will always be in that corner—even if the user resizes the Form. Docking attaches a control to a container such that the control stretches across an entire side. For example, a button docked to the top of a container stretches across the entire top of that container, regardless of the width of the container. When parent containers are resized, anchored controls are moved (and possibly resized) so that the distance from the sides to which they are anchored does not vary. By default, most controls are anchored to the top-left corner of the Form. To see the effects of anchoring a control, create a simple Windows application that contains two Buttons. Anchor one control to the right and bottom sides by setting the Anchor property as shown in Fig. 13.12. Leave the other control unanchored. Execute the application and enlarge the Form. Notice that the Button anchored to the bottom-right corner is always the same distance from the Form’s bottom-right corner (Fig. 13.13), but that the other control stays its original distance from the top-left corner of the Form. Sometimes, it is desirable for a control to span an entire side of the Form, even when the Form is resized. For example, a control such as a status bar typically should remain at the bottom of the Form. Docking allows a control to span an entire side (left, right, top or bottom) of its parent container or to fill the entire container. When the parent control is resized, the docked control resizes as well. In Fig. 13.14, a Button is docked at the top of the Form (spanning the top portion). When the Form is resized, the Button is resized to the Anchoring window Click down-arrow in Anchor property to display anchoring window
Darkened bars indicate the container’s side(s) to which the control is anchored; use mouse clicks to select or deselect a bar
Fig. 13.12 | Manipulating the Anchor property of a control.
474
Chapter 13
Graphical User Interface Concepts: Part 1
Before resizing
After resizing
Constant distance to right and bottom sides
Fig. 13.13 | Anchoring demonstration. Before resizing
After resizing
Control extends along entire top portion of form
Fig. 13.14 | Docking a Button to the top of a Form. Form’s
new width. Forms have a Padding property that specifies the distance between the docked controls and the Form edges. This property specifies four values (one for each side), and each value is set to 0 by default. Some common control layout properties are summarized in Fig. 13.15. Control layout
properties
Description
Anchor
Causes a control to remain at a fixed distance from the side(s) of the container even when the container is resized.
Dock
Allows a control to span one side of its container or to fill the entire container.
Padding
Sets the space between a container’s edges and docked controls. The default is 0, causing the control to appear flush with the container’s sides.
Location
Specifies the location (as a set of coordinates) of the upper-left corner of the control, in relation to its container.
Fig. 13.15
| Control
layout properties. (Part 1 of 2.)
13.4 Control Properties and Layout
475
Control layout
properties
Description
Size
Specifies the size of the control in pixels as a Size object, which has properties Width and Height.
MinimumSize, MaximumSize
Indicate the minimum and maximum size of a Control, respectively.
Fig. 13.15
| Control
layout properties. (Part 2 of 2.)
The Anchor and Dock properties of a Control are set with respect to the Control’s parent container, which could be a Form or another parent container (such as a Panel; discussed in Section 13.6). The minimum and maximum Form (or other Control) sizes can be set via properties MinimumSize and MaximumSize, respectively. Both are of type Size, which has properties Width and Height to specify the size of the Form. Properties MinimumSize and MaximumSize allow you to design the GUI layout for a given size range. The user cannot make a Form smaller than the size specified by property MinimumSize and cannot make a Form larger than the size specified by property MaximumSize. To set a Form to a fixed size (where the Form cannot be resized by the user), set its minimum and maximum size to the same value or set its FormBorderStyle property to FixedSingle.
Look-and-Feel Observation 13.2 For resizable Forms, ensure that the GUI layout appears consistent across various Form sizes.
13.2
Using Visual Studio To Edit a GUI’s Layout Visual Studio provides tools that help you with GUI layout. You may have noticed when dragging a control across a Form, that blue lines (known as snap lines) appear to help you position the control with respect to other controls (Fig. 13.16) and the Form’s edges. This new feature of Visual Studio 2005 makes the control you are dragging appear to “snap into
Snap line to help align controls on their left sides
Fig. 13.16 | Snap lines in Visual Studio 2005.
Snap line that indicates when a control reaches the minimum recommended distance from the edge of a Form.
476
Chapter 13
Graphical User Interface Concepts: Part 1
place” alongside other controls. Visual Studio also provides the Format menu, which contains several options for modifying your GUI’s layout. The Format menu does not appear in the IDE unless you select a control (or set of controls) in design view. When you select multiple controls, you can use the Format menu’s Align submenu to align the controls. The Format menu also enables you to modify the amount of space between controls or to center a control on the Form.
13.5 Labels, TextBoxes and Buttons Labels provide text information (as well as optional images) and are defined with class Label (a derived class of Control). A Label displays text that the user cannot directly modify. A Label’s text can be changed programmatically by modifying the Label’s Text property. Figure 13.17 lists common Label properties. A textbox (class TextBox) is an area in which either text can be displayed by a program or the user can type text via the keyboard. A password TextBox is a TextBox that hides the information entered by the user. As the user types characters, the password TextBox masks the user input by displaying a character you specify (usually *). If you set the PasswordChar property, the TextBox becomes a password TextBox. Users often encounter both types of TextBoxes, when logging into a computer or Web site—the username TextBox allows users to input their usernames; the password TextBox allows users to enter their passwords. Figure 13.18 lists the common properties and a common event of TextBoxes.
Common Label properties
Description
Font
The font of the text on the Label.
Text
The text on the Label.
TextAlign
The alignment of the Label’s text on the control—horizontally (left, center or right) and vertically (top, middle or bottom).
Fig. 13.17 | Common Label properties. TextBox properties
and event
Description
Common Properties AcceptsReturn
If true in a multiline TextBox, pressing Enter in the TextBox creates a new line. If false, pressing Enter is the same as pressing the default Button on the Form. The default Button is the one assigned to a Form’s AcceptButton property.
Multiline
If true, the TextBox can span multiple lines. The default value is false.
Fig. 13.18
| TextBox
properties and event. (Part 1 of 2.)
13.5 Labels, TextBoxes and Buttons
477
TextBox properties
and event
Description
PasswordChar
When this property is set to a character, the TextBox becomes a password box, and the specified character masks each character the user type. If no character is specified, the TextBox displays the typed text.
ReadOnly
If true, the TextBox has a gray background, and its text cannot be edited. The default value is false.
ScrollBars
For multiline textboxes, this property indicates which scrollbars appear (None, Horizontal, Vertical or Both).
Text
The TextBox’s text content.
Common Event TextChanged
Fig. 13.18
Generated when the text changes in a TextBox (i.e., when the user adds or deletes characters). When you double click the TextBox control in Design mode, an empty event handler for this event is generated.
| TextBox
properties and event. (Part 2 of 2.)
A button is a control that the user clicks to trigger a specific action or to select an option in a program. As you will see, a program can use several types of buttons, such as checkboxes and radio buttons. All the button classes derive from class ButtonBase (namespace System.Windows.Forms), which defines common button features. In this section, we discuss class Button, which typically enables a user to issue a command to an application. Figure 13.19 lists common properties and a common event of class Button. Button properties
and event
Description
Common Properties Text
Specifies the text displayed on the Button face.
FlatStyle
Modifies a Button’s appearance—attribute Flat (for the Button to display without a three-dimensional appearance), Popup (for the Button to appear flat until the user moves the mouse pointer over the Button), Standard (three-dimensional) and System, where the Button’s appearance is controlled by the operating system. The default value is Standard.
Common Event Click
Fig. 13.19
Generated when the user clicks the Button. When you double click a Button in design view, an empty event handler for this event is created. | Button
properties and event.
478
Chapter 13
Graphical User Interface Concepts: Part 1
Look-and-Feel Observation 13.3 Although Labels, TextBoxes and other controls can respond to mouse clicks, Buttons are more natural for this purpose. 13.3
Figure 13.20 uses a TextBox, a Button and a Label. The user enters text into a password box and clicks the Button, causing the text input to be displayed in the Label. Normally, we would not display this text—the purpose of password TextBoxes is to hide the text being entered by the user. When the user clicks the Show Me Button, this application retrieves the text that the user typed in the password TextBox and displays it in another TextBox. First, create the GUI by dragging the controls (a TextBox, a Button and a Label) on the Form. Once the controls are positioned, change their names in the Properties window from the default values—textBox1, button1 and label1—to the more descriptive displayPasswordLabel, displayPasswordButton and inputPasswordTextBox. The (Name) property in the Properties window enables us to change the variable name for a control. Visual Studio creates the necessary code and places it in method InitializeComponent of the partial class in the file LabelTextBoxButtonTestForm.Designer.cs. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
// Fig. 13.20: LabelTextBoxButtonTestForm.cs // Using a TextBox, Label and Button to display // the hidden text in a password TextBox. using System; using System.Windows.Forms; // Form that creates a password TextBox and // a Label to display TextBox contents public partial class LabelTextBoxButtonTestForm : Form { // default constructor public LabelTextBoxButtonTestForm() { InitializeComponent(); } // end constructor // display user input in Label private void displayPasswordButton_Click( object sender, EventArgs e ) { // display the text that the user typed displayPasswordLabel.Text = inputPasswordTextBox.Text; } // end method displayPasswordButton_Click } // end class LabelTextBoxButtonTestForm
Fig. 13.20 | Program to display hidden text in a password box.
13.6 GroupBoxes and Panels
479
We then set displayPasswordButton’s Text property to “Show Me” and clear the Text of displayPasswordLabel and inputPasswordTextBox so that they are blank when the program begins executing. The BorderStyle property of displayPasswordLabel is set to Fixed3D, giving our Label a three-dimensional appearance. The BorderStyle property of all TextBoxes is set to Fixed3D by default. The password character for inputPasswordTextBox is set by assigning the asterisk character (*) to the PasswordChar property. This property accepts only one character. We create an event handler for displayPasswordButton by double clicking this control in Design mode. We add line 22 to the event handler’s body. When the user clicks the Show Me Button in the executing application, line 22 obtains the text entered by the user in inputPasswordTextBox and displays the text in displayPasswordLabel.
13.6 GroupBoxes and Panels GroupBoxes
and Panels arrange controls on a GUI. GroupBoxes and Panels are typically used to group several controls of similar functionality or several controls that are related in a GUI. All of the controls in a GroupBox or Panel move together when the GroupBox or Panel is moved. The primary difference between these two controls is that GroupBoxes can display a caption (i.e., text) and do not include scrollbars, whereas Panels can include scrollbars and do not include a caption. GroupBoxes have thin borders by default; Panels can be set so that they also have borders by changing their BorderStyle property. Figures 13.21 and 13.22 list the common properties of GroupBoxes and Panels, respectively.
Look-and-Feel Observation 13.4 Panels
GroupBox
and GroupBoxes can contain other Panels and GroupBoxes for more complex layouts.
properties
Description
Controls
The set of controls that the GroupBox contains.
Text
Specifies the caption text displayed at the top of the GroupBox.
Fig. 13.21 Panel
| GroupBox
properties
properties. Description
AutoScroll
Indicates whether scrollbars appear when the Panel is too small to display all of its controls. The default value is false.
BorderStyle
Sets the border of the Panel. The default value is None; other options are Fixed3D and FixedSingle.
Controls
The set of controls that the Panel contains.
Fig. 13.22
| Panel
properties.
13.4
480
Chapter 13
Graphical User Interface Concepts: Part 1
Look-and-Feel Observation 13.5 You can organize a GUI by anchoring and docking controls inside a GroupBox or Panel. The GroupBox or Panel then can be anchored or docked inside a Form. This divides controls into functional “groups” that can be arranged easily. 13.5
To create a GroupBox, drag its icon from the Toolbox onto a Form. Then, drag new controls from the Toolbox into the GroupBox. These controls are added to the GroupBox’s Controls property and become part of the GroupBox. The GroupBox’s Text property specifies the caption. To create a Panel, drag its icon from the Toolbox onto the Form. You can then add controls directly to the Panel by dragging them from the Toolbox onto the Panel. To enable the scrollbars, set the Panel’s AutoScroll property to true. If the Panel is resized and cannot display all of its controls, scrollbars appear (Fig. 13.23). The scrollbars can be used to view all the controls in the Panel—both at design time and at execution time. In Fig. 13.23, we set the Panel’s BorderStyle property to FixedSingle so that you can see the Panel in the Form.
Look-and-Feel Observation 13.6 Use Panels with scrollbars to avoid cluttering a GUI and to reduce the GUI’s size.
13.6
The program in Fig. 13.24 uses a GroupBox and a Panel to arrange Buttons. When these Buttons are clicked, their event handlers change the text on a Label. The GroupBox (named mainGroupBox) has two Buttons—hiButton (which displays the text Hi) and byeButton (which displays the text Bye). The Panel (named mainPanel) also has two Buttons, leftButton (which displays the text Far Left) and rightButton (which displays the text Far Right). The mainPanel has its AutoScroll property set to true, allowing scrollbars to appear when the contents of the Panel require more space
// Fig. 13.24: GroupboxPanelExampleForm.cs // Using GroupBoxes and Panels to hold Buttons. using System; using System.Windows.Forms; // Form that displays a GroupBox and a Panel public partial class GroupBoxPanelExampleForm : Form { // default constructor public GroupBoxPanelExampleForm() { InitializeComponent(); } // end constructor // event handler for Hi Button private void hiButton_Click( object sender, EventArgs e ) { messageLabel.Text = "Hi pressed"; // change text in Label } // end method hiButton_Click // event handler for Bye Button private void byeButton_Click( object sender, EventArgs e ) { messageLabel.Text = "Bye pressed"; // change text in Label } // end method byeButton_Click // event handler for Far Left Button private void leftButton_Click( object sender, EventArgs e ) { messageLabel.Text = "Far left pressed"; // change text in Label } // end method leftButton_Click // event handler for Far Right Button private void rightButton_Click( object sender, EventArgs e ) { messageLabel.Text = "Far right pressed"; // change text in Label } // end method rightButton_Click } // end class GroupBoxPanelExampleForm
Fig. 13.24 | Using GroupBoxes and Panels to arrange Buttons.
482
Chapter 13
Graphical User Interface Concepts: Part 1
than the Panel’s visible area. The Label (named messageLabel) is initially blank. To add controls to mainGroupBox or mainPanel, Visual Studio calls method Add of each container’s Controls property. This code is placed in the partial class located in the file GroupBoxPanelExampleForm.Designer.cs. The event handlers for the four Buttons are located in lines 16–37. We added a line in each event handler (lines 18, 24, 30 and 36) to change the text of messageLabel to indicate which Button the user pressed.
13.7 CheckBoxes and RadioButtons C# has two types of state buttons that can be in the on/off or true/false states—CheckBoxes and RadioButtons. Like class Button, classes CheckBox and RadioButton are derived from class ButtonBase. CheckBoxes
A CheckBox is a small square that either is blank or contains a check mark. When the user clicks a CheckBox to select it, a check mark appears in the box. If the user clicks CheckBox again to deselect it, the check mark is removed. Any number of CheckBoxes can be selected at a time. A list of common CheckBox properties and events appears in Fig. 13.25.
properties and events CheckBox
Description
Common Properties Checked
Indicates whether the CheckBox is checked (contains a check mark) or unchecked (blank). This property returns a Boolean value.
CheckState
Indicates whether the CheckBox is checked or unchecked with a value from the CheckState enumeration (Checked, Unchecked or Indeterminate). Indeterminate is used when it is unclear whether the state should be Checked or Unchecked. For example, in Microsoft Word, when you select a paragraph that contains several character formats, then go to Format > Font, some of the CheckBoxes appear in the Indeterminate state. When CheckState is set to Indeterminate, the CheckBox is usually shaded.
Text
Specifies the text displayed to the right of the CheckBox.
Common Events CheckedChanged
Generated when the Checked property changes. This is a CheckBox’s default event. When a user double clicks the CheckBox control in design view, an empty event handler for this event is generated.
CheckStateChanged
Generated when the CheckState property changes.
Fig. 13.25
| CheckBox
properties and events.
13.7 CheckBoxes and RadioButtons
483
The program in Fig. 13.26 allows the user to select CheckBoxes to change a Label’s font style. The event handler for one CheckBox applies bold and the event handler for the other applies italic. If both CheckBoxes are selected, the font style is set to bold and italic. Initially, neither CheckBox is checked. The boldCheckBox has its Text property set to Bold. The italicCheckBox has its Text property set to Italic. The Text property of outputLabel is set to Watch the font style change. After creating the controls, we define their event handlers. Double clicking the CheckBoxes at design time creates empty CheckedChanged event handlers. To change the font style on a Label, you must set its Font property to a new Font object (lines 21–23 and 31–33). The Font constructor that we use here takes the font name, size and style as arguments. The first two arguments—outputLabel.Font.Name and outputLabel.Font.Size—use outputLabel’s original font name and size. The style is specified with a member of the FontStyle enumeration, which contains Regular, Bold, Italic, Strikeout and Underline. (The Strikeout style displays text with a line through 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
// Fig. 13.26: CheckBoxTestForm.cs // Using CheckBoxes to toggle italic and bold styles. using System; using System.Drawing; using System.Windows.Forms; // Form contains CheckBoxes to allow the user to modify sample text public partial class CheckBoxTestForm : Form { // default constructor public CheckBoxTestForm() { InitializeComponent(); } // end constructor // toggle the font style between bold and // not bold based on the current setting private void boldCheckBox_CheckedChanged( object sender, EventArgs e ) { outputLabel.Font = new Font( outputLabel.Font.Name, outputLabel.Font.Size, outputLabel.Font.Style ^ FontStyle.Bold ); } // end metod boldCheckBox_CheckedChanged // toggle the font style between italic and // not italic based on the current setting private void italicCheckBox_CheckedChanged( object sender, EventArgs e ) { outputLabel.Font = new Font( outputLabel.Font.Name, outputLabel.Font.Size, outputLabel.Font.Style ^ FontStyle.Italic ); } // end method italicCheckBox_CheckedChanged } // end class CheckBoxTestForm
Fig. 13.26 | Using CheckBoxes to change font styles. (Part 1 of 2.)
484
Chapter 13
Graphical User Interface Concepts: Part 1
Fig. 13.26 | Using CheckBoxes to change font styles. (Part 2 of 2.) it.) A Font object’s Style property is read-only, so it can be set only when the Font object is created. Styles can be combined via bitwise operators—operators that perform manipulation on bits of information. Recall from Chapter 1 that all data is represented in the computer as combinations of 0s and 1s. Each 0 or 1 represents a bit. FontStyle has a System.FlagAttribute, meaning that the FontStyle bit values are selected in a way that allows us to combine different FontStyle elements to create compound styles, using bitwise operators. These styles are not mutually exclusive, so we can combine different styles and remove them without affecting the combination of previous FontStyle elements. We can combine these various font styles, using either the logical OR (|) operator or the logical exclusive OR (^) operator. When the logical OR operator is applied to two bits, if at least one bit of the two has the value 1, then the result is 1. Combining styles using the conditional OR operator works as follows. Assume that FontStyle.Bold is represented by bits 01 and that FontStyle.Italic is represented by bits 10. When we use the conditional OR (||) to combine the styles, we obtain the bits 11. 01 10 -11
= =
Bold Italic
=
Bold and Italic
The conditional OR operator helps create style combinations. However, what happens if we want to undo a style combination, as we did in Fig. 13.26? The logical exclusive OR operator enables us to combine styles and to undo existing style settings. When logical exclusive OR is applied to two bits, if both bits have the same value, then the result is 0. If both bits are different, then the result is 1. Combining styles using logical exclusive OR works as follows. Assume, again, that FontStyle.Bold is represented by bits 01 and that FontStyle.Italic is represented by bits 10. When we use logical exclusive OR (^) on both styles, we obtain the bits 11. 01 10 -11
= =
Bold Italic
=
Bold and Italic
13.7 CheckBoxes and RadioButtons
485
Now, suppose that we would like to remove the FontStyle.Bold style from the previous combination of FontStyle.Bold and FontStyle.Italic. The easiest way to do so is to reapply the logical exclusive OR (^) operator to the compound style and FontStyle.Bold. 11 01 -10
= =
Bold and Italic Bold
=
Italic
This is a simple example. The advantages of using bitwise operators to combine FontStyle values become more evident when we consider that there are five different FontStyle values (Bold, Italic, Regular, Strikeout and Underline), resulting in 16 different FontStyle combinations. Using bitwise operators to combine font styles greatly reduces the amount of code required to check all possible font combinations. In Fig. 13.26, we need to set the FontStyle so that the text appears in bold if it was not bold originally, and vice versa. Notice that line 23 uses the bitwise logical exclusive OR operator to do this. If outputLabel.Font.Style is bold, then the resulting style is not bold. If the text is originally italic, the resulting style is bold and italic, rather than just bold. The same applies for FontStyle.Italic in line 33. If we did not use bitwise operators to compound FontStyle elements, we would have to test for the current style and change it accordingly. For example, in event handler boldCheckBox_CheckChanged, we could test for the regular style and make it bold; test for the bold style and make it regular; test for the italic style and make it bold italic; and test for the italic bold style and make it italic. This is cumbersome because, for every new style we add, we double the number of combinations. Adding a CheckBox for underline would require testing eight additional styles. Adding a CheckBox for strikeout would require testing 16 additional styles. RadioButtons
Radio buttons (defined with class RadioButton) are similar to CheckBoxes in that they also have two states—selected and not selected (also called deselected). However, RadioButtons normally appear as a group, in which only one RadioButton can be selected at a time. Selecting one RadioButton in the group forces all the others to be deselected. Therefore, RadioButtons are used to represent a set of mutually exclusive options (i.e., a set in which multiple options cannot be selected at the same time).
Look-and-Feel Observation 13.7 Use RadioButtons when the user should choose only one option in a group.
13.7
Look-and-Feel Observation 13.8 Use CheckBoxes when the user should be able to choose multiple options in a group.
13.8
All RadioButtons added to a container become part of the same group. To separate RadioButtons into several groups, the RadioButtons must be added to GroupBoxes or Panels. The common properties and a common event of class RadioButton are listed in Fig. 13.27.
486
Chapter 13
Graphical User Interface Concepts: Part 1
RadioButton
properties and event
Description
Common Properties Checked
Indicates whether the RadioButton is checked.
Text
Specifies the RadioButton’s text.
Common Event CheckedChanged
Fig. 13.27
Generated every time the RadioButton is checked or unchecked. When you double click a RadioButton control in design view, an empty event handler for this event is generated.
and Panels can act as logical groups for RadioButtons. The RadioButtons within each group are mutually exclusive to each other, but not to RadioButtons in different logical groups. 13.2
The program in Fig. 13.28 uses RadioButtons to enable users to select options for a MessageBox. After selecting the desired attributes, the user presses the Display Button to display the MessageBox. A Label in the lower-left corner shows the result of the MessageBox (i.e., which Button the user clicked—Yes, No, Cancel, etc.). To store the user’s choices, we create and initialize the iconType and buttonType objects (lines 11–12). Object iconType is of type MessageBoxIcon, and can have values Asterisk, Error, Exclamation, Hand, Information, None, Question, Stop and Warning. The sample output shows only Error, Exclamation, Information and Question icons. Object buttonType is of type MessageBoxButtons, and can have values AbortRetryIgnore, OK, OKCancel, RetryCancel, YesNo and YesNoCancel. The name indicates the options that are presented to the user in the MessageBox. The sample output windows show MessageBoxes for all of the MessageBoxButtons enumeration values. We created two GroupBoxes, one for each set of enumeration values. The GroupBox captions are Button Type and Icon. The GroupBoxes contain RadioButtons for the corresponding enumeration options, and the RadioButtons’ Text properties are set appropriately. Because the RadioButtons are grouped, only one RadioButton can be selected from each GroupBox. There is also a Button (displayButton) labeled Display. When a user clicks this Button, a customized MessageBox is displayed. A Label (displayLabel) displays which Button the user pressed within the MessageBox. The event handler for the RadioButtons handles the CheckedChanged event of each RadioButton. When a RadioButton contained in the Button Type GroupBox is checked, the checked RadioButton’s corresponding event handler sets buttonType to the appropriate value. Lines 21–45 contain the event handling for these RadioButtons. Similarly, when the user checks the RadioButtons belonging to the Icon GroupBox, the event handlers associated with these events (lines 48–80) set iconType to its corresponding value.
// Fig. 13.28: RadioButtonsTestForm.cs // Using RadioButtons to set message window options. using System; using System.Windows.Forms; // Form contains several RadioButtons--user chooses one // from each group to create a custom MessageBox public partial class RadioButtonsTestForm : Form { // create variables that store the user's choice of options private MessageBoxIcon iconType; private MessageBoxButtons buttonType; // default constructor public RadioButtonsTestForm() { InitializeComponent(); } // end constructor // change Buttons based on option chosen by sender private void buttonType_CheckedChanged( object sender, EventArgs e ) { if ( sender == okButton ) // display OK Button buttonType = MessageBoxButtons.OK; // display OK and Cancel Buttons else if ( sender == okCancelButton ) buttonType = MessageBoxButtons.OKCancel; // display Abort, Retry and Ignore Buttons else if ( sender == abortRetryIgnoreButton ) buttonType = MessageBoxButtons.AbortRetryIgnore; // display Yes, No and Cancel Buttons else if ( sender == yesNoCancelButton ) buttonType = MessageBoxButtons.YesNoCancel; // display Yes and No Buttons else if ( sender == yesNoButton ) buttonType = MessageBoxButtons.YesNo; // only on option left--display Retry and Cancel Buttons else buttonType = MessageBoxButtons.RetryCancel; } // end method buttonType_Changed // change Icon based on option chosen by sender private void iconType_CheckedChanged( object sender, EventArgs e ) { if ( sender == asteriskButton ) // display asterisk Icon iconType = MessageBoxIcon.Asterisk;
Fig. 13.28 | Using RadioButtons to set message-window options. (Part 1 of 4.)
// display error Icon else if ( sender == errorButton ) iconType = MessageBoxIcon.Error; // display exclamation point Icon else if ( sender == exclamationButton ) iconType = MessageBoxIcon.Exclamation; // display hand Icon else if ( sender == handButton ) iconType = MessageBoxIcon.Hand; // display information Icon else if ( sender == informationButton ) iconType = MessageBoxIcon.Information; // display question mark Icon else if ( sender == questionButton ) iconType = MessageBoxIcon.Question; // display stop Icon else if ( sender == stopButton ) iconType = MessageBoxIcon.Stop; // only one option left--display warning Icon else iconType = MessageBoxIcon.Warning; } // end method iconType_CheckChanged // display MessageBox and Button user pressed private void displayButton_Click( object sender, EventArgs e ) { // display MessageBox and store // the value of the Button that was pressed DialogResult result = MessageBox.Show( "This is your Custom MessageBox.", "Custon MessageBox", buttonType, iconType, 0, 0 ); // check to see which Button was pressed in the MessageBox // change text displayed accordingly switch (result) { case DialogResult.OK: displayLabel.Text = "OK was pressed."; break; case DialogResult.Cancel: displayLabel.Text = "Cancel was pressed."; break; case DialogResult.Abort: displayLabel.Text = "Abort was pressed."; break;
Fig. 13.28 | Using RadioButtons to set message-window options. (Part 2 of 4.)
13.7 CheckBoxes and RadioButtons
104 case DialogResult.Retry: 105 displayLabel.Text = "Retry was pressed."; 106 break; 107 case DialogResult.Ignore: 108 displayLabel.Text = "Ignore was pressed."; 109 break; 110 case DialogResult.Yes: 111 displayLabel.Text = "Yes was pressed."; 112 break; 113 case DialogResult.No: 114 displayLabel.Text = "No was pressed."; 115 break; 116 } // end switch 117 } // end method displayButton_Click 118 } // end class RadioButtonsTestForm (a)
(b)
(c) OKCancel button type
(d) OK button type
(e) AbortRetryIgnore button type
(f) YesNoCancel button type
Fig. 13.28 | Using RadioButtons to set message-window options. (Part 3 of 4.)
489
490
Chapter 13
Graphical User Interface Concepts: Part 1
(g) YesNo button type
(h) RetryCancel button type
Fig. 13.28 | Using RadioButtons to set message-window options. (Part 4 of 4.) The Click event handler for displayButton (lines 83–117) creates a MessageBox (lines 87–89). The MessageBox options are specified with the values stored in iconType and buttonType. When the user clicks one of the MessageBox’s buttons, the result of the message box is returned to the application. This result is a value from the DialogResult enumeration that contains Abort, Cancel, Ignore, No, None, OK, Retry or Yes. The switch statement in lines 93–116 tests for the result and sets displayLabel.Text appropriately.
13.8 PictureBoxes A PictureBox displays an image. The image can be one of several formats, such as bitmap, GIF (Graphics Interchange Format) and JPEG. (Images are discussed in Chapter 17, Graphics and Multimedia.) A PictureBox’s Image property specifies the image that is displayed, and the SizeMode property indicates how the image is displayed (Normal, StretchImage, Autosize or CenterImage). Figure 13.29 describes common PictureBox properties and a common event. Figure 13.30 uses a PictureBox named imagePictureBox to display one of three bitmap images—image0, image1 or image2. These images are located in the images direcPictureBox
properties and event
Description
Common Properties Image
Sets the image to display in the PictureBox.
SizeMode
Enumeration that controls image sizing and positioning. Values are Normal (default), StretchImage, AutoSize and CenterImage. Normal places the image in the top-left corner of the PictureBox, and CenterImage puts the image in the middle. These two options truncate the image if it is too large. StretchImage resizes the image to fit in the PictureBox. AutoSize resizes the PictureBox to hold the image.
Common Event Click
Fig. 13.29
Occurs when the user clicks the control. When you double click this control in the designer, an event handler is generated for this event. | PictureBox
// Fig. 13.30: PictureBoxTestForm.cs // Using a PictureBox to display images. using System; using System.Drawing; using System.Windows.Forms; using System.IO; // Form to display different images when PictureBox is clicked public partial class PictureBoxTestForm : Form { private int imageNum = -1; // determines which image is displayed // default constructor public PictureBoxTestForm() { InitializeComponent(); } // end constructor // change image whenever Next Button is clicked private void nextButton_Click( object sender, EventArgs e ) { imageNum = ( imageNum + 1 ) % 3; // imageNum cycles from 0 to 2 // create Image object from file, display in PicutreBox imagePictureBox.Image = Image.FromFile( Directory.GetCurrentDirectory() + @"\images\image" + imageNum + ".bmp" ); } // end method nextButton_Click } // end class PictureBoxTestForm
(a)
(b)
Fig. 13.30 | Using a PictureBox to display images. (Part 1 of 2.)
492
Chapter 13
Graphical User Interface Concepts: Part 1
(c)
Fig. 13.30 | Using a PictureBox to display images. (Part 2 of 2.) tory in the project’s bin/Debug and bin/Release directories. Whenever a user clicks the Next Image Button, the image changes to the next image in sequence. When the last image is displayed and the user clicks the Next Image Button, the first image is displayed again. Inside event handler nextButton_Click (lines 20–28), we use an int (imageNum) to store the number of the image we want to display. We then set the Image property of imagePictureBox to an Image (lines 25–27).
13.9 ToolTips In Chapter 2, we demonstrated tool tips—the helpful text that appears when the mouse hovers over an item in a GUI. Recall that the tool tips displayed in Visual Studio help you become familiar with the IDE’s features and serve as useful reminders for each toolbar icon’s functionality. Many programs use tool tips to remind users of each control’s purpose. For example, Microsoft Word has tool tips that help users determine the purpose of the application’s icons. This section demonstrates how use the ToolTip component to add tool tips to your applications. Figure 13.31 describes common properties and a common event of class ToolTip. ToolTip properties
and event
Description
Common Properties AutoPopDelay
The amount of time (in milliseconds) that the tool tip appears while the mouse is over a control.
InitialDelay
The amount of time (in milliseconds) that a mouse must hover over a control before a tool tip appears.
Fig. 13.31
| ToolTip
properties and event. (Part 1 of 2.)
13.9 ToolTips
493
ToolTip properties
and event
Description
ReshowDelay
The amount of time (in milliseconds) between which two different tool tips appear (when the mouse is moved from one control to another).
Common Event Draw
Raised when the tool tip is displayed. This event allows programmers to modify the appearance of the tool tip.
Fig. 13.31
| ToolTip
properties and event. (Part 2 of 2.)
When you add a ToolTip component from the Toolbox, it appears in the component tray—the gray region below the Form in Design mode. Once a ToolTip is added to a Form, a new property appears in the Properties window for the Form’s other controls. This property appears in the Properties window as ToolTip on, followed by the name of the ToolTip component. For instance, if our Form’s ToolTip were named helpfulToolTip, you would set a control’s ToolTip on helpfulToolTip property value to specify the control’s tool tip text. Figure 13.32 demonstrates the ToolTip component. For this example, we create a GUI containing two Labels, so we can demonstrate different tool tip text for each Label. To make the sample outputs clearer, we set the BorderStyle property of each Label to FixedSingle, which displays a solid border. Since there is no event handling code in this example, the class in Fig. 13.32 contains only a constructor. In this example, we named the ToolTip component labelsToolTip. Figure 13.33 shows the ToolTip in the component tray. We set the tool tip text for the first Label to "First Label" and the tool tip text for the second Label to "Second Label". Figure 13.34 demonstrates setting the tool tip text for the first Label. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
// Fig. 13.32: ToolTipExampleForm.cs // Demonstrating the ToolTip component. using System; using System.Windows.Forms; public partial class ToolTipExampleForm : Form { // default constructor public ToolTipExampleForm() { InitializeComponent(); } // end constructor // no event handlers needed for this example } // end class ToolTipExampleForm
Fig. 13.32 | Demonstrating the ToolTip component. (Part 1 of 2.)
494
Chapter 13
Graphical User Interface Concepts: Part 1
(a)
(b)
Fig. 13.32 | Demonstrating the ToolTip component. (Part 2 of 2.)
ToolTip in component tray
Fig. 13.33 | Demonstrating the component tray.
Property to set tool tip text
Fig. 13.34 | Setting a control’s tool tip text.
Tool tip text
13.10 NumericUpDown Control
495
13.10 NumericUpDown Control At times, we will want to restrict a user’s input choices to a specific range of numeric values. This is the purpose of the NumericUpDown control. This control appears as a TextBox, with two small Buttons on the right side—one with an up arrow and one with a down arrow. By default, a user can type numeric values into this control as if it were a TextBox or click the up and down arrows to increase or decrease the value in the control, respectively. The largest and smallest values in the range are specified with the Maximum and Minimum properties, respectively (both of type decimal). The Increment property (also of type decimal) specifies by how much the current number in the control changes when the user clicks the control’s up and down arrows. Figure 13.35 describes common properties and a common event of class NumericUpDown. NumericUpDown
properties and event
Description
Common Properties Increment
Specifies by how much the current number in the control changes when the user clicks the control’s up and down arrows.
Maximum
Largest value in the control’s range.
Minimum
Smallest value in the control’s range.
UpDownAlign
Modifies the alignment of the up and down Buttons on the NumericUpDown control. This property can be used to display these Buttons either to the left or to the right of the control.
Value
The numeric value currently displayed in the control.
Common Event ValueChanged
Fig. 13.35
This event is raised when the value in the control is changed. This is the default event for the NumericUpDown control.
| NumericUpDown
properties and event.
Figure 13.36 demonstrates using a NumericUpDown control for a GUI that calculates interest rate. The calculations performed in this application are similar to those performed in Fig. 6.6. TextBoxes are used to input the principal and interest rate amounts, and a 1 2 3 4 5 6 7
// Fig. 13.36: interestCalculatorForm.cs // Demonstrating the NumericUpDown control. using System; using System.Windows.Forms; public partial class interestCalculatorForm : Form {
Fig. 13.36 | Demonstrating the NumericUpDown control. (Part 1 of 2.)
// default constructor public interestCalculatorForm() { InitializeComponent(); } // end constructor private void calculateButton_Click( object sender, EventArgs e ) { // declare variables to store user input decimal principal; // store principal double rate; // store interest rate int year; // store number of years decimal amount; // store amount string output; // store output // retrieve user input principal = Convert.ToDecimal( principalTextBox.Text ); rate = Convert.ToDouble( interestTextBox.Text ); year = Convert.ToInt32( yearUpDown.Value ); // set output header output = "Year\tAmount on Deposit\r\n"; // calculate amount after each year and append to output for ( int yearCounter = 1; yearCounter Run...). For applications that are known to Windows, full path names are not needed, and the .exe extension often can be omitted. To open a file that has a file type that Windows recognizes, simply use the file’s full path name. The Windows operating system must be able to use the application associated with the given file’s extension to open the file. The event handler for driveLinkLabel’s LinkClicked event browses the C: drive (lines 17–24). Line 21 sets the LinkVisited property to true, which changes the link’s color from blue to purple (the LinkVisited colors can be configured through the Properties window in Visual Studio). The event handler then passes @"C:\" to method Start (line 23), which opens a Windows Explorer window. The @ symbol that we placed before
14.6 ListBox Control
523
indicates that all characters in the string should be interpreted literally. Thus, the backslash within the string is not considered to be the first character of an escape sequence. This simplifies strings that represent directory paths, since you do not need to use \\ for each \ character in the path. The event handler for deitelLinkLabel’s LinkClicked event (lines 27–35) opens the Web page www.deitel.com in Internet Explorer. We achieve this by passing the Web page address as a string (lines 33–34), which opens Internet Explorer. Line 31 sets the LinkVisited property to true. The event handler for notepadLinkLabel’s LinkClicked event (lines 38–47) opens the Notepad application. Line 42 sets the LinkVisited property to true so the link appears as a visited link. Line 46 passes the argument "notepad" to method Start, which runs notepad.exe. Note that in line 46, the .exe extension is not required—Windows can determine whether it recognizes the argument given to method Start as an executable file. "C:\"
14.6 ListBox Control The ListBox control allows the user to view and select from multiple items in a list. ListBoxes are static GUI entities, which means that items must be added to the list programmatically. The user can be provided with TextBoxes and Buttons with which to specify items to be added to the list, but the actual additions must be performed in code. The CheckedListBox control (Section 14.7) extends a ListBox by including CheckBoxes next to each item in the list. This allows users to place checks on multiple items at once, as is possible with CheckBox controls. (Users also can select multiple items from a ListBox by setting the ListBox’s SelectionMode property, which is discussed shortly.) Figure 14.15 displays a ListBox and a CheckedListBox. In both controls, scrollbars appear if the number of items exceeds the ListBox’s viewable area. Figure 14.16 lists common ListBox properties and methods, and a common event. The SelectionMode property determines the number of items that can be selected. This property has the possible values None, One, MultiSimple and MultiExtended (from the ListBox
Selected items Scrollbars appear if necessary
Checked item
CheckedListBox
Fig. 14.15
| ListBox
and CheckedListBox on a Form.
524
Chapter 14
properties, methods and event
Graphical User Interface Concepts: Part 2
ListBox
Description
Common Properties Items
The collection of items in the ListBox.
MultiColumn
Indicates whether the ListBox can break a list into multiple columns. Multiple columns eliminate vertical scrollbars from the display.
SelectedIndex
Returns the index of the selected item. If no items have been selected, the property returns -1. If the user selects multiple items, this property returns only one of the selected indices. For this reason, if multiple items are selected, you should use property SelectedIndices.
SelectedIndices
Returns a collection containing the indices for all selected items.
SelectedItem
Returns a reference to the selected item. If multiple items are selected, it returns the item with the lowest index number.
SelectedItems
Returns a collection of the selected item(s).
SelectionMode
Determines the number of items that can be selected, and the means through which multiple items can be selected. Values None, One, MultiSimple (multiple selection allowed) or MultiExtended (multiple selection allowed using a combination of arrow keys or mouse clicks and Shift and Ctrl keys).
Sorted
Indicates whether items are sorted alphabetically. Setting this property’s value to true sorts the items. The default value is false.
Common Methods ClearSelected
Deselects every item.
GetSelected
Takes an index as an argument, and returns true if the corresponding item is selected.
Common Event SelectedIndexChanged
Fig. 14.16
| ListBox
Generated when the selected index changes. This is the default event when the control is double clicked in the designer.
properties, methods and event.
enumeration)—the differences among these settings are explained in Fig. 14.16. The SelectedIndexChanged event occurs when the user selects a new item. Both the ListBox and CheckedListBox have properties Items, SelectedItem and SelectedIndex. Property Items returns all the list items as a collection. Collections are a common way of managing lists of objects in the .NET framework. Many .NET GUI components (e.g., ListBoxes) use collections to expose lists of internal objects (e.g., items SelectionMode
14.6 ListBox Control
525
contained within a ListBox). We discuss collections further in Chapter 26. The collection returned by property Items is represented as an object of type ObjectCollection. Property SelectedItem returns the ListBox’s currently selected item. If the user can select multiple items, use collection SelectedItems to return all the selected items as a collection. Property SelectedIndex returns the index of the selected item—if there could be more than one, use property SelectedIndices. If no items are selected, property SelectedIndex returns -1. Method GetSelected takes an index and returns true if the corresponding item is selected. To add items to a ListBox or to a CheckedListBox, we must add objects to its Items collection. This can be accomplished by calling method Add to add a string to the ListBox’s or CheckedListBox’s Items collection. For example, we could write myListBox.Items.Add( myListItem )
to add string myListItem to ListBox myListBox. To add multiple objects, you can either call method Add multiple times or call method AddRange to add an array of objects. Classes ListBox and CheckedListBox each call the submitted object’s ToString method to determine the Label for the corresponding object’s entry in the list. This allows you to add different objects to a ListBox or a CheckedListBox that later can be returned through properties SelectedItem and SelectedItems. Alternatively, you can add items to ListBoxes and CheckedListBoxes visually by examining the Items property in the Properties window. Clicking the ellipsis button opens the String Collection Editor, which contains a text area for adding items; each item appears on a separate line (Fig. 14.17). Visual Studio then writes code to add these strings to the Items collection inside method InitializeComponent. Figure 14.18 uses class ListBoxTestForm to add, remove and clear items from ListBox displayListBox. Class ListBoxTestForm uses TextBox inputTextBox to allow the user to type in a new item. When the user clicks the Add Button, the new item appears in displayListBox. Similarly, if the user selects an item and clicks Remove, the item is deleted. When clicked, Clear deletes all entries in displayListBox. The user terminates the application by clicking Exit. The addButton_Click event handler (lines 18–22) calls method Add of the Items collection in the ListBox. This method takes a string as the item to add to displayListBox.
Fig. 14.17 | String Collection Editor.
526
Chapter 14
Graphical User Interface Concepts: Part 2
In this case, the string used is the user-input text, or inputTextBox.Text (line 20). After the item is added, inputTextBox.Text is cleared (line 21). The removeButton_Click event handler (lines 25–30) uses method RemoveAt to remove an item from the ListBox. Event handler removeButton_Click first uses property SelectedIndex to determine which index is selected. If SelectedIndex is not –1 (i.e., an item is selected) line 29 removes the item that corresponds to the selected index.
// Fig. 14.18: ListBoxTestForm.cs // Program to add, remove and clear ListBox items using System; using System.Windows.Forms; // Form uses a TextBox and Buttons to add, // remove, and clear ListBox items public partial class ListBoxTestForm : Form { // default constructor public ListBoxTestForm() { InitializeComponent(); } // end constructor // add new item to ListBox (text from input TextBox) // and clear input TextBox private void addButton_Click( object sender, EventArgs e ) { displayListBox.Items.Add( inputTextBox.Text ); inputTextBox.Clear(); } // end method addButton_Click // remove item if one is selected private void removeButton_Click( object sender, EventArgs e ) { // check if item is selected, remove if selected if ( displayListBox.SelectedIndex != -1 ) displayListBox.Items.RemoveAt( displayListBox.SelectedIndex ); } // end method removeButton_Click // clear all items in ListBox private void clearButton_Click( object sender, EventArgs e ) { displayListBox.Items.Clear(); } // end method clearButton_Click // exit application private void exitButton_Click( object sender, EventArgs e ) { Application.Exit(); } // end method exitButton_Click } // end class ListBoxTestForm
Fig. 14.18 | Program that adds, removes and clears ListBox items. (Part 1 of 2.)
14.7 CheckedListBox Control
(a)
(b)
(c)
(d)
527
Fig. 14.18 | Program that adds, removes and clears ListBox items. (Part 2 of 2.) The clearButton_Click event handler (lines 33–36) calls method Clear of the Items collection (line 35). This removes all the entries in displayListBox. Finally, event handler exitButton_Click (lines 39–42) terminates the application by calling method Application.Exit (line 41).
14.7 CheckedListBox Control The CheckedListBox control derives from class ListBox and includes a CheckBox next to each item. As in ListBoxes, items can be added via methods Add and AddRange or through the String Collection Editor. CheckedListBoxes imply that multiple items can be selected, and the only possible values for the SelectionMode property are None and One. One allows multiple selection, because CheckBoxes imply that there are no logical restrictions on the items—the user can select as many items as required. Thus, the only choice is whether to give the user multiple selection or no selection at all. This keeps the CheckedListBox’s behavior consistent with that of CheckBoxes. Common properties events and common method of CheckedListBoxes appear in Fig. 14.19.
Common Programming Error 14.1 The IDE displays an error message if you attempt to set the SelectionMode property to MultiSimple or MultiExtended in the Properties window of a CheckedListBox. If this value is set programmatically, a runtime error occurs. 14.1
528
Chapter 14
Graphical User Interface Concepts: Part 2
CheckedListBox
properties, method and event
Description
Common Properties
(All the ListBox properties, methods and events are inherited by CheckedListBox.)
CheckedItems
Contains the collection of items that are checked. This is distinct from the selected item, which is highlighted (but not necessarily checked). [Note: There can be at most one selected item at any given time.]
CheckedIndices
Returns indices for all checked items.
SelectionMode
Determines how many items can be checked. The only possible values are One (allows multiple checks to be placed) or None (does not allow any checks to be placed).
Common Method GetItemChecked
Takes an index and returns true if the corresponding item is checked.
Common Event (Event arguments ItemCheckEventArgs) ItemCheck
Generated when an item is checked or unchecked.
ItemCheckEventArgs
Properties
CurrentValue
Indicates whether the current item is checked or unchecked. Possible values are Checked, Unchecked and Indeterminate.
Index
Returns the zero-based index of the item that changed.
NewValue
Specifies the new state of the item.
Fig. 14.19
| CheckedListBox
properties, method and event.
Event ItemCheck occurs whenever a user checks or unchecks a CheckedListBox item. Event argument properties CurrentValue and NewValue return CheckState values for the current and new state of the item, respectively. A comparison of these values allows you to determine whether the CheckedListBox item was checked or unchecked. The CheckedListBox control retains the SelectedItems and SelectedIndices properties (it inherits them from class ListBox). However, it also includes properties CheckedItems and CheckedIndices, which return information about the checked items and indices. In Fig. 14.20, class CheckedListBoxTestForm uses a CheckedListBox and a ListBox to display a user’s selection of books. The CheckedListBox allows the user to select multiple titles. In the String Collection Editor, items were added for some Deitel books: C++, Java™, Visual Basic, Internet & WWW, Perl, Python, Wireless Internet and Advanced Java (the acronym HTP stands for “How to Program”). The ListBox (named displayListBox) displays the user’s selection. In the screenshots accompanying this example, the CheckedListBox appears to the left, the ListBox on the right.
// Fig. 14.20: CheckedListBoxTestForm.cs // Using the checked ListBox to add items to a display ListBox using System; using System.Windows.Forms; // Form uses a checked ListBox to add items to a display ListBox public partial class CheckedListBoxTestForm : Form { // default constructor public CheckedListBoxTestForm() { InitializeComponent(); } // end constructor // item about to change // add or remove from display ListBox private void inputCheckedListBox_ItemCheck( object sender, ItemCheckEventArgs e ) { // obtain reference of selected item string item = inputCheckedListBox.SelectedItem.ToString(); // if item checked add to ListBox // otherwise remove from ListBox if ( e.NewValue == CheckState.Checked ) displayListBox.Items.Add( item ); else displayListBox.Items.Remove( item ); } // end method inputCheckedListBox_ItemCheck } // end class CheckedListBoxTestForm (a)
(b)
Fig. 14.20 | 1 of 2.)
CheckedListBox and ListBox used in a program to display a user selection. (Part
530
Chapter 14
Graphical User Interface Concepts: Part 2
(c)
(d)
Fig. 14.20 |
CheckedListBox and ListBox used in a program to display a user selection. (Part
2 of 2.)
When the user checks or unchecks an item in inputCheckedListBox, an ItemCheck event occurs and event handler inputCheckedListBox_ItemCheck (lines 17–29) executes. An if…else statement (lines 25–28) determines whether the user checked or unchecked an item in the CheckedListBox. Line 25 uses the NewValue property to determine whether the item is being checked (CheckState.Checked). If the user checks an item, line 26 adds the checked entry to the ListBox displayListBox. If the user unchecks an item, line 28 removes the corresponding item from displayListBox. This event handler was created by selecting the CheckedListBox in Design mode, viewing the control’s events in the Properties window and double clicking the ItemCheck event.
14.8 ComboBox Control The ComboBox control combines TextBox features with a drop-down list—a GUI component that contains a list from which a value can be selected. A ComboBox usually appears as a TextBox with a down arrow to its right. By default, the user can enter text into the TextBox or click the down arrow to display a list of predefined items. If a user chooses an element from this list, that element is displayed in the TextBox. If the list contains more elements than can be displayed in the drop-down list, a scrollbar appears. The maximum number of items that a drop-down list can display at one time is set by property MaxDropDownItems. Figure 14.21 shows a sample ComboBox in three different states. As with the ListBox control, you can add objects to collection Items programmatically, using methods Add and AddRange, or visually, with the String Collection Editor. Figure 14.22 lists common properties and a common event of class ComboBox.
14.8 ComboBox Control
Click the down arrow to display items in drop-down list
Fig. 14.21 |
ComboBox
531
Selecting an item from drop-down list changes text in TextBox portion
demonstration.
Look-and-Feel Observation 14.4 Use a ComboBox to save space on a GUI. A disadvantage is that, unlike with a ListBox, the user cannot see available items without expanding the drop-down list. 14.4
ComboBox
properties
and event
Description
Common Properties DropDownStyle
Determines the type of ComboBox. Value Simple means that the text portion is editable, and the list portion is always visible. Value DropDown (the default) means that the text portion is editable, but the user must click an arrow button to see the list portion. Value DropDownList means that the text portion is not editable, and the user must click the arrow button to see the list portion.
Items
The collection of items in the ComboBox control.
MaxDropDownItems
Specifies the maximum number of items (between 1 and 100) that the drop-down list can display. If the number of items exceeds the maximum number of items to display, a scrollbar appears.
SelectedIndex
Returns the index of the selected item. If there is no selected item, -1 is returned.
SelectedItem
Returns a reference to the selected item.
Sorted
Indicates whether items are sorted alphabetically. Setting this property’s value to true sorts the items. The default is false.
Common Event SelectedIndexChanged
Fig. 14.22
| ComboBox
Generated when the selected index changes (such as when a different item is selected). This is the default event when control is double clicked in the designer.
properties and event.
532
Chapter 14
Graphical User Interface Concepts: Part 2
Property DropDownStyle determines the type of ComboBox, and is represented as a value of the ComboBoxStyle enumeration, which contains values Simple, DropDown and DropDownList. Option Simple does not display a drop-down arrow. Instead, a scrollbar appears next to the control, allowing the user to select a choice from the list. The user also can type in a selection. Style DropDown (the default) displays a drop-down list when the down arrow is clicked (or the down-arrow key is pressed). The user can type a new item in the ComboBox. The last style is DropDownList, which displays a drop-down list but does not allow the user to type in the TextBox. The ComboBox control has properties Items (a collection), SelectedItem and SelectedIndex, which are similar to the corresponding properties in ListBox. There can be at most one selected item in a ComboBox. If no items are selected, then SelectedIndex is -1. When the selected item changes, a SelectedIndexChanged event occurs. Class ComboBoxTestForm (Fig. 14.23) allows users to select a shape to draw—circle, ellipse, square or pie (in both filled and unfilled versions)—by using a ComboBox. The ComboBox in this example is uneditable, so the user cannot type in the TextBox. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
// Fig. 14.23: ComboBoxTestForm.cs // Using ComboBox to select a shape to draw. using System; using System.Drawing; using System.Windows.Forms; // Form uses a ComboBox to select different shapes to draw public partial class ComboBoxTestForm : Form { // default constructor public ComboBoxTestForm() { InitializeComponent(); } // end constructor // get index of selected shape, draw shape private void imageComboBox_SelectedIndexChanged( object sender, EventArgs e ) { // create graphics object, Pen and SolidBrush Graphics myGraphics = base.CreateGraphics(); // create Pen using color DarkRed Pen myPen = new Pen( Color.DarkRed ); // create SolidBrush using color DarkRed SolidBrush mySolidBrush = new SolidBrush( Color.DarkRed ); // clear drawing area setting it to color white myGraphics.Clear( Color.White ); // find index, draw proper shape switch ( imageComboBox.SelectedIndex ) {
case 0: // case Circle is selected myGraphics.DrawEllipse( myPen, 50, 50, 150, 150 ); break; case 1: // case Rectangle is selected myGraphics.DrawRectangle( myPen, 50, 50, 150, 150 ); break; case 2: // case Ellipse is selected myGraphics.DrawEllipse( myPen, 50, 85, 150, 115 ); break; case 3: // case Pie is selected myGraphics.DrawPie( myPen, 50, 50, 150, 150, 0, 45 ); break; case 4: // case Filled Circle is selected myGraphics.FillEllipse( mySolidBrush, 50, 50, 150, 150 ); break; case 5: // case Filled Rectangle is selected myGraphics.FillRectangle( mySolidBrush, 50, 50, 150, 150 ); break; case 6: // case Filled Ellipse is selected myGraphics.FillEllipse( mySolidBrush, 50, 85, 150, 115 ); break; case 7: // case Filled Pie is selected myGraphics.FillPie( mySolidBrush, 50, 50, 150, 150, 0, 45 ); break; } // end switch myGraphics.Dispose(); // release the Graphics object } // end method imageComboBox_SelectedIndexChanged } // end class ComboBoxTestForm (a)
(b)
(c)
(d)
Fig. 14.23 |
ComboBox
used to draw a selected shape. (Part 2 of 2.)
534
Chapter 14
Graphical User Interface Concepts: Part 2
Look-and-Feel Observation 14.5 Make lists (such as ComboBoxes) editable only if the program is designed to accept user-submitted elements. Otherwise, the user might try to enter a custom item that is improper for the purposes of your application. 14.5
After creating ComboBox imageComboBox, make it uneditable by setting its DropDownStyle to DropDownList in the Properties window. Next, add items Circle, Square, Ellipse, Pie, Filled Circle, Filled Square, Filled Ellipse and Filled Pie to the Items collection using the String Collection Editor. Whenever the user selects an item from imageComboBox, a SelectedIndexChanged event occurs and event handler imageComboBox_SelectedIndexChanged (lines 17–60) executes. Lines 21–27 create a Graphics object, a Pen and a SolidBrush, which are used to draw on the Form. The Graphics object (line 21) allows a pen or brush to draw on a component using one of several Graphics methods. The Pen object (line 24) is used by methods DrawEllipse, DrawRectangle and DrawPie (lines 36, 39, 42 and 45) to draw the outlines of their corresponding shapes. The SolidBrush object (line 27) is used by methods FillEllipse, FillRectangle and FillPie (lines 48, 51, 54 and 57) to fill their corresponding solid shapes. Line 30 colors the entire Form White, using Graphics method Clear. These methods are discussed in greater detail in Chapter 17, Graphics and Multimedia. The application draws a shape based on the selected item’s index. The switch statement (lines 33–59) uses imageComboBox.SelectedIndex to determine which item the user selected. Graphics method DrawEllipse (line 36) takes a Pen, the x- and y-coordinates of the center and the width and height of the ellipse to draw. The origin of the coordinate system is in the upper-left corner of the Form; the x-coordinate increases to the right, and the y-coordinate increases downward. A circle is a special case of an ellipse (with the width and height equal). Line 36 draws a circle. Line 42 draws an ellipse that has different values for width and height. Class Graphics method DrawRectangle (line 39) takes a Pen, the x- and y-coordinates of the upper-left corner and the width and height of the rectangle to draw. Method DrawPie (line 45) draws a pie as a portion of an ellipse. The ellipse is bounded by a rectangle. Method DrawPie takes a Pen, the x- and y-coordinates of the upper-left corner of the rectangle, its width and height, the start angle (in degrees) and the sweep angle (in degrees) of the pie. Angles increase clockwise. The FillEllipse (lines 48 and 54), FillRectangle (line 51) and FillPie (line 57) methods are similar to their unfilled counterparts, except that they take a SolidBrush instead of a Pen. Some of the drawn shapes are illustrated in the screen shots of Fig. 14.23.
14.9 TreeView Control The TreeView control displays nodes hierarchically in a tree. Traditionally, nodes are objects that contain values and can refer to other nodes. A parent node contains child nodes, and the child nodes can be parents to other nodes. Two child nodes that have the same parent node are considered sibling nodes. A tree is a collection of nodes, usually organized in a hierarchical manner. The first parent node of a tree is the root node (a TreeView can have multiple roots). For example, the file system of a computer can be represented as a tree. The top-level directory (perhaps C:) would be the root, each subfolder of C: would be a child node and each child folder could have its own children. TreeView controls are
14.9 TreeView Control
535
useful for displaying hierarchal information, such as the file structure that we just mentioned. We cover nodes and trees in greater detail in Chapter 24, Data Structures. Figure 14.24 displays a sample TreeView control on a Form. A parent node can be expanded or collapsed by clicking the plus box or minus box to its left. Nodes without children do not have these boxes. The nodes in a TreeView are instances of class TreeNode. Each TreeNode has a Nodes collection (type TreeNodeCollection), which contains a list of other TreeNodes—known as its children. The Parent property returns a reference to the parent node (or null if the node is a root node). Figure 14.25 and Fig. 14.26 list the common properties of TreeViews and TreeNodes, common TreeNode methods and a common TreeView event. Click + to expand node and display child nodes
Click – to collapse node and hide child nodes
Root node
Child nodes (of Manager2)
Fig. 14.24 |
TreeView
displaying a sample tree.
TreeView properties
and event
Description
Common Properties CheckBoxes
Indicates whether CheckBoxes appear next to nodes. A value of true displays CheckBoxes. The default value is false.
ImageList
Specifies an ImageList object containing the node icons. An ImageList object is a collection that contains Image objects.
Nodes
Lists the collection of TreeNodes in the control. It contains methods Add (adds a TreeNode object), Clear (deletes the entire collection) and Remove (deletes a specific node). Removing a parent node deletes all of its children.
SelectedNode
The selected node.
Common Event (Event arguments TreeViewEventArgs) AfterSelect
Fig. 14.25
| TreeView
Generated after selected node changes. This is the default event when the control is double clicked in the designer.
properties and event.
536
Chapter 14
Graphical User Interface Concepts: Part 2
TreeNode properties
and methods
Description
Common Properties Checked
Indicates whether the TreeNode is checked (CheckBoxes property must be set to true in the parent TreeView).
FirstNode
Specifies the first node in the Nodes collection (i.e., the first child in the tree).
FullPath
Indicates the path of the node, starting at the root of the tree.
ImageIndex
Specifies the index of the image shown when the node is deselected.
LastNode
Specifies the last node in the Nodes collection (i.e., the last child in the tree).
NextNode
Next sibling node.
Nodes
Collection of TreeNodes contained in the current node (i.e., all the children of the current node). It contains methods Add (adds a TreeNode object), Clear (deletes the entire collection) and Remove (deletes a specific node). Removing a parent node deletes all of its children.
PrevNode
Previous sibling node.
SelectedImageIndex
Specifies the index of the image to use when the node is selected.
Text
Specifies the TreeNode’s text.
Common Methods Collapse
Collapses a node.
Expand
Expands a node.
ExpandAll
Expands all the children of a node.
GetNodeCount
Returns the number of child nodes.
Fig. 14.26 |
TreeNode
properties and methods.
To add nodes to the TreeView visually, click the ellipsis next to the Nodes property in the Properties window. This opens the TreeNode Editor (Fig. 14.27), which displays an empty tree representing the TreeView. There are Buttons to create a root, and to add or delete a node. To the right are the properties of current node. Here you can rename the node. To add nodes programmatically, first create a root node. Create a new TreeNode object and pass it a string to display. Then call method Add to add this new TreeNode to the TreeView’s Nodes collection. Thus, to add a root node to TreeView myTreeView, write myTreeView.Nodes.Add( new TreeNode( rootLabel ) );
14.9 TreeView Control
537
Delete current node
Fig. 14.27 | TreeNode Editor. where myTreeView is the TreeView to which we are adding nodes, and rootLabel is the text to display in myTreeView. To add children to a root node, add new TreeNodes to its Nodes collection. We select the appropriate root node from the TreeView by writing myTreeView.Nodes[ myIndex ]
where myIndex is the root node’s index in myTreeView’s Nodes collection. We add nodes to child nodes through the same process by which we added root nodes to myTreeView. To add a child to the root node at index myIndex, write myTreeView.Nodes[ myIndex ].Nodes.Add( new TreeNode( ChildLabel ) );
Class TreeViewDirectoryStructureForm (Fig. 14.28) uses a TreeView to display the contents of a directory chosen by the user. A TextBox and a Button are used to specify the directory. First, enter the full path of the directory you want to display. Then click the 1 2 3 4 5 6 7 8 9
// Fig. 14.28: TreeViewDirectoryStructureForm.cs // Using TreeView to display directory structure. using System; using System.Windows.Forms; using System.IO; // Form uses TreeView to display directory structure public partial class TreeViewDirectoryStructureForm : Form {
string substringDirectory; // store last part of full path name // default constructor public TreeViewDirectoryStructureForm() { InitializeComponent(); } // end constructor // populate current node with subdirectories public void PopulateTreeView( string directoryValue, TreeNode parentNode ) { // array stores all subdirectories in the directory string[] directoryArray = Directory.GetDirectories( directoryValue ); // populate current node with subdirectories try { // check to see if any subdirectories are present if ( directoryArray.Length != 0 ) { // for every subdirectory, create new TreeNode, // add as a child of current node and recursively // populate child nodes with subdirectories foreach ( string directory in directoryArray ) { // obtain last part of path name from the full path name // by finding the last occurence of "\" and returning the // part of the path name that comes after this occurrence substringDirectory = directory.Substring( directory.LastIndexOf( '\\' ) + 1, directory.Length - directory.LastIndexOf( '\\' ) - 1 ); // create TreeNode for current directory TreeNode myNode = new TreeNode( substringDirectory ); // add current directory node to parent node parentNode.Nodes.Add( myNode ); // recursively populate every subdirectory PopulateTreeView( directory, myNode ); } // end foreach } // end if } //end try // catch exception catch ( UnauthorizedAccessException ) { parentNode.Nodes.Add( "Access denied" ); } // end catch } // end method PopulateTreeView
// handles enterButton click event private void enterButton_Click( object sender, EventArgs e ) { // clear all nodes directoryTreeView.Nodes.Clear(); // // // if {
check if the directory entered by user exists if it does then fill in the TreeView, if not display error MessageBox ( Directory.Exists( inputTextBox.Text ) ) // add full path name to directoryTreeView directoryTreeView.Nodes.Add( inputTextBox.Text ); // insert subfolders PopulateTreeView( inputTextBox.Text, directoryTreeView.Nodes[ 0 ] );
} // display error MessageBox if directory not found else MessageBox.Show( inputTextBox.Text + " could not be found.", "Directory Not Found", MessageBoxButtons.OK, MessageBoxIcon.Error ); } // end method enterButton_Click } // end class TreeViewDirectoryStructureForm (a)
Fig. 14.28 |
(b)
TreeView
used to display directories. (Part 3 of 3.)
to set the specified directory as the root node in the TreeView. Each subdirectory of this directory becomes a child node. This layout is similar to that used in Windows Explorer. Folders can be expanded or collapsed by clicking the plus or minus boxes that appear to their left. When the user clicks the enterButton, all the nodes in directoryTreeView are cleared (line 67). Then the path entered in inputTextBox is used to create the root node. Line 75 adds the directory to directoryTreeView as the root node, and lines 78–79 call method PopulateTreeView (lines 19–61), which takes a directory (a string) and a parent Button
540
Chapter 14
Graphical User Interface Concepts: Part 2
node. Method PopulateTreeView then creates child nodes corresponding to the subdirectories of the directory it receives as an argument. Method PopulateTreeView (lines 19–61) obtains a list of subdirectories, using method GetDirectories of class Directory (namespace System.IO) in lines 23–24. Method GetDirectories takes a string (the current directory) and returns an array of strings (the subdirectories). If a directory is not accessible for security reasons, an UnauthorizedAccessException is thrown. Lines 57–60 catch this exception and add a node containing “Access Denied” instead of displaying the subdirectories. If there are accessible subdirectories, lines 40–42 use the Substring method to increase readability by shortening the full path name to just the directory name. Next, each string in the directoryArray is used to create a new child node (line 45). We use method Add (line 48) to add each child node to the parent. Then method PopulateTreeView is called recursively on every subdirectory (line 51), which eventually populates the TreeView with the entire directory structure. Note that our recursive algorithm may cause a delay when the program loads large directories. However, once the folder names are added to the appropriate Nodes collection, they can be expanded and collapsed without delay. In the next section, we present an alternate algorithm to solve this problem.
14.10 ListView Control The ListView control is similar to a ListBox in that both display lists from which the user can select one or more items (an example of a ListView can be found in Fig. 14.31). The important difference between the two classes is that a ListView can display icons next to the list items (controlled by its ImageList property). Property MultiSelect (a Boolean) determines whether multiple items can be selected. CheckBoxes can be included by setting property CheckBoxes (a Boolean) to true, making the ListView’s appearance similar to that of a CheckedListBox. The View property specifies the layout of the ListBox. Property Activation determines the method by which the user selects a list item. The details of these properties and the ItemActivate event are explained in Fig. 14.29. ListView
properties
and event
Description
Common Properties Activation
Determines how the user activates an item. This property takes a value in the ItemActivation enumeration. Possible values are OneClick (single-click activation), TwoClick (double-click activation, item changes color when selected) and Standard (double-click activation, item does not change color).
CheckBoxes
Indicates whether items appear with CheckBoxes. true displays CheckBoxes. The default is false.
LargeImageList
Specifies the ImageList containing large icons for display.
Fig. 14.29
| ListView
properties and event. (Part 1 of 2.)
14.10 ListView Control
ListView
541
properties
and event
Description
Items
Returns the collection of ListViewItems in the control.
MultiSelect
Determines whether multiple selection is allowed. The default is true, which enables multiple selection.
SelectedItems
Gets the collection of selected items.
SmallImageList
Specifies the ImageList containing small icons for display.
View
Determines appearance of ListViewItems. Possible values are LargeIcon (large icon displayed, items can be in multiple columns), SmallIcon (small icon displayed, items can be in multiple columns), List (small icons displayed, items appear in a single column), Details (like List, but multiple columns of information can be displayed per item) and Tile (large icons displayed, information provided to right of icon, valid only in Windows XP or later).
Common Event ItemActivate
Fig. 14.29
| ListView
generated when an item in the ListView is activated. Does not contain the specifics of which item is activated.
properties and event. (Part 2 of 2.)
ListView allows you to define the images used as icons for ListView items. To display images, an ImageList component is required. Create one by dragging it to a Form from the ToolBox. Then, select the Images property in the Properties window to display the Image Collection Editor (Fig. 14.30). Here you can browse for images that you wish to add to the ImageList, which contains an array of Images. Once the images have been defined, set property SmallImageList of the ListView to the new ImageList object. Property SmallImageList specifies the image list for the small icons. Property LargeImageList sets the ImageList for large icons. The items in a ListView are each of type ListViewItem.
Fig. 14.30 | Image Collection Editor window for an ImageList component.
542
Chapter 14
Graphical User Interface Concepts: Part 2
Icons for the ListView items are selected by setting the item’s ImageIndex property to the appropriate index. Class ListViewTestForm (Fig. 14.31) displays files and folders in a ListView, along with small icons representing each file or folder. If a file or folder is inaccessible because of permission settings, a MessageBox appears. The program scans the contents of the directory as it browses, rather than indexing the entire drive at once. To display icons beside list items, create an ImageList for the ListView browserListView. First, drag and drop an ImageList on the Form and open the Image Collection Editor. Select our two simple bitmap images, provided in the bin\Release folder of this example—one for a folder (array index 0) and the other for a file (array index 1). Then set the object browserListView property SmallImageList to the new ImageList in the Properties window. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
// Fig. 14.31: ListViewTestForm.cs // Displaying directories and their contents in ListView. using System; using System.Drawing; using System.Windows.Forms; using System.IO; // Form contains a ListView which displays // folders and files in a directory public partial class ListViewTestForm : Form { // store current directory string currentDirectory = Directory.GetCurrentDirectory(); // default constructor public ListViewTestForm() { InitializeComponent(); } // end constructor // browse directory user clicked or go up one level private void browserListView_Click( object sender, EventArgs e ) { // ensure an item is selected if ( browserListView.SelectedItems.Count != 0 ) { // if first item selected, go up one level if ( browserListView.Items[ 0 ].Selected ) { // create DirectoryInfo object for directory DirectoryInfo directoryObject = new DirectoryInfo( currentDirectory );
Fig. 14.31 |
// if directory has parent, load it if ( directoryObject.Parent != null ) LoadFilesInDirectory( directoryObject.Parent.FullName ); } // end if ListView
// selected directory or file else { // directory or file chosen string chosen = browserListView.SelectedItems[ 0 ].Text; // if item selected is directory, load selected directory if ( Directory.Exists( currentDirectory + @"\" + chosen ) ) { // if currently in C:\, do not need '\'; otherwise we do if ( currentDirectory == @"C:\" ) LoadFilesInDirectory( currentDirectory + chosen ); else LoadFilesInDirectory( currentDirectory + @"\" + chosen ); } // end if } // end else // update displayLabel displayLabel.Text = currentDirectory; } // end if } // end method browserListView_Click // display files/subdirectories of current directory public void LoadFilesInDirectory( string currentDirectoryValue ) { // load directory information and display try { // clear ListView and set first item browserListView.Items.Clear(); browserListView.Items.Add( "Go Up One Level" );
Fig. 14.31 |
// update current directory currentDirectory = currentDirectoryValue; DirectoryInfo newCurrentDirectory = new DirectoryInfo( currentDirectory ); // put files and directories into arrays DirectoryInfo[] directoryArray = newCurrentDirectory.GetDirectories(); FileInfo[] fileArray = newCurrentDirectory.GetFiles(); // add directory names to ListView foreach ( DirectoryInfo dir in directoryArray ) { // add directory to ListView ListViewItem newDirectoryItem = browserListView.Items.Add( dir.Name ); newDirectoryItem.ImageIndex = 0; } // end foreach ListView
// set directory image
displaying files and folders. (Part 2 of 4.)
544
Chapter 14
Graphical User Interface Concepts: Part 2
91 92 // add file names to ListView 93 foreach ( FileInfo file in fileArray ) 94 { 95 // add file to ListView ListViewItem newFileItem = 96 browserListView.Items.Add( file.Name ); 97 98 newFileItem.ImageIndex = 1; // set file image 99 100 } // end foreach 101 } // end try 102 103 // access denied 104 catch ( UnauthorizedAccessException ) 105 { 106 MessageBox.Show( "Warning: Some fields may not be " + 107 "visible due to permission settings", 108 "Attention", 0, MessageBoxIcon.Warning ); 109 } // end catch 110 } // end method LoadFilesInDirectory 111 112 // handle load event when Form displayed for first time 113 private void ListViewTestForm_Load( object sender, EventArgs e ) 114 { 115 // set Image list 116 Image folderImage = Image.FromFile( 117 currentDirectory + @"\images\folder.bmp" ); 118 119 Image fileImage = Image.FromFile( 120 currentDirectory + @"\images\file.bmp" ); 121 fileFolder.Images.Add( folderImage ); 122 fileFolder.Images.Add( fileImage ); 123 124 125 // load current directory into browserListView 126 LoadFilesInDirectory( currentDirectory ); 127 displayLabel.Text = currentDirectory; 128 } // end method ListViewTestForm_Load 129 } // end class ListViewTestForm (a)
Fig. 14.31 |
ListView
displaying files and folders. (Part 3 of 4.)
14.10 ListView Control
545
(b)
(c)
Fig. 14.31 |
ListView
displaying files and folders. (Part 4 of 4.)
Method LoadFilesInDirectory (lines 63–110) populates browserListView with the directory passed to it (currentDirectoryValue). It clears browserListView and adds the element "Go Up One Level". When the user clicks this element, the program attempts to move up one level (we see how shortly). The method then creates a DirectoryInfo object initialized with the string currentDirectory (lines 74–75). If permission is not given to browse the directory, an exception is thrown (and caught in line 104). Method LoadFilesInDirectory works differently from method PopulateTreeView in the previous program (Fig. 14.28). Instead of loading all the folders on the hard drive, method LoadFilesInDirectory loads only the folders in the current directory. Class DirectoryInfo (namespace System.IO) enables us to browse or manipulate the directory structure easily. Method GetDirectories (line 79) returns an array of DirectoryInfo objects containing the subdirectories of the current directory. Similarly, method GetFiles (line 80) returns an array of class FileInfo objects containing the files in the current directory. Property Name (of both class DirectoryInfo and class FileInfo) contains only the directory or file name, such as temp instead of C:\myfolder\temp. To access the full name, use property FullName. Lines 83–90 and lines 93–100 iterate through the subdirectories and files of the current directory and add them to browserListView. Lines 89 and 99 set the ImageIndex properties of the newly created items. If an item is a directory, we set its icon to a directory icon (index 0); if an item is a file, we set its icon to a file icon (index 1). Method browserListView_Click (lines 22–60) responds when the user clicks control browserListView. Line 25 checks whether anything is selected. If a selection has been made, line 28 determines whether the user chose the first item in browserListView. The first item in browserListView is always Go up one level; if it is selected, the program attempts to go up a level. Lines 31–32 create a DirectoryInfo object for the current directory. Line 35 tests property Parent to ensure that the user is not at the root of the directory tree. Property Parent indicates the parent directory as a DirectoryInfo object; if no
546
Chapter 14
Graphical User Interface Concepts: Part 2
parent directory exists, Parent returns the value null. If a parent does directory exist, then line 36 passes the full name of the parent directory to method LoadFilesInDirectory. If the user did not select the first item in browserListView, lines 40–55 allow the user to continue navigating through the directory structure. Line 43 creates string chosen, which receives the text of the selected item (the first item in collection SelectedItems). Line 46 determines whether the user has selected a valid directory (rather than a file). The program combines variables currentDirectory and chosen (the new directory), separated by a backslash (\), and passes this value to class Directory’s method Exists. Method Exists returns true if its string parameter is a directory. If this occurs, the program passes the string to method LoadFilesInDirectory. Because the C:\ directory already includes a backslash, a backslash is not needed when combining currentDirectory and chosen (line 50). However, other directories must include the slash (lines 52–53). Finally, displayLabel is updated with the new directory (line 58). This program loads quickly, because it indexes only the files in the current directory. This means that a small delay may occur when a new directory is loaded. In addition, changes in the directory structure can be shown by reloading a directory. The previous program (Fig. 14.28) may have a large initial delay as it loads an entire directory structure. This type of trade-off is typical in the software world.
Software Engineering Observation 14.2 When designing applications that run for long periods of time, you might choose a large initial delay to improve performance throughout the rest of the program. However, in applications that run for only short periods of time, developers often prefer fast initial loading times and small delays after each action. 14.2
14.11 TabControl Control The TabControl control creates tabbed windows, such as the ones we have seen in Visual Studio (Fig. 14.32). This allows you to specify more information in the same space on a Form. TabControls contain TabPage objects, which are similar to Panels and GroupBoxes in that TabPages also can contain controls. You first add controls to the TabPage objects, then add the TabPages to the TabControl. Only one TabPage is displayed at a time. To add objects to the TabPage and the TabControl, write myTabPage.Controls.Add(myControl) myTabControl.Controls.Add(myTabPage)
These statements call method Add of the Controls collection. The example adds TabControl myControl to TabPage myTabPage, then adds myTabPage to myTabControl. Alternatively, we can use method AddRange to add an array of TabPages or controls to a TabControl or TabPage, respectively. Figure 14.33 depicts a sample TabControl. You can add TabControls visually by dragging and dropping them onto a Form in Design mode. To add TabPages in Design mode, right click the TabControl and select Add Tab (Fig. 14.34). Alternatively, click the TabPages property in the Properties window, and add tabs in the dialog that appears. To change a tab label, set the Text property of the TabPage. Note that clicking the tabs selects the TabControl—to select the TabPage, click the control area underneath the tabs. You can add controls to the TabPage by dragging and dropping
14.11 TabControl Control
547
Tab windows
Fig. 14.32 | Tabbed windows in Visual Studio.
TabPage
TabControl
Controls in TabPage
Fig. 14.33 |
TabControl
with TabPages example.
items from the ToolBox. To view different TabPages, click the appropriate tab (in either design or run mode). Common properties and a common event of TabControls are described in Fig. 14.35. Each TabPage generates a Click event when its tab is clicked. Event handlers for this event can be created by double clicking the body of the TabPage. Class UsingTabsForm (Fig. 14.36) uses a TabControl to display various options relating to the text on a label (Color, Size and Message). The last TabPage displays an About message, which describes the use of TabControls.
548
Chapter 14
Fig. 14.34 | TabControl
Graphical User Interface Concepts: Part 2
TabPages
added to a TabControl.
properties
and event
Description
Common Properties ImageList
Specifies images to be displayed on tabs.
ItemSize
Specifies the tab size.
Multiline
Indicates whether multiple rows of tabs can be displayed.
SelectedIndex
Index of the selected TabPage.
SelectedTab
The selected TabPage.
TabCount
Returns the number of tab pages.
TabPages
Collection of TabPages within the TabControl.
Common Event SelectedIndexChanged
Fig. 14.35 | 1 2 3 4
Generated when SelectedIndex changes (i.e., another TabPage is selected).
TabControl
properties and event.
// Fig. 14.36: UsingTabsForm.cs // Using TabControl to display various font settings. using System; using System.Drawing;
Fig. 14.36 |
TabControl
used to display various font settings. (Part 1 of 3.)
using System.Windows.Forms; // Form uses Tabs and RadioButtons to display various font settings public partial class UsingTabsForm : Form { // default constructor public UsingTabsForm() { InitializeComponent(); } // end constructor // event handler for Black RadioButton private void blackRadioButton_CheckedChanged( object sender, EventArgs e ) { displayLabel.ForeColor = Color.Black; // change font color to black } // end method blackRadioButton_CheckedChanged // event handler for Red RadioButton private void redRadioButton_CheckedChanged( object sender, EventArgs e ) { displayLabel.ForeColor = Color.Red; // change font color to red } // end method redRadioButton_CheckedChanged // event handler for Green RadioButton private void greenRadioButton_CheckedChanged( object sender, EventArgs e ) { displayLabel.ForeColor = Color.Green; // change font color to green } // end method greenRadioButton_CheckedChanged // event handler for 12 point RadioButton private void size12RadioButton_CheckedChanged( object sender, EventArgs e ) { // change font size to 12 displayLabel.Font = new Font( displayLabel.Font.Name, 12 ); } // end method size12RadioButton_CheckedChanged // event handler for 16 point RadioButton private void size16RadioButton_CheckedChanged( object sender, EventArgs e ) { // change font size to 16 displayLabel.Font = new Font( displayLabel.Font.Name, 16 ); } // end method size16RadioButton_CheckedChanged // event handler for 20 point RadioButton private void size20RadioButton_CheckedChanged( object sender, EventArgs e ) {
Fig. 14.36 |
TabControl
used to display various font settings. (Part 2 of 3.)
// change font size to 20 displayLabel.Font = new Font( displayLabel.Font.Name, 20 ); } // end method size20RadioButton_CheckedChanged // event handler for Hello! RadioButton private void helloRadioButton_CheckedChanged( object sender, EventArgs e ) { displayLabel.Text = "Hello!"; // change text to Hello! } // end method helloRadioButton_CheckedChanged // event handler for Goodbye! RadioButton private void goodbyeRadioButton_CheckedChanged( object sender, EventArgs e ) { displayLabel.Text = "Goodbye!"; // change text to Goodbye! } // end method goodbyeRadioButton_CheckedChanged } // end class UsingTabsForm
(a)
(b)
(c)
(d)
Fig. 14.36 |
TabControl
used to display various font settings. (Part 3 of 3.)
The textOptionsTabControl and the colorTabPage, sizeTabPage, messageTabPage and aboutTabPage are created in the designer (as described previously). The colorTabPage contains three RadioButtons for the colors black (blackRadioButton), red (redRadioButton)
14.12 Multiple Document Interface (MDI) Windows
551
and green (greenRadioButton). This TabPage is displayed in Fig. 14.36(a). The CheckChanged event handler for each RadioButton updates the color of the text in displayLabel (lines 20, 27 and 34). The sizeTabPage (Fig. 14.36(b)) has three RadioButtons, corresponding to font sizes 12 (size12RadioButton), 16 (size16RadioButton) and 20 (size20RadioButton), which change the font size of displayLabel—lines 42, 50 and 58, respectively. The messageTabPage (Fig. 14.36(c)) contains two RadioButtons for the messages Hello! (helloRadioButton) and Goodbye! (goodbyeRadioButton). The two RadioButtons determine the text on displayLabel (lines 65 and 72, respectively). The aboutTabPage (Fig. 14.36(d)) contains a Label (messageLabel) describing the purpose of TabControls.
Software Engineering Observation 14.3 A TabPage can act as a container for a single logical group of RadioButtons, enforcing their mutual exclusivity. To place multiple RadioButton groups inside a single TabPage, you should group RadioButtons within Panels or GroupBoxes contained within the TabPage. 14.3
14.12 Multiple Document Interface (MDI) Windows In previous chapters, we have built only single document interface (SDI) applications. Such programs (including Microsoft’s Notepad and Paint) can support only one open window or document at a time. SDI applications usually have limited abilities—Paint and Notepad, for example, have limited image- and text-editing features. To edit multiple documents, the user must execute another instance of the SDI application. Most recent applications are multiple document interface (MDI) programs, which allow users to edit multiple documents at once (e.g. Microsoft Office products). MDI programs also tend to be more complex—PaintShop Pro and Photoshop have a greater number of image-editing features than does Paint. The main application window of an MDI program is called the parent window, and each window inside the application is referred to as a child window. Although an MDI application can have many child windows, each has only one parent window. Furthermore, a maximum of one child window can be active at once. Child windows cannot be parents themselves and cannot be moved outside their parent. Otherwise, a child window behaves like any other window (with regard to closing, minimizing, resizing, etc.). A child window’s functionality can be different from the functionality of other child windows of the parent. For example, one child window might allow the user to edit images, another might allow the user to edit text and a third might display network traffic graphically, but all could belong to the same MDI parent. Figure 14.37 depicts a sample MDI application. To create an MDI Form, create a new Form and set its IsMdiContainer property to true. The Form changes appearance, as in Fig. 14.38. Next, create a child Form class to be added to the Form. To do this, right click the project in the Solution Explorer, select Project > Add Windows Form… and name the file. Edit the Form as you like. To add the child Form to the parent, we must create a new child Form object, set its MdiParent property to the parent Form and call the child Form’s Show method. In general, to add a child Form to a parent, write ChildFormClass childForm = New ChildFormClass(); childForm.MdiParent = parentForm; childForm.Show();
552
Chapter 14
Graphical User Interface Concepts: Part 2
MDI parent
MDI child MDI child
Fig. 14.37 | MDI parent window and MDI child windows. Single Document Interface (SDI)
Multiple Document Interface (MDI)
Fig. 14.38 | SDI and MDI forms. In most cases, the parent Form creates the child, so the parentForm reference is this. The code to create a child usually lies inside an event handler, which creates a new window in response to a user action. Menu selections (such as File, followed by a submenu option of New, followed by a submenu option of Window) are common techniques for creating new child windows. Class Form property MdiChildren returns an array of child Form references. This is useful if the parent window wants to check the status of all its children (for example, ensuring that all are saved before the parent closes). Property ActiveMdiChild returns a reference to the active child window; it returns Nothing if there are no active child windows. Other features of MDI windows are described in Fig. 14.39. Child windows can be minimized, maximized and closed independently of each other and the parent window. Figure 14.40 shows two images: one containing two minimized child windows and a second containing a maximized child window. When the parent is
14.12 Multiple Document Interface (MDI) Windows
MDI Form properties, a method and event
553
Description
Common MDI Child Properties IsMdiChild
Indicates whether the Form is an MDI child. If true, Form is an MDI child (read-only property).
MdiParent
Specifies the MDI parent Form of the child.
Common MDI Parent Properties ActiveMdiChild
Returns the Form that is the currently active MDI child (returns null if no children are active).
IsMdiContainer
Indicates whether a Form can be an MDI parent. If true, the Form can be an MDI parent. The default value is false.
MdiChildren
Returns the MDI children as an array of Forms.
Common Method LayoutMdi
Determines the display of child forms on an MDI parent. The method takes as a parameter an MdiLayout enumeration with possible values ArrangeIcons, Cascade, TileHorizontal and TileVertical. Figure 14.42 depicts the effects of these values.
Common Event MdiChildActivate
Generated when an MDI child is closed or activated.
Fig. 14.39 | MDI parent and MDI child properties, method and event.
minimized or closed, the child windows are minimized or closed as well. Notice that the title bar in Fig. 14.40(b) is Form1 - [Child2]. When a child window is maximized, its title bar text is inserted into the parent window’s title bar. When a child window is minimized or maximized, its title bar displays a restore icon, which can be used to return the child window to its previous size (its size before it was minimized or maximized). C# provides a property that helps track which child windows are open in an MDI container. Property MdiWindowListItem of class MenuStrip specifies which menu, if any, displays a list of open child windows. When a new child window is opened, an entry is added to the list (as in the first screen of Figure 14.41). If nine or more child windows are open, the list includes the option More Windows..., which allows the user to select a window from a list in a dialog.
Good Programming Practice 14.1 When creating MDI applications, include a menu that displays a list of the open child windows. This helps the user select a child window quickly, rather than having to search for it in the parent window. 14.1
554
Chapter 14
Graphical User Interface Concepts: Part 2
Parent window icons: minimize, maximize and close (a)
Maximized child window icons: minimize, restore and
(b)
Minimized child window icons: restore, maximize and close
Parent title bar indicates maximized child
Fig. 14.40 | Minimized and maximized child windows. Child windows list
9 or more child windows enables the More Windows… option
Fig. 14.41 |
MenuItem
property MdiList example.
14.12 Multiple Document Interface (MDI) Windows
555
MDI containers allow you to organize the placement ofs child windows. The child windows in an MDI application can be arranged by calling method LayoutMdi of the parent Form. Method LayoutMdi takes a MdiLayout enumeration, which can have values ArrangeIcons, Cascade, TileHorizontal and TileVertical. Tiled windows completely fill the parent and do not overlap; such windows can be arranged horizontally (value TileHorizontal) or vertically (value TileVertical). Cascaded windows (value Cascade) overlap—each is the same size and displays a visible title bar, if possible. Value ArrangeIcons arranges the icons for any minimized child windows. If minimized windows are scattered around the parent window, value ArrangeIcons orders them neatly at the bottom-left corner of the parent window. Figure 14.42 illustrates the values of the MdiLayout enumeration.
(a) ArrangeIcons
(b) Cascade
(c) TileHorizontal
(d) TileVertical
Fig. 14.42
| MdiLayout
enumeration values.
556
Chapter 14
Graphical User Interface Concepts: Part 2
Class UsingMDIForm (Fig. 14.43) demonstrates MDI windows. Class UsingMDIForm uses three instances of child Form ChildForm (Fig. 14.44), each containing a PictureBox that displays an image. The parent MDI Form contains a menu enabling users to create and arrange child Forms. The program in Fig. 14.43 is the application. The MDI parent Form, which is created first, contains two top-level menus. The first of these menus, File (fileToolStripMenuItem), contains both an Exit item (exitToolStripMenuItem) and a New submenu (newToolStripMenuItem) consisting of items for each child window. The second menu, Window (windowToolStripMenuItem), provides options for laying out the MDI children, plus a list of the active MDI children. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
// Fig. 14.43: UsingMDIForm.cs // Demonstrating use of MDI parent and child windows. using System; using System.Windows.Forms; // Form demonstrates the use of MDI parent and child windows public partial class UsingMDIForm : Form { // default constructor public UsingMDIForm() { InitializeComponent(); } // end constructor // create Child 1 window when child1ToolStrip MenuItem is clicked private void child1ToolStripMenuItem_Click( object sender, EventArgs e ) { // create new child ChildForm formChild = new ChildForm( "Child 1", @"\images\csharphtp1.jpg" ); formChild.MdiParent = this; // set parent formChild.Show(); // display child } // end method child1ToolStripMenuItem_Click // create Child 2 window when child2ToolStripMenuItem is clicked private void child2ToolStripMenuItem_Click( object sender, EventArgs e ) { // create new child ChildForm formChild = new ChildForm( "Child 2", @"\images\vbnethtp2.jpg" ); formChild.MdiParent = this; // set parent formChild.Show(); // display child } // end method child2ToolStripMenuItem_Click // create Child 3 window when child3ToolStripMenuItem is clicked private void child3ToolStripMenuItem_Click( object sender, EventArgs e ) {
Fig. 14.43 | MDI parent-window class. (Part 1 of 3.)
// create new child Child formChild = new Child( "Child 3", @"\images\pythonhtp1.jpg" ); formChild.MdiParent = this; // set parent formChild.Show(); // display child } // end method child3ToolStripMenuItem_Click // exit application private void exitToolStripMenuItem_Click( object sender, EventArgs e ) { Application.Exit(); } // end method exitToolStripMenuItem_Click // set Cascade layout private void cascadeToolStripMenuItem_Click( object sender, EventArgs e ) { this.LayoutMdi( MdiLayout.Cascade ); } // end method cascadeToolStripMenuItem_Click // set TileHorizontal layout private void tileHorizontalToolStripMenuItem_Click( object sender, EventArgs e ) { this.LayoutMdi( MdiLayout.TileHorizontal ); } // end method tileHorizontalToolStripMenuItem // set TileVertical layout private void tileVerticalToolStripMenuItem_Click( object sender, EventArgs e ) { this.LayoutMdi( MdiLayout.TileVertical ); } // end method tileVerticalToolStripMenuItem_Click } // end class UsingMDIForm
(b)
Fig. 14.43 | MDI parent-window class. (Part 2 of 3.)
558
Chapter 14
Graphical User Interface Concepts: Part 2
(c)
(d)
Fig. 14.43 | MDI parent-window class. (Part 3 of 3.) In the Properties window, we set the Form’s IsMdiContainer property to true, making the Form an MDI parent. In addition, we set the MenuStrip’s MdiWindowListItem property to windowToolStripMenuItem. This enables the Window menu to contain the list of child MDI windows. The Cascade menu item (cascadeToolStripMenuItem) has an event handler (cascadeToolStripMenuItem_Click, lines 55–59) that arranges the child windows in a cascading manner. The event handler calls method LayoutMdi with the argument Cascade from the MdiLayout enumeration (line 58). The Tile Horizontal menu item (tileHorizontalToolStripMenuItem) has an event handler (tileHorizontalToolStripMenuItem_Click, lines 62–66) that arranges the child windows in a horizontal manner. The event handler calls method LayoutMdi with the argument TileHorizontal from the MdiLayout enumeration (line 65). Finally, the Tile Vertical menu item (tileVerticalToolStripMenuItem) has an event handler (tileVerticalToolStripMenuItem_Click, lines 69–73) that arranges the child windows in a vertical manner. The event handler calls method LayoutMdi with the argument TileVertical from the MdiLayout enumeration (line 72). At this point, the application is still incomplete—we must define the MDI child class. To do this, right click the project in the Solution Explorer and select Add > Windows Form…. Then name the new class in the dialog as ChildForm (Fig. 14.44). Next, we add a PictureBox (picDisplay) to ChildForm. In the constructor, line 15 sets the title bar text. Lines 18–19 set ChildForm’s Image property to an Image, using method FromFile. 1 2 3 4 5 6 7
// Fig. 14.44: ChildForm.cs // Child window of MDI parent. using System; using System.Drawing; using System.Windows.Forms; using System.IO;
public partial class ChildForm : Form { public ChildForm( string title, string fileName ) { // Required for Windows Form Designer support InitializeComponent(); Text = title; // set title text // set image to display in pictureBox picDisplay.Image = Image.FromFile( Directory.GetCurrentDirectory() + fileName ); } // end constructor } // end class ChildForm
Fig. 14.44 | MDI child ChildForm. (Part 2 of 2.) After the MDI child class is defined, the parent MDI Form (Fig. 14.43) can create new child windows. The event handlers in lines 16–46 create a new child Form corresponding to the menu item clicked. Lines 20–21, 31–32 and 42–43 create new instances of ChildForm. Lines 22, 33 and 44 set each Child’s MdiParent property to the parent Form. Lines 23, 34 and 45 call method Show to display each child Form.
14.13 Visual Inheritance Chapter 10 discussed how to create classes by inheriting from other classes. We have also used inheritance to create Forms that display a GUI, by deriving our new Form classes from class System.Windows.Forms.Form. This is an example of visual inheritance. The derived Form class contains the functionality of its Form base class, including any base-class properties, methods, variables and controls. The derived class also inherits all visual aspects— such as sizing, component layout, spacing between GUI components, colors and fonts— from its base class. Visual inheritance enables you to achieve visual consistency across applications. For example, you could define a base Form that contains a product’s logo, a specific background color, a predefined menu bar and other elements. You then could use the base Form throughout an application for uniformity and branding. Class VisualInheritanceForm (Fig. 14.45) derives from Form. The output depicts the workings of the program. The GUI contains two Labels with text Bugs, Bugs, Bugs and Copyright 2006, by deitel.com., as well as one Button displaying the text Learn More. When a user presses the Learn More Button, method learnMoreButton_Click (lines 16– 22) is invoked. This method displays a MessageBox that provides some informative text. 1 2 3 4
// Fig. 14.45: VisualInheritanceForm.cs // Base Form for use with visual inheritance. using System; using System.Windows.Forms;
Fig. 14.45 | Class VisualInheritanceForm, which inherits from class Form, contains a Button (Learn More).
// base Form used to demonstrate visual inheritance public partial class VisualInheritanceForm : Form { // default constructor public VisualInheritanceForm() { InitializeComponent(); } // end constructor // display MessageBox when Button is clicked private void learnMoreButton_Click( object sender, EventArgs e ) { MessageBox.Show( "Bugs, Bugs, Bugs is a product of deitel.com", "Learn More", MessageBoxButtons.OK, MessageBoxIcon.Information ); } // end method learnMoreButton_Click } // end class VisualInheritanceForm
Fig. 14.45 | Class VisualInheritanceForm, which inherits from class Form, contains a Button (Learn More).
(Part 2 of 2.)
To allow other Forms to inherit from VisualInheritanceForm, we must package VisualInheritanceForm as a .dll (class library). Right click the project name in the Soluand select Properties, then choose the Application tab. In the Output type drop-down list, change Windows Application to Class Library. Building the project produces the .dll. To visually inherit from VisualInheritanceForm, first create a new Windows application. In this application, add a reference to the .dll you just created (located in the previous application’s bin/Release folder). Then open the file that defines the new application’s GUI and modify the first line of the class so that it inherits from class VisualInheritanceForm. Note that you will only need to specify the class name. In design view, the new application’s Form should now display the controls of the base Form (Fig. 14.46). We can still add more components to the Form. Class VisualInheritanceTestForm (Fig. 14.47) is a derived class of VisualInheritanceForm. The output illustrates the functionality of the program. The GUI contains those components derived from class VisualInheritanceForm, as well as an additional Button with text Learn The Program. When a user presses this Button, method tion Explorer
14.13 Visual Inheritance
Fig. 14.46 |
Form
561
demonstrating visual inheritance.
learnProgramButton_Click (lines 17–22) is invoked. This method displays another MessageBox providing different informative text.
Figure 14.47 demonstrates that the components, their layouts and the functionality of base-class VisualInheritanceForm (Fig. 14.45) are inherited by VisualInheritanceTestForm. If a user clicks the button Learn More, the base class event handler learnMoreButton_Click displays a MessageBox. VisualInheritanceForm uses a private access modifier to declare its controls, so class VisualInheritanceTestForm cannot modify the controls inherited from class VisualInheritanceForm. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
// Fig. 14.47: VisualInheritanceTestForm.cs // Derived Form using visual inheritance. using System; using System.Windows.Forms; // derived form using visual inheritance public partial class VisualInheritanceTestForm : VisualInheritanceForm // code for inheritance { // default constructor public VisualInheritanceTestForm() { InitializeComponent(); } // end constructor // display MessageBox when Button is clicked private void learnProgramButton_Click(object sender, EventArgs e) { MessageBox.Show( "This program was created by Deitel & Associates", "Learn the Program", MessageBoxButtons.OK, MessageBoxIcon.Information ); } // end method learnProgramButton_Click } // end class VisualInheritanceTestForm
Fig. 14.47 | Class VisualInheritanceTestForm, which inherits from class VisualInheritanceForm,
contains an additional Button. (Part 1 of 2.)
562
Chapter 14
Graphical User Interface Concepts: Part 2
Derived class cannot modify these controls.
Derived class can modify this control.
Fig. 14.47 | Class VisualInheritanceTestForm, which inherits from class VisualInheritanceForm,
contains an additional Button. (Part 2 of 2.)
14.14 User-Defined Controls The .NET Framework allows you to create custom controls. These custom controls appear in the user’s Toolbox and can be added to Forms, Panels or GroupBoxes in the same way that we add Buttons, Labels and other predefined controls. The simplest way to create a custom control is to derive a class from an existing control, such as a Label. This is useful if you want to add functionality to an existing control, rather than having to reimplement the existing control to include the desired functionality. For example, you can create a new type of Label that behaves like a normal Label but has a different appearance. You accomplish this by inheriting from class Label and overriding method OnPaint. All controls contain method OnPaint, which the system calls when a component must be redrawn (such as when the component is resized). Method OnPaint is passed a PaintEventArgs object, which contains graphics information—property Graphics is the graphics object used to draw, and property ClipRectangle defines the rectangular boundary of the control. Whenever the system raises the Paint event, our control’s base class catches the event. Through polymorphism, our control’s OnPaint method is called. Our base class’s OnPaint implementation is not called, so we must call it explicitly from our OnPaint implementation before we execute our custom-paint code. In most cases, you want to do this to ensure that the original painting code executes in addition to the code you define in the custom control’s class. Alternately, if we do not wish to let the base class OnPaint method execute, we do not call it. To create a new control composed of existing controls, use class UserControl. Controls added to a custom control are called constituent controls. For example, a programmer could create a UserControl composed of a Button, a Label and a TextBox, each associated with some functionality (for example, the Button setting the Label’s text to that contained in the TextBox). The UserControl acts as a container for the controls added to
14.14 User-Defined Controls
563
it. The UserControl contains constituent controls, so it does not determine how these constituent controls are displayed. Method OnPaint of the UserControl cannot be overridden. To control the appearance of each constituent control, you must handle each control’s Paint event. The Paint event handler is passed a PaintEventArgs object, which can be used to draw graphics (lines, rectangles, etc.) on the constituent controls. Using another technique, a programmer can create a brand new control by inheriting from class Control. This class does not define any specific behavior; that task is left to you. Instead, class Control handles the items associated with all controls, such as events and sizing handles. Method OnPaint should contain a call to the base class’s OnPaint method, which calls the Paint event handlers. You must then add code that draws custom graphics inside the overridden OnPaint method when drawing the control. This technique allows for the greatest flexibility, but also requires the most planning. All three approaches are summarized in Fig. 14.48. We create a “clock” control in Fig. 14.49. This is a UserControl composed of a Label and a Timer—whenever the Timer raises an event, the Label is updated to reflect the current time. Timers (System.Windows.Forms namespace) are invisible components that reside on a Form, generating Tick events at a set interval. This interval is set by the Timer’s Interval Custom control techniques and PaintEventArgs properties
Description
Custom Control Techniques Inherit from Windows Forms control
You can do this to add functionality to a pre-existing control. If you override method OnPaint, call the base class’s OnPaint method. Note that you only can add to the original control’s appearance, not redesign it.
Create a UserControl
You can create a UserControl composed of multiple pre-existing controls (e.g., to combine their functionality). Note that you cannot override the OnPaint methods of custom controls. Instead, you must place drawing code in a Paint event handler. Again, note that you only can add to the original control’s appearance, not redesign it
Inherit from class Control
Define a brand new control. Override method OnPaint, then call base class method OnPaint and include methods to draw the control. With this method you can customize control appearance and functionality.
PaintEventArgs
Properties
Graphics
The graphics object of the control. It is used to draw on the control.
ClipRectangle
Specifies the rectangle indicating the boundary of the control.
// Fig. 14.49: ClockUserControl.cs // User-defined control with a timer and a Label. using System; using System.Windows.Forms; // UserControl that displays the time on a Label public partial class ClockUserControl : UserControl { // default constructor public ClockUserControl() { InitializeComponent(); } // end constructor // update Label at every tick private void clockTimer_Tick(object sender, EventArgs e) { // get current time (Now), convert to string displayLabel.Text = DateTime.Now.ToLongTimeString(); } // end method clockTimer_Tick } // end class ClockUserControl
Fig. 14.49 |
UserControl-defined
clock.
property, which defines the number of milliseconds (thousandths of a second) between events. By default, timers are disabled and do not generate events. This application contains a user control (ClockUserControl) and a Form that displays the user control. We begin by creating a Windows application. Next, we create a UserControl class for the project by selecting Project > Add User Control…. This displays a dialog from which we can select the type of control to add—user controls are already selected. We then name the file (and the class) ClockUserControl. Our empty ClockUserControl is displayed as a grey rectangle. You can treat this control like a Windows Form, meaning that you can add controls using the ToolBox and set properties using the Properties window. However, instead of creating an application, you are simply creating a new control composed of other controls. Add a Label (displayLabel) and a Timer (clockTimer) to the UserControl. Set the Timer interval to 1000 milliseconds and set displayLabel’s text with each event (lines 16– 20). To generate events, clockTimer must be enabled by setting property Enabled to true in the Properties window. Structure DateTime (namespace System) contains property Now, which is the current time. Method ToLongTimeString converts Now to a string containing the current hour,
14.15 Wrap-Up
565
minute and second (along with AM or PM). We use this to set the time in displayLabel in line 19. Once created, our clock control appears as an item on the ToolBox. You may need to switch to the application’s Form before the item appears in the ToolBox. To use the control, simply drag it to the Form and run the Windows application. We gave the ClockUserControl object a white background to make it stand out in the Form. Figure 14.49 shows the output of ClockForm, which contains our ClockUserControl. There are no event handlers in ClockForm, so we show only the code for ClockUserControl. Visual Studio allows you to share custom controls with other developers. To create a UserControl that can be exported to other solutions, do the following: 1. Create a new Class Library project. 2. Delete Class1.cs, initially provided with the application. 3. Right click the project in the Solution Explorer and select Add > User Control…. In the dialog that appears, name the user control file and click Add. 4. Inside the project, add controls and functionality to the (Fig. 14.50).
UserControl
5. Build the project. Visual Studio creates a .dll file for the UserControl in the output directory (bin/Release). The file is not executable; class libraries are used to define classes that are reused in other executable applications. 6. Create a new Windows application. 7. In the new Windows application, right click the ToolBox and select Choose Items…. In the Choose Toolbox Items dialog that appears, click Browse…. Browse for the .dll file from the class library created in Steps 1–5. The item will then appear in the Choose Toolbox Items dialog (Fig. 14.51). If it is not already checked, check this item. Click OK to add the item to the Toolbox. This control can now be added to the Form as if it were any other control (Fig. 14.52).
14.15 Wrap-Up Many of today’s commercial applications provide GUIs that are easy to use and manipulate. Because of this demand for user-friendly GUIs, the ability to design sophisticated GUIs is an essential programming skill. Visual Studio’s IDE makes GUI development quick and easy. In Chapters 13 and 14, we presented basic GUI development techniques. In Chapter 14, we demonstrated how to create menus, which provide users easy access to
Fig. 14.50 | Custom-control creation.
566
Chapter 14
Graphical User Interface Concepts: Part 2
Fig. 14.51 | Custom control added to the ToolBox. New ToolBox
Newly inserted control
Fig. 14.52 | Custom control added to a Form. an application’s functionality. You learned the DateTimePicker and MonthCalendar controls, which allow users to input date and time values. We demonstrated LinkLabels, which are used to link the user to an application or a Web page. You used several controls that provide lists of data to the user—ListBoxes, CheckedListBoxes and ListViews. We used the ComboBox control to create drop-down lists, and the TreeView control to display data in hierarchical form. We then introduced complex GUIs that use tabbed windows and multiple document interfaces. The chapter concluded with demonstrations of visual inheritance and creating custom controls. The next chapter explores multithreading. In many programming languages, you can create multiple threads, enabling several activities to proceed in parallel.
15 Multithreading Do not block the way of inquiry. —Charles Sanders Peirce
A person with one watch knows what time it is; a person with two watches is never sure. —Proverb
OBJECTIVES In this chapter you will learn: I
What threads are and why they are useful.
Learn to labor and to wait.
I
How threads enable you to manage concurrent activities.
—Henry Wadsworth Longfellow
I
The life cycle of a thread.
I
Thread priorities and scheduling.
I
To create and execute Threads.
I
Thread synchronization.
I
What producer/consumer relationships are and how they are implemented with multithreading.
I
To display output from multiple threads in a GUI.
The most general definition of beauty…Multeity in Unity. —Samuel Taylor Coleridge
The world is moving so fast these days that the man who says it can’t be done is generally interrupted by someone doing it. —Elbert Hubbard
Introduction Thread States: Life Cycle of a Thread Thread Priorities and Thread Scheduling Creating and Executing Threads Thread Synchronization and Class Monitor Producer/Consumer Relationship without Thread Synchronization Producer/Consumer Relationship with Thread Synchronization Producer/Consumer Relationship: Circular Buffer Multithreading with GUIs Wrap-Up
15.1 Introduction It would be nice if we could perform one action at a time and perform it well, but that is usually difficult to do. The human body performs a great variety of operations in parallel—or, as we will say throughout this chapter, concurrently. Respiration, blood circulation and digestion, for example, can occur concurrently. All the senses—sight, touch, smell, taste and hearing—can be employed at once. Computers, too, perform operations concurrently. It is common for your computer to be compiling a program, sending a file to a printer and receiving electronic mail messages over a network concurrently. Ironically, most programming languages do not enable programmers to specify concurrent activities. Rather, programming languages generally provide only a simple set of control statements that enable programmers to perform one action at a time, proceeding to the next action after the previous one has finished. Historically, the type of concurrency that computers perform today generally has been implemented as operating system “primitives” available only to highly experienced “systems programmers.” The Ada programming language, developed by the United States Department of Defense, made concurrency primitives widely available to defense contractors building military command-and-control systems. However, Ada has not been widely used in academia and commercial industry. The .NET Framework Class Library provides concurrency primitives. You specify that applications contain “threads of execution,” each of which designates a portion of a program that may execute concurrently with other threads—this capability is called multithreading. Multithreading is available to all .NET programming languages, including C#, Visual Basic and Visual C++. The .NET Framework Class Library includes multithreading capabilities in namespace System.Threading.
Performance Tip 15.1 A problem with single-threaded applications is that lengthy activities must complete before other activities can begin. In a multithreaded application, threads can be distributed across multiple processors (if they are available) so that multiple tasks are performed concurrently, allowing the application to operate more efficiently. Multithreading can also increase performance on singleprocessor systems that simulate concurrency—when one thread cannot proceed, another can use the processor. 15.1
15.2 Thread States: Life Cycle of a Thread
569
We discuss many applications of concurrent programming. When programs download large files, such as audio clips or video clips over the Internet, users do not want to wait until an entire clip downloads before starting the playback. To solve this problem, we can put multiple threads to work—one thread downloads a clip, while another plays the clip. These activities proceed concurrently. To avoid choppy playback, we synchronize the threads so that the player thread does not begin until there is a sufficient amount of the clip in memory to keep the player thread busy. Another example of multithreading is the CLR’s automatic garbage collection. C and C++ require programmers to reclaim dynamically allocated memory explicitly. The CLR provides a garbage-collector thread, which reclaims dynamically allocated memory that is no longer needed.
Good Programming Practice 15.1 Set an object reference to null when the program no longer needs that object. This enables the garbage collector to determine at the earliest possible moment that the object can be garbage collected. If such an object has other references to it, that object cannot be collected. 15.1
Writing multithreaded programs can be tricky. Although the human mind can perform functions concurrently, people find it difficult to jump between parallel “trains of thought.” To see why multithreading can be difficult to program and understand, try the following experiment: Open three books to page 1 and try reading the books concurrently. Read a few words from the first book, then read a few words from the second book, then read a few words from the third book, then loop back and read the next few words from the first book, etc. After this experiment, you will appreciate the challenges of multithreading—switching between books, reading briefly, remembering your place in each book, moving the book you are reading closer so you can see it, pushing books you are not reading aside and, amid all this chaos, trying to comprehend the content of the books!
15.2 Thread States: Life Cycle of a Thread At any time, a thread is said to be in one of several thread states that are illustrated in the UML state diagram of Fig. 15.1. This section discusses these states and the transitions between states. Two classes critical for multithreaded applications are Thread and Monitor (System.Threading namespace). This section also discusses several methods of classes Thread and Monitor that cause state transitions. A few of the terms in the diagram are discussed in later sections. A Thread object begins its life cycle in the Unstarted state when the program creates the object and passes a ThreadStart delegate to the object’s constructor. The ThreadStart delegate, which specifies the actions the thread will perform during its life cycle, must be initialized with a method that returns void and takes no arguments. [Note: .NET 2.0 also includes a ParameterizedThreadStart delegate to which you can pass a method that takes arguments. For more information, visit the site msdn2.microsoft.com/en-us/library/ xzehzsds.] The thread remains in the Unstarted state until the program calls the Thread’s Start method, which places the thread in the Running state and immediately returns control to the part of the program that called Start. Then the newly Running thread and any other threads in the program can execute concurrently on a multiprocessor system or share the processor on a system with a single processor.
570
Chapter 15
Multithreading
Unstarted
d pen Sus
Res
ume
in Jo p,
ee
e ls Pu
le, ab t ail es av qu un re ck I/O le, Lo ue ab n ail tio Iss av ple ck om c I/O
e
Stopped
Lo
rt
Suspended
bo
WaitSleepJoin
A plet Com
Sle In , P ep te ul int rru se erv p Al Wa al t, l, it ex , pir es Sl
Start
Running
Blocked
Fig. 15.1 | Thread life cycle. While in the Running state, the thread may not actually be executing all the time. The thread executes in the Running state only when the operating system assigns a processor to the thread. When a Running thread receives a processor for the first time, the thread begins executing the method specified by its ThreadStart delegate. A Running thread enters the Stopped (or Aborted) state when its ThreadStart delegate terminates, which normally indicates that the thread has completed its task. Note that a program can force a thread into the Stopped state by calling Thread method Abort on the appropriate Thread object. Method Abort throws a ThreadAbortException in the thread, normally causing the thread to terminate. When a thread is in the Stopped state and there are no references to the thread object, the garbage collector can remove the thread object from memory. [Note: Internally, when a thread’s Abort method is called, the thread actually enters the AbortRequested state before entering the Stopped state. The thread remains in the AbortRequested state while waiting to receive the pending ThreadAbortException. When Abort is called, if the thread is in the WaitSleepJoin, Suspended or Blocked state, the thread resides in its current state and the AbortRequested state, and cannot receive the ThreadAbortException until it leaves its current state.] A thread is considered Blocked if it is unable to use a processor even if one is available. For example, a thread becomes blocked when it issues an input/output (I/O) request. The operating system blocks the thread from executing until the operating system can complete the I/O request for which the thread is waiting. At that point, the thread returns to the Running state, so it can resume execution. Another case in which a thread becomes
15.3 Thread Priorities and Thread Scheduling
571
blocked is in thread synchronization (Section 15.5). A thread being synchronized must acquire a lock on an object by calling Monitor method Enter. If a lock is not available, the thread is blocked until the desired lock becomes available. [Note: The Blocked state is not an actual state in .NET. It is a conceptual state that describes a thread that is not Running.] There are three ways in which a Running thread enters the WaitSleepJoin state. If a thread encounters code that it cannot execute yet (normally because a condition is not satisfied), the thread can call Monitor method Wait to enter the WaitSleepJoin state. Once in this state, a thread returns to the Running state when another thread invokes Monitor method Pulse or PulseAll. Method Pulse moves the next waiting thread back to the Running state. Method PulseAll moves all waiting threads back to the Running state. A Running thread can call Thread method Sleep to enter the WaitSleepJoin state for a period of milliseconds specified as the argument to Sleep. A sleeping thread returns to the Running state when its designated sleep time expires. Sleeping threads cannot use a processor, even if one is available. Any thread that enters the WaitSleepJoin state by calling Monitor method Wait or by calling Thread method Sleep also leaves the WaitSleepJoin state and returns to the Running state if the sleeping or waiting Thread’s Interrupt method is called by another thread in the program. The Interrupt method causes a ThreadInterruptionException to be thrown in the interrupted thread. If a thread cannot continue executing (we will call this the dependent thread) unless another thread terminates, the dependent thread calls the other thread’s Join method to “join” the two threads. When two threads are “joined,” the dependent thread leaves the WaitSleepJoin state and re-enters the Running state when the other thread finishes execution (enters the Stopped state). If a Running Thread’s Suspend method is called, the Running thread enters the Suspended state. A Suspended thread returns to the Running state when another thread in the program invokes the Suspended thread’s Resume method. [Note: Internally, when a thread’s Suspend method is called, the thread actually enters the SuspendRequested state before entering the Suspended state. The thread remains in the SuspendRequested state while waiting to respond to the Suspend request. If the thread is in the WaitSleepJoin state or is blocked when its Suspend method is called, the thread resides in its current state and the SuspendRequested state, and cannot respond to the Suspend request until it leaves its current state.] Methods Suspend and Resume are now deprecated and should not be used. In Section 15.9, we show how to emulate these methods using thread synchronization. If a thread’s IsBackground property is set to true, the thread resides in the Background state (not shown in Fig. 15.1). A thread can reside in the Background state and any other state simultaneously. A process must wait for all foreground threads (threads not in the Background state) to finish executing and enter the Stopped state before the process can terminate. However, if the only threads remaining in a process are Background threads, the CLR terminates each thread by invoking its Abort method, and the process terminates.
15.3 Thread Priorities and Thread Scheduling Every thread has a priority in the range between ThreadPriority.Lowest to ThreadPriority.Highest. These values come from the ThreadPriority enumeration (namespace System.Threading), which consists of the values Lowest, BelowNormal, Normal, AboveNormal and Highest. By default, each thread has priority Normal.
572
Chapter 15
Multithreading
The Windows operating system supports a concept, called timeslicing, that enables threads of equal priority to share a processor. Without timeslicing, each thread in a set of equal-priority threads runs to completion (unless the thread leaves the Running state and enters the WaitSleepJoin, Suspended or Blocked state) before the thread’s peers get a chance to execute. With timeslicing, each thread receives a brief burst of processor time, called a quantum, during which the thread can execute. At the completion of the quantum, even if the thread has not finished executing, the processor is taken away from that thread and given to the next thread of equal priority, if one is available. The job of the thread scheduler is to keep the highest-priority thread running at all times and, if there is more than one highest-priority thread, to ensure that all such threads execute for a quantum in round-robin fashion. Figure 15.2 illustrates the multilevel priority queue for threads. In Fig. 15.2, assuming a single-processor computer, threads A and B each execute for a quantum in round-robin fashion until both threads complete execution. This means that A gets a quantum of time to run. Then B gets a quantum. Then A gets another quantum. Then B gets another quantum. This continues until one thread completes. The processor then devotes all its power to the thread that remains (unless another thread of that priority is started). Next, thread C runs to completion. Threads D, E and F each execute for a quantum in round-robin fashion until they all complete execution. This process continues until all threads run to completion. Note that, depending on the operating system, new higher-priority threads could postpone—possibly indefinitely—the execution of lower-priority threads. Such indefinite postponement often is referred to more colorfully as starvation.
Ready threads
Priority Highest
A
Priority AboveNormal
C
B
Priority Normal
Priority BelowNormal
D
Priority Lowest
G
Fig. 15.2 | Thread-priority scheduling.
E
F
15.4 Creating and Executing Threads
573
A thread’s priority can be adjusted with the Priority property, which accepts values from the ThreadPriority enumeration. If the value specified is not one of the valid thread-priority constants, an ArgumentException occurs. A thread executes until it dies, becomes Blocked for I/O (or some other reason), calls Sleep, calls Monitor method Wait or Join, is pre-empted by a thread of higher priority or has its quantum expire. A thread with a higher priority than the Running thread can become Running (and hence pre-empt the first Running thread) if a sleeping thread wakes up, if I/O completes for a thread that Blocked for that I/O, if either Pulse or PulseAll is called on an object on which Wait was called, if a thread is Resumed from the Suspended state or if a thread to which the high-priority thread was joined completes.
15.4 Creating and Executing Threads Figure 15.3 demonstrates basic threading techniques, including constructing Thread objects and the use of the Thread class’s static method Sleep. The program creates three threads of execution, each with the default priority Normal. Each thread displays a message indicating that it is going to sleep for a random interval of from 0 to 5000 milliseconds, then goes to sleep. When each thread awakens, the thread displays its name, indicates that it is done sleeping, terminates and enters the Stopped state. You will see that method Main (i.e., the Main thread of execution) terminates before the application terminates. The program consists of two classes—ThreadTester (lines 7–35), which creates the three threads, and MessagePrinter (lines 38–64), which defines a Print method containing the actions each thread will perform. Objects of class MessagePrinter (lines 38–64) control the life cycle of each of the three threads created in class ThreadTester’s Main method. Class MessagePrinter con1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
// Fig. 15.3: ThreadTester.cs // Multiple threads printing at different intervals. using System; using System.Threading; // class ThreadTester demonstrates basic threading concepts class ThreadTester { static void Main( string[] args ) { // Create and name each thread. Use MessagePrinter's // Print method as argument to ThreadStart delegate. MessagePrinter printer1 = new MessagePrinter(); Thread thread1 = new Thread ( new ThreadStart( printer1.Print ) ); thread1.Name = "thread1"; MessagePrinter printer2 = new MessagePrinter(); Thread thread2 = new Thread ( new ThreadStart( printer2.Print ) ); thread2.Name = "thread2"; MessagePrinter printer3 = new MessagePrinter(); Thread thread3 = new Thread ( new ThreadStart( printer3.Print ) );
Fig. 15.3 | Threads sleeping and printing. (Part 1 of 3.)
thread3.Name = "thread3"; Console.WriteLine( "Starting threads" ); // call each thread's Start method to place each // thread in Running state thread1.Start(); thread2.Start(); thread3.Start(); Console.WriteLine( "Threads started\n" ); } // end method Main } // end class ThreadTester // Print method of this class used to control threads class MessagePrinter { private int sleepTime; private static Random random = new Random(); // constructor to initialize a MessagePrinter object public MessagePrinter() { // pick random sleep time between 0 and 5 seconds sleepTime = random.Next( 5001 ); // 5001 milliseconds } // end constructor // method Print controls thread that prints messages public void Print() { // obtain reference to currently executing thread Thread current = Thread.CurrentThread; // put thread to sleep for sleepTime amount of time Console.WriteLine( "{0} going to sleep for {1} milliseconds", current.Name, sleepTime ); Thread.Sleep( sleepTime ); // sleep for sleepTime milliseconds // print thread name Console.WriteLine( "{0} done sleeping", current.Name ); } // end method Print } // end class MessagePrinter
Starting threads thread1 going to sleep for 1603 milliseconds thread2 going to sleep for 2355 milliseconds thread3 going to sleep for 285 milliseconds Threads started thread3 done sleeping thread1 done sleeping thread2 done sleeping
Fig. 15.3 | Threads sleeping and printing. (Part 2 of 3.)
15.4 Creating and Executing Threads
575
Starting threads thread1 going to sleep for 4245 milliseconds thread2 going to sleep for 1466 milliseconds Threads started thread3 thread2 thread3 thread1
going to sleep for 1929 milliseconds done sleeping done sleeping done sleeping
Fig. 15.3 | Threads sleeping and printing. (Part 3 of 3.) sists of instance variable sleepTime (line 40), static variable random (line 41), a constructor (lines 44–48) and a Print method (lines 51–63). Variable sleepTime stores a random integer value chosen when a new MessagePrinter object’s constructor is called. Each thread controlled by a MessagePrinter object sleeps for the amount of time specified by the corresponding MessagePrinter object’s sleepTime. The MessagePrinter constructor (lines 44–48) initializes sleepTime to a random number of milliseconds from 0 up to, but not including, 5001 (i.e., from 0 to 5000). Method Print begins by obtaining a reference to the currently executing thread (line 54) via class Thread’s static property CurrentThread. The currently executing thread is the one that invokes method Print. Next, lines 57–58 display a message indicating the name of the currently executing thread and stating that the thread is going to sleep for a certain number of milliseconds. Note that line 58 uses the currently executing thread’s Name property to obtain the thread’s name (set in method Main when each thread is created). Line 59 invokes static Thread method Sleep to place the thread in the WaitSleepJoin state. At this point, the thread loses the processor, and the system allows another thread to execute if one is ready to run. When the thread awakens, it re-enters the Running state and waits to be assigned a processor by the thread scheduler. When the MessagePrinter object enters the Running state again, line 62 outputs the thread’s name in a message that indicates the thread is done sleeping, and method Print terminates. Class ThreadTester’s Main method (lines 9–34) creates three objects of class MessagePrinter, at lines 13, 17 and 21, respectively. Lines 14, 18 and 22 create and initialize three Thread objects. Note that each Thread’s constructor receives a ThreadStart delegate as an argument. A ThreadStart delegate represents a method with no arguments and a void return type that specifies the actions a thread will perform. Line 14 initializes the ThreadStart delegate for thread1 with printer1’s Print method. When thread1 enters the Running state for the first time, thread1 will invoke printer1’s Print method to perform the tasks specified in method Print’s body. Thus, thread1 will print its name, display the amount of time for which it will go to sleep, sleep for that amount of time, wake up and display a message indicating that the thread is done sleeping. At that point, method Print will terminate. A thread completes its task when the method specified by its ThreadStart delegate terminates, at which point the thread enters the Stopped state. When thread2 and thread3 enter the Running state for the first time, they invoke the Print methods of printer2 and printer3, respectively. thread2 and thread3 perform the same tasks as thread1 by executing the Print methods of the objects to which printer2 and printer3 refer (each of which has its own randomly chosen sleep time). Lines 15, 19 and 23 set each Thread’s Name property, which we use for output purposes.
576
Chapter 15
Multithreading
Error-Prevention Tip 15.1 Naming threads helps in the debugging of a multithreaded program. Visual Studio .NET’s debugger provides a Threads window that displays the name of each thread and enables you to view the execution of any thread in the program. 15.1
Lines 29–31 invoke each Thread’s Start method to place the threads in the Running state. Method Start returns immediately from each invocation, then line 33 outputs a message indicating that the threads were started, and the Main thread of execution terminates. The program itself does not terminate, however, because there are still non-background threads that are alive (i.e., the threads are Running and have not yet reached the Stopped state). The program will not terminate until its last non-background thread dies. When the system assigns a processor to a thread, the thread enters the Running state and calls the method specified by the thread’s ThreadStart delegate. In this program, each thread invokes method Print of the appropriate MessagePrinter object to perform the tasks discussed previously. Note that the sample outputs for this program show each thread and the thread’s sleep time in milliseconds as the thread goes to sleep. The thread with the shortest sleep time normally awakens first, then indicates that it is done sleeping and terminates. In Section 15.8, we discuss multithreading issues that could prevent the thread with the shortest sleep time from awakening first (none of this is guaranteed). Notice in the second sample output that thread1 and thread2 were able to report their sleep times before Main could output its final message. This means that the main thread’s quantum ended before it could finish executing Main, and thread1 and thread2 each got a chance to execute.
15.5 Thread Synchronization and Class Monitor Often, multiple threads of execution manipulate shared data. If threads with access to shared data simply read that data, then any number of threads could access that data simultaneously and no problems would arise. However, when multiple threads share data and that data is modified by one or more of those threads, then indeterminate results may occur. If one thread is in the process of updating the data and another thread tries to update it too, the data will reflect only the later update. If the data is an array or other data structure in which the threads could update separate parts of the data concurrently, it is possible that part of the data will reflect the information from one thread while part of the data will reflect information from another thread. When this happens, the program has difficulty determining when the data has been updated properly. The problem can be solved by giving one thread at a time exclusive access to code that manipulates the shared data. During that time, other threads wishing to manipulate the data should be kept waiting. When the thread with exclusive access to the data completes its data manipulations, one of the waiting threads should be allowed to proceed. In this fashion, each thread accessing the shared data excludes all other threads from doing so simultaneously. This is called mutual exclusion or thread synchronization. C# uses the .NET Framework’s monitors to perform synchronization. Class Monitor provides the methods for locking objects to implement synchronized access to shared data. Locking an object means that only one thread can access that object at a time. When a thread wishes to acquire exclusive control over an object, the thread invokes Monitor method Enter to acquire the lock on that data object. Each object has a SyncBlock that
15.5 Thread Synchronization and Class Monitor
577
maintains the state of that object’s lock. Methods of class Monitor use the data in an object’s SyncBlock to determine the state of the lock for that object. After acquiring the lock for an object, a thread can manipulate that object’s data. While the object is locked, all other threads attempting to acquire the lock on that object are blocked from acquiring the lock—such threads enter the Blocked state. When the thread that locked the shared object no longer requires the lock, that thread invokes Monitor method Exit to release the lock. This updates the SyncBlock of the shared object to indicate that the lock for the object is available again. At this point, if there is a thread that was previously blocked from acquiring the lock on the shared object, that thread acquires the lock to begin its processing of the object. If all threads with access to an object attempt to acquire the object’s lock before manipulating the object, only one thread at a time will be allowed to manipulate the object. This helps ensure the integrity of the data.
Common Programming Error 15.1 Make sure that all code that updates a shared object locks the object before doing so. Otherwise, a thread calling a method that does not lock the object can make the object unstable even when another thread has acquired the lock for the object. 15.1
Common Programming Error 15.2 Deadlock occurs when a waiting thread (let us call this thread1) cannot proceed because it is waiting (either directly or indirectly) for another thread (let us call this thread2) to proceed, while simultaneously thread2 cannot proceed because it is waiting (either directly or indirectly) for thread1 to proceed. Two threads are waiting for each other, so the actions that would enable either thread to continue execution never occur. 15.2
C# provides another means of manipulating an object’s lock—keyword lock. Placing before a block of code (designated with braces) as in
lock
lock ( objectReference ) { // code that requires synchronization goes here }
obtains the lock on the object to which the objectReference in parentheses refers. The objectReference is the same reference that normally would be passed to Monitor methods Enter, Exit, Pulse and PulseAll. When a lock block terminates for any reason, C# releases the lock on the object to which the objectReference refers. We explain lock further in Section 15.8. If a thread that owns the lock on an object determines that it cannot continue with its task until some condition is satisfied, the thread should call Monitor method Wait and pass as an argument the object on which the thread will wait until the thread can perform its task. Calling method Monitor.Wait from a thread releases the lock the thread has on the object that Wait receives as an argument and places that thread in the WaitSleepJoin state for that object. A thread in the WaitSleepJoin state of a specific object leaves that state when a separate thread invokes Monitor method Pulse or PulseAll with that object as an argument. Method Pulse transitions the object’s first waiting thread from the WaitSleepJoin state to the Running state. Method PulseAll transitions all threads in the object’s WaitSleepJoin state to the Running state. The transition to the Running state enables the thread (or threads) to get ready to continue executing.
578
Chapter 15
Multithreading
There is a difference between threads waiting to acquire an object’s lock and threads waiting in an object’s WaitSleepJoin state. Threads that call Monitor method Wait with an object as an argument are placed in that object’s WaitSleepJoin state. Threads that are simply waiting to acquire the lock enter the conceptual Blocked state and wait until the object’s lock becomes available. Then, a Blocked thread can acquire the object’s lock. Monitor methods Enter, Exit, Wait, Pulse and PulseAll all take a reference to an object—usually keyword this—as their argument.
Common Programming Error 15.3 A thread in the WaitSleepJoin state cannot re-enter the Running state to continue execution until a separate thread invokes Monitor method Pulse or PulseAll with the appropriate object as an argument. If this does not occur, the waiting thread will wait forever—essentially the equivalent of deadlock. 15.3
Error-Prevention Tip 15.2 When multiple threads manipulate a shared object using monitors, ensure that if one thread calls Monitor method Wait to enter the WaitSleepJoin state for the shared object, a separate thread eventually will call Monitor method Pulse to transition the thread waiting on the shared object back to the Running state. If multiple threads may be waiting for the shared object, a separate thread can call Monitor method PulseAll as a safeguard to ensure that all waiting threads have another opportunity to perform their tasks. If this is not done, indefinite postponement or deadlock could occur. 15.2
Performance Tip 15.2 Synchronization to achieve correctness in multithreaded programs can make programs run more slowly, as a result of monitor overhead and the frequent transitioning of threads between the WaitSleepJoin and Running states. There is not much to say, however, for highly efficient, yet incorrect multithreaded programs! 15.2
15.6 Producer/Consumer Relationship without Thread Synchronization In a producer/consumer relationship, the producer portion of an application generates data and the consumer portion of an application uses that data. In a multithreaded producer/ consumer relationship, a producer thread calls a produce method to generate data and place it in a shared region of memory, called a buffer. A consumer thread calls a consume method to read that data. If the producer wishes to put the next data in the buffer but determines that the consumer has not yet read the previous data from the buffer, the producer thread should call Wait. Otherwise, the consumer would never see the previous data, which would be lost to that application. When the consumer thread reads the data, it should call Pulse to allow a waiting producer to proceed, since there is now free space in the buffer. If a consumer thread finds the buffer empty or finds that the previous data has already been read, the consumer should call Wait. Otherwise, the consumer might read “garbage” from the buffer, or the consumer might process a previous data item more than once—each of these possibilities results in a logic error in the application. When the producer places the next data into the buffer, the producer should call Pulse to allow the consumer thread to proceed and read that data.
15.6 Producer/Consumer Relationship without Thread Synchronization
579
Let us consider how logic errors can arise if we do not synchronize access among multiple threads manipulating shared data. Consider a producer/consumer relationship in which a producer thread writes a sequence of numbers (we use 1–10) into a shared buffer—a memory location shared between multiple threads. The consumer thread reads this data from the shared buffer, then displays the data. We display in the program’s output the values that the producer writes (produces) and that the consumer reads (consumes). Figures 15.4–15.8 demonstrate a producer thread and a consumer thread accessing a single shared int variable without any synchronization. The producer thread writes to the variable; the consumer thread reads from it. We would like each value the producer thread writes to the shared variable to be consumed exactly once by the consumer thread. However, the threads in this example are not synchronized. Therefore, data can be lost if the producer places new data in the variable before the consumer consumes the previous data. Also, data can be incorrectly repeated if the consumer consumes data again before the producer produces the next value. If the consumer attempts to read before the producer produces the first value, the consumer reads garbage. To show these possibilities, the consumer thread in the example keeps a total of all the values it reads. The producer thread produces values from 1 to 10. If the consumer reads each value produced once and only once, the total would be 55. However, when you execute this program several times, you will see that the total is rarely, if ever, 55. Also, to emphasize our point, the producer and consumer threads in the example each sleep for random intervals of up to three seconds between performing their tasks. Thus, we do not know exactly when the producer thread will attempt to write a new value, nor do we know when the consumer thread will attempt to read a value. The program consists of interface Buffer (Fig. 15.4) and classes Producer (Fig. 15.5), Consumer (Fig. 15.6), UnsynchronizedBuffer (Fig. 15.7) and UnsynchronizedBufferTest (Fig. 15.8). Interface Buffer declares an int property called Buffer. Any implementation of Buffer must provide a get accessor and a set accessor for this property to allow the producer and consumer to access the shared data. Class Producer (Figure 15.5) consists of instance variable sharedLocation (line 10) of type Buffer, instance variable randomSleepTime (line 11) of type Random, a constructor (lines 14–18) to initialize the instance variables and a Produce method (lines 21–33). The 1 2 3 4 5 6 7 8 9 10 11 12 13 14
// Fig. 15.4: Buffer.cs // Interface for a shared buffer of int. using System; // this interface represents a shared buffer public interface Buffer { // property Buffer int Buffer { get; set; } // end property Buffer } // end interface Buffer
Fig. 15.4 |
Buffer
interface used in producer/consumer examples.
580
Chapter 15
Multithreading
constructor initializes instance variable sharedLocation to refer to the Buffer object received from method Main as the parameter shared. The producer thread in this program executes the tasks specified in method Produce of class Producer. The for statement in method Produce (lines 25–29) loops 10 times. Each iteration of the loop first invokes Thread method Sleep to place the producer thread in the WaitSleepJoin state for a random time interval between 0 and 3 seconds. When the thread awakens, line 28 assigns the value of control variable count to sharedLocation’s Buffer property. When the loop completes, lines 31–32 display a line of text in the console window indicating that the thread finished producing data and that the thread is terminating. The Produce method then terminates, and the producer thread enters the Stopped state. Class Consumer (Figure 15.6) consists of instance variable sharedLocation (line 10) of type Buffer, instance variable randomSleepTime (line 11) of type Random, a constructor (lines 14–18) to initialize the instance variables and a Consume method (lines 21–36). The constructor initializes sharedLocation to refer to the Buffer object received from Main as
// Fig. 15.5: Producer.cs // Producer produces 10 integer values in the shared buffer. using System; using System.Threading; // class Producer's Produce method controls a thread that // stores values from 1 to 10 in sharedLocation public class Producer { private Buffer sharedLocation; private Random randomSleepTime; // constructor public Producer( Buffer shared, Random random ) { sharedLocation = shared; randomSleepTime = random; } // end constructor // store values 1-10 in object sharedLocation public void Produce() { // sleep for random interval up to 3000 milliseconds // then set sharedLocation's Buffer property for ( int count = 1; count = 0; i-- ) Console.Write( string1[ i ] ); // copy characters from string1 into characterArray string1.CopyTo( 0, characterArray, 0, characterArray.Length ); Console.Write( "\nThe character array is: " ); for ( int i = 0; i < characterArray.Length; i++ ) Console.Write( characterArray[ i ] ); Console.WriteLine( "\n" ); } // end method Main } // end class StringMethods
string1: "hello there" Length of string1: 11 The string reversed is: ereht olleh The character array is: hello
Fig. 16.2 |
string
indexer, Length property and CopyTo method. (Part 2 of 2.)
This application determines the length of a string, displays its characters in reverse order and copies a series of characters from the string to a character array. Line 20 uses string property Length to determine the number of characters in string1. Like arrays, strings always know their own size. Lines 25–26 write the characters of string1 in reverse order using the string indexer. The string indexer treats a string as an array of chars and returns the character at a specific position in the string. The indexer receives an integer argument as the position number and returns the character at that position. As with arrays, the first element of a string is considered to be at position 0.
Common Programming Error 16.1 Attempting to access a character that is outside a string’s bounds (i.e., an index less than 0 or an index greater than or equal to the string’s length) results in an IndexOutOfRangeException. 16.1
Line 29 uses string method CopyTo to copy the characters of string1 into a character array (characterArray). The first argument given to method CopyTo is the index from which the method begins copying characters in the string. The second argument is the character array into which the characters are copied. The third argument is the index specifying the starting location at which the method begins placing the copied characters into the character array. The last argument is the number of characters that the method will copy from the string. Lines 32–33 output the char array contents one character at a time.
16.5 Comparing strings
613
16.5 Comparing strings The next two examples demonstrate various methods for comparing strings. To understand how one string can be “greater than” or “less than” another string, consider the process of alphabetizing a series of last names. The reader would, no doubt, place "Jones" before "Smith", because the first letter of "Jones" comes before the first letter of "Smith" in the alphabet. The alphabet is more than just a set of 26 letters—it is an ordered list of characters in which each letter occurs in a specific position. For example, Z is more than just a letter of the alphabet; Z is specifically the twenty-sixth letter of the alphabet. Computers can order characters alphabetically because the characters are represented internally as Unicode numeric codes. When comparing two strings, C# simply compares the numeric codes of the characters in the strings. Class string provides several ways to compare strings. The application in Fig. 16.3 demonstrates the use of method Equals, method CompareTo and the equality operator (==). 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
string1 equals "hello" string1 equals "hello" string3 does not equal string4 string1.CompareTo( string2.CompareTo( string1.CompareTo( string3.CompareTo( string4.CompareTo(
Fig. 16.3
| string
string2 string1 string1 string4 string3
) ) ) ) )
is is is is is
1 -1 0 1 -1
test to determine equality. (Part 2 of 2.)
The condition in the if statement (line 21) uses string method Equals to compare string1 and literal string "hello" to determine whether they are equal. Method Equals (inherited from object and overridden in string) tests any two objects for equality (i.e., checks whether the objects contain identical contents). The method returns true if the objects are equal and false otherwise. In this instance, the preceding condition returns true, because string1 references string literal object "hello". Method Equals uses a lexicographical comparison—the integer Unicode values that represent each character in each string are compared. A comparison of the string "hello" with the string "HELLO" would return false, because the numeric representations of lowercase letters are different from the numeric representations of corresponding uppercase letters. The condition in line 27 uses the equality operator (==) to compare string string1 with the literal string "hello" for equality. In C#, the equality operator also uses a lexicographical comparison to compare two strings. Thus, the condition in the if statement evaluates to true, because the values of string1 and "hello" are equal. We present the test for string equality between string3 and string4 (line 33) to illustrate that comparisons are indeed case sensitive. Here, static method Equals is used to compare the values of two strings. "Happy Birthday" does not equal "happy birthday", so the condition of the if statement fails, and the message "string3 does not equal string4" is output (line 36).
16.5 Comparing strings
615
Lines 40–48 use string method CompareTo to compare strings. Method CompareTo returns 0 if the strings are equal, a negative value if the string that invokes CompareTo is less than the string that is passed as an argument and a positive value if the string that invokes CompareTo is greater than the string that is passed as an argument. Method CompareTo uses a lexicographical comparison. Notice that CompareTo considers string3 to be larger than string4. The only difference between these two strings is that string3 contains two uppercase letters in positions where string4 contains lowercase letters. The application in Fig. 16.4 shows how to test whether a string instance begins or ends with a given string. Method StartsWith determines whether a string instance starts with the string text passed to it as an argument. Method EndsWith determines whether a string instance ends with the string text passed to it as an argument. Class stringStartEnd’s Main method defines an array of strings (called strings), which contains "started", "starting", "ended" and "ending". The remainder of method Main tests the elements of the array to determine whether they start or end with a particular set of characters. Line 14 uses method StartsWith, which takes a string argument. The condition in the if statement determines whether the string at index i of the array starts with the characters "st". If so, the method returns true, and strings[ i ] is output along with a message. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
// Fig. 16.4: StringStartEnd.cs // Demonstrating StartsWith and EndsWith methods. using System; class StringStartEnd { public static void Main() { string[] strings = { "started", "starting", "ended", "ending" }; // test every string to see if it starts with "st" for ( int i = 0; i < strings.Length; i++ ) if ( strings[ i ].StartsWith( "st" ) ) Console.WriteLine( "\"" + strings[ i ] + "\"" + " starts with \"st\"" ); Console.WriteLine( "" ); // test every string to see if it ends with "ed" for ( int i = 0; i < strings.Length; i++ ) if ( strings[ i ].EndsWith( "ed" ) ) Console.WriteLine( "\"" + strings[ i ] + "\"" + " ends with \"ed\"" ); Console.WriteLine( "" ); } // end method Main } // end class StringStartEnd
Fig. 16.4
| StartsWith
and EndsWith methods. (Part 1 of 2.)
616
Chapter 16
Strings, Characters and Regular Expressions
"started" starts with "st" "starting" starts with "st" "started" ends with "ed" "ended" ends with "ed"
Fig. 16.4
| StartsWith
and EndsWith methods. (Part 2 of 2.)
Line 22 uses method EndsWith, which also takes a string argument. The condition in the if statement determines whether the string at index i of the array ends with the characters "ed". If so, the method returns true, and strings[ i ] is displayed along with a message.
16.6 Locating Characters and Substrings in strings In many applications, it is necessary to search for a character or set of characters in a string. For example, a programmer creating a word processor would want to provide capabilities for searching through documents. The application in Fig. 16.5 demonstrates some of the many versions of string methods IndexOf, IndexOfAny, LastIndexOf and LastIndexOfAny, which search for a specified character or substring in a string. We perform all searches in this example on the string letters (initialized with "abcdefghijklmabcdefghijklm") located in method Main of class StringIndexMethods. Lines 14, 16 and 18 use method IndexOf to locate the first occurrence of a character or substring in a string. If it finds a character, IndexOf returns the index of the specified character in the string; otherwise, IndexOf returns –1. The expression in line 16 uses a version of method IndexOf that takes two arguments—the character to search for and the starting index at which the search of the string should begin. The method does not examine any characters that occur prior to the starting index (in this case, 1). The expression in line 18 uses another version of method IndexOf that takes three arguments—the character to search for, the index at which to start searching and the number of characters to search. Lines 22, 24 and 26 use method LastIndexOf to locate the last occurrence of a character in a string. Method LastIndexOf performs the search from the end of the string to the beginning of the string. If it finds the character, LastIndexOf returns the index of the specified character in the string; otherwise, LastIndexOf returns –1. There are three versions of LastIndexOf. The expression in line 22 uses the version of method LastIndexOf that takes as an argument the character for which to search. The expression in line 24 uses the version of method LastIndexOf that takes two arguments—the character for which to search and the highest index from which to begin searching backward for the character. The expression in line 26 uses a third version of method LastIndexOf that takes three arguments—the character for which to search, the starting index from which to start searching backward and the number of characters (the portion of the string) to search. Lines 29–44 use versions of IndexOf and LastIndexOf that take a string instead of a character as the first argument. These versions of the methods perform identically to those described above except that they search for sequences of characters (or substrings) that are specified by their string arguments.
16.6 Locating Characters and Substrings in strings
// Fig. 16.5: StringIndexMethods.cs // Using string searching methods. using System; class StringIndexMethods { public static void Main() { string letters = "abcdefghijklmabcdefghijklm"; char[] searchLetters = { 'c', 'a', '$' }; // test IndexOf to locate a character in a string Console.WriteLine( "First 'c' is located at index " + letters.IndexOf( 'c' ) ); Console.WriteLine( "First 'a' starting at 1 is located at index " + letters.IndexOf( 'a', 1 ) ); Console.WriteLine( "First '$' in the 5 positions starting at 3 " + "is located at index " + letters.IndexOf( '$', 3, 5 ) ); // test LastIndexOf to find a character in a string Console.WriteLine( "\nLast 'c' is located at index " + letters.LastIndexOf( 'c' ) ); Console.WriteLine( "Last 'a' up to position 25 is located at " + "index " + letters.LastIndexOf( 'a', 25 ) ); Console.WriteLine( "Last '$' in the 5 positions starting at 15 " + "is located at index " + letters.LastIndexOf( '$', 15, 5 ) ); // test IndexOf to locate a substring in a string Console.WriteLine( "\nFirst \"def\" is located at index " + letters.IndexOf( "def" ) ); Console.WriteLine( "First \"def\" starting at 7 is located at " + "index " + letters.IndexOf( "def", 7 ) ); Console.WriteLine( "First \"hello\" in the 15 positions " + "starting at 5 is located at index " + letters.IndexOf( "hello", 5, 15 ) ); // test LastIndexOf to find a substring in a string Console.WriteLine( "\nLast \"def\" is located at index " + letters.LastIndexOf( "def" ) ); Console.WriteLine( "Last \"def\" up to position 25 is located " + "at index " + letters.LastIndexOf( "def", 25 ) ); Console.WriteLine( "Last \"hello\" in the 15 positions " + "ending at 20 is located at index " + letters.LastIndexOf( "hello", 20, 15 ) ); // test IndexOfAny to find first occurrence of character in array Console.WriteLine( "\nFirst 'c', 'a' or '$' is " + "located at index " + letters.IndexOfAny( searchLetters ) ); Console.WriteLine( "First 'c', 'a' or '$' starting at 7 is " + "located at index " + letters.IndexOfAny( searchLetters, 7 ) ); Console.WriteLine( "First 'c', 'a' or '$' in the 5 positions " + "starting at 7 is located at index " + letters.IndexOfAny( searchLetters, 7, 5 ) );
Fig. 16.5 | Searching for characters and substrings in strings. (Part 1 of 2.)
618 54 55 56 57 58 59 60 61 62 63 64 65 66
Chapter 16
Strings, Characters and Regular Expressions
// test LastIndexOfAny to find last occurrence of character // in array Console.WriteLine( "\nLast 'c', 'a' or '$' is " + "located at index " + letters.LastIndexOfAny( searchLetters ) ); Console.WriteLine( "Last 'c', 'a' or '$' up to position 1 is " + "located at index " + letters.LastIndexOfAny( searchLetters, 1 ) ); Console.WriteLine( "Last 'c', 'a' or '$' in the 5 positions " + "ending at 25 is located at index " + letters.LastIndexOfAny( searchLetters, 25, 5 ) ); } // end method Main } // end class StringIndexMethods
First 'c' is located at index 2 First 'a' starting at 1 is located at index 13 First '$' in the 5 positions starting at 3 is located at index -1 Last 'c' is located at index 15 Last 'a' up to position 25 is located at index 13 Last '$' in the 5 positions starting at 15 is located at index -1 First "def" is located at index 3 First "def" starting at 7 is located at index 16 First "hello" in the 15 positions starting at 5 is located at index -1 Last "def" is located at index 16 Last "def" up to position 25 is located at index 16 Last "hello" in the 15 positions ending at 20 is located at index -1 First 'c', 'a' or '$' is located at index 0 First 'c', 'a' or '$' starting at 7 is located at index 13 First 'c', 'a' or '$' in the 5 positions starting at 7 is located at index -1 Last 'c', 'a' or '$' is located at index 15 Last 'c', 'a' or '$' up to position 1 is located at index 0 Last 'c', 'a' or '$' in the 5 positions ending at 25 is located at index -1
Fig. 16.5 | Searching for characters and substrings in strings. (Part 2 of 2.) Lines 47–64 use methods IndexOfAny and LastIndexOfAny, which take an array of characters as the first argument. These versions of the methods also perform identically to those described above except that they return the index of the first occurrence of any of the characters in the character array argument.
Common Programming Error 16.2 In the overloaded methods LastIndexOf and LastIndexOfAny that take three parameters, the second argument must be greater than or equal to the third. This might seem counterintuitive, but remember that the search moves from the end of the string toward the start of the string. 16.2
16.7 Extracting Substrings from strings Class string provides two Substring methods, which are used to create a new string by copying part of an existing string. Each method returns a new string. The application in Fig. 16.6 demonstrates the use of both methods.
16.8 Concatenating strings
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
619
// Fig. 16.6: SubString.cs // Demonstrating the string Substring method. using System; class SubString { public static void Main() { string letters = "abcdefghijklmabcdefghijklm"; // invoke Substring method and pass it one parameter Console.WriteLine( "Substring from index 20 to end is \"" + letters.Substring( 20 ) + "\"" ); // invoke Substring method and pass it two parameters Console.WriteLine( "Substring from index 0 of length 6 is \"" + letters.Substring( 0, 6 ) + "\"" ); } // end method Main } // end class SubString
Substring from index 20 to end is "hijklm" Substring from index 0 of length 6 is "abcdef"
Fig. 16.6 | Substrings generated from strings. The statement in line 13 uses the Substring method that takes one int argument. The argument specifies the starting index from which the method copies characters in the original string. The substring returned contains a copy of the characters from the starting index to the end of the string. If the index specified in the argument is outside the bounds of the string, the program throws an ArgumentOutOfRangeException. The second version of method Substring (line 17) takes two int arguments. The first argument specifies the starting index from which the method copies characters from the original string. The second argument specifies the length of the substring to be copied. The substring returned contains a copy of the specified characters from the original string.
16.8 Concatenating strings The + operator (discussed in Chapter 3, Introduction to C# Programming) is not the only way to perform string concatenation. The static method Concat of class string (Fig. 16.7) concatenates two strings and returns a new string containing the combined characters from both original strings. Line 16 appends the characters from string2 to the end of a copy of string1, using method Concat. The statement in line 16 does not modify the original strings.
16.9 Miscellaneous string Methods Class string provides several methods that return modified copies of strings. The application in Fig. 16.8 demonstrates the use of these methods, which include string methods Replace, ToLower, ToUpper and Trim.
Replacing "e" with "E" in string1: "chEErs!" string1.ToUpper() = "CHEERS!" string2.ToLower() = "good bye " string3 after trim = "spaces" string1 = "cheers!"
Fig. 16.8 |
string
methods Replace, ToLower, ToUpper and Trim. (Part 2 of 2.)
Line 21 uses string method Replace to return a new string, replacing every occurrence in string1 of character 'e' with 'E'. Method Replace takes two arguments—a string for which to search and another string with which to replace all matching occurrences of the first argument. The original string remains unchanged. If there are no occurrences of the first argument in the string, the method returns the original string. string method ToUpper generates a new string (line 25) that replaces any lowercase letters in string1 with their uppercase equivalents. The method returns a new string containing the converted string; the original string remains unchanged. If there are no characters to convert, the original string is returned. Line 26 uses string method ToLower to return a new string in which any uppercase letters in string2 are replaced by their lowercase equivalents. The original string is unchanged. As with ToUpper, if there are no characters to convert to lowercase, method ToLower returns the original string. Line 30 uses string method Trim to remove all whitespace characters that appear at the beginning and end of a string. Without otherwise altering the original string, the method returns a new string that contains the string, but omits leading or trailing whitespace characters. Another version of method Trim takes a character array and returns a string that does not contain the characters in the array argument.
16.10 Class StringBuilder The string class provides many capabilities for processing strings. However a string’s contents can never change. Operations that seem to concatenate strings are in fact assign-
622
Chapter 16
Strings, Characters and Regular Expressions
ing string references to newly created strings (e.g., the += operator creates a new string and assigns the initial string reference to the newly created string). The next several sections discuss the features of class StringBuilder (namespace System.Text), used to create and manipulate dynamic string information—i.e., mutable strings. Every StringBuilder can store a certain number of characters that is specified by its capacity. Exceeding the capacity of a StringBuilder causes the capacity to expand to accommodate the additional characters. As we will see, members of class StringBuilder, such as methods Append and AppendFormat, can be used for concatenation like the operators + and += for class string.
Performance Tip 16.2 Objects of class string are immutable (i.e., constant strings), whereas object of class StringBuilder are mutable. C# can perform certain optimizations involving strings (such as the sharing of one string among multiple references), because it knows these objects will not change. 16.2
Class StringBuilder provides six overloaded constructors. Class StringBuilderCon(Fig. 16.9) demonstrates three of these overloaded constructors. Line 12 employs the no-parameter StringBuilder constructor to create a StringBuilder that contains no characters and has a default initial capacity of 16 characters. Line 13 uses the StringBuilder constructor that takes an int argument to create a StringBuilder that contains no characters and has the initial capacity specified in the int argument (i.e., 10). Line 14 uses the StringBuilder constructor that takes a string argument to create a StringBuilder containing the characters of the string argument. The initial capacity is the smallest power of two greater than or equal to the number of characters in the argument string, with a minimum of 16. Lines 16–18 implicitly use StringBuilder method ToString to obtain string representations of the StringBuilders’ contents. structor
// Fig. 16.9: StringBuilderConstructor.cs // Demonstrating StringBuilder class constructors. using System; using System.Text; class StringBuilderConstructor public static void Main() { StringBuilder buffer1, buffer2, buffer3; buffer1 = new StringBuilder(); buffer2 = new StringBuilder( 10 ); buffer3 = new StringBuilder( "hello" ); Console.WriteLine( "buffer1 = \"" + buffer1 + "\"" ); Console.WriteLine( "buffer2 = \"" + buffer2 + "\"" ); Console.WriteLine( "buffer3 = \"" + buffer3 + "\"" ); } // end method Main } // end class StringBuilderConstructor
Fig. 16.9 |
StringBuilder
class constructors. (Part 1 of 2.)
16.11 Class StringBuilder
623
buffer1 = "" buffer2 = "" buffer3 = "hello"
Fig. 16.9 |
StringBuilder
class constructors. (Part 2 of 2.)
16.11 Length and Capacity Properties, EnsureCapacity Method and Indexer of Class StringBuilder
Class StringBuilder provides the Length and Capacity properties to return the number of characters currently in a StringBuilder and the number of characters that a StringBuilder can store without allocating more memory, respectively. These properties also can increase or decrease the length or the capacity of the StringBuilder. Method EnsureCapacity allows you to reduce the number of times that a StringBuilder’s capacity must be increased. The method doubles the StringBuilder instance’s current capacity. If this doubled value is greater than the value that the programmer wishes to ensure, that value becomes the new capacity. Otherwise, EnsureCapacity alters the capacity to make it equal to the requested number. For example, if the current capacity is 17 and we wish to make it 40, 17 multiplied by 2 is not greater than 40, so the call will result in a new capacity of 40. If the current capacity is 23 and we wish to make it 40, 23 will be multiplied by 2 to result in a new capacity of 46. Both 40 and 46 are greater than or equal to 40, so a capacity of 40 is indeed ensured by method EnsureCapacity. The program in Fig. 16.10 demonstrates the use of these methods and properties. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
// Fig. 16.10: StringBuilderFeatures.cs // Demonstrating some features of class StringBuilder. using System; using System.Text; class StringBuilderFeatures { public static void Main() { StringBuilder buffer = new StringBuilder( "Hello, how are you?" ); // use Length and Capacity properties Console.WriteLine( "buffer = " + buffer + "\nLength = " + buffer.Length + "\nCapacity = " + buffer.Capacity ); buffer.EnsureCapacity( 75 ); // ensure a capacity of at least 75 Console.WriteLine( "\nNew capacity = " + buffer.Capacity );
Fig. 16.10 |
StringBuilder
size manipulation. (Part 1 of 2.)
624 22 23 24 25 26 27 28 29 30 31 32 33
Chapter 16
Strings, Characters and Regular Expressions
// truncate StringBuilder by setting Length property buffer.Length = 10; Console.Write( "\nNew length = " + buffer.Length + "\nbuffer = " ); // use StringBuilder indexer for ( int i = 0; i < buffer.Length; i++ ) Console.Write( buffer[ i ] ); Console.WriteLine( "\n" ); } // end method Main } // end class StringBuilderFeatures
buffer = Hello, how are you? Length = 19 Capacity = 32 New capacity = 75 New length = 10 buffer = Hello, how
Fig. 16.10 |
StringBuilder
size manipulation. (Part 2 of 2.)
The program contains one StringBuilder, called buffer. Lines 10–11 of the program use the StringBuilder constructor that takes a string argument to instantiate the StringBuilder and initialize its value to "Hello, how are you?". Lines 14–16 output the content, length and capacity of the StringBuilder. In the output window, notice that the capacity of the StringBuilder is initially 32. Remember, the StringBuilder constructor that takes a string argument creates a StringBuilder object with an initial capacity that is the smallest power of two greater than or equal to the number of characters in the string passed as an argument. Line 18 expands the capacity of the StringBuilder to a minimum of 75 characters. The current capacity (32) multiplied by two is less than 75, so method EnsureCapacity increases the capacity to 75. If new characters are added to a StringBuilder so that its length exceeds its capacity, the capacity grows to accommodate the additional characters in the same manner as if method EnsureCapacity had been called. Line 23 uses property Length to set the length of the StringBuilder to 10. If the specified length is less than the current number of characters in the StringBuilder, the contents of the StringBuilder are truncated to the specified length. If the specified length is greater than the number of characters currently in the StringBuilder, space characters are appended to the StringBuilder until the total number of characters in the StringBuilder is equal to the specified length.
Common Programming Error 16.3 Assigning null to a string reference can lead to logic errors if you attempt to compare null to an empty string. The keyword null is a value that represents a null reference (i.e., a reference that does not refer to an object), not an empty string (which is a string object that is of length 0 and contains no characters). 16.3
16.12 Class StringBuilder
625
16.12 Append and AppendFormat Methods of Class StringBuilder
Class StringBuilder provides 19 overloaded Append methods that allow various types of values to be added to the end of a StringBuilder. The FCL provides versions for each of the simple types and for character arrays, strings and objects. (Remember that method ToString produces a string representation of any object.) Each of the methods takes an argument, converts it to a string and appends it to the StringBuilder. Figure 16.11 demonstrates the use of several Append methods. Lines 22–40 use 10 different overloaded Append methods to attach the string representations of objects created in lines 10–18 to the end of the StringBuilder. Append behaves similarly to the + operator, which is used to concatenate strings. Class StringBuilder also provides method AppendFormat, which converts a string to a specified format, then appends it to the StringBuilder. The example in Fig. 16.12 demonstrates the use of this method. Line 14 creates a string that contains formatting information. The information enclosed in braces specifies how to format a specific piece of data. Formats have the form {X[,Y][:FormatString]}, where X is the number of the argument to be formatted, counting from zero. Y is an optional argument, which can be positive or negative, indicating how many characters should be in the result. If the resulting string is less than the number Y, the string will be padded with spaces to make up for the difference. A positive integer aligns the string to the right; a negative integer aligns it to the left. The optional FormatString applies a particular format to the argument—currency, decimal or scientific, among others. In this case, “{0}” means the first argument will be printed out. “{1:C}” specifies that the second argument will be formatted as a currency value. Line 23 shows a version of AppendFormat that takes two parameters—a string specifying the format and an array of objects to serve as the arguments to the format string. The argument referred to by “{0}” is in the object array at index 0. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
// Fig. 16.11: StringBuilderAppend.cs // Demonstrating StringBuilder Append methods. using System; using System.Text; class StringBuilderAppend { public static void Main( string[] args ) { object objectValue = "hello"; string stringValue = "good bye"; char[] characterArray = { 'a', 'b', 'c', 'd', 'e', 'f' }; bool booleanValue = true; char characterValue = 'Z'; int integerValue = 7; long longValue = 1000000; float floatValue = 2.5F; // F suffix indicates that 2.5 is a float double doubleValue = 33.333; StringBuilder buffer = new StringBuilder();
Fig. 16.11 | Append methods of StringBuilder. (Part 1 of 2.)
// append to buffer formatted string with argument buffer.AppendFormat( string1, objectArray ); // formatted string string2 = "Number:{0:d3}.\n" + "Number right aligned with spaces:{0, 4}.\n" + "Number left aligned with spaces:{0, -4}."; // append to buffer formatted string with argument buffer.AppendFormat( string2, 5 ); // display formatted strings Console.WriteLine( buffer.ToString() ); } // end method Main } // end class StringBuilderAppendFormat
This car costs: $1,234.56. Number:005. Number right aligned with spaces: Number left aligned with spaces:5
Fig. 16.12 |
5. .
StringBuilder’s AppendFormat
method. (Part 2 of 2.)
Lines 26–28 define another string used for formatting. The first format “{0:d3}”, specifies that the first argument will be formatted as a three-digit decimal, meaning any number that has fewer than three digits will have leading zeros placed in front to make up the difference. The next format, “{0, 4}”, specifies that the formatted string should have four characters and should be right aligned. The third format, “{0, -4}”, specifies that the strings should be aligned to the left. For more formatting options, please refer to the online help documentation. Line 31 uses a version of AppendFormat that takes two parameters—a string containing a format and an object to which the format is applied. In this case, the object is the number 5. The output of Fig. 16.12 displays the result of applying these two versions of AppendFormat with their respective arguments.
16.13 Insert, Remove and Replace Methods of Class StringBuilder
Class StringBuilder provides 18 overloaded Insert methods to allow various types of data to be inserted at any position in a StringBuilder. The class provides versions for each of the simple types and for character arrays, strings and objects. Each method takes its second argument, converts it to a string and inserts the string into the StringBuilder in front of the character in the position specified by the first argument. The index specified by the first argument must be greater than or equal to 0 and less than the length of the StringBuilder; otherwise, the program throws an ArgumentOutOfRangeException. Class StringBuilder also provides method Remove for deleting any portion of a StringBuilder. Method Remove takes two arguments—the index at which to begin deletion and the number of characters to delete. The sum of the starting index and the number of characters to be deleted must always be less than the length of the StringBuilder; oth-
628
Chapter 16
Strings, Characters and Regular Expressions
erwise, the program throws an ArgumentOutOfRangeException. The Insert and Remove methods are demonstrated in Fig. 16.13. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
// Fig. 16.13: StringBuilderInsertRemove.cs // Demonstrating methods Insert and Remove of the // StringBuilder class. using System; using System.Text; class StringBuilderInsertRemove { public static void Main() { object objectValue = "hello"; string stringValue = "good bye"; char[] characterArray = { 'a', 'b', 'c', 'd', 'e', 'f' }; bool booleanValue = true; char characterValue = 'K'; int integerValue = 7; long longValue = 10000000; float floatValue = 2.5F; // F suffix indicates that 2.5 is a float double doubleValue = 33.333; StringBuilder buffer = new StringBuilder(); // insert values into buffer buffer.Insert( 0, objectValue ); buffer.Insert( 0, " " ); buffer.Insert( 0, stringValue ); buffer.Insert( 0, " " ); buffer.Insert( 0, characterArray ); buffer.Insert( 0, " " ); buffer.Insert( 0, booleanValue ); buffer.Insert( 0, " " ); buffer.Insert( 0, characterValue ); buffer.Insert( 0, " " ); buffer.Insert( 0, integerValue ); buffer.Insert( 0, " " ); buffer.Insert( 0, longValue ); buffer.Insert( 0, " " ); buffer.Insert( 0, floatValue ); buffer.Insert( 0, " " ); buffer.Insert( 0, doubleValue ); buffer.Insert( 0, " " ); Console.WriteLine( "buffer after Inserts: \n" + buffer + "\n" ); buffer.Remove( 10, 1 ); // delete 2 in 2.5 buffer.Remove( 4, 4 ); // delete .333 in 33.333 Console.WriteLine( "buffer after Removes:\n" + buffer.ToString() ); } // end method Main } // end class StringBuilderInsertRemove
Fig. 16.13
| StringBuilder
text insertion and removal. (Part 1 of 2.)
16.13 Class StringBuilder
buffer after Inserts: 33.333 2.5 10000000 buffer after Removes: 33 .5 10000000 7
Fig. 16.13
7 K
| StringBuilder
K True
True
abcdef
abcdef
good bye
good bye
629
hello
hello
text insertion and removal. (Part 2 of 2.)
Another useful method included with StringBuilder is Replace. Replace searches for a specified string or character and substitutes another string or character in its place. Figure 16.14 demonstrates this method. Line 18 uses method Replace to replace all instances of the string "Jane" with the string "Greg" in builder1. Another overload of this method takes two characters as parameters and replaces each occurrence of the first character with the second character. Line 19 uses an overload of Replace that takes four parameters, the first two of which are characters and the second two of which are ints. The method replaces all instances of the first character with the second character, beginning at the index specified by the first int and continuing for a count specified by the second int. Thus, in this case, Replace looks 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
// Fig. 16.14: StringBuilderReplace.cs // Demonstrating method Replace. using System; using System.Text; class StringBuilderReplace { public static void Main() { StringBuilder builder1 = new StringBuilder( "Happy Birthday Jane" ); StringBuilder builder2 = new StringBuilder( "good bye greg" ); Console.WriteLine( "Before replacements:\n" + builder1.ToString() + "\n" + builder2.ToString() ); builder1.Replace( "Jane", "Greg" ); builder2.Replace( 'g', 'G', 0, 5 ); Console.WriteLine( "\nAfter replacements:\n" + builder1.ToString() + "\n" + builder2.ToString() ); } // end method Main } // end class StringBuilderReplace
Before Replacements: Happy Birthday Jane good bye greg After replacements: Happy Birthday Greg Good bye greg
Fig. 16.14 |
StringBuilder
text replacement.
630
Chapter 16
Strings, Characters and Regular Expressions
through only five characters, starting with the character at index 0. As the output illustrates, this version of Replace replaces g with G in the word "good", but not in "greg". This is because the gs in "greg" are not in the range indicated by the int arguments (i.e., between indexes 0 and 4).
16.14 Char Methods C# provides a type called a struct (short for structure) that is similar to a class. Although structs and classes are comparable in many ways, structs represent value types. Like classes, structs can have methods and properties, and can use the access modifiers public and private. Also, struct members are accessed via the member access operator (.). The simple types are actually aliases for struct types. For instance, an int is defined by struct System.Int32, a long by System.Int64 and so on. All struct types derive from class ValueType, which in turn derives from object. Also, all struct types are implicitly sealed, so they do not support virtual or abstract methods, and their members cannot be declared protected or protected internal. In this section, we present struct Char,2 which is the struct for characters. Most Char methods are static, take at least one character argument and perform either a test or a manipulation on the character. We present several of these methods in the next example. Figure 16.15 demonstrates static methods that test characters to determine whether they are of a specific character type and static methods that perform case conversions on characters. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
// Fig. 16.15: StaticCharMethods.cs // Demonstrates static character testing methods // from Char struct using System; using System.Windows.Forms; public partial class StaticCharMethodsForm : Form { // default constructor public StaticCharMethodsForm() { InitializeComponent(); } // end constructor // handle analyzeButton_Click private void analyzeButton_Click( object sender, EventArgs e ) { // convert string entered to type char char character = Convert.ToChar( inputTextBox.Text ); string output;
Fig. 16.15 2.
| Char’s static
character-testing and case-conversion methods. (Part 1 of 2.)
Just as keyword string is an alias for class String, keyword char is an alias for struct Char. In this text, we use the term Char when calling a static method of struct Char and the term char elsewhere.
output = "is digit: " + Char.IsDigit( character ) + "\r\n"; output += "is letter: " + Char.IsLetter( character ) + "\r\n"; output += "is letter or digit: " + Char.IsLetterOrDigit( character ) + "\r\n"; output += "is lower case: " + Char.IsLower( character ) + "\r\n"; output += "is upper case: " + Char.IsUpper( character ) + "\r\n"; output += "to upper case: " + Char.ToUpper( character ) + "\r\n"; output += "to lower case: " + Char.ToLower( character ) + "\r\n"; output += "is punctuation: " + Char.IsPunctuation( character ) + "\r\n"; output += "is symbol: " + Char.IsSymbol( character ); outputTextBox.Text = output; } // end method analyzeButton_Click } // end class StaticCharMethodsForm
(a)
(c)
(b)
(d)
Fig. 16.15
(e)
| Char’s static
character-testing and case-conversion methods. (Part 2 of 2.)
631
632
Chapter 16
Strings, Characters and Regular Expressions
This Windows application contains a prompt, a TextBox in which the user can input a character, a button that the user can press after entering a character and a second TextBox that displays the output of our analysis. When the user clicks the Analyze Character button, event handler analyzeButton_Click (lines 16–40) is invoked. This event handler converts the input from a string to a char, using method Convert.ToChar (line 19). Line 23 uses Char method IsDigit to determine whether character is defined as a digit. If so, the method returns true; otherwise, it returns false (note again that bool values are output capitalized). Line 25 uses Char method IsLetter to determine whether character character is a letter. Line 27 uses Char method IsLetterOrDigit to determine whether character character is a letter or a digit. Line 29 uses Char method IsLower to determine whether character character is a lowercase letter. Line 31 uses Char method IsUpper to determine whether character character is an uppercase letter. Line 33 uses Char method ToUpper to convert character character to its uppercase equivalent. The method returns the converted character if the character has an uppercase equivalent; otherwise, the method returns its original argument. Line 35 uses Char method ToLower to convert character character to its lowercase equivalent. The method returns the converted character if the character has a lowercase equivalent; otherwise, the method returns its original argument. Line 37 uses Char method IsPunctuation to determine whether character is a punctuation mark, such as "!", ":" or ")". Line 38 uses Char method IsSymbol to determine whether character character is a symbol, such as "+", "=" or "^". Structure type Char also contains other methods not shown in this example. Many of the static methods are similar—for instance, IsWhiteSpace is used to determine whether a certain character is a whitespace character (e.g., newline, tab or space). The struct also contains several public instance methods; many of these, such as methods ToString and Equals, are methods that we have seen before in other classes. This group includes method CompareTo, which is used to compare two character values with one another.
16.15 Card Shuffling and Dealing Simulation In this section, we use random-number generation to develop a program that simulates card shuffling and dealing. These techniques can form the basis of programs that implement specific card games. We include several exercises at the end of this chapter that require card shuffling and dealing capabilities. Class Card (Fig. 16.16) contains two string instance variables—face and suit— that store references to the face value and suit name of a specific card. The constructor for the class receives two strings that it uses to initialize face and suit. Method ToString (lines 16–19) creates a string consisting of the face of the card and the suit of the card to identify the card when it is dealt. We develop the DeckForm application (Fig. 16.17), which creates a deck of 52 playing cards, using Card objects. Users can deal each card by clicking the Deal Card button. Each dealt card is displayed in a Label. Users can also shuffle the deck at any time by clicking the Shuffle Cards button. Method DeckForm_Load (lines 19–31 of Fig. 16.17) uses a for statement (lines 29– 30) to fill the deck array with Cards. Note that each Card is instantiated and initialized with two strings—one from the faces array (strings "Ace" through "King") and one from the suits array ("Hearts", "Diamonds", "Clubs" or "Spades"). The calculation i %
// Fig. 16.16: Card.cs // Stores suit and face information on each card. using System; public class Card { private string face; private string suit; public Card( string faceValue, string suitValue ) { face = faceValue; suit = suitValue; } // end constructor public override string ToString() { return face + " of " + suit; } // end method ToString } // end class Card
Fig. 16.16
| Card
class.
always results in a value from 0 to 12 (the thirteen subscripts of the faces array), and the calculation i / 13 always results in a value from 0 to 3 (the four subscripts in the suits array). The initialized deck array contains the cards with faces Ace through King for each suit. When the user clicks the Deal Card button, event handler dealButton_Click (lines 34–50) invokes method DealCard (defined in lines 75–90) to get the next card in the deck array. If the deck is not empty, the method returns a Card object reference; otherwise, it returns null. If the reference is not null, lines 42–43 display the Card in displayLabel and display the card number in statusLabel. 13
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
// Fig. 16.17: DeckForm.cs // Simulating card shuffling and dealing. using System; using System.Windows.Forms; public partial class DeckForm : Form { private Card[] deck = new Card[ 52 ]; // deck of 52 cards private int currentCard; // count which card was just dealt // default constructor public DeckForm() { // Required for Windows Form Designer support InitializeComponent(); } // end constructor
Fig. 16.17 | Card shuffling and dealing simulation. (Part 1 of 4.)
dealButton.Enabled = true; // shuffled deck can now deal cards } // end method Shuffle // deal a card if the deck is not empty private Card DealCard() { // if there is a card to deal then deal it // otherwise signal that cards need to be shuffled by // disabling dealButton and returning null if ( currentCard + 1 < deck.Length ) { currentCard++; // increment count return deck[ currentCard ]; // return new card } // end if else { dealButton.Enabled = false; // empty deck cannot deal cards return null; // do not return a card } // end else } // end method DealCard // handles shuffleButton Click private void shuffleButton_Click( object sender, EventArgs e ) { displayLabel.Text = "SHUFFLING..."; Shuffle(); displayLabel.Text = "DECK IS SHUFFLED"; } // end method shuffleButton_Click } // end class DeckForm (a)
(b)
Fig. 16.17 | Card shuffling and dealing simulation. (Part 3 of 4.)
635
636
Chapter 16
Strings, Characters and Regular Expressions
(c)
(d)
Fig. 16.17 | Card shuffling and dealing simulation. (Part 4 of 4.) If DealCard returns null, the string "NO MORE CARDS TO DEAL" is displayed in displayLabel, and the string "Shuffle cards to continue" is displayed in statusLabel. When the user clicks the Shuffle Cards button, event handler shuffleButton_Click (lines 93–98) invokes method Shuffle (defined on lines 53–72) to shuffle the cards. The method loops through all 52 cards (array subscripts 0–51). For each card, the method randomly picks a number in the range 0–51. Then the current Card object and the randomly selected Card object are swapped in the array. To shuffle the cards, method Shuffle makes a total of only 52 swaps during a single pass of the entire array. When the shuffling is complete, displayLabel displays the string "DECK IS SHUFFLED".
16.16 Regular Expressions and Class Regex Regular expressions are specially formatted strings used to find patterns in text. They can be useful during information validation, to ensure that data is in a particular format. For example, a ZIP code must consist of five digits, and a last name must start with a capital letter. Compilers use regular expressions to validate the syntax of programs. If the program code does not match the regular expression, the compiler indicates that there is a syntax error. The .NET Framework provides several classes to help developers recognize and manipulate regular expressions. Class Regex (of the System.Text.RegularExpressions namespace) represents an immutable regular expression. Regex method Match returns an object of class Match that represents a single regular expression match. Regex also provides method Matches, which finds all matches of a regular expression in an arbitrary string and returns an object of the class MatchCollection object containing all the Matches. A collection is a data structure, similar to an array and can be used with a foreach statement to iterate through the collection’s elements. We discuss collections in more detail in
16.16 Regular Expressions and Class Regex Chapter 26, Collections. To use class Regex, you should add a namespace System.Text.RegularExpressions.
using
637
directive for the
Regular Expression Character Classes The table in Fig. 16.18 specifies some character classes that can be used with regular expressions. Please do not confuse a character class with a C# class declaration. A character class is simply an escape sequence that represents a group of characters that might appear in a string. A word character is any alphanumeric character or underscore. A whitespace character is a space, a tab, a carriage return, a newline or a form feed. A digit is any numeric character. Regular expressions are not limited to the character classes in Fig. 16.18. As you will see in our first example, regular expressions can use other notations to search for complex patterns in strings.
16.16.1 Regular Expression Example The program of Fig. 16.19 tries to match birthdays to a regular expression. For demonstration purposes, the expression matches only birthdays that do not occur in April and that belong to people whose names begin with "J". Character class
// match regular expression to string and // print out all matches foreach ( Match myMatch in expression.Matches( string1 ) ) Console.WriteLine( myMatch ); } // end method Main } // end class RegexMatches
Jane's Birthday is 05-12-75 Joe's Birthday is 12-17-77
Fig. 16.19 | Regular expressions checking birthdays. (Part 2 of 2.) Lines 11–12 create a Regex object and pass a regular expression pattern string to the constructor. Note that we precede the string with @. Recall that backslashes within the double quotation marks following the @ character are regular backslash characters, not the beginning of escape sequences. To define the regular expression without prefixing @ to the string, you would need to escape every backslash character, as in Regex
"J.*\\d[0-35-9]-\\d\\d-\\d\\d"
which makes the regular expression more difficult to read. The first character in the regular expression, "J", is a literal character. Any string matching this regular expression is required to start with "J". In a regular expression, the dot character "." matches any single character except a newline character. When the dot character is followed by an asterisk, as in ".*", the regular expression matches any number of unspecified characters except newlines. In general, when the operator "*" is applied to a pattern, the pattern will match zero or more occurrences. By contrast, applying the operator "+" to a pattern causes the pattern to match one or more occurrences. For example, both "A*" and "A+" will match "A", but only "A*" will match an empty string. As indicated in Fig. 16.18, "\d" matches any numeric digit. To specify sets of characters other than those that belong to a predefined character class, characters can be listed in square brackets, []. For example, the pattern "[aeiou]" matches any vowel. Ranges of characters are represented by placing a dash (-) between two characters. In the example, "[0-35-9]" matches only digits in the ranges specified by the pattern—i.e., any digit between 0 and 3 or between 5 and 9; therefore, it matches any digit except 4. You can also specify that a pattern should match anything other than the characters in the brackets. To do so, place ^ as the first character in the brackets. It is important to note that "[^4]" is not the same as "[0-35-9]"; "[^4]" matches any non-digit and digits other than 4. Although the "–" character indicates a range when it is enclosed in square brackets, instances of the "-" character outside grouping expressions are treated as literal characters. Thus, the regular expression in line 12 searches for a string that starts with the letter "J", followed by any number of characters, followed by a two-digit number (of which the second digit cannot be 4), followed by a dash, another two-digit number, a dash and another two-digit number. Lines 21–22 use a foreach statement to iterate through the MatchCollection returned by the expression object’s Matches method, which received string1 as an argument. The elements in the MatchCollection are Match objects, so the foreach statement
16.16 Regular Expressions and Class Regex
639
declares variable myMatch to be of type Match. For each Match, line 22 outputs the text that matched the regular expression. The output in Fig. 16.19 indicates the two matches that were found in string1. Notice that both matches conform to the pattern specified by the regular expression.
Quantifiers The asterisk (*) in line 12 of Fig. 16.19 is more formally called a quantifier. Figure 16.20 lists various quantifiers that you can place after a pattern in a regular expression and the purpose of each quantifier. We have already discussed how the asterisk (*) and plus (+) quantifiers work. The question mark (?) quantifier matches zero or one occurrences of the pattern that it quantifies. A set of braces containing one number ({n}) matches exactly n occurrences of the pattern it quantifies. We demonstrate this quantifier in the next example. Including a comma after the number enclosed in braces matches at least n occurrences of the quantified pattern. The set of braces containing two numbers ({n,m}), matches between n and m occurrences (inclusively) of the pattern that it qualifies. All of the quantifiers are greedy— they will match as many occurrences of the pattern as possible until the pattern fails to make a match. If a quantifier is followed by a question mark (?), the quantifier becomes lazy and will match as few occurrences as possible as long as there is a successful match.
16.16.2 Validating User Input with Regular Expressions The Windows application in Fig. 16.21 presents a more involved example that uses regular expressions to validate name, address and telephone number information input by a user. When a user clicks the OK button, the program checks to make sure that none of the fields is empty (lines 19–22). If one or more fields are empty, the program displays a message to the user (lines 25–26) that all fields must be filled in before the program can validate the input information. Line 27 calls lastNameTextBox’s Focus method to place the cursor in the lastNameTextBox. The program then exits the event handler (line 28). If there are no empty fields, lines 32–105 validate the user input. Lines 32–40 validate the last name by calling static method Match of class Regex, passing both the string to validate and the regular expression as arguments. Method Match returns a Match object. This object contains a
Quantifier
Matches
*
Matches zero or more occurrences of the preceding pattern.
+
Matches one or more occurrences of the preceding pattern.
?
Matches zero or one occurrences of the preceding pattern.
{n}
Matches exactly n occurrences of the preceding pattern.
{n,}
Matches at least n occurrences of the preceding pattern.
{n,m}
Matches between n and m (inclusive) occurrences of the preceding pattern.
Fig. 16.20 | Quantifiers used in regular expressions.
640
Chapter 16
Strings, Characters and Regular Expressions
Success property that indicates whether method Match’s first argument matched the pattern specified by the regular expression in the second argument. If the value of Success is false
(i.e., there was no match), lines 36–37 display an error message, line 38 sets the focus back to the lastNameTextBox so that the user can retype the input and line 39 terminates the event handler. If there is a match, the event handler proceeds to validate the first name. This process continues until the event handler validates the user input in all the TextBoxes or until a validation fails. If all of the fields contain valid information, the program displays a message dialog stating this, and the program exits when the user dismisses the dialog.
// Fig. 16.21: Validate.cs // Validate user information using regular expressions. using System; using System.Text.RegularExpressions; using System.Windows.Forms; public partial class ValidateForm : Form { // default constructor public ValidateForm() { InitializeComponent(); } // end constructor // handles OkButton Click event private void okButton_Click( object sender, EventArgs e ) { // ensures no TextBoxes are empty if ( lastNameTextBox.Text == "" || firstNameTextBox.Text == "" || addressTextBox.Text == "" || cityTextBox.Text == "" || stateTextBox.Text == "" || zipCodeTextBox.Text == "" || phoneTextBox.Text == "" ) { // display popup box MessageBox.Show( "Please fill in all fields", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error ); lastNameTextBox.Focus(); // set focus to lastNameTextBox return; } // end if // if last name format invalid show message if ( !Regex.Match( lastNameTextBox.Text, "^[A-Z][a-zA-Z]*$" ).Success ) { // last name was incorrect MessageBox.Show( "Invalid last name", "Message", MessageBoxButtons.OK, MessageBoxIcon.Error ); lastNameTextBox.Focus(); return; } // end if
Fig. 16.21 | Validating user information using regular expressions. (Part 1 of 4.)
// if first name format invalid show message if ( !Regex.Match( firstNameTextBox.Text, "^[A-Z][a-zA-Z]*$" ).Success ) { // first name was incorrect MessageBox.Show( "Invalid first name", "Message", MessageBoxButtons.OK, MessageBoxIcon.Error ); firstNameTextBox.Focus(); return; } // end if // if address format invalid show message if ( !Regex.Match( addressTextBox.Text, @"^[0-9]+\s+([a-zA-Z]+|[a-zA-Z]+\s[a-zA-Z]+)$" ).Success ) { // address was incorrect MessageBox.Show( "Invalid address", "Message", MessageBoxButtons.OK, MessageBoxIcon.Error ); addressTextBox.Focus(); return; } // end if // if city format invalid show message if ( !Regex.Match( cityTextBox.Text, @"^([a-zA-Z]+|[a-zA-Z]+\s[a-zA-Z]+)$" ).Success ) { // city was incorrect MessageBox.Show( "Invalid city", "Message", MessageBoxButtons.OK, MessageBoxIcon.Error ); cityTextBox.Focus(); return; } // end if // if state format invalid show message if ( !Regex.Match( stateTextBox.Text, @"^([a-zA-Z]+|[a-zA-Z]+\s[a-zA-Z]+)$" ).Success ) { // state was incorrect MessageBox.Show( "Invalid state", "Message", MessageBoxButtons.OK, MessageBoxIcon.Error ); stateTextBox.Focus(); return; } // end if // if zip code format invalid show message if ( !Regex.Match( zipCodeTextBox.Text, @"^\d{5}$" ).Success ) { // zip was incorrect MessageBox.Show( "Invalid zip code", "Message", MessageBoxButtons.OK, MessageBoxIcon.Error ); zipCodeTextBox.Focus(); return; } // end if
Fig. 16.21 | Validating user information using regular expressions. (Part 2 of 4.)
641
642
Chapter 16
Strings, Characters and Regular Expressions
95 96 // if phone number format invalid show message if ( !Regex.Match( phoneTextBox.Text, 97 @"^[1-9]\d{2}-[1-9]\d{2}-\d{4}$" ).Success ) 98 99 { 100 // phone number was incorrect 101 MessageBox.Show( "Invalid phone number", "Message", 102 MessageBoxButtons.OK, MessageBoxIcon.Error ); 103 phoneTextBox.Focus(); 104 return; 105 } // end if 106 107 // information is valid, signal user and exit application this.Hide(); // hide main window while MessageBox displays 108 109 MessageBox.Show( "Thank You!", "Information Correct", 110 MessageBoxButtons.OK, MessageBoxIcon.Information ); Application.Exit(); 111 112 } // end method okButton_Click 113 } // end class ValidateForm (a)
(b)
Fig. 16.21 | Validating user information using regular expressions. (Part 3 of 4.)
16.16 Regular Expressions and Class Regex
643
(c)
(d)
Fig. 16.21 | Validating user information using regular expressions. (Part 4 of 4.) In the previous example, we searched a string for substrings that matched a regular expression. In this example, we want to ensure that the entire string in each TextBox conforms to a particular regular expression. For example, we want to accept "Smith" as a last name, but not "9@Smith#". In a regular expression that begins with a "^" character and ends with a "$" character, the characters "^" and "$" represent the beginning and end of a string, respectively. These characters force a regular expression to return a match only if the entire string being processed matches the regular expression. The regular expression in line 33 uses the square bracket and range notation to match an uppercase first letter, followed by letters of any case—a-z matches any lowercase letter, and A-Z matches any uppercase letter. The * quantifier signifies that the second range of characters may occur zero or more times in the string. Thus, this expression matches any string consisting of one uppercase letter, followed by zero or more additional letters. The notation \s matches a single whitespace character (lines 55, 66 and 77). The expression \d{5}, used in the Zip (zip code) field, matches any five digits (line 87). Note that without the "^" and "$" characters, the regular expression would match any five con-
644
Chapter 16
Strings, Characters and Regular Expressions
secutive digits in the string. By including the "^" and "$" characters, we ensure that only five-digit zip codes are allowed. The character "|" (lines 55, 66 and 77) matches the expression to its left or the expression to its right. For example, Hi (John|Jane) matches both Hi John and Hi Jane. In line 55, we use the character "|" to indicate that the address can contain a word of one or more characters or a word of one or more characters followed by a space and another word of one or more characters. Note the use of parentheses to group parts of the regular expression. Quantifiers may be applied to patterns enclosed in parentheses to create more complex regular expressions. The Last name and First name fields both accept strings of any length that begin with an uppercase letter. The regular expression for the Address field (line 55) matches a number of at least one digit, followed by a space and then either one or more letters or else one or more letters followed by a space and another series of one or more letters. Therefore, "10 Broadway" and "10 Main Street" are both valid addresses. As currently formed, the regular expression in line 55 does not match an address that does not start with a number or that has more than two words. The regular expressions for the City (line 66) and State (line 77) fields match any word of at least one character or, alternatively, any two words of at least one character if the words are separated by a single space. This means both Waltham and West Newton would match. Again, these regular expressions would not accept names that have more than two words. The regular expression for the Zip code field (line 87) ensures that the zip code is a five-digit number. The regular expression for the Phone field (line 98) indicates that the phone number must be of the form xxx-yyy-yyyy, where the xs represent the area code and the ys the number. The first x and the first y cannot be zero, as specified by the range [1–9] in each case.
16.16.3 Regex methods Replace and Split Sometimes it is useful to replace parts of one string with another or to split a string according to a regular expression. For this purpose, the Regex class provides static and instance versions of methods Replace and Split, which are demonstrated in Fig. 16.22. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
// Fig. 16.22: RegexSubstitution.cs // Using Regex method Replace. using System; using System.Text.RegularExpressions; class RegexSubstitution { public static void Main() { string testString1 = "This sentence ends in 5 stars *****"; string output = ""; string testString2 = "1, 2, 3, 4, 5, 6, 7, 8"; Regex testRegex1 = new Regex( @"\d" ); string[] result;
Console.WriteLine( "Original string: " + testString1 ); testString1 = Regex.Replace( testString1, @"\*", "^" ); Console.WriteLine( "^ substituted for *: " + testString1 ); testString1 = Regex.Replace( testString1, "stars", "carets" ); Console.WriteLine( "\"carets\" substituted for \"stars\": " + testString1 ); Console.WriteLine( "Every word replaced by \"word\": " + Regex.Replace( testString1, @"\w+", "word" ) ); Console.WriteLine( "\nOriginal string: " + testString2 ); Console.WriteLine( "Replace first 3 digits by \"digit\": " + testRegex1.Replace( testString2, "digit", 3 ) ); Console.Write( "string split at commas [" ); result = Regex.Split( testString2, @",\s" ); foreach ( string resultString in result ) output += "\"" + resultString + "\", "; // Delete ", " at the end of output string Console.WriteLine( output.Substring( 0, output.Length - 2 ) + "]" ); } // end method Main } // end class RegexSubstitution
Original string: This sentence ends in 5 stars ***** ^ substituted for *: This sentence ends in 5 stars ^^^^^ "carets" substituted for "stars": This sentence ends in 5 carets ^^^^^ Every word replaced by "word": word word word word word word ^^^^^ Original string: 1, 2, 3, 4, 5, 6, 7, 8 Replace first 3 digits by "digit": digit, digit, digit, 4, 5, 6, 7, 8 string split at commas ["1", "2", "3", "4", "5", "6", "7", "8"]
Fig. 16.22 |
Regex
methods Replace and Split. (Part 2 of 2.)
Method Replace replaces text in a string with new text wherever the original string matches a regular expression. We use two versions of this method in Fig. 16.22. The first version (line 19) is static and takes three parameters—the string to modify, the string containing the regular expression to match and the replacement string. Here, Replace replaces every instance of "*" in testString1 with "^". Notice that the regular expression (@"\*") precedes character * with a backslash, \. Normally, * is a quantifier indicating that a regular expression should match any number of occurrences of a preceding pattern. However, in line 19, we want to find all occurrences of the literal character *; to do this, we must escape character * with character \. By escaping a special regular expression character with a \, we tell the regular-expression matching engine to find the actual character * rather than use it as a quantifier. The second version of method Replace (line 29) is an instance method that uses the regular expression passed to the constructor for testRegex1 (line 14) to perform the replacement operation. Line 14 instantiates testRegex1 with argument @"\d". The call to instance method Replace in line 29 takes three arguments—a string to modify, a string
646
Chapter 16
Strings, Characters and Regular Expressions
containing the replacement text and an int specifying the number of replacements to make. In this case, line 29 replaces the first three instances of a digit ("\d") in testString2 with the text "digit". Method Split divides a string into several substrings. The original string is broken at delimiters that match a specified regular expression. Method Split returns an array containing the substrings. In line 32, we use the static version of method Split to separate a string of comma-separated integers. The first argument is the string to split; the second argument is the regular expression that represents the delimiter. The regular expression @",\s" separates the substrings at each comma. By matching any whitespace characters (\s* in the regular expression), we eliminate extra spaces from the resulting substrings.
16.17 Wrap-Up In this chapter, you learned about the FCL’s string and character processing capabilities. We overviewed the fundamentals of characters and strings. You saw how to determine the length of strings, copy strings, access the individual characters in strings, search strings, obtain substrings from larger strings, compare strings, concatenate strings, replace characters in strings and convert strings to uppercase or lowercase letters. We showed how to use class StringBuilder to build strings dynamically. You learned how to determine and specify the size of a StringBuilder object, and how to append, insert, remove and replace characters in a StringBuilder object. We then introduced the character-testing methods of type Char that enable a program to determine whether a character is a digit, a letter, a lowercase letter, an uppercase letter, a punctuation mark or a symbol other than a punctuation mark, and the methods for converting a character to uppercase or lowercase. Finally, we discussed classes Regex and Match from the System.Text.RegularExpressions namespace and the symbols that are used to form regular expressions. You learned how to find patterns in a string and match entire strings to patterns with Regex methods Match and Matches, how to replace characters in a string with Regex method Replace and how to split strings at delimiters with Regex method Split. In the next chapter, you will learn how to add graphics and other multimedia capabilities to your Windows applications.
17 Graphics and Multimedia One picture is worth ten thousand words. —Chinese proverb
Treat nature in terms of the cylinder, the sphere, the cone, all in perspective. —Paul Cezanne
Nothing ever becomes real till it is experienced—even a proverb is no proverb to you till your life has illustrated it. —John Keats
A picture shows me at a glance what it takes dozens of pages of a book to expound. —Ivan Sergeyevich
OBJECTIVES In this chapter you will learn: I
To understand graphics contexts and graphics objects.
I
To manipulate colors and fonts.
I
To understand and use GDI+ Graphics methods to draw lines, rectangles, strings and images.
I
To use class Image to manipulate and display images.
I
To draw complex shapes from simple shapes with class GraphicsPath.
I
To use Windows Media Player to play audio or video in a C# application.
I
To use Microsoft Agent to add interactive animated characters to a C# application.
Introduction Drawing Classes and the Coordinate System Graphics Contexts and Graphics Objects Color Control Font Control Drawing Lines, Rectangles and Ovals Drawing Arcs Drawing Polygons and Polylines Advanced Graphics Capabilities Introduction to Multimedia Loading, Displaying and Scaling Images Animating a Series of Images Windows Media Player Microsoft Agent Wrap-Up
17.1 Introduction In this chapter, we overview C#’s tools for drawing two-dimensional shapes and for controlling colors and fonts. C# supports graphics that enable programmers to enhance their Windows applications visually. The FCL contains many sophisticated drawing capabilities as part of namespace System.Drawing and the other namespaces that make up the .NET resource GDI+. GDI+ is an application programming interface (API) that provides classes for creating two-dimensional vector graphics (a way to describe graphics so that they may be easily manipulated with high-performance techniques), manipulating fonts and inserting images. We begin with an introduction to the .NET framework’s drawing capabilities. We then present more powerful drawing capabilities, such as changing the styles of lines used to draw shapes and controlling the colors and patterns of filled shapes. Later in this chapter, we explore techniques for manipulating images and creating smooth animations. We also discuss class Image, which can store and manipulate images of various formats. We explain how to combine the graphical rendering capabilities covered in the early sections of the chapter with those for image manipulation. This chapter ends with several multimedia examples in which you build an animation, use the Windows Media Player control and use Microsoft Agent—a technology for adding interactive animated characters to applications or Web pages.
17.2 Drawing Classes and the Coordinate System Figure 17.1 depicts a portion of namespace System.Drawing, including several graphics classes and structures covered in this chapter. Namespaces System.Drawing and System.Drawing.Drawing2D contain the most commonly used GDI+ components.
17.2 Drawing Classes and the Coordinate System
649
System.Object
System.MarshalByRefObject
Font Color FontFamily Point Graphics Rectangle Icon Size Pen
Class Graphics contains methods used for drawing strings, lines, rectangles and other shapes on a Control. The drawing methods of class Graphics usually require a Pen or Brush object to render a specified shape. The Pen draws shape outlines; the Brush draws solid objects. The Color structure contains numerous static properties, which set the colors of various graphical components, plus methods that allow users to create new colors. Class Font contains properties that define unique fonts. Class FontFamily contains methods for obtaining font information.
650
Chapter 17
Graphics and Multimedia
To begin drawing in C#, we first must understand GDI+’s coordinate system (Fig. 17.2), a scheme for identifying every point on the screen. By default, the upper-left corner of a GUI component (such as a Panel or a Form) has the coordinates (0, 0). A coordinate pair has both an x-coordinate (the horizontal coordinate) and a y-coordinate (the vertical coordinate). The x-coordinate is the horizontal distance (to the right) from the upper-left corner. The y-coordinate is the vertical distance (downward) from the upperleft corner. The x-axis defines every horizontal coordinate, and the y-axis defines every vertical coordinate. Programmers position text and shapes on the screen by specifying their (x, y) coordinates. Coordinate units are measured in pixels (“picture elements”), which are the smallest units of resolution on a display monitor.
Portability Tip 17.1 Different display monitors have different resolutions, so the density of pixels on such monitors will vary. This might cause the sizes of graphics to appear different on different monitors. 17.1
The System.Drawing namespace provides several structures that represent sizes and locations in the coordinate system. The Point structure represents the x-y coordinates of a point on a two-dimensional plane. The Rectangle structure defines the loading width and height of a rectangular shape. The Size structure represents the width and height of a shape.
17.3 Graphics Contexts and Graphics Objects A C# graphics context represents a drawing surface that enables drawing on the screen. A Graphics object manages a graphics context by controlling how information is drawn. Graphics objects contain methods for drawing, font manipulation, color manipulation and other graphics-related actions. Every derived class of System.Windows.Forms.Form inherits a virtual OnPaint method in which most graphics operations are performed. The arguments to the OnPaint method include a PaintEventArgs object from which we can obtain a Graphics object for the Form. We must obtain the Graphics object on each call to the method, because the properties of the graphics context that the graphics object represents could change. Method OnPaint triggers the Control’s Paint event.
+x
(0, 0)
+y
X axis
(x, y)
Y axis
Fig. 17.2 | GDI+ coordinate system. Units are measured in pixels.
17.4 Color Control
651
When drawing on a Form, you can override method OnPaint to retrieve a Graphics object from argument PaintEventArgs or to create a new Graphics object associated with the appropriate surface. We demonstrate these drawing techniques in C# later in the chapter. To override the inherited OnPaint method, use the following method header: protected override void OnPaint( PaintEventArgs e )
Next, extract the incoming Graphics object from argument PaintEventArg, as in: Graphics graphicsObject = e.Graphics;
Variable graphicsObject can now be used to draw shapes and strings on the form. Calling the OnPaint method raises the Paint event. Instead of overriding the OnPaint method, programmers can add an event handler for the Paint event. Visual Studio .NET generates the Paint event handler in this form: protected void MyEventHandler_Paint( object sender, PaintEventArgs e )
Programmers seldom call the OnPaint method directly, because drawing graphics is an event-driven process. An event—such as covering, uncovering or resizing a window— calls the OnPaint method of that Form. Similarly, when any control (such as a TextBox or Label) is displayed, that control’s OnPaint method is called. You can force a call to OnPaint by calling a Control’s Invalidate method. This method refreshes a control and implicitly repaints all its graphical components. Class Control has several overloaded Invalidate methods that allow programmers to update portions of a control.
Performance Tip 17.1 Calling the Invalidate method to refresh the Control can be inefficient if only a small portion of a Control needs refreshing. Calling Invalidate with a Rectangle parameter refreshes only the area designated by the rectangle. This improves program performance. 17.1
Controls, such as Labels and Buttons, do not have their own graphics contexts, but you can create them. To draw on a control, first create a graphics object by invoking the control’s CreateGraphics method, as in: Graphics graphicsObject = controlName.CreateGraphics();
Now you can use the methods provided in class Graphics to draw on the control.
17.4 Color Control Colors can enhance a program’s appearance and help convey meaning. For example, a red traffic light indicates stop, yellow indicates caution and green indicates go. Structure Color defines methods and constants used to manipulate colors. Every color can be created from a combination of alpha, red, green and blue components (called ARGB values). All four ARGB components are bytes that represent integer values in the range 0 to 255. The alpha value determines the opacity of the color. For
652
Chapter 17
Graphics and Multimedia
example, the alpha value 0 represents a transparent color, and the value 255 represents an opaque color. Alpha values between 0 and 255 result in a weighted blending effect of the color’s RGB value with that of any background color, causing a semitransparent effect. The first number in the RGB value defines the amount of red in the color, the second defines the amount of green and the third defines the amount of blue. The larger the value, the greater the amount of that particular color. C# enables programmers to choose from almost 17 million colors. If a particular computer cannot display all these colors, it will display the color closest to the one specified. Figure 17.3 summarizes some predefined Color constants (all are public and static), and Fig. 17.4 describes several Color methods and properties. For a complete list of predefined Color constants, methods and properties, see the online documentation for the Color structure (msdn2.microsoft.com/ en-us/library/system.drawing.color). Constants in structure Color
RGB value
Constants in structure Color
RGB value
Orange
255, 200, 0
White
255, 255, 255
Pink
255, 175, 175
Gray
128, 128, 128
Cyan
0, 255, 255
DarkGray
64, 64, 64
Magenta
255, 0, 255
Red
255, 0, 0
Yellow
255, 255, 0
Green
0, 255, 0
Black
0, 0, 0
Blue
0, 0, 255
Fig. 17.3
| Color
Structure Color methods and properties
structure static constants and their RGB values.
Description
Common Methods FromArgb
A static method that creates a color based on red, green and blue values expressed as ints from 0 to 255. The overloaded version allows specification of alpha, red, green and blue values.
FromName
A static method that creates a color from a name, passed as a string.
Common Properties A
A byte between 0 and 255, representing the alpha component.
R
A byte between 0 and 255, representing the red component.
G
A byte between 0 and 255, representing the green component.
B
A byte between 0 and 255, representing the blue component.
Fig. 17.4
| Color
structure methods and properties.
17.4 Color Control
653
The table in Fig. 17.4 describes two FromArgb method calls. One takes three int arguments, and one takes four int arguments (all argument values must be between 0 and 255, inclusive). Both take int arguments specifying the amount of red, green and blue. The overloaded version also allows the user to specify the alpha component; the three-argument version defaults the alpha to 255 (opaque). Both methods return a Color object. Color properties A, R, G and B return bytes that represent int values from 0 to 255, corresponding to the amounts of alpha, red, green and blue, respectively. Programmers draw shapes and strings with Brushes and Pens. A Pen, which functions similarly to an ordinary pen, is used to draw lines. Most drawing methods require a Pen object. The overloaded Pen constructors allow programmers to specify the colors and widths of the lines that they wish to draw. The System.Drawing namespace also provides a Pens class containing predefined Pens. All classes derived from abstract class Brush define objects that color the interiors of graphical shapes. For example, the SolidBrush constructor takes a Color object—the color to draw. In most Fill methods, Brushes fill a space with a color, pattern or image. Figure 17.5 summarizes various Brushes and their functions.
Manipulating Colors Figure 17.6 demonstrates several of the methods and properties described in Fig. 17.4. It displays two overlapping rectangles, allowing you to experiment with color values, color names and alpha values (for transparency). Class
Description
HatchBrush
Fills a region with a pattern. The pattern is defined by a member of the HatchStyle enumeration, a foreground color (with which the pattern is drawn) and a background color.
LinearGradientBrush
Fills a region with a gradual blend of one color to another. Linear gradients are defined along a line. They can be specified by the two colors, the angle of the gradient and either the width of a rectangle or two points.
SolidBrush
Fills a region with one color that is specified by a Color object.
TextureBrush
Fills a region by repeating a specified Image across the surface.
Fig. 17.5 | Classes that derive from class Brush. 1 2 3 4 5 6 7 8
// Fig 17.6: ShowColors.cs // Color value and alpha demonstration. using System; using System.Drawing; using System.Windows.Forms; public partial class ShowColors : Form {
Fig. 17.6 | Color value and alpha demonstration. (Part 1 of 3.)
// color for back rectangle private Color backColor = Color.Wheat; // color for front rectangle private Color frontColor = Color.FromArgb( 100, 0, 0, 255 ); // default constructor public ShowColors() { InitializeComponent(); } // end constructor // override Form OnPaint method protected override void OnPaint( PaintEventArgs e ) { Graphics graphicsObject = e.Graphics; // get graphics // create text brush SolidBrush textBrush = new SolidBrush( Color.Black ); // create solid brush SolidBrush brush = new SolidBrush( Color.White ); // draw white background graphicsObject.FillRectangle( brush, 4, 4, 275, 180 ); // display name of backColor graphicsObject.DrawString( backColor.Name, this.Font, textBrush, 40, 5 ); // set brush color and display back rectangle brush.Color = backColor; graphicsObject.FillRectangle( brush, 45, 20, 150, 120 ); // display Argb values of front color graphicsObject.DrawString( "Alpha: " + frontColor.A + " Red: " + frontColor.R + " Green: " + frontColor.G + " Blue: " + frontColor.B, this.Font, textBrush, 55, 165 ); // set brush color and display front rectangle brush.Color = frontColor; graphicsObject.FillRectangle( brush, 65, 35, 170, 130 ); } // end method OnPaint // handle colorNameButton click event private void colorNameButton_Click( object sender, EventArgs e ) { // set backColor to color specified in text box backColor = Color.FromName( colorNameTextBox.Text ); Invalidate(); // refresh Form } // end method colorNameButton_Click
Fig. 17.6 | Color value and alpha demonstration. (Part 2 of 3.)
17.4 Color Control
62 63 64 65 66 67 68 69 70 71 72 73 74
655
// handle colorValueButton click event private void colorValueButton_Click( object sender, EventArgs e ) { // obtain new front color from text boxes frontColor = Color.FromArgb( Convert.ToInt32( alphaTextBox.Text ), Convert.ToInt32( redTextBox.Text ), Convert.ToInt32( greenTextBox.Text ), Convert.ToInt32( blueTextBox.Text ) ); Invalidate(); // refresh Form } // end method colorValueButton_Click } // end class ShowColors
Fig. 17.6 | Color value and alpha demonstration. (Part 3 of 3.) When the application begins executing, its Form is displayed. This results in a call to ShowColors’s OnPaint method to paint the Form’s contents. Line 24 gets a reference to PaintEventArgs e’s Graphics object and assigns it to graphicsObject. Lines 27 and 30 create a black and a white SolidBrush for drawing solid shapes on the Form. Class SolidBrush derives from abstract base class Brush, so a SolidBrush can be passed to any method that expects a Brush parameter. Line 33 uses Graphics method FillRectangle to draw a solid white rectangle using the SolidBrush created in line 30. FillRectangle takes as parameters a Brush, the x- and y-coordinates of the rectangle’s upper-left corner, and the width and height of the rectangle. Lines 36–37 display the Name property of backColor with Graphics method DrawString. There are several overloaded DrawString methods; the version demonstrated in lines 36–37 takes as arguments the string to display, the display Font, the Brush to use for drawing and the x- and y-coordinates of the location for the string’s first character. Lines 40–41 assign the backColor value to brush’s Color property and display a rectangle. Lines 44–46 extract and display frontColor’s ARGB values and draw a string containing those values. Lines 49–50 assign the frontColor value to brush’s Color property, then draw a filled rectangle in the frontColor that overlaps the rectangle drawn at line 41.
656
Chapter 17
Graphics and Multimedia
Button event handler colorNameButton_Click (lines 54–60) uses class Color’s static method FromName to create a new Color object from the colorName that a user enters in a TextBox. This Color is assigned to backColor (line 57). Then line 59 invokes the Form’s Invalidate method to indicate that the Form should be repainted, which results in a call to OnPaint to update the Form on the screen. Button event handler colorValueButton_Click (lines 63–73) uses Color method FromArgb to construct a new Color object from the ARGB values that a user specifies via TextBoxes, then assigns the newly created Color to frontColor. Line 72 invokes the Form’s Invalidate method to indicate that the Form should be repainted, which results in a call to OnPaint to update the Form on the screen. If the user assigns an alpha value between 0 and 255 for the frontColor, the effects
of alpha blending are apparent. In the sample output, the red back rectangle blends with the blue front rectangle to create purple where the two overlap. Note that you cannot change the characteristics of an existing Color object. To use a different color, create a new Color object.
Using the ColorDialog to Select Colors from a Color Palette The predefined GUI component ColorDialog is a dialog box that allows users to select from a palette of available colors or to create custom colors. Figure 17.7 demonstrates the ColorDialog. When a user selects a color and presses OK, the application retrieves the user’s selection via the ColorDialog’s Color property. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
// Fig. 17.7: ShowColorsComplex.cs // ColorDialog used to change background and text color. using System; using System.Drawing; using System.Windows.Forms; // allows users to change colors using a ColorDialog public partial class ShowColorsComplex : Form { // create ColorDialog object private static ColorDialog colorChooser = new ColorDialog();
Fig. 17.7
// default constructor public ShowColorsComplex() { InitializeComponent(); } // end constructor // change text color private void textColorButton_Click( object sender, EventArgs e ) { // get chosen color DialogResult result = colorChooser.ShowDialog(); if ( result == DialogResult.Cancel ) return;
| ColorDialog
used to change background and text color. (Part 1 of 2.)
// assign forecolor to result of dialog backgroundColorButton.ForeColor = colorChooser.Color; textColorButton.ForeColor = colorChooser.Color; } // end method textColorButton_Click // change background color private void backgroundColorButton_Click( object sender, EventArgs e ) { // show ColorDialog and get result colorChooser.FullOpen = true; DialogResult result = colorChooser.ShowDialog(); if ( result == DialogResult.Cancel ) return; // set background color this.BackColor = colorChooser.Color; } // end method backgroundColorButton_Click } // end class ShowColorsComplex
Fig. 17.7
| ColorDialog
used to change background and text color. (Part 2 of 2.)
The GUI for this application contains two Buttons. The backgroundColorButton allows the user to change the form background color. The textColorButton allows the user to change the button text colors. Line 11 creates a private static ColorDialog named colorChooser, which is used in the event handlers for both Buttons.
658
Chapter 17
Graphics and Multimedia
Lines 20–31 define the textColorButtonClick event handler, which invokes colormethod (line 23) to display the dialog. The dialog’s Color property stores the user’s selection. Lines 29–30 set the text color of both buttons to the selected color. Lines 34–45 define the backgroundColorButtonClick event handler, which modifies the background color of the form by setting its BackColor property to the dialog’s Color property (line 44). The method sets the ColorDialog’s FullOpen property to true (line 37), so the dialog displays all available colors, as shown in the screen capture in Fig. 17.7. When FullOpen is false, the dialog shows only the color swatches. Users are not restricted to the ColorDialog’s 48 color swatches. To create a custom color, users can click anywhere in the ColorDialog’s large rectangle, which displays various color shades. Adjust the slider, hue and other features to refine the color. When finished, click the Add to Custom Colors button, which adds the custom color to a square in the Custom Colors section of the dialog. Clicking OK sets the Color property of the ColorDialog to that color. Chooser’s ShowDialog
17.5 Font Control This section introduces methods and constants that are related to font control. The properties of Font objects cannot be modified. If you need a different Font, you must create a new Font object. There are many overloaded versions of the Font constructor for initializing Font objects. Some properties of class Font are summarized in Fig. 17.8. Note that the Size property returns the font size as measured in design units, whereas SizeInPoints returns the font size as measured in points (the more common measurement). Design units allow the font size to be specified in one of several units of measurement, such as inches or millimeters. Some versions of the Font constructor accept a
Property
Description
Bold
Returns true if the font is bold.
FontFamily
Returns the Font’s FontFamily—a grouping structure to organize fonts and define their similar properties.
Height
Returns the height of the font.
Italic
Returns true if the font is italic.
Name
Returns the font’s name as a string.
Size
Returns a float value indicating the current font size measured in design units (design units are any specified unit of measurement for the font).
SizeInPoints
Returns a float value indicating the current font size measured in points.
Strikeout
Returns true if the font is in strikeout format.
Underline
Returns true if the font is underlined.
Fig. 17.8 |
Font
class read-only properties.
17.5 Font Control
659
argument. GraphicsUnit is an enumeration that allows you to specify the unit of measurement that describes the font size. Members of the GraphicsUnit enumeration include Point (1/72 inch), Display (1/75 inch), Document (1/300 inch), Millimeter, Inch and Pixel. If this argument is provided, the Size property contains the size of the font as measured in the specified design unit, and the SizeInPoints property contains the corresponding size of the font in points. For example, if we create a Font having size 1 and specify the unit of measurement as GraphicsUnit.Inch, the Size property will be 1 and the SizeInPoints property will be 72, because there are 72 points in an inch. The default measurement for the font size is GraphicsUnit.Point (thus, the Size and SizeInPoints properties will be equal). Class Font has several constructors. Most require a font name, which is a string representing a font currently supported by the system. Common fonts include Microsoft SansSerif and Serif. Most Font constructors also require as arguments the font size and font style. The font style is specified with a constant from the FontStyle enumeration (Bold, Italic, Regular, Strikeout and Underline, or a combination of these). You can combine font styles with the | operator, as in FontStyle.Italic | FontStyle.Bold, which makes a font both italic and bold. Graphics method DrawString sets the current drawing font—the font in which the text displays—to its Font argument. GraphicsUnit
Common Programming Error 17.1 Specifying a font that is not available on a system is a logic error. If this occurs, C# will substitute that system’s default font. 17.1
Drawing Strings in Different Fonts The program in Fig. 17.9 displays text in different fonts and sizes. The program uses the Font constructor to initialize the Font objects (lines 24, 28, 32 and 36). Each call to the Font constructor passes a font name (e.g., Arial, Times New Roman, Courier New or Tahoma) as a string, a font size (a float) and a FontStyle object (style). Graphics method DrawString sets the font and draws the text at the specified location. Note that line 20 creates a DarkBlue SolidBrush object (brush). All strings drawn with that brush appear in DarkBlue. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
// Fig. 17.9 UsingFonts.cs // Fonts and FontStyles. using System; using System.Drawing; using System.Windows.Forms; // demonstrate font constructors and properties public partial class UsingFonts : Form { // default constructor public UsingFonts() { InitializeComponent(); } // end constructor
// demonstrate various font and style settings protected override void OnPaint( PaintEventArgs paintEvent ) { Graphics graphicsObject = paintEvent.Graphics; SolidBrush brush = new SolidBrush( Color.DarkBlue ); // arial, 12 pt bold FontStyle style = FontStyle.Bold; Font arial = new Font( "Arial" , 12, style ); // times new roman, 12 pt regular style = FontStyle.Regular; Font timesNewRoman = new Font( "Times New Roman", 12, style ); // courier new, 16 pt bold and italic style = FontStyle.Bold | FontStyle.Italic; Font courierNew = new Font( "Courier New", 16, style ); // tahoma, 18 pt strikeout style = FontStyle.Strikeout; Font tahoma = new Font( "Tahoma", 18, style ); graphicsObject.DrawString( arial.Name + " 12 point bold.", arial, brush, 10, 10 ); graphicsObject.DrawString( timesNewRoman.Name + " 12 point plain.", timesNewRoman, brush, 10, 30 ); graphicsObject.DrawString( courierNew.Name + " 16 point bold and italic.", courierNew, brush, 10, 54 ); graphicsObject.DrawString( tahoma.Name + " 18 point strikeout.", tahoma, brush, 10, 75 ); } // end method OnPaint } // end class UsingFonts
Fig. 17.9
| Fonts
and FontStyles. (Part 2 of 2.)
Font Metrics You can determine precise information about a font’s metrics (or properties), such as height, descent (the amount that characters dip below the baseline), ascent (the amount that characters rise above the baseline) and leading (the difference between the ascent of one line and the decent of the previous line). Figure 17.10 illustrates these font metrics.
17.5 Font Control
Xy1Õ
height
661
leading
ascent
descent
baseline
Fig. 17.10 | Font metrics illustration. Class FontFamily defines characteristics common to a group of related fonts. Class FontFamily provides several methods used to determine the font metrics that are shared by members of a particular family. These methods are summarized in Fig. 17.11. The program in Fig. 17.12 displays the metrics of two fonts. Line 23 creates Font object arial and sets it to 12-point Arial font. Line 24 uses Font property FontFamily to obtain object arial’s FontFamily object. Lines 27–28 output the string representation of the font. Lines 30–44 then use methods of class FontFamily to obtain the ascent, descent, height and leading of the font and draw strings containing that information. Lines 47–68 repeat this process for font sansSerif, a Font object derived from the MS Sans Serif FontFamily. Method
Description
GetCellAscent
Returns an int representing the ascent of a font as measured in design units.
GetCellDescent
Returns an int representing the descent of a font as measured in design units.
GetEmHeight
Returns an int representing the height of a font as measured in design units.
GetLineSpacing
Returns an int representing the distance between two consecutive lines of text as measured in design units.
Fig. 17.11 | 1 2 3 4 5 6 7 8 9
FontFamily
methods that return font-metric information.
// Fig 17.12: UsingFontMetrics.cs // Displaying font metric information using System; using System.Drawing; using System.Windows.Forms; // display font information public partial class UsingFontMetrics : Form {
Fig. 17.12 |
FontFamily
class used to obtain font-metric information. (Part 1 of 3.)
class used to obtain font-metric information. (Part 3 of 3.)
17.6 Drawing Lines, Rectangles and Ovals This section presents Graphics methods for drawing lines, rectangles and ovals. Each of the drawing methods has several overloaded versions. Methods that draw hollow shapes typically require as arguments a Pen and four ints. Methods that draw solid shapes typically require as arguments a Brush and four ints. The first two int arguments represent the coordinates of the upper-left corner of the shape (or its enclosing area), and the last two ints indicate the shape’s (or enclosing area’s) width and height. Figure 17.13 summarizes several Graphics methods and their parameters. [Note: Many of these methods are overloaded—consult the documentation for a complete listing (msdn2.microsoft.com/ en-us/library/system.drawing.graphics).] Graphics Drawing Methods and Descriptions DrawLine( Pen p, int x1, int y1, int x2, int y2 )
Draws a line from (x1, y1) to (x2, y2). The Pen determines the line’s color, style and width. DrawRectangle( Pen p, int x, int y, int width, int height )
Draws a rectangle of the specified width and height. The top-left corner of the rectangle is at point (x, y). The Pen determines the rectangle’s color, style and border width. FillRectangle( Brush b, int x, int y, int width, int height )
Draws a solid rectangle of the specified width and height. The top-left corner of the rectangle is at point (x, y). The Brush determines the fill pattern inside the rectangle.
Fig. 17.13
| Graphics
methods that draw lines, rectangles and ovals. (Part 1 of 2.)
664
Chapter 17
Graphics and Multimedia
Graphics Drawing Methods and Descriptions DrawEllipse( Pen p, int x, int y, int width, int height )
Draws an ellipse inside a bounding rectangle of the specified width and height. The top-left corner of the bounding rectangle is located at (x, y). The Pen determines the color, style and border width of the ellipse. FillEllipse( Brush b, int x, int y, int width, int height )
Draws a filled ellipse inside a bounding rectangle of the specified width and height. The topleft corner of the bounding rectangle is located at (x, y). The Brush determines the pattern inside the ellipse.
Fig. 17.13
| Graphics
methods that draw lines, rectangles and ovals. (Part 2 of 2.)
The application in Fig. 17.14 draws lines, rectangles and ellipses. In this application, we also demonstrate methods that draw filled and unfilled shapes. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
// Fig. 17.14: LinesRectanglesOvals.cs // Demonstrating lines, rectangles and ovals. using System; using System.Drawing; using System.Windows.Forms; // draw shapes on Form public partial class LinesRectanglesOvals : Form { // default constructor public LinesRectanglesOvals() { InitializeComponent(); } // end constructor // override Form OnPaint method protected override void OnPaint( PaintEventArgs paintEvent ) { // get graphics object Graphics g = paintEvent.Graphics; SolidBrush brush = new SolidBrush( Color.Blue ); Pen pen = new Pen( Color.AliceBlue ); // create filled rectangle g.FillRectangle( brush, 90, 30, 150, 90 ); // draw lines to g.DrawLine( pen, g.DrawLine( pen, g.DrawLine( pen, g.DrawLine( pen,
// draw top rectangle g.DrawRectangle( pen, 110, 40, 150, 90 ); // set brush to red brush.Color = Color.Red; // draw base Ellipse g.FillEllipse( brush, 280, 75, 100, 50 ); // draw connecting lines g.DrawLine( pen, 380, 55, 380, 100 ); g.DrawLine( pen, 280, 55, 280, 100 ); // draw Ellipse outline g.DrawEllipse( pen, 280, 30, 100, 50 ); } // end method OnPaint } // end class LinesRectanglesOvals
Fig. 17.14 | Demonstration of methods that draw lines, rectangles and ellipses. (Part 2 of 2.) Methods FillRectangle and DrawRectangle (lines 25 and 34) draw rectangles on the screen. For each method, the first argument specifies the drawing object to use. The FillRectangle method uses a Brush object (in this case, an instance of SolidBrush—a class that derives from Brush), whereas the DrawRectangle method uses a Pen object. The next two arguments specify the coordinates of the upper-left corner of the bounding rectangle, which represents the area in which the rectangle will be drawn. The fourth and fifth arguments specify the rectangle’s width and height. Method DrawLine (lines 28–31) takes a Pen and two pairs of ints, specifying the start and end of a line. The method then draws a line, using the Pen object. Methods FillEllipse and DrawEllipse (lines 40 and 47) each provide overloaded versions that take five arguments. In both methods, the first argument specifies the drawing object to use. The next two arguments specify the upper-left coordinates of the bounding rectangle representing the area in which the ellipse will be drawn. The last two arguments specify the bounding rectangle’s width and height, respectively. Figure 17.15 depicts an ellipse bounded by a rectangle. The ellipse touches the midpoint of each of the four sides of the bounding rectangle. The bounding rectangle is not displayed on the screen.
666
Chapter 17
Graphics and Multimedia
(x,y)
height
width
Fig. 17.15 | Ellipse bounded by a rectangle.
17.7 Drawing Arcs Arcs are portions of ellipses and are measured in degrees, beginning at a starting angle and continuing for a specified number of degrees called the arc angle. An arc is said to sweep (traverse) its arc angle, beginning from its starting angle. Arcs that sweep in a clockwise direction are measured in positive degrees, whereas arcs that sweep in a counterclockwise direction are measured in negative degrees. Figure 17.16 depicts two arcs. Note that the arc at the left of the figure sweeps upward from zero degrees to approximately –110 degrees. Similarly, the arc at the right of the figure sweeps downward from zero degrees to approximately 110 degrees. Notice the dashed boxes around the arcs in Fig. 17.16. Each arc is drawn as part of an oval (the rest of which is not visible). When drawing an oval, we specify the oval’s dimensions in the form of a bounding rectangle that encloses the oval. The boxes in Fig. 17.16 correspond to these bounding rectangles. The Graphics methods used to draw arcs— DrawArc, DrawPie and FillPie—are summarized in Fig. 17.17. The program in Fig. 17.18 draws six images (three arcs and three filled pie slices) to demonstrate the arc methods listed in Fig. 17.17. To illustrate the bounding rectangles that determine the sizes and locations of the arcs, the arcs are displayed inside red rectangles that have the same x-y coordinates, width and height arguments as those that define the bounding rectangles for the arcs.
Negative angles 90º
180º
Positive angles 90º
0º
270º
Fig. 17.16 | Positive and negative arc angles.
180º
0º
270º
17.7 Drawing Arcs
667
Graphics Methods And Descriptions Note: Many of these methods are overloaded—consult the documentation for a complete listing. DrawArc( Pen p, int x, int y, int width, int height, int startAngle, int sweepAngle )
Draws an arc beginning from angle startAngle (in degrees) and sweeping sweepAngle degrees. The ellipse is defined by a bounding rectangle of width, height and upper-left corner (x,y). The Pen determines the color, border width and style of the arc. DrawPie( Pen p, int x, int y, int width, int height, int startAngle, int sweepAngle )
Draws a pie section of an ellipse beginning from angle startAngle (in degrees) and sweeping sweepAngle degrees. The ellipse is defined by a bounding rectangle of width, height and upper-left corner (x,y). The Pen determines the color, border width and style of the arc. FillPie( Brush b, int x, int y, int width, int height, int startAngle, int sweepAngle )
Functions similarly to DrawPie, except draws a solid arc (i.e., a sector). The Brush determines the fill pattern for the solid arc.
// start at 0 and sweep 110 degrees rectangle1.Location = new Point( 100, 35 ); graphicsObject.DrawRectangle( pen1, rectangle1 ); graphicsObject.DrawArc( pen2, rectangle1, 0, 110 ); // start at 0 and sweep -270 degrees rectangle1.Location = new Point( 185, 35 ); graphicsObject.DrawRectangle( pen1, rectangle1 ); graphicsObject.DrawArc( pen2, rectangle1, 0, -270 ); // start at 0 and sweep 360 degrees rectangle1.Location = new Point( 15, 120 ); rectangle1.Size = new Size( 80, 40 ); graphicsObject.DrawRectangle( pen1, rectangle1 ); graphicsObject.FillPie( brush2, rectangle1, 0, 360 ); // start at 270 and sweep -90 degrees rectangle1.Location = new Point( 100, 120 ); graphicsObject.DrawRectangle( pen1, rectangle1 ); graphicsObject.FillPie( brush2, rectangle1, 270, -90 ); // start at 0 and sweep -270 degrees rectangle1.Location = new Point( 185, 120 ); graphicsObject.DrawRectangle( pen1, rectangle1 ); graphicsObject.FillPie( brush2, rectangle1, 0, -270 ); } // end method DrawArcs_Paint } // end class DrawArcs
Fig. 17.18 | Drawing various arcs on a Form. (Part 2 of 2.) Lines 20–25 create the objects that we need to draw various arcs—a Graphics object, a Rectangle, SolidBrushes and Pens. Lines 28–29 then draw a rectangle and an arc inside the rectangle. The arc sweeps 360 degrees, forming a circle. Line 32 changes the location of the Rectangle by setting its Location property to a new Point. The Point constructor takes as arguments the x- and y-coordinates of the new point. The Location property determines the upper-left corner of the Rectangle. After drawing the rectangle, the program draws an arc that starts at 0 degrees and sweeps 110 degrees. Because angles increase in a clockwise direction, the arc sweeps downward.
17.8 Drawing Polygons and Polylines
669
Lines 37–39 perform similar functions, except that the specified arc sweeps –270 degrees. The Size property of a Rectangle determines the arc’s height and width. Line 43 sets the Size property to a new Size object, which changes the size of the rectangle. The remainder of the program is similar to the portions described above, except that a SolidBrush is used with method FillPie. The resulting arcs, which are filled, can be seen in the bottom half of the sample output (Fig. 17.18).
17.8 Drawing Polygons and Polylines Polygons are multisided shapes. There are several Graphics methods used to draw polygons—DrawLines draws a series of connected lines, DrawPolygon draws a closed polygon and FillPolygon draws a solid polygon. These methods are described in Fig. 17.19. The program in Fig. 17.20 allows users to draw polygons and connected lines via the methods listed in Fig. 17.19. Method
Description
DrawLines
Draws a series of connected lines. The coordinates of each point are specified in an array of Point objects. If the last point is different from the first point, the figure is not closed.
DrawPolygon
Draws a polygon. The coordinates of each point are specified in an array of Point objects. If the last point is different from the first point, those two points are connected to close the polygon.
FillPolygon
Draws a solid polygon. The coordinates of each point are specified in an array of Point objects. If the last point is different from the first point, those two points are connected to close the polygon.
Fig. 17.19 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| Graphics
methods for drawing polygons.
// Fig. 17.20: DrawPolygons.cs // Demonstrating polygons. using System; using System.Collections; using System.Drawing; using System.Windows.Forms; // demonstrating polygons public partial class PolygonForm : Form { // default constructor public PolygonForm() { InitializeComponent(); } // end constructor
Fig. 17.20 | Polygon-drawing demonstration. (Part 1 of 4.)
// handle line ReadioButton CheckedChanged event private void lineOption_CheckedChanged( object sender, System.EventArgs e ) { drawPanel.Invalidate(); // refresh panel } // end method lineOption_CheckedChanged // handle filled polygon RadioButton CheckedChanged event private void filledPolygonOption_CheckedChanged( object sender, System.EventArgs e ) { drawPanel.Invalidate(); // refresh panel } // end method filledPolygonOption_CheckedChanged // handle colorButton Click event private void colorButton_Click( object sender, EventArgs e ) { // create new color dialog ColorDialog dialogColor = new ColorDialog(); // show dialog and obtain result DialogResult result = dialogColor.ShowDialog(); // return if user cancels if ( result == DialogResult.Cancel ) return; pen.Color = dialogColor.Color; // set pen to color brush.Color = dialogColor.Color; // set brush drawPanel.Invalidate(); // refresh panel; } // end method colorButton_Click } // end class PolygonForm
Fig. 17.20 | Polygon-drawing demonstration. (Part 3 of 4.)
671
672
Chapter 17
Graphics and Multimedia
Fig. 17.20 | Polygon-drawing demonstration. (Part 4 of 4.) To allow the user to specify a variable number of points, line 18 declares ArrayList points as a container for our Point objects. An ArrayList is similar to an array, but an ArrayList can grow dynamically to accommodate more elements. Lines 21–22 declare the Pen and Brush used to color our shapes. The MouseDown event handler (lines 25–30) for drawPanel stores mouse-click locations in points with ArrayList method Add (line 28). The event handler then calls method Invalidate of drawPanel (line 29) to ensure that the panel refreshes to accommodate the new point. Method drawPanel_Paint (lines 33–52) handles the Panel’s Paint event. It obtains the Panel’s Graphics object (line 36) and, if the ArrayList points contains two or more Points (line 39), displays the polygon with the method that the user selected via the GUI radio buttons (lines 45–50). In lines 42–43, we extract an array from the ArrayList via method ToArray. Method ToArray can take a single argument to determine the type of the returned array; we obtain the type from the first element in the ArrayList by calling the element’s GetType method. Method clearButton_Click (lines 55–59) handles the Clear button’s Click event by calling ArrayList method Clear (causing the old list to be erased) and refreshing the display. Lines 62–80 define the event handlers for each radio button’s CheckedChanged event. Each method invalidates drawPanel to ensure that the panel is repainted to reflect the selected shape type. Event handler colorButton_Click (83–98) allows the user to select a new drawing color with a ColorDialog, using the techniques demonstrated in Fig. 17.7.
17.9 Advanced Graphics Capabilities C# offers many additional graphics capabilities. The Brush hierarchy, for example, also includes HatchBrush, LinearGradientBrush, PathGradientBrush and TextureBrush.
Gradients, Line Styles and Fill Patterns The program in Fig. 17.21 demonstrates several graphics features, such as dashed lines, thick lines and the ability to fill shapes with various patterns. These represent just a few of the additional capabilities of the System.Drawing namespace. Lines 18–88 define the DrawShapesForm Paint event handler. Lines 25–27 create an object of class LinearGradientBrush named linearBrush. A LinearGradientBrush (namespace System.Drawing.Drawing2D) enables users to draw with a color gradient. The
// Fig. 17.21: DrawShapes.cs // Drawing various shapes on a Form. using System; using System.Drawing; using System.Drawing.Drawing2D; using System.Windows.Forms; // draws shapes with different brushes public partial class DrawShapesForm : Form { // default constructor public DrawShapesForm() { InitializeComponent(); } // end constructor // draw various shapes on Form private void DrawShapesForm_Paint( object sender, PaintEventArgs e ) { // references to object we will use Graphics graphicsObject = e.Graphics; // ellipse rectangle and gradient brush Rectangle drawArea1 = new Rectangle( 5, 35, 30, 100 ); LinearGradientBrush linearBrush = new LinearGradientBrush( drawArea1, Color.Blue, Color.Yellow, LinearGradientMode.ForwardDiagonal ); // draw ellipse filled with a blue-yellow gradient graphicsObject.FillEllipse( linearBrush, 5, 30, 65, 100 ); // pen and location for red outline rectangle Pen thickRedPen = new Pen( Color.Red, 10 ); Rectangle drawArea2 = new Rectangle( 80, 30, 65, 100 ); // draw thick rectangle outline in red graphicsObject.DrawRectangle( thickRedPen, drawArea2 ); // bitmap texture Bitmap textureBitmap = new Bitmap( 10, 10 ); // get bitmap graphics Graphics graphicsObject2 = Graphics.FromImage( textureBitmap ); // brush and pen used throughout program SolidBrush solidColorBrush = new SolidBrush( Color.Red ); Pen coloredPen = new Pen( solidColorBrush ); // fill textureBitmap with yellow solidColorBrush.Color = Color.Yellow; graphicsObject2.FillRectangle( solidColorBrush, 0, 0, 10, 10 );
Fig. 17.21 | Shapes drawn on a form. (Part 1 of 2.)
// draw small black rectangle in textureBitmap coloredPen.Color = Color.Black; graphicsObject2.DrawRectangle( coloredPen, 1, 1, 6, 6 ); // draw small blue rectangle in textureBitmap solidColorBrush.Color = Color.Blue; graphicsObject2.FillRectangle( solidColorBrush, 1, 1, 3, 3 ); // draw small red square in textureBitmap solidColorBrush.Color = Color.Red; graphicsObject2.FillRectangle( solidColorBrush, 4, 4, 3, 3 ); // create textured brush and // display textured rectangle TextureBrush texturedBrush = new TextureBrush( textureBitmap ); graphicsObject.FillRectangle( texturedBrush, 155, 30, 75, 100 ); // draw pie-shaped arc in white coloredPen.Color = Color.White; coloredPen.Width = 6; graphicsObject.DrawPie( coloredPen, 240, 30, 75, 100, 0, 270 ); // draw lines in green and yellow coloredPen.Color = Color.Green; coloredPen.Width = 5; graphicsObject.DrawLine( coloredPen, 395, 30, 320, 150 ); // draw a rounded, dashed yellow line coloredPen.Color = Color.Yellow; coloredPen.DashCap = DashCap.Round; coloredPen.DashStyle = DashStyle.Dash; graphicsObject.DrawLine( coloredPen, 320, 30, 395, 150 ); } // end method DrawShapesForm_Paint } // end class DrawShapesForm
Fig. 17.21 | Shapes drawn on a form. (Part 2 of 2.) LinearGradientBrush used in this example takes four arguments—a Rectangle, two Colors and a member of enumeration LinearGradientMode. In C#, all linear gradients are
defined along a line that determines the gradient endpoints. This line can be specified
17.9 Advanced Graphics Capabilities
675
either by the starting and ending points or by the diagonal of a rectangle. The first argument, Rectangle drawArea1, represents the endpoints of the linear gradient—the upperleft corner is the starting point and the bottom-right corner is the ending point. The second and third arguments specify the colors that the gradient will use. In this case, the color of the ellipse will gradually change from Color.Blue to Color.Yellow. The last argument, a type from the enumeration LinearGradientMode, specifies the linear gradient’s direction. In our case, we use LinearGradientMode.ForwardDiagonal, which creates a gradient from the upper-left to the lower-right corner. We then use Graphics method FillEllipse in line 30 to draw an ellipse with linearBrush; the color gradually changes from blue to yellow, as described above. In line 33, we create Pen object thickRedPen. We pass to thickRedPen’s constructor Color.Red and int argument 10, indicating that we want thickRedPen to draw red lines that are 10 pixels wide. Line 40 creates a new Bitmap image, which initially is empty. Class Bitmap can produce images in color and gray scale; this particular Bitmap is 10 pixels wide and 10 pixels tall. Method FromImage (line 43–44) is a static member of class Graphics and retrieves the Graphics object associated with an Image, which may be used to draw on an image. Lines 52–65 draw on the Bitmap a pattern consisting of black, blue, red and yellow rectangles and lines. A TextureBrush is a brush that fills the interior of a shape with an image, rather than a solid color. In line 71, TextureBrush object textureBrush fills a rectangle with our Bitmap. The TextureBrush constructor used in lines 69–70 takes as an argument an image that defines its texture. Next, we draw a pie-shaped arc with a thick white line. Lines 74–75 set coloredPen’s color to White and modify its width to be six pixels. We then draw the pie on the form by specifying the Pen, the x-coordinate, y-coordinate, width and height of the bounding rectangle and the start and sweep angles. Lines 79–81 draw a five-pixel-wide green line. Finally, lines 85–86 use enumerations DashCap and DashStyle (namespace System.Drawing.Drawing2D) to specify settings for a dashed line. Line 85 sets the DashCap property of coloredPen (not to be confused with the DashCap enumeration) to a member of the DashCap enumeration. The DashCap enumeration specifies the styles for the start and end of a dashed line. In this case, we want both ends of the dashed line to be rounded, so we use DashCap.Round. Line 86 sets the DashStyle property of coloredPen (not to be confused with the DashStyle enumeration) to DashStyle.Dash, indicating that we want our line to consist entirely of dashes.
General Paths Our next example demonstrates the use of a general path. A general path is a shape constructed from straight lines and complex curves. An object of class GraphicsPath (namespace System.Drawing.Drawing2D) represents a general path. The GraphicsPath class provides functionality that enables the creation of complex shapes from vector-based primitive graphics objects. A GraphicsPath object consists of figures defined by simple shapes. The start point of each vector-graphics object (such as a line or arc) that is added to the path is connected by a straight line to the end point of the previous object. When called, the CloseFigure method attaches the final vector-graphic object end point to the initial starting point for the current figure by a straight line, then starts a new figure. Method StartFigure begins a new figure within the path without closing the previous figure.
676
Chapter 17
Graphics and Multimedia
The program of Fig. 17.22 draws general paths in the shape of five-pointed stars. Lines 26–29 define two int arrays, representing the x- and y-coordinates of the points in the star, and line 32 defines GraphicsPath object star. A loop (lines 35–37) then creates lines to connect the points of the star and adds these lines to star. We use GraphicsPath method AddLine to append a line to the shape. The arguments of AddLine specify the coordinates for the line’s endpoints; each new call to AddLine adds a line from the previous point to the current point. Line 40 uses GraphicsPath method CloseFigure to complete the shape.
// Fig. 17.22: DrawStarsForm.cs // Using paths to draw stars on the form. using System; using System.Drawing; using System.Drawing.Drawing2D; using System.Windows.Forms; // draws randomly colored stars public partial class DrawStarsForm : Form { // default constructor public DrawStarsForm() { InitializeComponent(); } // end constructor // create path and draw stars along it private void DrawStarsForm_Paint(object sender, PaintEventArgs e) { Graphics graphicsObject = e.Graphics; Random random = new Random(); SolidBrush brush = new SolidBrush( Color.DarkMagenta ); // x and y points of the path int[] xPoints = { 55, 67, 109, 73, 83, 55, 27, 37, 1, 43 }; int[] yPoints = { 0, 36, 36, 54, 96, 72, 96, 54, 36, 36 }; // create graphics path for star; GraphicsPath star = new GraphicsPath(); // create star from series of points for ( int i = 0; i 225 ) { MessageBox.Show( " Height or Width too large" ); return; } // end if // clear the Form then draw the image graphicsObject.Clear( this.BackColor ); graphicsObject.DrawImage( image, 5, 5, width, height ); } // end method setButton_Click } // end class DisplayLogoForm
Fig. 17.23 | Image resizing. (Part 2 of 2.) Line 10 declares Image variable image and uses static Image method FromFile to load an image from a file on disk. Line 16 uses the Form’s CreateGraphics method to create a Graphics object for drawing on the Form. Method CreateGraphics is inherited from class Control. When you click the Set Button, lines 28–32 validate the width and height to ensure that they are not too large. If the parameters are valid, line 35 calls
680
Chapter 17
Graphics and Multimedia
Graphics method Clear to paint the entire Form in the current background color. Line 36 calls Graphics method DrawImage, passing as arguments the image to draw, the x-coordinate of the image’s upper-left corner, the y-coordinate of the image’s upper-left corner, the width of the image and the height of the image. If the width and height do not correspond to the image’s original dimensions, the image is scaled to fit the new width and height.
17.12 Animating a Series of Images The next example animates a series of images stored in an array. The application uses the same technique to load and display Images as shown in Fig. 17.23. The animation in Fig. 17.24 uses a PictureBox, which contains the images that we animate. We use a Timer to cycle through the images and display a new image every 50 milliseconds. Variable count keeps track of the current image number and increases by one every time we display a new image. The array includes 30 images (numbered 0–29); when the application reaches image 29, it returns to image 0. The 30 images are located in the images folder inside the project’s bin/Debug and bin/Release directories. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
// Fig. 17.24: LogoAnimator.cs // Program that animates a series of images. using System; using System.Drawing; using System.Windows.Forms; // animates a series of 30 images public partial class LogoAnimator : Form { private Image[] images = new Image[ 30 ]; private int count = -1; // LogoAnimator constructor public LogoAnimator() { InitializeComponent(); for ( int i = 0; i < 30; i++ ) images[ i ] = Image.FromFile( @"images\deitel" + i + ".gif" ); logoPictureBox.Image = images[ 0 ]; // display first image // set PictureBox to be the same size as Image logoPictureBox.Size = logoPictureBox.Image.Size; } // end LogoAnimator constructor // event handler for timer's Tick event private void timer_Tick( object sender, EventArgs e ) { count = ( count + 1 ) % 30; // increment counter logoPictureBox.Image = images[ count ]; // display next image } // end method timer_Tick } // end class LogoAnimator
Fig. 17.24 | Animation of a series of images. (Part 1 of 2.)
17.12 Animating a Series of Images
681
Fig. 17.24 | Animation of a series of images. (Part 2 of 2.) Lines 18–19 load each of 30 images and place them in an array of Images. Line 21 places the first image in the PictureBox. Line 24 modifies the size of the PictureBox so that it is equal to the size of the Image it is displaying. The event handler for timer’s Tick event (line 28–32) responds to each event by displaying the next image from the array.
Performance Tip 17.2 It is more efficient to load an animation’s frames as one image than to load each image separately. (A painting program, such as Adobe Photoshop®, or Jasc® Paint Shop Pro™, can be used to combine the animation’s frames into one image.) If the images are being loaded separately from the Web, each loaded image requires a separate connection to the site on which the images are stored; this process can result in poor performance. 17.2
Chess Example The following chess example demonstrates techniques for two-dimensional collision detection, selecting single frames from a multiframe image, and regional invalidation, refreshing only the parts of the screen that have changed, to increase performance. Twodimensional collision detection enables a program to detect whether two shapes overlap or whether a point is contained within a shape. In the next example, we demonstrate the simplest form of collision detection, which determines whether a point (the mouse-click location) is contained within a rectangle (a chess-piece image). Class ChessPiece (Fig. 17.25) represents the individual chess pieces. Lines 10–18 define a public enumeration of constants that identify each chess-piece type. The constants also serve to identify the location of each piece in the chess-piece image file. Rectangle object targetRectangle (lines 24–25) identifies the image location on the chessboard. The x and y properties of the rectangle are assigned in the ChessPiece constructor, and all chess-piece images have a width and height of 75 pixels. The ChessPiece constructor (lines 28–39) receives the chess-piece type, its x and y location and the Bitmap containing all chess-piece images. Rather than loading the chesspiece image within the class, we allow the calling class to pass the image. This increases the flexibility of the class by allowing the user to change images. Lines 36–38 extract a subimage that contains only the current piece’s bitmap data. Our chess-piece images are defined in a specific manner: One image contains six chess-piece images, each defined within a 75-pixel block, resulting in a total image size of 450-by-75. We obtain a single
// Fig. 17.25: ChessPiece.cs // Class that represents chess piece attributes. using System; using System.Drawing; // represents a chess piece class ChessPiece { // define chess-piece type constants public enum Types { KING, QUEEN, BISHOP, KNIGHT, ROOK, PAWN } // end enum Types private int currentType; // this object's type private Bitmap pieceImage; // this object's image // default display location private Rectangle targetRectangle = new Rectangle( 0, 0, 75, 75 ); // construct piece public ChessPiece( int type, int xLocation, int yLocation, Bitmap sourceImage ) { currentType = type; // set current type targetRectangle.X = xLocation; // set current x location targetRectangle.Y = yLocation; // set current y location // obtain pieceImage from section of sourceImage pieceImage = sourceImage.Clone( new Rectangle( type * 75, 0, 75, 75 ), System.Drawing.Imaging.PixelFormat.DontCare ); } // end method ChessPiece // draw chess piece public void Draw( Graphics graphicsObject ) { graphicsObject.DrawImage( pieceImage, targetRectangle ); } // end method Draw // obtain this piece's location rectangle public Rectangle GetBounds() { return targetRectangle; } // end method GetBounds
Fig. 17.25 | Class that represents chess piece attributes. (Part 1 of 2.)
17.12 Animating a Series of Images
53 54 55 56 57 58 59
683
// set this piece's location public void SetLocation( int xLocation, int yLocation ) { targetRectangle.X = xLocation; targetRectangle.Y = yLocation; } // end method SetLocation } // end class ChessPiece
Fig. 17.25 | Class that represents chess piece attributes. (Part 2 of 2.) image via Bitmap’s Clone method, which allows us to specify a rectangle image location and the desired pixel format. The location is a 75-by-75 pixel block with its upper-left corner x equal to 75 * type and the corresponding y equal to 0. For the pixel format, we specify constant DontCare, causing the format to remain unchanged. Method Draw (lines 42–45) causes the ChessPiece to draw pieceImage in the targetRectangle using the Graphics object passed as Draw’s argument. Method GetBounds (lines 48–51) returns the targetRectangle object for use in collision detection, and method SetLocation (lines 54–58) allows the calling class to specify a new piece location. Class ChessGame (Fig. 17.26) defines the game and graphics code for our chess game. Lines 11–15 define instance variables the program requires. ArrayList chessTile (line 11) stores the board tile images. ArrayList chessPieces (line 12) stores all active ChessPiece objects, and int selectedIndex (line 13) identifies the index in chessPieces of the currently selected piece. The board (line 14) is an 8-by-8, two-dimensional int array corresponding to the squares of a chess board. Each board element is an integer from 0 to 3 that corresponds to an index in chessTile and is used to specify the chessboard-square image. const TILESIZE (line 15) defines the size of each tile in pixels. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
// Fig. 17.26: ChessGame.cs // Chess Game graphics code. using System; using System.Collections; using System.Drawing; using System.Windows.Forms; // allows 2 players to play chess public partial class ChessGame : Form { private ArrayList chessTile = new ArrayList(); // for tile images private ArrayList chessPieces = new ArrayList(); // for chess pieces private int selectedIndex = -1; // index for selected piece private int[ , ] board = new int[ 8, 8 ]; // board array private const int TILESIZE = 75; // chess tile size in pixels // default constructor public ChessGame() { // Required for Windows Form Designer support InitializeComponent(); } // end constructor
// load tile bitmaps and reset game private void ChessGame_Load( object sender, EventArgs e ) { // load chess board tiles chessTile.Add( Bitmap.FromFile( @"images\lightTile1.png" ) ); chessTile.Add( Bitmap.FromFile( @"images\lightTile2.png" ) ); chessTile.Add( Bitmap.FromFile( @"images\darkTile1.png" ) ); chessTile.Add( Bitmap.FromFile( @"images\darkTile2.png" ) ); ResetBoard(); // initialize board Invalidate(); // refresh form } // end method ChessGame_Load // initialize pieces to start and rebuild board private void ResetBoard() { int current = -1; ChessPiece piece; Random random = new Random(); bool light = false; int type; chessPieces.Clear(); // ensure empty arraylist // load whitepieces image Bitmap whitePieces = ( Bitmap ) Image.FromFile( @"images\whitePieces.png" ); // load blackpieces image Bitmap blackPieces = ( Bitmap ) Image.FromFile( @"images\blackPieces.png" ); // set whitepieces to be drawn first Bitmap selected = whitePieces; // traverse board rows in outer loop for ( int row = 0; row 5 ) selected = blackPieces; // traverse board columns in inner loop for ( int column = 0; column -1 ) chessPieces.RemoveAt( remove ); } // end if pieceBox.Invalidate(); // ensure artifact removal } // end method pieceBox_MouseUp
Fig. 17.26 | Chess-game code. (Part 5 of 7.)
688
Chapter 17
Graphics and Multimedia
230 // helper function to convert 231 // ArrayList object to ChessPiece 232 private ChessPiece GetPiece( int i ) 233 { 234 return ( ChessPiece ) chessPieces[ i ]; 235 } // end method GetPiece 236 237 // handle NewGame menu option click 238 private void newGameItem_Click( 239 object sender, System.EventArgs e ) 240 { 241 ResetBoard(); // reinitialize board 242 Invalidate(); // refresh form 243 } // end method newGameItem_Click 244 } // end class ChessGame
Fig. 17.26 | Chess-game code. (Part 6 of 7.)
17.12 Animating a Series of Images
689
Fig. 17.26 | Chess-game code. (Part 7 of 7.) The chess game GUI consists of Form ChessGame, the area in which we draw the tiles; Panel pieceBox, the area in which we draw the pieces (note that pieceBox’s background color is set to "transparent"); and a Menu that allows the user to begin a new game. Although the pieces and tiles could have been drawn on the same form, doing so would decrease performance. We would be forced to refresh the board and all the pieces every time we refreshed the control. The ChessGame_Load event handler (lines 25–35) loads four tile images into chessTile—two light tiles and two dark tiles for variety. It then calls method ResetBoard to refresh the Form and begin the game. Method ResetBoard (lines 38–127) clears chessPieces, loads images for both the black and the white chess-piece sets and creates Bitmap selected to define the currently selected Bitmap set. Lines 60–126 loop through the board’s 64 positions, setting the tile color and piece for each tile. Lines 63–64 cause the currently selected image to switch to the blackPieces after the fifth row. If the row counter is on the first or last row, lines 71–100 add a new piece to chessPieces. The type of the piece is based on the current column we are initializing. Pieces in chess are positioned in the following order, from left to right: Rook, knight, bishop, queen, king, bishop, knight and rook. Lines 103–109 add a new pawn at the current location if the current row is second or seventh. A chessboard is defined by alternating light and dark tiles across a row in a pattern where the color that starts each row is equal to the color of the last tile of the previous row.
690
Chapter 17
Graphics and Multimedia
Lines 113–122 assign the current board-tile color to an element in the board array. Based on the alternating value of bool variable light and the results of the random operation on line 111, we assign an int to the board to determine the color of that tile—0 and 1 represent light tiles; 2 and 3 represent dark tiles. Line 125 inverts the value of light at the end of each row to maintain the staggered effect of a chessboard. Method ChessGame_Paint (lines 130–146) handles the Form’s Paint event and draws the tiles according to their values in the board array. Since the default height of a MenuStrip is 24 pixels, we use the TranslateTransform method of class Graphics to shift the origin of the Form down 24 pixels (line 133). This shift prevents the top row of tiles from being hidden behind the MenuStrip. Method pieceBox_Paint (lines 168–174), which handles the Paint event for the pieceBox Panel, iterates through each element of the chessPiece ArrayList and calls its Draw method. The pieceBox MouseDown event handler (lines 177–182) calls CheckBounds (lines 150–165) with the location of the mouse to determine whether the user selected a piece. The pieceBox MouseMove event handler (lines 185–200) moves the selected piece with the mouse. Lines 190–192 define a region of the Panel that spans two tiles in every direction from the pointer. As mentioned previously, Invalidate is slow. This means that the pieceBox MouseMove event handler might be called several times before the Invalidate method completes. If a user working on a slow computer moves the mouse quickly, the application could leave behind artifacts. An artifact is any unintended visual abnormality in a graphical program. By causing the program to refresh a two-square rectangle, which should suffice in most cases, we achieve a significant performance enhancement over an entire component refresh during each MouseMove event. Lines 195–196 set the selected piece location to the mouse-cursor position, adjusting the location to center the image on the mouse. Line 198 invalidates the region defined in lines 190–192 so that it will be refreshed. Lines 203–228 define the pieceBox MouseUp event handler. If a piece has been selected, lines 208–225 determine the index in chessPieces of any piece collision, remove the collided piece, snap (align) the current piece to a valid location and deselect the piece. We check for piece collisions to allow the chess piece to “take” other chess pieces. Line 216 checks whether any piece (excluding the currently selected piece) is beneath the current mouse location. If a collision is detected, the returned piece index is assigned to remove. Lines 211–213 determine the closest valid chess tile and “snaps” the selected piece to that location. If remove contains a positive value, line 224 removes the object at that index from the chessPieces ArrayList. Finally, the entire Panel is invalidated in line 227 to display the new piece location and remove any artifacts created during the move. Method CheckBounds (lines 150–165) is a collision-detection helper method; it iterates through ArrayList chessPieces and returns the index of any piece’s rectangle that contains the point passed to the method (the mouse location, in this example). CheckBounds uses Rectangle method Contains to determine whether a point is in the Rectangle. Method CheckBounds optionally can exclude a single piece index (to ignore the selected index in the pieceBox MouseUp event handler, in this example). Lines 232–235 define helper function GetPiece, which simplifies the conversion from objects in ArrayList chessPieces to ChessPiece types. Method newGameItem_Click (lines 238–243) handles the NewGame menu item click event, calls RefreshBoard to reset the game and invalidates the entire form.
17.13 Windows Media Player
691
17.13 Windows Media Player The Windows Media Player control enables an application to play video and sound in many multimedia formats. These include MPEG (Motion Pictures Experts Group) audio and video, AVI (audio-video interleave) video, WAV (Windows wave-file format) audio and MIDI (Musical Instrument Digital Interface) audio. Users can find pre-existing audio and video on the Internet, or they can create their own files, using available sound and graphics packages. The application in Fig. 17.27 demonstrates the Windows Media Player control. To use this control, you must add the control to the Toolbox. First select Tools > Choose Toolbox Items… to display the Choose Toolbox Items dialog. Click the COM components tab, then scroll down and select the option Windows Media Player. Click the OK button to dismiss the dialog. The Windows Media Player control now appears at the bottom of the Toolbox. The Windows Media Player control provides several buttons that allow the user to play the current file, pause, stop, play the previous file, rewind, forward and play the next file. The control also includes a volume control and trackbars to select a specific position in the media file. Our application provides a File menu containing the Open and Exit menu items. When a user chooses Open from the File menu, event handler openItem_Click (lines 15– 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
// Fig. 17.27: MediaPlayerTest.cs // Windows Media Player control used to play media files. using System; using System.Windows.Forms; public partial class MediaPlayer : Form { // default constructor public MediaPlayer() { InitializeComponent(); } // end constructor // open new media file in Windows Media Player private void openItem_Click( object sender, EventArgs e ) { openMediaFileDialog.ShowDialog(); // load and play the media clip player.URL = openMediaFileDialog.FileName; } // end method openItem_Click // exit program when exit menu item is clicked private void exitItem_Click( object sender, EventArgs e ) { Application.Exit(); } // end method exitItem_Click } // end class MediaPlayer
Fig. 17.27 | Windows Media Player demonstration. (Part 1 of 2.)
692
Chapter 17
Graphics and Multimedia
Fig. 17.27 | Windows Media Player demonstration. (Part 2 of 2.) 21) executes. An OpenFileDialog box displays (line 17) to allow the user to select a file. The program then sets the URL property of the player (the Windows Media Player control object of type AxMediaPlayer) to the name of the file chosen by the user. The URL property specifies the file that Windows Media Player is currently using. The exitItem_Click event handler (lines 24–27) executes when the user selects Exit from the File menu. This event handler simply calls Application.Exit to terminate the application. We provide sample audio and video files in the directory that contains this example.
17.14 Microsoft Agent Microsoft Agent is a technology used to add interactive animated characters to Windows applications or Web pages. Microsoft Agent characters can speak and respond to user input via speech recognition and synthesis. Microsoft employs its Agent technology in ap-
17.14 Microsoft Agent
693
plications such as Word, Excel and PowerPoint. Agents in these programs aid users in finding answers to questions and in understanding how the applications function. The Microsoft Agent control provides programmers with access to four predefined characters—Genie (a genie), Merlin (a wizard), Peedy (a parrot) and Robby (a robot). Each character has a unique set of animations that programmers can use in their applications to illustrate different points and functions. For instance, the Peedy character-animation set includes different flying animations, which the programmer might use to move Peedy on the screen. Microsoft provides basic information on Agent technology at www.microsoft.com/msagent
Microsoft Agent technology enables users to interact with applications and Web pages through speech, the most natural form of human communication. To understand speech, the control uses a speech recognition engine—an application that translates vocal sound input from a microphone to language that the computer understands. The Microsoft Agent control also uses a text-to-speech engine, which generates characters’ spoken responses. A text-to-speech engine is an application that translates typed words into audio sound that users hear through headphones or speakers connected to a computer. Microsoft provides speech recognition and text-to-speech engines for several languages at www.microsoft.com/msagent/downloads/user.asp
Programmers can even create their own animated characters with the help of the Microsoft Agent Character Editor and the Microsoft Linguistic Sound Editing Tool. These products are available free for download from www.microsoft.com/msagent/downloads/developer.asp
This section introduces the basic capabilities of the Microsoft Agent control. For complete details on downloading this control, visit www.microsoft.com/msagent/downloads/user.asp
The following example, Peedy’s Pizza Palace, was developed by Microsoft to illustrate the capabilities of the Microsoft Agent control. Peedy’s Pizza Palace is an online pizza shop where users can place their orders via voice input. The Peedy character interacts with users by helping them choose toppings and calculating the totals for their orders. You can view this example at agent.microsoft.com/agent2/sdk/samples/html/peedypza.htm
To run the example, you must go to www.microsoft.com/msagent/downloads/user.asp and download and install the Peedy character file, a text-to-speech engine and a speechrecognition engine. When the window opens, Peedy introduces himself (Fig. 17.28), and the words he speaks appear in a cartoon bubble above his head. Notice that Peedy’s animations correspond to the words he speaks. Programmers can synchronize character animations with speech output to illustrate a point or to convey a character’s mood. For instance, Fig. 17.29 depicts Peedy’s Pleased animation. The Peedy character-animation set includes eighty-five different animations, each of which is unique to the Peedy character.
694
Chapter 17
Graphics and Multimedia
Bubble contains text equivalent to words Peedy speaks
Fig. 17.28 | Peedy introducing himself when the window opens. Look-and-Feel Observation 17.1 Agent characters remain on top of all active windows while a Microsoft Agent application is running. Their motions are not limited by the boundaries of the browser or application window. 17.1
Fig. 17.29 | Peedy’s Pleased animation.
17.14 Microsoft Agent
695
Peedy also responds to input from the keyboard and mouse. Figure 17.30 shows what happens when a user clicks Peedy with the mouse pointer. Peedy jumps up, ruffles his feathers and exclaims, “Hey that tickles!” or “Be careful with that pointer!” Users can relocate Peedy on the screen by dragging him with the mouse. However, even when the user moves Peedy to a different part of the screen, he continues to perform his preset animations and location changes. Many location changes involve animations. For instance, Peedy can hop from one screen location to another, or he can fly (Fig. 17.31).
Pointer clicking Peedy
Fig. 17.30 | Peedy’s reaction when he is clicked.
Fig. 17.31 | Peedy flying animation.
696
Chapter 17
Graphics and Multimedia
Once Peedy completes the ordering instructions, a tool tip appears beneath him indicating that he is listening for a voice command (Fig. 17.32). You can enter the type of pizza to order either by speaking the style name into a microphone or by clicking the radio button corresponding to your choice. If you choose speech input, a box appears below Peedy displaying the words that Peedy “heard” (i.e., the words translated to the program by the speech-recognition engine). Once he recognizes your input, Peedy gives you a description of the selected pizza. Figure 17.33 shows what happens when you choose Seattle as the pizza style. Peedy then asks you to choose additional toppings. Again, you can either speak or use the mouse to make a selection. Checkboxes corresponding to toppings that come with the selected pizza style are checked for you. Figure 17.34 shows what happens when you choose anchovies as an additional topping. Peedy makes a wisecrack about your choice. You can submit the order either by pressing the Place My Order button or by speaking “Place order” into the microphone. Peedy recounts the order while writing down the order items on his notepad (Fig. 17.35). He then calculates the figures on his calculator and reports the total price (Fig. 17.36).
Pizza style options
Fig. 17.32 | Peedy waiting for speech input.
Tool tip indicates that Peedy is waiting for user input
17.14 Microsoft Agent
Tool tip indicates recognized speech
Fig. 17.33 | Peedy repeating a request for Seattle-style pizza.
Fig. 17.34 | Peedy repeating a request for anchovies as an additional topping.
697
698
Chapter 17
Graphics and Multimedia
Fig. 17.35 | Peedy recounting the order.
Fig. 17.36 | Peedy calculating the total. Creating an Application That Uses Microsoft Agent [Note: Before running this example, you must first download and install the Microsoft Agent control, a speech-recognition engine, a text-to-speech engine and the four character definitions from the Microsoft Agent Web site, as we discussed at the beginning of this section.]
17.14 Microsoft Agent
699
The following example (Fig. 17.37) demonstrates how to build a simple application with the Microsoft Agent control. This application contains two drop-down lists from which the user can choose an Agent character and a character animation. When the user chooses from these lists, the chosen character appears and performs the selected animation. The application uses speech recognition and synthesis to control the character animations and speech—you can tell the character which animation to perform by pressing the Scroll Lock key, then speaking the animation name into a microphone. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
// Fig. 17.28: Agent.cs // Microsoft Agent demonstration. using System; using System.Collections; using System.Windows.Forms; using System.IO; public partial class Agent : Form { // current agent object private AgentObjects.IAgentCtlCharacter speaker; // default constructor public Agent() { InitializeComponent(); // initialize the characters try { // load characters into agent object mainAgent.Characters.Load( "Genie", @"C:\windows\msagent\chars\Genie.acs" ); mainAgent.Characters.Load( "Merlin", @"C:\windows\msagent\chars\Merlin.acs" ); mainAgent.Characters.Load( "Peedy", @"C:\windows\msagent\chars\Peedy.acs" ); mainAgent.Characters.Load( "Robby", @"C:\windows\msagent\chars\Robby.acs" ); // set current character to Genie and show him speaker = mainAgent.Characters[ "Genie" ]; GetAnimationNames(); // obtain an animation name list speaker.Show( 0 ); // display Genie characterCombo.SelectedText = "Genie"; } // end try catch ( FileNotFoundException ) { MessageBox.Show( "Invalid character location", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error ); } // end catch } // end constructor
Fig. 17.37 | Microsoft Agent demonstration. (Part 1 of 5.)
// event handler for Speak Button private void speakButton_Click( object sender, EventArgs e ) { // if textbox is empty, have the character ask // user to type the words into the TextBox; otherwise, // have the character say the words in the TextBox if ( speechTextBox.Text == "" ) speaker.Speak( "Please, type the words you want me to speak", "" ); else speaker.Speak( speechTextBox.Text, "" ); } // end method speakButton_Click // event handler for Agent control's ClickEvent private void mainAgent_ClickEvent( object sender, AxAgentObjects._AgentEvents_ClickEvent e ) { speaker.Play( "Confused" ); speaker.Speak( "Why are you poking me?", "" ); speaker.Play( "RestPose" ); } // end method mainAgent_ClickEvent // ComboBox changed event, switch active agent character private void characterCombo_SelectedIndexChanged( object sender, EventArgs e ) { ChangeCharacter( characterCombo.Text ); } // end method characterCombo_SelectedIndexChanged // utility method to change characters private void ChangeCharacter( string name ) { speaker.StopAll( "Play" ); speaker.Hide( 0 ); speaker = mainAgent.Characters[ name ]; // regenerate animation name list GetAnimationNames(); speaker.Show( 0 ); } // end method ChangeCharacter // get animation names and store in ArrayList private void GetAnimationNames() { // ensure thread safety lock ( this ) { // get animation names IEnumerator enumerator = mainAgent.Characters[ speaker.Name ].AnimationNames.GetEnumerator(); string voiceString;
Fig. 17.37 | Microsoft Agent demonstration. (Part 2 of 5.)
// clear actionsCombo actionsCombo.Items.Clear(); speaker.Commands.RemoveAll(); // copy enumeration to ArrayList while ( enumerator.MoveNext() ) { // remove underscores in speech string voiceString = ( string ) enumerator.Current; voiceString = voiceString.Replace( "_", "underscore" ); actionsCombo.Items.Add( enumerator.Current ); // add all animations as voice enabled commands speaker.Commands.Add( ( string ) enumerator.Current, enumerator.Current, voiceString, true, false ); } // end while // add custom command speaker.Commands.Add( "MoveToMouse", "MoveToMouse", "MoveToMouse", true, true ); } // end lock } // end method GetAnimationNames // user selects new action private void actionsCombo_SelectedIndexChanged( object sender, EventArgs e ) { speaker.StopAll( "Play" ); speaker.Play( actionsCombo.Text ); speaker.Play( "RestPose" ); } // end method actionsCombo_SelectedIndexChanged // event handler for Agent commands private void mainAgent_Command( object sender, AxAgentObjects._AgentEvents_CommandEvent e ) { // get UserInput object AgentObjects.IAgentCtlUserInput command = ( AgentObjects.IAgentCtlUserInput ) e.userInput; // change character if user speaks character name if ( command.Voice == "Peedy" || command.Voice == "Robby" || command.Voice == "Merlin" || command.Voice == "Genie" ) { ChangeCharacter( command.Voice ); return; } // end if // send agent to mouse if ( command.Voice == "MoveToMouse" ) {
Fig. 17.37 | Microsoft Agent demonstration. (Part 3 of 5.)
701
702
Chapter 17
Graphics and Multimedia
speaker.MoveTo( Convert.ToInt16( Cursor.Position.X - 60 ), 149 Convert.ToInt16( Cursor.Position.Y - 60 ), 5 ); 150 151 return; 152 } // end if 153 154 // play new animation speaker.StopAll( "Play" ); 155 speaker.Play( command.Name ); 156 157 } 158 } // end class Agent Genie performing Writing animation
Drop-down list from which you can choose a character animation
Writing animation selected
Tool tip indicating that Merlin is listening for a voice command
Merlin responding to user spoken animation command. Tool tip shows the words that the speech recognition engine translated to the application
Fig. 17.37 | Microsoft Agent demonstration. (Part 4 of 5.)
17.14 Microsoft Agent
703
Text input
Peedy repeating the words entered by the user. Peedy’s speech can be heard through your computer’s speakers.
Robby responding to being clicked with the mouse pointer.
The commands pop-up window
Fig. 17.37 | Microsoft Agent demonstration. (Part 5 of 5.) The example also allows you to switch to a new character by speaking its name and creates a custom command, MoveToMouse. In addition, when you press the Speak Button, the characters speak any text that you typed in the TextBox. To use the Microsoft Agent control, you must add it to the Toolbox. Select Tools > Choose Toolbox Items… to display the Choose Toolbox Items dialog. In the dialog, select the COM Components tab, then scroll down and select the Microsoft Agent Control 2.0 option. When this option is selected properly, a small check mark appears in the box to the left of the option. Click OK to dismiss the dialog. The icon for the Microsoft Agent control now appears at the bottom of the Toolbox. Drag the Microsoft Agent Control 2.0 control onto your Form and name the object mainAgent.
704
Chapter 17
Graphics and Multimedia
In addition to the Microsoft Agent object mainAgent (of type AxAgent) that manages the characters, you also need a variable of type IAgentCtlCharacter to represent the current character. We create this variable, named speaker, in line 11. When you execute this program, class Agent’s constructor (lines 14–42) loads the character descriptions for the predefined animated characters (lines 22–29). If the specified location of the characters is incorrect, or if any character is missing, a FileNotFoundException is thrown. By default, the character descriptions are stored in C:\Windows\msagent\chars. If your system uses another name for the Windows directory, you’ll need to modify the paths in lines 22–29. Lines 32–34 set Genie as the default character, obtain all animation names via our utility method GetAnimationNames and call IAgentCtlCharacter method Show to display the character. We access characters through property Characters of mainAgent, which contains all characters that have been loaded. We use the indexer of the Characters property to specify the name of the character that we wish to load (Genie).
Responding to the Agent Control’s ClickEvent When a user clicks the character (i.e., pokes it with the mouse), event handler mainAgent_ClickEvent (lines 58–64) executes. First, speaker method Play plays an animation. This method accepts as an argument a string representing one of the predefined animations for the character (a list of animations for each character is available at the Microsoft Agent Web site; each character provides over 70 animations). In our example, the argument to Play is "Confused"—this animation is defined for all four characters, each of which expresses this emotion in a unique way. The character then speaks, "Why are you poking me?" via a call to method Speak. Finally, we play the RestPose animation, which returns the character to its neutral, resting pose. Obtaining a Character’s List of Animations and Defining Its Commands The list of valid commands for a character is contained in property Commands of the IAgentCtlCharacter object (speaker, in this example). The commands for an Agent character can be viewed in the Commands pop-up window, which displays when the user right-clicks an Agent character (the last screenshot in Fig. 17.37). Method Add of property Commands adds a new command to the command list. Method Add takes three string arguments and two bool arguments. The first string argument identifies the name of the command, which we use to identify the command programmatically. The second string defines the command name as it appears in the Commands pop-up window. The third string defines the voice input that triggers the command. The first bool specifies whether the command is active, and the second bool indicates whether the command is visible in the Commands pop-up window. A command is triggered when the user selects the command from the Commands pop-up window or speaks the voice input into a microphone. Command logic is handled in the Command event handler of the AxAgent control (mainAgent, in this example). In addition, Agent defines several global commands that have predefined functions (for example, speaking a character name causes that character to appear). Method GetAnimationNames (lines 86–119) fills the actionsCombo ComboBox with the current character’s animation listing and defines the valid commands that can be used with the character. The method contains a lock block to prevent errors resulting from rapid character changes. The method uses an IEnumerator (lines 92–93) to obtain the current character’s animations. Lines 98–99 clear the existing items in the ComboBox and the
17.14 Microsoft Agent
705
character’s Commands property. Lines 102–113 iterate through all items in the animationname enumerator. For each animation, line 105 assigns the animation name to string voiceString. Line 106 removes any underscore characters (_) and replaces them with the string "underscore"; this changes the string so that a user can pronounce and employ it as a command activator. Line 108 adds the animation’s name to the actionsCombo ComboBox. The Add method of the Commands property (lines 111–112) adds a new command to the current character. In this example, we add every animation name as a command. Each call to Add receives the animation name as both the name of the command and the string that appears in the Commands pop-up window. The third argument is the voice command, and the last two arguments enable the command but indicate that it is not available via the Commands pop-up window. Thus, the command can be activated only by voice input. Lines 116–117 create a new command, named MoveToMouse, which is visible in the Commands pop-up window.
Responding to Selections from the actionsCombo ComboBox After the GetAnimationNames method has been called, the user can select a value from the actionsCombo ComboBox. Event handler actionsCombo_SelectedIndexChanged (lines 122–128) stops any current animation, then plays the animation that the user selected from the ComboBox, followed by the RestPose animation. Speaking the Text Typed by the User You can also type text in the TextBox and click Speak. This causes event handler speakButton_Click (line 45–55) to call speaker method Speak, supplying as an argument the text in speechTextBox. If the user clicks Speak without providing text, the character speaks, "Please, type the words you want me to speak". Changing Characters At any point in the program, the user can choose a different character from the charactersCombo ComboBox. When this happens, the SelectedIndexChanged event handler for characterCombo (lines 67–71) executes. The event handler calls method ChangeCharacter (lines 74–83) with the text in the characterCombo as an argument. Method ChangeCharacter stops any current animation, then calls the Hide method of speaker (line 77) to remove the current character from view. Line 78 assigns the newly selected character to speaker, line 81 generates the character’s animation names and commands, and line 82 displays the character via a call to method Show. Responding to Commands Each time a user presses the Scroll Lock key and speaks into a microphone or selects a command from the Commands pop-up window, event handler mainAgent_Command (lines 131–157) is called. This method is passed an argument of type AxAgentObjects._AgentEvents_CommandEvent, which contains a single property, userInput. The userInput property returns an Object that can be converted to type AgentObjects.IAgentCtlUserInput. Lines 135–136 assign the userInput object to an IAgentCtlUserInput object named command, which is used to identify the command, so the program can respond appropriately. Lines 139–144 use method ChangeCharacter to change the current Agent character if the user speaks a character name. Microsoft Agent always will show a character when a user speaks its name; however, by controlling the char-
706
Chapter 17
Graphics and Multimedia
acter change, we can ensure that only one Agent character is displayed at a time. Lines 147–152 move the character to the current mouse location if the user invokes the MoveToMouse command. Agent method MoveTo takes x- and y-coordinate arguments and moves the character to the specified screen position, applying appropriate movement animations. For all other commands, we Play the command name as an animation in line 156.
17.15 Wrap-Up This chapter began with an introduction to the .NET framework’s drawing capabilities. We then presented more powerful drawing capabilities, such as changing the styles of lines used to draw shapes and controlling the colors and patterns of filled shapes. Next, you learned techniques for manipulating images and creating smooth animations. We discussed class Image, which can store and manipulate images of various formats. We explained how to combine the graphical rendering capabilities covered in the early sections of the chapter with those for image manipulation. You also learned how to incorporate the Windows Media Player control in an application to play audio or video. Finally, we demonstrated Microsoft Agent—a technology for adding interactive animated characters to applications or Web pages—then showed how to incorporate Microsoft Agent in an application to add speech synthesis and recognition capabilities. In the next chapter, we discuss file processing techniques that enable programs to store and retrieve data from persistent storage, such as your computer’s hard disk. We also explore several types of streams included in Visual Studio .NET.
18 Files and Streams I can only assume that a “Do Not File” document is filed in a “Do Not File” file. —Senator Frank Church Senate Intelligence Subcommittee Hearing, 1975
OBJECTIVES In this chapter you will learn:
Consciousness … does not appear to itself chopped up in bits. … A “river” or a “stream” are the metaphors by which it is most naturally described. —William James
I read part of it all the way through.
I
To create, read, write and update files.
I
The C# streams class hierarchy.
I
To use classes File and Directory to obtain information about files and directories on your computer.
I
To become familiar with sequential-access file processing.
I
To use classes FileStream, StreamReader and StreamWriter to read text from and write text to files.
I
To use classes FileStream and BinaryFormatter to read objects from and write objects to files.
Introduction Data Hierarchy Files and Streams Classes File and Directory Creating a Sequential-Access Text File Reading Data from a Sequential-Access Text File Serialization Creating a Sequential-Access File Using Object Serialization Reading and Deserializing Data from a Sequential-Access Text File Wrap-Up
18.1 Introduction Variables and arrays offer only temporary storage of data—the data is lost when a local variable “goes out of scope” or when the program terminates. By contrast, files (and databases, which we cover in Chapter 18) are used for long-term retention of large amounts of data, even after the program that created the data terminates. Data maintained in files often is called persistent data. Computers store files on secondary storage devices, such as magnetic disks, optical disks and magnetic tapes. In this chapter, we explain how to create, update and process data files in C# programs. We begin with an overview of the data hierarchy from bits to files. Next, we overview some of the FCL’s file-processing classes. We then present two examples that show how you can determine information about the files and directories on your computer. The remainder of the chapter shows how to write to and read from text files that are human readable and binary files that store entire objects in binary format.
18.2 Data Hierarchy Ultimately, all data items that computers process are reduced to combinations of 0s and 1s. This occurs because it is simple and economical to build electronic devices that can assume two stable states—one state represents 0 and the other represents 1. It is remarkable that the impressive functions performed by computers involve only the most fundamental manipulations of 0s and 1s. The smallest data item that computers support is called a bit (short for “binary digit”—a digit that can assume one of two values). Each data item, or bit, can assume either the value 0 or the value 1. Computer circuitry performs various simple bit manipulations, such as examining the value of a bit, setting the value of a bit and reversing a bit (from 1 to 0 or from 0 to 1). Programming with data in the low-level form of bits is cumbersome. It is preferable to program with data in forms such as decimal digits (i.e., 0, 1, 2, 3, 4, 5, 6, 7, 8 and 9), letters (i.e., A–Z and a–z) and special symbols (i.e., $, @, %, &, *, (, ), -, +, ", :, ?, / and many others). Digits, letters and special symbols are referred to as characters. The set of all characters used to write programs and represent data items on a particular computer is called that computer’s character set. Because computers can process only 0s and 1s, every
18.2 Data Hierarchy
709
character in a computer’s character set is represented as a pattern of 0s and 1s Bytes are composed of eight bits. C# uses the Unicode® character set (www.unicode.org) in which characters are composed of 2 bytes. Programmers create programs and data items with characters; computers manipulate and process these characters as patterns of bits. Just as characters are composed of bits, fields are composed of characters. A field is a group of characters that conveys meaning. For example, a field consisting of uppercase and lowercase letters can represent a person’s name. Data items processed by computers form a data hierarchy (Fig. 18.1), in which data items become larger and more complex in structure as we progress from bits to characters to fields to larger data aggregates. Typically, a record (which can be represented as a class) is composed of several related fields. In a payroll system, for example, a record for a particular employee might include the following fields: 1. Employee identification number 2. Name 3. Address
Judy
J u d y
01001010
1
Bit
Fig. 18.1 | Data hierarchy.
Sally
Black
Tom
Blue
Judy
Green
Iris
Orange
Randy
Red
File
Record
Green
Field
Byte (ASCII character J)
710
Chapter 18
Files and Streams
4. Hourly pay rate 5. Number of exemptions claimed 6. Year-to-date earnings 7. Amount of taxes withheld In the preceding example, each field is associated with the same employee. A file is a group of related records.1 A company’s payroll file normally contains one record for each employee. A payroll file for a small company might contain only 22 records, whereas one for a large company might contain 100,000 records. It is not unusual for a company to have many files, some containing millions, billions or even trillions of characters of information. To facilitate the retrieval of specific records from a file, at least one field in each record is chosen as a record key, which identifies a record as belonging to a particular person or entity and distinguishes that record from all others. For example, in a payroll record, the employee identification number normally would be the record key. There are many ways to organize records in a file. A common organization is called a sequential file, in which records typically are stored in order by a record-key field. In a payroll file, records usually are placed in order by employee identification number. The first employee record in the file contains the lowest employee identification number, and subsequent records contain increasingly higher ones. Most businesses use many different files to store data. For example, a company might have payroll files, accounts-receivable files (listing money due from clients), accounts-payable files (listing money due to suppliers), inventory files (listing facts about all the items handled by the business) and many other files. A group of related files often are stored in a database. A collection of programs designed to create and manage databases is called a database management system (DBMS). We discuss databases in Chapter 20.
18.3 Files and Streams C# views each file as a sequential stream of bytes (Fig. 18.2). Each file ends either with an end-of-file marker or at a specific byte number that is recorded in a system-maintained administrative data structure. When a file is opened, an object is created and a stream is associated with the object. When a program executes, the runtime environment creates three stream objects that are accessible via properties Console.Out, Console.In and Console.Error, respectively. These objects facilitate communication between a program and a particular file or device. Console.In refers to the standard input stream object, which 0
1
2
3
4
5
6
7
8
9
... ...
n-1
end-of-file marker
Fig. 18.2 | C#’s view of an n-byte file. 1.
Generally, a file can contain arbitrary data in arbitrary formats. In some operating systems, a file is viewed as nothing more than a collection of bytes, and any organization of the bytes in a file (such as organizing the data into records) is a view created by the application programmer.
18.4 Classes File and Directory
711
enables a program to input data from the keyboard. Console.Out refers to the standard output stream object, which enables a program to output data to the screen. Console.Error refers to the standard error stream object, which enables a program to output error messages to the screen. We have been using Console.Out and Console.In in our console applications—Console methods Write and WriteLine use Console.Out to perform output, and Console methods Read and ReadLine use Console.In to perform input. There are many file-processing classes in the FCL. The System.IO namespace includes stream classes such as StreamReader (for text input from a file), StreamWriter (for text output to a file) and FileStream (for both input from and output to a file). These stream classes inherit from abstract classes TextReader, TextWriter and Stream, respectively. Actually, properties Console.In and Console.Out are of type TextReader and TextWriter, respectively. The system creates objects of TextReader and TextWriter derived classes to initialize Console properties Console.In and Console.Out. Abstract class Stream provides functionality for representing streams as bytes. Classes FileStream, MemoryStream and BufferedStream (all from namespace System.IO) inherit from class Stream. Class FileStream can be used to write data to and read data from files. Class MemoryStream enables the transfer of data directly to and from memory—this is much faster than reading from and writing to external devices. Class BufferedStream uses buffering to transfer data to or from a stream. Buffering is an I/O performance enhancement technique, in which each output operation is directed to a region in memory, called a buffer, that is large enough to hold the data from many output operations. Then actual transfer to the output device is performed in one large physical output operation each time the buffer fills. The output operations directed to the output buffer in memory often are called logical output operations. Buffering can also be used to speed input operations by initially reading more data than is required into a buffer, so subsequent reads get data from memory rather than an external device. In this chapter, we use key stream classes to implement file processing programs that create and manipulate sequential-access files. In Chapter 23, Networking: Streams-Based Sockets and Datagrams, we use stream classes to implement networking applications.
18.4 Classes File and Directory Information is stored in files, which are organized in directories. Classes File and Directory enable programs to manipulate files and directories on disk. Class File can determine information about files and can be used to open files for reading or writing. We discuss techniques for writing to and reading from files in subsequent sections. Figure 18.3 lists several of class File’s static methods for manipulating and determining information about files. We demonstrate several of these methods in Fig. 18.5. static
Method
Description
AppendText
Returns a StreamWriter that appends text to an existing file or creates a file if one does not exist.
Copy
Copies a file to a new file.
Fig. 18.3 |
File
class static methods (partial list). (Part 1 of 2.)
712
static
Chapter 18
Files and Streams
Method
Description
Create
Creates a file and returns its associated FileStream.
CreateText
Creates a text file and returns its associated StreamWriter.
Delete
Deletes the specified file.
Exists
Returns true if the specified file exists and false otherwise.
GetCreationTime
Returns a DateTime object representing when the file was created.
GetLastAccessTime
Returns a DateTime object representing when the file was last accessed.
GetLastWriteTime
Returns a DateTime object representing when the file was last modified.
Move
Moves the specified file to a specified location.
Open
Returns a FileStream associated with the specified file and equipped with the specified read/write permissions.
OpenRead
Returns a read-only FileStream associated with the specified file.
OpenText
Returns a StreamReader associated with the specified file.
OpenWrite
Returns a read/write FileStream associated with the specified file.
Fig. 18.3 |
File
class static methods (partial list). (Part 2 of 2.)
Class Directory provides capabilities for manipulating directories. Figure 18.4 lists some of class Directory’s static methods for directory manipulation. Figure 18.5 demonstrates several of these methods, as well. The DirectoryInfo object returned by method CreateDirectory contains information about a directory. Much of the information contained in class DirectoryInfo also can be accessed via the methods of class Directory. static
Method
Description
CreateDirectory
Creates a directory and returns its associated DirectoryInfo object.
Delete
Deletes the specified directory.
Exists
Returns true if the specified directory exists and false otherwise.
GetDirectories
Returns a string array containing the names of the subdirectories in the specified directory.
GetFiles
Returns a string array containing the names of the files in the specified directory.
GetCreationTime
Returns a DateTime object representing when the directory was created.
GetLastAccessTime
Returns a DateTime object representing when the directory was last accessed.
Fig. 18.4 |
Directory
class static methods. (Part 1 of 2.)
18.4 Classes File and Directory
static
Method
713
Description
GetLastWriteTime
Returns a DateTime object representing when items were last written to the directory.
Move
Moves the specified directory to a specified location.
Fig. 18.4 |
Directory
class static methods. (Part 2 of 2.)
Demonstrating Classes File and Directory Class FileTestForm (Fig. 18.5) uses File and Directory methods to access file and directory information. This Form contains the control inputTextBox, in which the user enters a file or directory name. For each key that the user presses while typing in the TextBox, the program calls event handler inputTextBox_KeyDown (lines 17–74). If the user presses the Enter key (line 20), this method displays either the file’s or directory’s contents, depending on the text the user input. (If the user does not press the Enter key, this method returns without displaying any content.) Line 28 uses File method Exists to determine whether the user-specified text is the name of an existing file. If so, line 32 invokes private method GetInformation (lines 77–97), which calls File methods GetCreationTime (line 86), GetLastWriteTime (line 90) and GetLastAccessTime (line 94) to access file information. When method GetInformation returns, line 38 instantiates a StreamReader for reading text from the file. The StreamReader constructor takes as an argument a string containing the name of the file to open. Line 39 calls StreamReader method ReadToEnd to read the entire contents of the file as a string, then appends the string to outputTextBox. .
// Fig 18.5: FileTestForm.cs // Using classes File and Directory. using System; using System.Windows.Forms; using System.IO; // displays contents of files and directories public partial class FileTestForm : Form { // parameterless constructor public FileTestForm() { InitializeComponent(); } // end constructor // invoked when user presses key private void inputTextBox_KeyDown( object sender, KeyEventArgs e ) { // determine whether user pressed Enter key if ( e.KeyCode == Keys.Enter ) { string fileName; // name of file or directory
Fig. 18.5 | Testing classes File and Directory. (Part 1 of 3.)
// get information on file or directory private string GetInformation( string fileName ) { string information; // output that file or directory exists information = fileName + " exists\r\n\r\n"; // output when file or directory was created information += "Created: " + File.GetCreationTime( fileName ) + "\r\n"; // output when file or directory was last modified information += "Last modified: " + File.GetLastWriteTime( fileName ) + "\r\n"; // output when file or directory was last accessed information += "Last accessed: " + File.GetLastAccessTime( fileName ) + "\r\n" + "\r\n"; return information; } // end method GetInformation } // end class FileTestForm
(a)
(b)
(c)
(d)
Fig. 18.5 | Testing classes File and Directory. (Part 3 of 3.) If line 28 determines that the user-specified text is not a file, line 49 determines whether it is a directory using Directory method Exists. If the user specified an existing directory, line 55 invokes method GetInformation to access the directory information.
716
Chapter 18
Files and Streams
Line 58 calls Directory method GetDirectories to obtain a string array containing the names of subdirectories in the specified directory. Lines 63–64 display each element in the string array. Note that, if line 49 determines that the user-specified text is not a directory name, lines 69–71 notify the user (via a MessageBox) that the name the user entered does not exist as a file or directory.
Finding Directories with Regular Expressions We now consider another example that uses C#’s file- and directory-manipulation capabilities. Class FileSearchForm (Fig. 18.6) uses classes File and Directory, and regular expression capabilities, to report the number of files of each file type that exist in the specified directory path. The program also serves as a “clean-up” utility—when the program encounters a file that has the .bak filename extension (i.e., a backup file), the program displays a MessageBox asking the user whether that file should be removed, then responds appropriately to the user’s input. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
// Fig 18.6: FileSearchForm.cs // Using regular expressions to determine file types. using System; using System.Windows.Forms; using System.IO; using System.Text.RegularExpressions; using System.Collections.Specialized; // uses regular expressions to determine file types public partial class FileSearchForm : Form { string currentDirectory = Directory.GetCurrentDirectory(); string[] directoryList; // subdirectories string[] fileArray; // store extensions found and number found NameValueCollection found = new NameValueCollection(); // parameterless constructor public FileSearchForm() { InitializeComponent(); } // end constructor // invoked when user types in text box private void inputTextBox_KeyDown( object sender, KeyEventArgs e ) { // determine whether user pressed Enter if ( e.KeyCode == Keys.Enter ) searchButton_Click( sender, e ); } // end method inputTextBox_KeyDown // invoked when user clicks "Search Directory" button private void searchButton_Click( object sender, EventArgs e ) {
Fig. 18.6 | Regular expression used to determine file types. (Part 1 of 4.)
// check for user input; default is current directory if ( inputTextBox.Text != "" ) { // verify that user input is valid directory name if ( Directory.Exists( inputTextBox.Text ) ) { currentDirectory = inputTextBox.Text; // reset input text box and update display directoryLabel.Text = "Current Directory:" + "\r\n" + currentDirectory; } // end if else { // show error if user does not specify valid directory MessageBox.Show( "Invalid Directory", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error ); } // end else } // end if // clear text boxes inputTextBox.Text = ""; outputTextBox.Text = ""; SearchDirectory( currentDirectory );
// search directory
// summarize and print results foreach ( string current in found ) { outputTextBox.Text += "* Found " + found[ current ] + " " + current + " files.\r\n"; } // end foreach // clear output for new search found.Clear(); } // end method searchButton_Click // search directory using regular expression private void SearchDirectory( string currentDirectory ) { // for file name without directory path try { string fileName = ""; // regular expression for extensions matching pattern Regex regularExpression = new Regex( @"[a-zA-Z0-9]+\.(?\w+)" ); // stores regular-expression match result Match matchResult; string fileExtension; // holds file extensions
Fig. 18.6 | Regular expression used to determine file types. (Part 2 of 4.)
// number of files with given extension in directory int extensionCount; // get directories directoryList = Directory.GetDirectories( currentDirectory ); // get list of files in current directory fileArray = Directory.GetFiles( currentDirectory ); // iterate through list of files foreach ( string myFile in fileArray ) { // remove directory path from file name fileName = myFile.Substring( myFile.LastIndexOf( @"\" ) + 1 ); // obtain result for regular-expression search matchResult = regularExpression.Match( fileName ); // check for match if ( matchResult.Success ) fileExtension = matchResult.Result( "${extension}" ); else fileExtension = "[no extension]"; // store value from container if ( found[ fileExtension ] == null ) found.Add( fileExtension, "1" ); else { extensionCount = Int32.Parse( found[ fileExtension ] ) + 1; found[ fileExtension ] = extensionCount.ToString(); } // end else // search for backup( .bak ) files if ( fileExtension == "bak" ) { // prompt user to delete ( .bak ) file DialogResult result = MessageBox.Show( "Found backup file " + fileName + ". Delete?", "Delete Backup", MessageBoxButtons.YesNo, MessageBoxIcon.Question ); // delete file if user clicked 'yes' if ( result == DialogResult.Yes ) { File.Delete( myFile ); extensionCount = Int32.Parse( found[ "bak" ] ) - 1; found[ "bak" ] = extensionCount.ToString(); } // end if } // end if } // end foreach
Fig. 18.6 | Regular expression used to determine file types. (Part 3 of 4.)
18.4 Classes File and Directory
719
142 // recursive call to search files in subdirectory 143 foreach ( string myDirectory in directoryList ) 144 SearchDirectory( myDirectory ); 145 } // end try 146 // handle exception if files have unauthorized access 147 catch ( UnauthorizedAccessException ) 148 { 149 MessageBox.Show( "Some files may not be visible" + 150 " due to permission settings", "Warning", 151 MessageBoxButtons.OK, MessageBoxIcon.Information ); 152 } // end catch 153 } // end method SearchDirectory 154 } // end class FileSearchForm
Fig. 18.6 | Regular expression used to determine file types. (Part 4 of 4.) When the user presses the Enter key or clicks the Search Directory button, the program invokes method searchButton_Click (lines 34–71), which searches recursively through the directory path that the user provides. If the user inputs text in the TextBox, line 40 calls Directory method Exists to determine whether that text is a valid directory path and name. If not, lines 51–52 notify the user of the error. If the user specifies a valid directory, line 60 passes the directory name as an argument to private method SearchDirectory (lines 74–153). This method locates files that match the regular expression defined in lines 82–83. This regular expression matches any sequence of numbers or letters followed by a period and one or more letters. Notice the substring of format (?\w+) in the argument to the Regex constructor (line 83). This indicates that the part of the string that matches \w+ (i.e., the filename extension that appears after a period in the file name) should be placed in the regular expression variable named extension. This variable’s value is retrieved later from Match object matchResult to obtain the filename extension so we can summarize the types of files in the directory.
720
Chapter 18
Files and Streams
Line 94 calls Directory method GetDirectories to retrieve the names of all subdirectories that belong to the current directory. Line 97 calls Directory method GetFiles to store in string array fileArray the names of files in the current directory. The foreach loop in lines 100–140 searches for all files with extension .bak. The loop at lines 143–144 then calls SearchDirectory recursively (line 144) for each subdirectory in the current directory. Line 103 eliminates the directory path, so the program can test only the file name when using the regular expression. Line 106 uses Regex method Match to match the regular expression with the file name, then assigns the result to Match object matchResult. If the match is successful, line 110 uses Match method Result to assign to fileExtension the value of the regular expression variable extension from object matchResult. If the match is unsuccessful, line 112 sets fileExtension to "[no extension]". Class FileSearchForm uses an instance of class NameValueCollection (declared in line 17) to store each filename-extension type and the number of files for each type. A NameValueCollection (namespace System.Collections.Specialized) contains a collection of key-value pairs of strings, and provides method Add to add a key-value pair to the collection. The indexer for this class can index according to the order that the items were added or according to the keys. Line 115 uses NameValueCollection found to determine whether this is the first occurrence of the filename extension (the expression returns null if the collection does not contain a key-value pair for the specified fileExtension). If this is the first occurrence, line 116 adds that extension to found as a key with the value 1. Otherwise, line 119 increments the value associated with the extension in found to indicate another occurrence of that file extension, and line 120 assigns the new value to the key-value pair. Line 124 determines whether fileExtension equals “bak”—i.e., whether the file is a backup file. If so, lines 127–130 prompt the user to indicate whether the file should be removed; if the user clicks Yes (line 133), lines 135–137 delete the file and decrement the value for the “bak” file type in found. Lines 143–144 call method SearchDirectory for each subdirectory. Using recursion, we ensure that the program performs the same logic for finding .bak files in each subdirectory. After each subdirectory has been checked for .bak files, method SearchDirectory completes, and lines 63–67 display the results.
18.5 Creating a Sequential-Access Text File C# imposes no structure on files. Thus, the concept of a “record” does not exist in C# files. This means that you must structure files to meet the requirements of your applications. In the next few examples, we use text and special characters to organize our own concept of a “record.”
Class BankUIForm The following examples demonstrate file processing in a bank-account maintenance application. These programs have similar user interfaces, so we created reusable class BankUIForm (Fig. 18.7 from the Visual Studio Form designer) to encapsulate a base-class GUI (see the screen capture in Fig. 18.7). Class BankUIForm contains four Labels and four TextBoxes. Methods ClearTextBoxes (lines 26–40), SetTextBoxValues (lines 43–61) and GetTextBoxValues (lines 64–75) clear, set the values of and get the values of the text in the TextBoxes, respectively.
// Fig. 18.7: BankUIForm.cs // A reusable Windows Form for the examples in this chapter. using System; using System.Windows.Forms; public partial class BankUIForm : Form { protected int TextBoxCount = 4; // number of TextBoxes on Form // enumeration constants specify TextBox indices public enum TextBoxIndices { ACCOUNT, FIRST, LAST, BALANCE } // end enum // parameterless constructor public BankUIForm() { InitializeComponent(); } // end constructor // clear all TextBoxes public void ClearTextBoxes() { // iterate through every Control on form for ( int i = 0; i < Controls.Count; i++ ) { Control myControl = Controls[ i ]; // get control // determine whether Control is TextBox if ( myControl is TextBox ) { // clear Text property ( set to empty string ) myControl.Text = ""; } // end if } // end for } // end method ClearTextBoxes // set text box values to string array values public void SetTextBoxValues( string[] values ) { // determine whether string array has correct length if ( values.Length != TextBoxCount ) { // throw exception if not correct length throw( new ArgumentException( "There must be " + ( TextBoxCount + 1 ) + " strings in the array" ) ); } // end if
Fig. 18.7 | Base class for GUIs in our file-processing applications. (Part 1 of 2.)
// set array values if array has correct length else { // set array values to text box values accountTextBox.Text = values[ ( int ) TextBoxIndices.ACCOUNT ]; firstNameTextBox.Text = values[ ( int ) TextBoxIndices.FIRST ]; lastNameTextBox.Text = values[ ( int ) TextBoxIndices.LAST ]; balanceTextBox.Text = values[ ( int ) TextBoxIndices.BALANCE ]; } // end else } // end method SetTextBoxValues // return text box values as string array public string[] GetTextBoxValues() { string[] values = new string[ TextBoxCount ]; // copy values[ values[ values[ values[
text box fields to string array ( int ) TextBoxIndices.ACCOUNT ] = accountTextBox.Text; ( int ) TextBoxIndices.FIRST ] = firstNameTextBox.Text; ( int ) TextBoxIndices.LAST ] = lastNameTextBox.Text; ( int ) TextBoxIndices.BALANCE ] = balanceTextBox.Text;
return values; } // end method GetTextBoxValues } // end class BankUIForm
Fig. 18.7 | Base class for GUIs in our file-processing applications. (Part 2 of 2.) To reuse class BankUIForm, you must compile the GUI into a DLL by creating a project of type Windows Control Library (we named it BankLibrary). This library is provided with the code for this chapter. You might need to change references to this library in our examples when you copy them to your system, since the library most likely will reside in a different location on your system.
Class Record Figure 18.8 contains class Record that Fig. 18.9, Fig. 18.11 and Fig. 18.12 use for maintaining the information in each record that is written to or read from a file. This class also belongs to the BankLibrary DLL, so it is located in the same project as class BankUIForm.
18.5 Creating a Sequential-Access Text File
723
Class Record contains private instance variables account, firstName, lastName and (lines 9–12), which collectively represent all the information for a record. The parameterless constructor (lines 15–17) sets these members by calling the four-argument constructor with 0 for the account number, empty strings ("") for the first and last name and 0.0M for the balance. The four-argument constructor (lines 20–27) sets these members to the specified parameter values. Class Record also provides properties Account (lines 30–40), FirstName (lines 43–53), LastName (lines 56–66) and Balance (lines 69–79) for accessing each record’s account number, first name, last name and balance, respectively. balance
// Fig. 18.8: Record.cs // Serializable class that represents a data record. using System; using System.Collections.Generic; using System.Text; public class Record { private int account; private string firstName; private string lastName; private decimal balance; // parameterless constructor sets members to default values public Record() : this( 0, "", "", 0.0M ) { } // end constructor // overloaded constructor sets members to parameter values public Record( int accountValue, string firstNameValue, string lastNameValue, decimal balanceValue ) { Account = accountValue; FirstName = firstNameValue; LastName = lastNameValue; Balance = balanceValue; } // end constructor // property that gets and sets Account public int Account { get { return account; } // end get set { account = value; } // end set } // end property Account
Fig. 18.8 | Record for sequential-access file-processing applications. (Part 1 of 2.)
// property that gets and sets FirstName public string FirstName { get { return firstName; } // end get set { firstName = value; } // end set } // end property FirstName // property that gets and sets LastName public string LastName { get { return lastName; } // end get set { lastName = value; } // end set } // end property LastName // property that gets and sets Balance public decimal Balance { get { return balance; } // end get set { balance = value; } // end set } // end property Balance } // end class Record
Fig. 18.8 | Record for sequential-access file-processing applications. (Part 2 of 2.)
Using a Character Stream to Create an Output File Class CreateFileForm (Fig. 18.9) uses instances of class Record to create a sequential-access file that might be used in an accounts receivable system—i.e., a program that organizes data regarding money owed by a company’s credit clients. For each client, the program obtains an account number and the client’s first name, last name and balance (i.e., the amount of money that the client owes to the company for previously received goods and services). The data obtained for each client constitutes a record for that client. In this application, the account number is used as the record key—files are created and maintained in account-number order. This program assumes that the user enters records in account-
18.5 Creating a Sequential-Access Text File
725
number order. However, a comprehensive accounts receivable system would provide a sorting capability, so the user could enter the records in any order. Class CreateFileForm either creates or opens a file (depending on whether one exists), then allows the user to write records to that file. The using directive in line 6 enables us to use the classes of the BankLibrary namespace; this namespace contains class BankUIForm, from which class CreateFileForm inherits (line 8). Class CreateFileForm’s GUI enhances that of class BankUIForm with buttons Save As, Enter and Exit. When the user clicks the Save As button, the program invokes the event handler saveButton_Click (lines 20–63). Line 23 instantiates an object of class SaveFileDialog (namespace System.Windows.Forms). Objects of this class are used for selecting files (see the second screen in Fig. 18.9). Line 24 calls SaveFileDialog method ShowDialog to display the dialog. When displayed, a SaveFileDialog prevents the user from interacting with any other window in the program until the user closes the SaveFileDialog by clicking either Save or Cancel. Dialogs that behave in this manner are called modal dialogs. The user selects the appropriate drive, directory and file name, then clicks Save. Method ShowDialog returns a DialogResult specifying which button (Save or Cancel) the user clicked to close the dialog. This is assigned to DialogResult variable result (line 24). Line 30 tests whether the user clicked Cancel by comparing this value to DialogResult.Cancel. If the values are equal, method saveButton_Click returns (line 31). Otherwise, line 33 uses SaveFileDialog property FileName to obtain the user-selected file. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
// Fig. 18.9: CreateFileForm.cs // Creating a sequential-access file. using System; using System.Windows.Forms; using System.IO; using BankLibrary; public partial class CreateFileForm : BankUIForm { private StreamWriter fileWriter; // writes data to text file private FileStream output; // maintains connection to file // parameterless constructor public CreateFileForm() { InitializeComponent(); } // end constructor // event handler for Save Button private void saveButton_Click( object sender, EventArgs e ) { // create dialog box enabling user to save file SaveFileDialog fileChooser = new SaveFileDialog(); DialogResult result = fileChooser.ShowDialog(); string fileName; // name of file to save data fileChooser.CheckFileExists = false; // allow user to create file
Fig. 18.9 | Creating and writing to a sequential-access file. (Part 1 of 5.)
// exit event handler if user clicked "Cancel" if ( result == DialogResult.Cancel ) return; fileName = fileChooser.FileName; // get specified file name // show error if user specified invalid file if ( fileName == "" || fileName == null ) MessageBox.Show( "Invalid File Name", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error ); else { // save file via FileStream if user specified valid file try { // open file with write access output = new FileStream( fileName, FileMode.OpenOrCreate, FileAccess.Write ); // sets file to where data is written fileWriter = new StreamWriter( output ); // disable Save button and enable Enter button saveButton.Enabled = false; enterButton.Enabled = true; } // end try // handle exception if there is a problem opening the file catch ( IOException ) { // notify user if file does not exist MessageBox.Show( "Error opening file", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error ); } // end catch } // end else } // end method saveButton_Click // handler for enterButton Click private void enterButton_Click( object sender, EventArgs e ) { // store TextBox values string array string[] values = GetTextBoxValues(); // Record containing TextBox values to serialize Record record = new Record(); // determine whether TextBox account field is empty if ( values[ ( int ) TextBoxIndices.ACCOUNT ] != "" ) { // store TextBox values in Record and serialize Record try {
Fig. 18.9 | Creating and writing to a sequential-access file. (Part 2 of 5.)
// get account number value from TextBox int accountNumber = Int32.Parse( values[ ( int ) TextBoxIndices.ACCOUNT ] ); // determine whether accountNumber is valid if ( accountNumber > 0 ) { // store TextBox fields in Record record.Account = accountNumber; record.FirstName = values[ ( int ) TextBoxIndices.FIRST ]; record.LastName = values[ ( int ) TextBoxIndices.LAST ]; record.Balance = Decimal.Parse( values[ ( int ) TextBoxIndices.BALANCE ] ); // write Record to file, fields separated by commas fileWriter.WriteLine( record.Account + "," + record.FirstName + "," + record.LastName + "," + record.Balance ); } // end if else { // notify user if invalid account number MessageBox.Show( "Invalid Account Number", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error ); } // end else } // end try // notify user if error occurs in serialization catch ( IOException ) { MessageBox.Show( "Error Writing to File", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error ); } // end catch // notify user if error occurs regarding parameter format catch ( FormatException ) { MessageBox.Show( "Invalid Format", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error ); } // end catch } // end if ClearTextBoxes(); // clear TextBox values } // end method enterButton_Click // handler for exitButton Click private void exitButton_Click( object sender, EventArgs e ) { // determine whether file exists if ( output != null ) { try { fileWriter.Close(); // close StreamWriter
Fig. 18.9 | Creating and writing to a sequential-access file. (Part 3 of 5.)
728
Chapter 18
Files and Streams
output.Close(); // close file 132 133 } // end try 134 // notify user of error closing file 135 catch ( IOException ) 136 { 137 MessageBox.Show( "Cannot close file", "Error", 138 MessageBoxButtons.OK, MessageBoxIcon.Error ); 139 } // end catch 140 } // end if 141 142 Application.Exit(); 143 } // end method exitButton_Click 144 } // end class CreateFileForm (a)
BankUI graphical
user interface
(b) SaveFileDialog
Files and directories
Fig. 18.9 | Creating and writing to a sequential-access file. (Part 4 of 5.)
18.5 Creating a Sequential-Access Text File
(c)
(d)
(e)
(f)
(g)
(h)
729
Fig. 18.9 | Creating and writing to a sequential-access file. (Part 5 of 5.) You can open files to perform text manipulation by creating objects of class FileStream. In this example, we want the file to be opened for output, so lines 45–46 create a FileStream object. The FileStream constructor that we use receives three arguments—a string containing the path and name of the file to open, a constant describing
how to open the file and a constant describing the file permissions. The constant FileMode.OpenOrCreate (line 46) indicates that the FileStream object should open the file if the file exists or create the file if it does not exist. There are other FileMode constants describing how to open files; we introduce these constants as we use them in examples. The constant FileAccess.Write indicates that the program can perform only write oper-
730
Chapter 18
Files and Streams
ations with the FileStream object. There are two other constants for the third constructor parameter—FileAccess.Read for read-only access and FileAccess.ReadWrite for both read and write access. Line 56 catches an IOException if there is a problem opening the file or creating the StreamWriter. If so, the program displays an error message (lines 59– 60). If no exception occurs, the file is open for writing.
Good Programming Practice 18.1 When opening files, use the FileAccess enumeration to control user access to these files.
18.1
Common Programming Error 18.1 Failure to open a file before attempting to reference it in a program is a logic error.
18.1
After the user types information in each TextBox, the user clicks the Enter button, which calls event handler enterButton_Click (lines 66–121) to save the data from the TextBoxes into the user-specified file. If the user entered a valid account number (i.e., an integer greater than zero), lines 88–92 store the TextBox values in an object of type Record (created at line 72). If the user entered invalid data in one of the TextBoxes (such as nonnumeric characters in the Balance field), the program throws a FormatException. The catch block in lines 113–117 handles such exceptions by notifying the user (via a MessageBox) of the improper format. If the user entered valid data, lines 95–97 write the record to the file by invoking method WriteLine of the StreamWriter object that was created at line 49. Method WriteLine writes a sequence of characters to a file. The StreamWriter object is constructed with a FileStream argument that specifies the file to which the StreamWriter will output text. Class StreamWriter belongs to the System.IO namespace. When the user clicks the Exit button, event handler exitButton_Click (lines 124– 143) exits the application. Line 131 closes the StreamWriter, and line 132 closes the FileStream, then line 142 terminates the program. Note that the call to method Close is located in a try block. Method Close throws an IOException if the file or stream cannot be closed properly. In this case, it is important to notify the user that the information in the file or stream might be corrupted.
Performance Tip 18.1 Close each file explicitly when the program no longer needs to reference the file. This can reduce resource usage in programs that continue executing long after they finish using a specific file. The practice of explicitly closing files also improves program clarity. 18.1
Performance Tip 18.2 Releasing resources explicitly when they are no longer needed makes them immediately available for reuse by other programs, thus improving resource utilization. 18.2
In the sample execution for the program in Fig. 18.9, we entered information for the five accounts shown in Fig. 18.10. The program does not depict how the data records are rendered in the file. To verify that the file has been created successfully, we create a program in the next section to read and display the file. Since this is a text file, you can actually open the file in any text editor to see its contents.
18.6 Reading Data from a Sequential-Access Text File
Account Number
First Name
Last Name
Balance
100
Nancy
Brown
-25.54
200
Stacey
Dunn
314.33
300
Doug
Barker
0.00
400
Dave
Smith
258.34
500
Sam
Stone
34.98
731
Fig. 18.10 | Sample data for the program of Fig. 18.9.
18.6 Reading Data from a Sequential-Access Text File The previous section demonstrated how to create a file for use in sequential-access applications. In this section, we discuss how to read (or retrieve) data sequentially from a file. Class ReadSequentialAccessFileForm (Fig. 18.11) reads records from the file created by the program in Fig. 18.9, then displays the contents of each record. Much of the code in this example is similar to that of Fig. 18.9, so we discuss only the unique aspects of the application. .
// Fig. 18.11: ReadSequentialAccessFileForm.cs // Reading a sequential-access file. using System; using System.Windows.Forms; using System.IO; using BankLibrary; public partial class ReadSequentialAccessFileForm : BankUIForm { private FileStream input; // maintains connection to a file private StreamReader fileReader; // reads data from a text file // paramterless constructor public ReadSequentialAccessFileForm() { InitializeComponent(); } // end constructor // invoked when user clicks the Open button private void openButton_Click( object sender, EventArgs e ) { // create dialog box enabling user to open file OpenFileDialog fileChooser = new OpenFileDialog(); DialogResult result = fileChooser.ShowDialog(); string fileName; // name of file containing data
// exit event handler if user clicked Cancel if ( result == DialogResult.Cancel ) return; fileName = fileChooser.FileName; // get specified file name ClearTextBoxes(); // show error if user specified invalid file if ( fileName == "" || fileName == null ) MessageBox.Show( "Invalid File Name", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error ); else { // create FileStream to obtain read access to file input = new FileStream( fileName, FileMode.Open, FileAccess.Read ); // set file from where data is read fileReader = new StreamReader( input ); openButton.Enabled = false; // disable Open File button nextButton.Enabled = true; // enable next record button } // end else } // end method openButton_Click // invoked when user clicks Next button private void nextButton_Click( object sender, EventArgs e ) { try { // get next record available in file string inputRecord = fileReader.ReadLine(); string[] inputFields; // will store individual pieces of data
Fig. 18.11
if ( inputRecord != null ) { inputFields = inputRecord.Split( ',' ); Record record = new Record( Convert.ToInt32( inputFields[ 0 ] ), inputFields[ 1 ], inputFields[ 2 ], Convert.ToDecimal( inputFields[ 3 ] ) ); // copy string array values to TextBox values SetTextBoxValues( inputFields ); } // end if else { fileReader.Close(); // close StreamReader input.Close(); // close FileStream if no Records in file openButton.Enabled = true; // enable Open File button nextButton.Enabled = false; // disable Next Record button ClearTextBoxes();
|
Reading sequential-access files. (Part 2 of 4.)
18.6 Reading Data from a Sequential-Access Text File
80 81 82 83 84 85 86 87 88 89 90 91
// notify user if no Records in file MessageBox.Show( "No more records in file", "", MessageBoxButtons.OK, MessageBoxIcon.Information ); } // end else } // end try catch ( IOException ) { MessageBox.Show( "Error Reading from File", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error ); } // end catch } // end method nextButton_Click } // end class readSequentialAccessFileForm
(a)
(b)
Fig. 18.11
|
Reading sequential-access files. (Part 3 of 4.)
733
734
Chapter 18
Files and Streams
(c)
(d)
(e)
(f)
(g)
(h)
Fig. 18.11
|
Reading sequential-access files. (Part 4 of 4.)
When the user clicks the Open File button, the program calls event handler openButton_Click (lines 20–50). Line 23 creates an OpenFileDialog, and line 24 calls its ShowDialog method to display the Open dialog (see the second screenshot in Fig. 18.11). The behavior and GUI for the Save and Open dialog types are identical, except that Save is replaced by Open. If the user inputs a valid file name, lines 41–42 create a FileStream object and assign it to reference input. We pass constant FileMode.Open as the second
18.6 Reading Data from a Sequential-Access Text File
735
argument to the FileStream constructor to indicate that the FileStream should open the file if it exists or should throw a FileNotFoundException if the file does not exist. (In this example, the FileStream constructor will not throw a FileNotFoundException, because the OpenFileDialog requires the user to enter a name of a file that exists.) In the last example (Fig. 18.9), we wrote text to the file using a FileStream object with write-only access. In this example (Fig. 18.11), we specify read-only access to the file by passing constant FileAccess.Read as the third argument to the FileStream constructor. This FileStream object is used to create a StreamReader object in line 45. The FileStream object specifies the file from which the StreamReader object will read text.
Error-Prevention Tip 18.1 Open a file with the FileAccess.Read file-open mode if the contents of the file should not be modified. This prevents unintentional modification of the contents. 18.1
When the user clicks the Next Record button, the program calls event handler nextButton_Click (lines 53–90), which reads the next record from the user-specified file.
(The user must click Next Record after opening the file to view the first record.) Line 58 calls StreamReader method ReadLine to read the next record. If an error occurs while reading the file, an IOException is thrown (caught at line 85), and the user is notified (line 87–88). Otherwise, line 61 determines whether StreamReader method ReadLine returned null (i.e., there is no more text in the file). If not, line 63 uses method Split of class string to separate the stream of characters that was read from the file into strings that represent the Record’s properties. These properties are then stored by constructing a Record object using the properties as arguments (lines 65–67). Line 70 displays the Record values in the TextBoxes. If ReadLine returns null, the program closes both the StreamReader object (line 74) and the FileStream object (line 75), then notifies the user that there are no more records (lines 81–82).
Searching a Sequential-Access File To retrieve data sequentially from a file, programs normally start from the beginning of the file, reading consecutively until the desired data is found. It sometimes is necessary to process a file sequentially several times (from the beginning of the file) during the execution of a program. A FileStream object can reposition its file-position pointer (which contains the byte number of the next byte to be read from or written to the file) to any position in the file. When a FileStream object is opened, its file-position pointer is set to byte position 0 (i.e., the beginning of the file) We now present a program that builds on the concepts employed in Fig. 18.11. Class CreditInquiryForm (Fig. 18.12) is a credit-inquiry program that enables a credit manager to search for and display account information for those customers with credit balances (i.e., customers to whom the company owes money), zero balances (i.e., customers who do not owe the company money) and debit balances (i.e., customers who owe the company money for previously received goods and services). We use a RichTextBox in the program to display the account information. RichTextBoxes provide more functionality than regular TextBoxes—for example, RichTextBoxes offer method Find for searching individual strings and method LoadFile for displaying file contents. Classes RichTextBox and TextBox both inherit from abstract class System.Windows.Forms.TextBoxBase. We chose a RichTextBox in this example, because it displays multiple lines of text by default,
736
Chapter 18
Files and Streams
whereas a regular TextBox displays only one. Alternatively, we could have specified that a TextBox object display multiple lines of text by setting its Multiline property to true.
// Fig. 18.12: CreditInquiryForm.cs // Read a file sequentially and display contents based on // account type specified by user ( credit, debit or zero balances ). using System; using System.Windows.Forms; using System.IO; using BankLibrary; public partial class CreditInquiryForm : Form { private FileStream input; // maintains the connection to the file private StreamReader fileReader; // reads data from text file // name of file that stores credit, debit and zero balances private string fileName; // parameterless constructor public CreditInquiryForm() { InitializeComponent(); } // end constructor // invoked when user clicks Open File button private void openButton_Click( object sender, EventArgs e ) { // create dialog box enabling user to open file OpenFileDialog fileChooser = new OpenFileDialog(); DialogResult result = fileChooser.ShowDialog();
Fig. 18.12
// exit event handler if user clicked Cancel if ( result == DialogResult.Cancel ) return; fileName = fileChooser.FileName; // get name from user // show error if user specified invalid file if ( fileName == "" || fileName == null ) MessageBox.Show( "Invalid File Name", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error ); else { // create FileStream to obtain read access to file input = new FileStream( fileName, FileMode.Open, FileAccess.Read ); // set file from where data is read fileReader = new StreamReader( input );
| Credit-inquiry program. (Part 1 of 5.)
18.6 Reading Data from a Sequential-Access Text File
// enable all GUI buttons, except for Open File button openButton.Enabled = false; creditButton.Enabled = true; debitButton.Enabled = true; zeroButton.Enabled = true; } // end else } // end method openButton_Click // invoked when user clicks credit balances, // debit balances or zero balances button private void getBalances_Click( object sender, System.EventArgs e ) { // convert sender explicitly to object of type button Button senderButton = ( Button )sender;
Fig. 18.12
// get text from clicked Button, which stores account type string accountType = senderButton.Text; // read and display file information try { // go back to the beginning of the file input.Seek( 0, SeekOrigin.Begin ); displayTextBox.Text = "The accounts are:\r\n"; // traverse file until end of file while ( true ) { string[] inputFields; // will store individual pieces of data Record record; // store each Record as file is read decimal balance; // store each Record's balance // get next Record available in file string inputRecord = fileReader.ReadLine(); // when at the end of file, exit method if ( inputRecord == null ) return; inputFields = inputRecord.Split( ',' ); // parse input // create Record from input record = new Record( Convert.ToInt32( inputFields[ 0 ] ), inputFields[ 1 ], inputFields[ 2 ], Convert.ToDecimal( inputFields[ 3 ] ) ); // store record's last field in balance balance = record.Balance; // determine whether to display balance if ( ShouldDisplay( balance, accountType ) ) {
// display record string output = record.Account + "\t" + record.FirstName + "\t" + record.LastName + "\t"; // display balance with correct monetary format output += string.Format( "{0:F}", balance ) + "\r\n"; displayTextBox.Text += output; // copy output to screen } // end if } // end while } // end try // handle exception when file cannot be read catch ( IOException ) { MessageBox.Show( "Cannot Read File", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error ); } // end catch } // end method getBalances_Click // determine whether to display given record private bool ShouldDisplay( decimal balance, string accountType ) { if ( balance > 0 ) { // display credit balances if ( accountType == "Credit Balances" ) return true; } // end if else if ( balance < 0 ) { // display debit balances if ( accountType == "Debit Balances" ) return true; } // end else if else // balance == 0 { // display zero balances if ( accountType == "Zero Balances" ) return true; } // end else return false; } // end method ShouldDisplay // invoked when user clicks Done button private void doneButton_Click( object sender, EventArgs e ) { // determine whether file exists if ( input != null ) { // close file and StreamReader try {
Fig. 18.12
| Credit-inquiry program. (Part 3 of 5.)
18.6 Reading Data from a Sequential-Access Text File
155 input.Close(); 156 fileReader.Close(); 157 } // end try 158 // handle exception if FileStream does not exist 159 catch( IOException ) 160 { 161 // notify user of error closing file 162 MessageBox.Show( "Cannot close file", "Error", 163 MessageBoxButtons.OK, MessageBoxIcon.Error ); 164 } // end catch 165 } // end if 166 167 Application.Exit(); 168 } // end method doneButton_Click 169 } // end class CreditInquiryForm (a)
(b)
Fig. 18.12
| Credit-inquiry program. (Part 4 of 5.)
739
740
Chapter 18
Files and Streams
(c)
(d)
(e)
Fig. 18.12
| Credit-inquiry program. (Part 5 of 5.)
The program displays buttons that enable a credit manager to obtain credit information. The Open File button opens a file for gathering data. The Credit Balances button displays a list of accounts that have credit balances, the Debit Balances button displays a list of accounts that have debit balances and the Zero Balances button displays a list of accounts that have zero balances. The Done button exits the application. When the user clicks the Open File button, the program calls the event handler openButton_Click (lines 24–55). Line 27 creates an OpenFileDialog, and line 28 calls its ShowDialog method to display the Open dialog, in which the user selects the file to open. Lines 43–44 create a FileStream object with read-only file access and assign it to reference input. Line 47 creates a StreamReader object that we use to read text from the FileStream. When the user clicks Credit Balances, Debit Balances or Zero Balances, the program invokes method getBalances_Click (lines 59–119). Line 62 casts the sender parameter, which is an object reference to the control that generated the event, to a Button object. Line 65 extracts the Button object’s text, which the program uses to determine which type
18.7 Serialization
741
of accounts to display. Line 71 uses FileStream method Seek to reset the file-position pointer back to the beginning of the file. FileStream method Seek allows you to reset the file-position pointer by specifying the number of bytes it should be offset from the file’s beginning, end or current position. The part of the file you want to be offset from is chosen using constants from the SeekOrigin enumeration. In this case, our stream is offset by 0 bytes from the file’s beginning (SeekOrigin.Begin). Lines 76–111 define a while loop that uses private method ShouldDisplay (lines 122–144) to determine whether to display each record in the file. The while loop obtains each record by repeatedly calling StreamReader method ReadLine (line 83) and splitting the text into tokens that are used to initialize object record (lines 89–94). Line 86 determines whether the file-position pointer has reached the end of the file. If so, the program returns from method getBalances_Click (line 87).
18.7 Serialization Section 18.5 demonstrated how to write the individual fields of a Record object to a text file, and Section 18.6 demonstrated how to read those fields from a file and place their values in a Record object in memory. In the examples, Record was used to aggregate the information for one record. When the instance variables for a Record were output to a disk file, certain information was lost, such as the type of each value. For instance, if the value "3" is read from a file, there is no way to tell if the value came from an int, a string or a decimal. We have only data, not type information, on disk. If the program that is going to read this data “knows” what object type the data corresponds to, then the data can be read directly into objects of that type. For example, in Fig. 18.9, we know that we are inputting an int (the account number), followed by two strings (the first and last name) and a decimal (the balance). We also know that these values are separated by commas, with only one record on each line. So, we are able to parse the strings and convert the account number to an int and the balance to a decimal. Sometimes it would be easier to read or write entire objects. C# provides such a mechanism, called object serialization. A serialized object is an object represented as a sequence of bytes that includes the object’s data, as well as information about the object’s type and the types of data stored in the object. After a serialized object has been written to a file, it can be read from the file and deserialized—that is, the type information and bytes that represent the object and its data can be used to recreate the object in memory. Class BinaryFormatter (namespace System.Runtime.Serialization.Formatters. Binary) enables entire objects to be written to or read from a stream. BinaryFormatter method Serialize writes an object’s representation to a file. BinaryFormatter method Deserialize reads this representation from a file and reconstructs the original object. Both methods throw a SerializationException if an error occurs during serialization or deserialization. Both methods require a Stream object (e.g., the FileStream) as a parameter so that the BinaryFormatter can access the correct stream. As you will see in Chapter 23, Networking: Streams-Based Sockets and Datagrams, serialization can be used to transmit objects between applications over a network. In Sections 18.8–18.9, we create and manipulate sequential-access files using object serialization. Object serialization is performed with byte-based streams, so the sequential files created and manipulated will be binary files. Binary files are not human readable. For this reason, we write a separate application that reads and displays serialized objects.
742
Chapter 18
Files and Streams
18.8 Creating a Sequential-Access File Using Object Serialization We begin by creating and writing serialized objects to a sequential-access file. In this section, we reuse much of the code from Section 18.5, so we focus only on the new features.
Defining the RecordSerializable Class Let us begin by modifying our Record class (Fig. 18.8) so that objects of this class can be serialized. Class RecordSerializable (Fig. 18.13) is marked with the [Serializable] attribute (line 5), which indicates to the CLR that objects of class Record can be serialized. The classes for objects that we wish to write to or read from a stream must include this attribute in their declarations or must implement interface ISerializable. Class RecordSerializable contains private data members account, firstName, lastName and balance. This class also provides public properties for accessing the private fields. In a class that is marked with the [Serializable] attribute or that implements interface ISerializable, you must ensure that every instance variable of the class is also serializable. All simple-type variables and strings are serializable. For variables of reference types, you must check the class declaration (and possibly its base classes) to ensure that the type is serializable. By default, array objects are serializable. However, if the array contains references to other objects, those objects may or may not be serializable. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
// Fig. 18.13: RecordSerializable.cs // Serializable class that represents a data record. using System; [ Serializable ] public class RecordSerializable { private int account; private string firstName; private string lastName; private decimal balance; // parameterless constructor sets members to default values public RecordSerializable() : this( 0, "", "", 0.0M ) { } // end constructor // overloaded constructor sets members to parameter values public RecordSerializable( int accountValue, string firstNameValue, string lastNameValue, decimal balanceValue ) { Account = accountValue; FirstName = firstNameValue; LastName = lastNameValue; Balance = balanceValue; } // end constructor
Fig. 18.13 |
RecordSerializable
class for serializable objects. (Part 1 of 2.)
18.8 Creating a Sequential-Access File Using Object Serialization
// property that gets and sets Account public int Account { get { return account; } // end get set { account = value; } // end set } // end property Account // property that gets and sets FirstName public string FirstName { get { return firstName; } // end get set { firstName = value; } // end set } // end property FirstName // property that gets and sets LastName public string LastName { get { return lastName; } // end get set { lastName = value; } // end set } // end property LastName // property that gets and sets Balance public decimal Balance { get { return balance; } // end get set { balance = value; } // end set } // end property Balance } // end class RecordSerializable
Fig. 18.13 |
RecordSerializable
class for serializable objects. (Part 2 of 2.)
743
744
Chapter 18
Files and Streams
Using Serialization Stream to Create an Output File Now let’s create a sequential-access file with serialization (Fig. 18.14). Line 13 creates a BinaryFormatter for writing serialized objects. Lines 48–49 open the FileStream to which this program writes the serialized objects. The string argument that is passed to the FileStream’s constructor represents the name and path of the file to be opened. This specifies the file to which the serialized objects will be written.
Common Programming Error 18.2 It is a logic error to open an existing file for output when the user wishes to preserve the file. The original file’s contents will be lost. 18.2
This program assumes that data is input correctly and in the proper record-number order. Event handler enterButton_Click (lines 66–119) performs the write operation. Line 72 creates a RecordSerializable object, which is assigned values in lines 88–92. Line 95 calls method Serialize to write the RecordSerializable object to the output file. Method Serialize takes the FileStream object as the first argument so that the BinaryFormatter can write its second argument to the correct file. Note that only one statement is required to write the entire object. In the sample execution for the program in Fig. 18.14, we entered information for five accounts—the same information shown in Fig. 18.10. The program does not show how the data records actually appear in the file. Remember that we are now using binary files, which are not human readable. To verify that the file was created successfully, the next section presents a program to read the file’s contents. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
// Fig 18.14: CreateFileForm.cs // Creating a sequential-access file using serialization. using System; using System.Windows.Forms; using System.IO; using System.Runtime.Serialization.Formatters.Binary; using System.Runtime.Serialization; using BankLibrary; public partial class CreateFileForm : BankUIForm { // object for serializing Records in binary format private BinaryFormatter formatter = new BinaryFormatter(); private FileStream output; // stream for writing to a file // parameterless constructor public CreateFileForm() { InitializeComponent(); } // end constructor // handler for saveButton_Click private void saveButton_Click( object sender, EventArgs e ) {
Fig. 18.14 | Sequential file created using serialization. (Part 1 of 5.)
18.8 Creating a Sequential-Access File Using Object Serialization
// create dialog box enabling user to save file SaveFileDialog fileChooser = new SaveFileDialog(); DialogResult result = fileChooser.ShowDialog(); string fileName; // name of file to save data fileChooser.CheckFileExists = false; // allow user to create file // exit event handler if user clicked "Cancel" if ( result == DialogResult.Cancel ) return; fileName = fileChooser.FileName; // get specified file name // show error if user specified invalid file if ( fileName == "" || fileName == null ) MessageBox.Show( "Invlaid File Name", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error ); else { // save file via FileStream if user specified valid file try { // open file with write access output = new FileStream( fileName, FileMode.OpenOrCreate, FileAccess.Write ); // disable Save button and enable Enter button saveButton.Enabled = false; enterButton.Enabled = true; } // end try // handle exception if there is a problem opening the file catch ( IOException ) { // notify user if file does not exist MessageBox.Show( "Error opening file", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error ); } // end catch } // end else } // end method saveButton_Click // handler for enterButton Click private void enterButton_Click( object sender, EventArgs e ) { // store TextBox values string array string[] values = GetTextBoxValues(); // Record containing TextBox values to serialize RecordSerializable record = new RecordSerializable(); // determine whether TextBox account field is empty if ( values[ ( int ) TextBoxIndices.ACCOUNT ] != "" ) {
Fig. 18.14 | Sequential file created using serialization. (Part 2 of 5.)
// store TextBox values in Record and serialize Record try { // get account number value from TextBox int accountNumber = Int32.Parse( values[ ( int ) TextBoxIndices.ACCOUNT ] ); // determine whether accountNumber is valid if ( accountNumber > 0 ) { // store TextBox fields in Record record.Account = accountNumber; record.FirstName = values[ ( int ) TextBoxIndices.FIRST ]; record.LastName = values[ ( int ) TextBoxIndices.LAST ]; record.Balance = Decimal.Parse( values[ ( int ) TextBoxIndices.BALANCE ] ); // write Record to FileStream ( serialize object ) formatter.Serialize( output, record ); } // end if else { // notify user if invalid account number MessageBox.Show( "Invalid Account Number", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error ); } // end else } // end try // notify user if error occurs in serialization catch ( SerializationException ) { MessageBox.Show( "Error Writing to File", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error ); } // end catch // notify user if error occurs regarding parameter format catch ( FormatException ) { MessageBox.Show( "Invalid Format", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error ); } // end catch } // end if ClearTextBoxes(); // clear TextBox values } // end method enterButton_Click // handler for exitButton Click private void exitButton_Click( object sender, EventArgs e ) { // determine whether file exists if ( output != null ) { // close file try {
Fig. 18.14 | Sequential file created using serialization. (Part 3 of 5.)
18.8 Creating a Sequential-Access File Using Object Serialization
130 output.Close(); 131 } // end try 132 // notify user of error closing file 133 catch ( IOException ) 134 { 135 MessageBox.Show( "Cannot close file", "Error", 136 MessageBoxButtons.OK, MessageBoxIcon.Error ); 137 } // end catch 138 } // end if 139 140 Application.Exit(); 141 } // end method exitButton_Click 142 } // end class CreateFileForm (a)
BankUI graphical
user interface
(b) SaveFileDialog
Files and directories
Fig. 18.14 | Sequential file created using serialization. (Part 4 of 5.)
747
748
Chapter 18
Files and Streams
(c)
(d)
(e)
(f)
(g)
Fig. 18.14 | Sequential file created using serialization. (Part 5 of 5.)
18.9 Reading and Deserializing Data from a SequentialAccess Text File The preceding section showed how to create a sequential-access file using object serialization. In this section, we discuss how to read serialized objects sequentially from a file. Figure 18.15 reads and displays the contents of the file created by the program in Fig. 18.14. Line 13 creates the BinaryFormatter that will be used to read objects. The program opens the file for input by creating a FileStream object (lines 44–45). The name of the file to open is specified as the first argument to the FileStream constructor.
18.9 Reading and Deserializing Data from a Sequential-Access Text File
// Fig. 18.15: ReadSequentialAccessFileForm.cs // Reading a sequential-access file using deserialization. using System; using System.Windows.Forms; using System.IO; using System.Runtime.Serialization.Formatters.Binary; using System.Runtime.Serialization; using BankLibrary; public partial class ReadSequentialAccessFileForm : BankUIForm { // object for deserializing Record in binary format private BinaryFormatter reader = new BinaryFormatter(); private FileStream input; // stream for reading from a file // parameterless constructor public ReadSequentialAccessFileForm() { InitializeComponent(); } // end constructor // invoked when user clicks Open button private void openButton_Click( object sender, EventArgs e ) { // create dialog box enabling user to open file OpenFileDialog fileChooser = new OpenFileDialog(); DialogResult result = fileChooser.ShowDialog(); string fileName; // name of file containing data // exit event handler if user clicked Cancel if ( result == DialogResult.Cancel ) return; fileName = fileChooser.FileName; // get specified file name ClearTextBoxes(); // show error if user specified invalid file if ( fileName == "" || fileName == null ) MessageBox.Show( "Invalid File Name", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error ); else { // create FileStream to obtain read access to file input = new FileStream( fileName, FileMode.Open, FileAccess.Read ); openButton.Enabled = false; // disable Open File button nextButton.Enabled = true; // enable Next Record button } // end else } // end method openButton_Click
Fig. 18.15 | Sequential file read using deserialzation. (Part 1 of 4.)
// invoked when user clicks Next button private void nextButton_Click( object sender, EventArgs e ) { // deserialize Record and store data in TextBoxes try { // get next RecordSerializable available in file RecordSerializable record = ( RecordSerializable ) reader.Deserialize( input ); // store Record values in temporary string array string[] values = new string[] { record.Account.ToString(), record.FirstName.ToString(), record.LastName.ToString(), record.Balance.ToString() }; // copy string array values to TextBox values SetTextBoxValues( values ); } // end try // handle exception when there are no Records in file catch( SerializationException ) { input.Close(); // close FileStream if no Records in file openButton.Enabled = true; // enable Open File button nextButton.Enabled = false; // disable Next Record button ClearTextBoxes(); // notify user if no Records in file MessageBox.Show( "No more records in file", "", MessageBoxButtons.OK, MessageBoxIcon.Information ); } // end catch } // end method nextButton_Click } // end class readSequentialAccessFileForm
Fig. 18.15 | Sequential file read using deserialzation. (Part 2 of 4.)
18.9 Reading and Deserializing Data from a Sequential-Access Text File
Fig. 18.15 | Sequential file read using deserialzation. (Part 3 of 4.)
751
752
Chapter 18
Files and Streams
Fig. 18.15 | Sequential file read using deserialzation. (Part 4 of 4.) The program reads objects from a file in event handler nextButton_Click (lines 53– 85). We use method Deserialize (of the BinaryFormatter created in line 13) to read the data (lines 59–60). Note that we cast the result of Deserialize to type RecordSerializable (line 60)—this cast is necessary, because Deserialize returns a reference of type object and we need to access properties that belong to class RecordSerializable. If an error occurs during deserialization, a SerializationException is thrown, and the FileStream object is closed (line 75).
18.10 Wrap-Up In this chapter, you learned how to use file processing to manipulate persistent data. You learned that data is stored in computers as 0s and 1s, and that combinations of these values are used to form bytes, fields, records and eventually files. We overviewed the differences between character-based and byte-based streams, as well as several file-processing classes from the System.IO namespace. You used class File to manipulate files and class Directory to manipulate directories. Next, you learned how to use sequential-access file processing to manipulate records in text files. We then discussed the differences between text-file processing and object serialization, and used serialization to store entire objects in and retrieve entire objects from files. In the next chapter, we present Extensible Markup Language (XML)—a widely supported technology for describing data. Using XML, we can describe any type of data, such as mathematical formulas, music and financial reports. We’ll demonstrate how to describe data with XML and how to write programs that can process XML encoded data.
19 Extensible Markup Language (XML) Knowing trees, I understand the meaning of patience. Knowing grass, I can appreciate persistence. —Hal Borland
Like everything metaphysical, the harmony between thought and reality is to be found in the grammar of the language.
OBJECTIVES In this chapter you will learn: I
To mark up data using XML.
I
How XML namespaces help provide unique XML element and attribute names.
I
To create DTDs and schemas for specifying and validating the structure of an XML document.
I
To create and use simple XSL style sheets to render XML document data.
I
To retrieve and modify XML data programmatically using .NET Framework classes.
I
To validate XML documents against schemas using class XmlReader.
I
To transform XML documents into XHTML using class XslCompiledTransform.
—Ludwig Wittgenstein
I played with an idea, and grew willful; tossed it into the air; transformed it; let it escape and recaptured it; made it iridescent with fancy, and winged it with paradox. —Oscar Wilde
Introduction XML Basics Structuring Data XML Namespaces Document Type Definitions (DTDs) W3C XML Schema Documents (Optional) Extensible Stylesheet Language and XSL Transformations (Optional) Document Object Model (DOM) (Optional) Schema Validation with Class XmlReader (Optional) XSLT with Class XslCompiledTransform Wrap-Up Web Resources
19.1 Introduction The Extensible Markup Language (XML) was developed in 1996 by the World Wide Web Consortium’s (W3C’s) XML Working Group. XML is a widely supported open technology (i.e., nonproprietary technology) for describing data that has become the standard format for data exchanged between applications over the Internet. The .NET Framework uses XML extensively. The Framework Class Library provides an extensive set of XML-related classes, and much of Visual Studio’s internal implementation also employs XML. Sections 19.2–19.6 introduce XML and XML-related technologies—XML namespaces for providing unique XML element and attribute names, and Document Type Definitions (DTDs) and XML Schemas for validating XML documents. These sections are required to support the use of XML in Chapters 20–22. Sections 19.7– 19.10 present additional XML technologies and key .NET Framework classes for creating and manipulating XML documents programmatically—this material is optional but recommended for readers who plan to employ XML in their own C# applications.
19.2 XML Basics XML permits document authors to create markup (i.e., a text-based notation for describing data) for virtually any type of information. This enables document authors to create entirely new markup languages for describing any type of data, such as mathematical formulas, software-configuration instructions, chemical molecular structures, music, news, recipes and financial reports. XML describes data in a way that both human beings and computers can understand. Figure 19.1 is a simple XML document that describes information for a baseball player. We focus on lines 5–11 to introduce basic XML syntax. You will learn about the other elements of this document in Section 19.3. XML documents contain text that represents content (i.e., data), such as John (line 6 of Fig. 19.1), and elements that specify the document’s structure, such as firstName (line 6 of Fig. 19.1). XML documents delimit elements with start tags and end tags. A start tag
19.2 XML Basics
1 2 3 4 5 6 7 8 9 10 11
755
John Doe 0.375
Fig. 19.1 | XML that describes a baseball player’s information. consists of the element name in angle brackets (e.g., and in lines 5 and 6, respectively). An end tag consists of the element name preceded by a forward slash (/) in angle brackets (e.g., and in lines 6 and 11, respectively). An element’s start and end tags enclose text that represents a piece of data (e.g., the firstName of the player—John—in line 6, which is enclosed by the start tag and end tag). Every XML document must have exactly one root element that contains all the other elements. In Fig. 19.1, player (lines 5–11) is the root element. Some XML-based markup languages include XHTML (Extensible HyperText Markup Language—HTML’s replacement for marking up Web content), MathML (for mathematics), VoiceXML™ (for speech), CML (Chemical Markup Language—for chemistry) and XBRL (Extensible Business Reporting Language—for financial data exchange). These markup languages are called XML vocabularies and provide a means for describing particular types of data in standardized, structured ways. Massive amounts of data are currently stored on the Internet in a variety of formats (e.g., databases, Web pages, text files). Based on current trends, it is likely that much of this data, especially that which is passed between systems, will soon take the form of XML. Organizations see XML as the future of data encoding. Information technology groups are planning ways to integrate XML into their systems. Industry groups are developing custom XML vocabularies for most major industries that will allow computer-based business applications to communicate in common languages. For example, Web services, which we discuss in Chapter 22, allow Web-based applications to exchange data seamlessly through standard protocols based on XML. The next generation of the Internet and World Wide Web will almost certainly be built on a foundation of XML, which will permit the development of more sophisticated Web-based applications. As is discussed in this chapter, XML allows you to assign meaning to what would otherwise be random pieces of data. As a result, programs can “understand” the data they manipulate. For example, a Web browser might view a street address listed on a simple HTML Web page as a string of characters without any real meaning. In an XML document, however, this data can be clearly identified (i.e., marked up) as an address. A program that uses the document can recognize this data as an address and provide links to a map of that location, driving directions from that location or other location-specific information. Likewise, an application can recognize names of people, dates, ISBN numbers and any other type of XML-encoded data. Based on this data, the
756
Chapter 19
Extensible Markup Language (XML)
application can present users with other related information, providing a richer, more meaningful user experience.
Viewing and Modifying XML Documents XML documents are highly portable. Viewing or modifying an XML document—which is a text file that ends with the .xml filename extension—does not require special software, although many software tools exist, and new ones are frequently released that make it more convenient to develop XML-based applications. Any text editor that supports ASCII/Unicode characters can open XML documents for viewing and editing. Also, most Web browsers can display XML documents in a formatted manner that makes it easier to see the XML’s structure. We demonstrate this using Internet Explorer in Section 19.3. One important characteristic of XML is that it is both human readable and machine readable. Processing XML Documents Processing an XML document requires software called an XML parser (or XML processor). A parser makes the document’s data available to applications. While reading the contents of an XML document, a parser checks that the document follows the syntax rules specified by the W3C’s XML Recommendation (www.w3.org/XML). XML syntax requires a single root element, a start tag and end tag for each element, and properly nested tags (i.e., the end tag for a nested element must appear before the end tag of the enclosing element). Furthermore, XML is case sensitive, so the proper capitalization must be used in elements. A document that conforms to this syntax is a well-formed XML document, and is syntactically correct. We present fundamental XML syntax in Section 19.3. If an XML parser can process an XML document successfully, that XML document is well formed. Parsers can provide access to XML-encoded data in well-formed documents only. Often, XML parsers are built into software such as Visual Studio or available for download over the Internet. Popular parsers include Microsoft XML Core Services (MSXML), the Apache Software Foundation’s Xerces (xml.apache.org) and the opensource Expat XML Parser (expat.sourceforge.net). In this chapter, we use MSXML. Validating XML Documents An XML document can optionally reference a Document Type Definition (DTD) or a schema that defines the proper structure of the XML document. When an XML document references a DTD or a schema, some parsers (called validating parsers) can read the DTD/schema and check that the XML document follows the structure defined by the DTD/schema. If the XML document conforms to the DTD/schema (i.e., the document has the appropriate structure), the XML document is valid. For example, if in Fig. 19.1 we were referencing a DTD that specifies that a player element must have firstName, lastName and battingAverage elements, then omitting the lastName element (line 8 in Fig. 19.1) would cause the XML document player.xml to be invalid. However, the XML document would still be well formed, because it follows proper XML syntax (i.e., it has one root element, and each element has a start tag and an end tag). By definition, a valid XML document is well formed. Parsers that cannot check for document conformity against DTDs/schemas are nonvalidating parsers—they determine only whether an XML document is well formed, not whether it is valid. We discuss validation, DTDs and schemas, as well as the key differences between these two types of structural specifications, in Sections 19.5–19.6. For now, note that
19.3 Structuring Data
757
schemas are XML documents themselves, whereas DTDs are not. As you will learn in Section 19.6, this difference presents several advantages in using schemas over DTDs.
Software Engineering Observation 19.1 DTDs and schemas are essential for business-to-business (B2B) transactions and mission-critical systems. Validating XML documents ensures that disparate systems can manipulate data structured in standardized ways and prevents errors caused by missing or malformed data. 19.1
Formatting and Manipulating XML Documents XML documents contain only data, not formatting instructions, so applications that process XML documents must decide how to manipulate or display each document’s data. For example, a PDA (personal digital assistant) may render an XML document differently than a wireless phone or a desktop computer. You can use Extensible Stylesheet Language (XSL) to specify rendering instructions for different platforms. We discuss XSL in Section 19.7. XML-processing programs can also search, sort and manipulate XML data using technologies such as XSL. Some other XML-related technologies are XPath (XML Path Language—a language for accessing parts of an XML document), XSL-FO (XSL Formatting Objects—an XML vocabulary used to describe document formatting) and XSLT (XSL Transformations—a language for transforming XML documents into other documents). We present XSLT in Section 19.7. We also introduce XPath in Section 19.7, then discuss it in greater detail in Section 19.8.
19.3 Structuring Data In this section and throughout this chapter, we create our own XML markup. XML allows you to describe data precisely in a well-structured format.
XML Markup for an Article In Fig. 19.2, we present an XML document that marks up a simple article using XML. The line numbers shown are for reference only and are not part of the XML document. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
Simple XML May 5, 2005 John Doe XML is pretty easy.
Fig. 19.2 | XML used to mark up an article. (Part 1 of 2.)
758 17 18 19 20
Chapter 19
Extensible Markup Language (XML)
In this chapter, we present a wide variety of examples that use XML.
Fig. 19.2 | XML used to mark up an article. (Part 2 of 2.) This document begins with an XML declaration (line 1), which identifies the document as an XML document. The version attribute specifies the XML version to which the document conforms. The current XML standard is version 1.0. Though the W3C released a version 1.1 specification in February 2004, this newer version is not yet widely supported. The W3C may continue to release new versions as XML evolves to meet the requirements of different fields.
Portability Tip 19.1 Documents should include the XML declaration to identify the version of XML used. A document that lacks an XML declaration might be assumed to conform to the latest version of XML—when it does not, errors could result. 19.1
Common Programming Error 19.1 Placing whitespace characters before the XML declaration is an error.
19.1
XML comments (lines 2–3), which begin with , can be placed almost anywhere in an XML document. XML comments can span to multiple lines—an end marker on each line is not needed; the end marker can appear on a subsequent line as long as there is exactly one end marker (-->) for each begin marker ( Jane Doe Box 12345 15 Any Ave. Othertown Otherstate 67890 555-4321 John Doe 123 Main St. Anytown Anystate 12345 555-1234 Dear Sir: It is our privilege to inform you about our new database managed with XML. This new system allows you to reduce the load on your inventory list server by having the client machine perform the work of sorting and filtering the data. Please visit our Web site for availability and pricing. Sincerely, Ms. Jane Doe
Fig. 19.4 | Business letter marked up as XML.
Error-Prevention Tip 19.1 An XML document is not required to reference a DTD, but validating XML parsers can use a DTD to ensure that the document has the proper structure. 19.1
Portability Tip 19.2 Validating an XML document helps guarantee that independent developers will exchange data in a standardized form that conforms to the DTD. 19.2
762
Chapter 19
Extensible Markup Language (XML)
The DTD reference (line 5) contains three items, the name of the root element that the DTD specifies (letter); the keyword SYSTEM (which denotes an external DTD—a DTD declared in a separate file, as opposed to a DTD declared locally in the same file); and the DTD’s name and location (i.e., letter.dtd in the current directory). DTD document filenames typically end with the .dtd extension. We discuss DTDs and letter.dtd in detail in Section 19.5. Several tools (many of which are free) validate documents against DTDs and schemas (discussed in Section 19.5 and Section 19.6, respectively). Microsoft’s XML Validator is available free of charge from the Download Sample link at msdn.microsoft.com/archive/en-us/samples/internet/ xml/xml_validator/default.asp
This validator can validate XML documents against both DTDs and Schemas. To install it, run the downloaded executable file xml_validator.exe and follow the steps to complete the installation. Once the installation is successful, open the validate_js.htm file located in your XML Validator installation directory in IE to validate your XML documents. We installed the XML Validator at C:\XMLValidator (Fig. 19.5). The output (Fig. 19.6) shows the results of validating the document using Microsoft’s XML Validator. Visit www.w3.org/XML/Schema for a list of additional validation tools. Root element letter (lines 7–44 of Fig. 19.4) contains the child elements contact, contact, salutation, paragraph, paragraph, closing and signature. In addition to being placed between tags, data also can be placed in attributes—name-value pairs that appear within the angle brackets of start tags. Elements can have any number of attributes (separated by spaces) in their start tags. The first contact element (lines 8–17) has an attribute named type with attribute value "sender", which indicates that this contact element identifies the letter’s sender. The second contact element (lines 19–28) has attribute type with value "receiver", which indicates that this contact element identifies
Fig. 19.5 | Validating an XML document with Microsoft’s XML Validator.
19.3 Structuring Data
763
Fig. 19.6 | Validation result using Microsoft’s XML Validator. the letter’s recipient. Like element names, attribute names are case sensitive, can be any length, may contain letters, digits, underscores, hyphens and periods, and must begin with either a letter or an underscore character. A contact element stores various items of information about a contact, such as the contact’s name (represented by element name), address (represented by elements address1, address2, city, state and zip), phone number (represented by element phone) and gender (represented by attribute gender of element flag). Element salutation (line 30) marks up the letter’s salutation. Lines 32–40 mark up the letter’s body using two paragraph elements. Elements closing (line 42) and signature (line 43) mark up the closing sentence and the author’s “signature,” respectively.
Common Programming Error 19.6 Failure to enclose attribute values in double ("") or single ('') quotes is a syntax error.
19.6
Line 16 introduces the empty element flag. An empty element is one that does not contain any content. Instead, an empty element sometimes contains data in attributes. Empty element flag contains an attribute that indicates the gender of the contact (represented by the parent contact element). Document authors can close an empty element either by placing a slash immediately preceding the right angle bracket, as shown in line 16, or by explicitly writing an end tag, as in line 22
Note that the address2 element in line 22 is empty because there is no second part to this contact’s address. However, we must include this element to conform to the structural rules specified in the XML document’s DTD—letter.dtd (which we present in Section 19.5). This DTD specifies that each contact element must have an address2 child element (even if it is empty). In Section 19.5, you will learn how DTDs indicate that certain elements are required while others are optional.
764
Chapter 19
Extensible Markup Language (XML)
19.4 XML Namespaces XML allows document authors to create custom elements. This extensibility can result in naming collisions among elements in an XML document that each have the same name. For example, we may use the element book to mark up data about a Deitel publication. A stamp collector may use the element book to mark up data about a book of stamps. Using both of these elements in the same document could create a naming collision, making it difficult to determine which kind of data each element contains. An XML namespace is a collection of element and attribute names. Like C# namespaces, XML namespaces provide a means for document authors to unambiguously refer to elements with the same name (i.e., prevent collisions). For example, Math
and Cardiology
use element subject to mark up data. In the first case, the subject is something one studies in school, whereas in the second case, the subject is a field of medicine. Namespaces can differentiate these two subject elements. For example Math
and Cardiology
Both school and medical are namespace prefixes. A document author places a namespace prefix and colon (:) before an element name to specify the namespace to which that element belongs. Document authors can create their own namespace prefixes using virtually any name except the reserved namespace prefix xml. In the next subsections, we demonstrate how document authors ensure that namespaces are unique.
Common Programming Error 19.7 Attempting to create a namespace prefix named xml in any mixture of uppercase and lowercase letters is a syntax error—the xml namespace prefix is reserved for internal use by XML itself. 19.7
Differentiating Elements with Namespaces Figure 19.7 demonstrates namespaces. In this document, namespaces differentiate two distinct elements—the file element related to a text file and the file document related to an image file. Lines 6–7 use the XML-namespace reserved attribute xmlns to create two namespace prefixes—text and image. Each namespace prefix is bound to a series of characters called a Uniform Resource Identifier (URI) that uniquely identifies the namespace. Document authors create their own namespace prefixes and URIs. A URI is a way to identifying a resource, typically on the Internet. Two popular types of URI are Uniform Resource Name (URN) and Uniform Resource Locator (URL). To ensure that namespaces are unique, document authors must provide unique URIs. In this example, we use the text urn:deitel:textInfo and urn:deitel:imageInfo as
19.4 XML Namespaces
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
765
A book list A funny picture
Fig. 19.7 | XML namespaces demonstration. URIs. These URIs employ the URN scheme frequently used to identify namespaces. Under this naming scheme, a URI begins with “urn:”, followed by a unique series of additional names separated by colons. Another common practice is to use URLs, which specify the location of a file or a resource on the Internet. For example, www.deitel.com is the URL that identifies the home page of the Deitel & Associates Web site. Using URLs guarantees that the namespaces are unique because the domain names (e.g., www.deitel.com) are guaranteed to be unique. For example, lines 5–7 could be rewritten as
where URLs related to the Deitel & Associates, Inc. domain name serve as URIs to identify the text and image namespaces. The parser does not visit these URLs, nor do these URLs need to refer to actual Web pages. They each simply represent a unique series of
766
Chapter 19
Extensible Markup Language (XML)
characters used to differentiate URI names. In fact, any string can represent a namespace. For example, our image namespace URI could be hgjfkdlsa4556, in which case our prefix assignment would be xmlns:image = "hgjfkdlsa4556"
Lines 9–11 use the text namespace prefix for elements file and description. Note that the end tags must also specify the namespace prefix text. Lines 13–16 apply namespace prefix image to the elements file, description and size. Note that attributes do not require namespace prefixes (although they can have them), because each attribute is already part of an element that specifies the namespace prefix. For example, attribute filename (line 9) is implicitly part of namespace text because its element (i.e., file) specifies the text namespace prefix.
Specifying a Default Namespace To eliminate the need to place namespace prefixes in each element, document authors may specify a default namespace for an element and its children. Figure 19.8 demonstrates using a default namespace (urn:deitel:textInfo) for element directory. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
A book list A funny picture
Fig. 19.8 | Default namespace demonstration.
19.5 Document Type Definitions (DTDs)
767
Line 5 defines a default namespace using attribute xmlns with a URI as its value. Once we define this default namespace, child elements belonging to the namespace need not be qualified by a namespace prefix. Thus, element file (lines 8–10) is in the default namespace urn:deitel:textInfo. Compare this to lines 8–10 of Fig. 19.7, where we had to prefix the file and description element names with the namespace prefix text. The default namespace applies to the directory element and all elements that are not qualified with a namespace prefix. However, we can use a namespace prefix to specify a different namespace for particular elements. For example, the file element in lines 12– 15 includes the image namespace prefix, indicating that this element is in the urn:deitel:imageInfo namespace, not the default namespace.
Namespaces in XML Vocabularies XML-based languages, such as XML Schema (Section 19.6), Extensible Stylesheet Language (XSL) (Section 19.7) and BizTalk (www.microsoft.com/biztalk), often use namespaces to identify their elements. Each of these vocabularies defines special-purpose elements that are grouped in namespaces. These namespaces help prevent naming collisions between predefined elements and user-defined elements.
19.5 Document Type Definitions (DTDs) Document Type Definitions (DTDs) are one of two main types of documents you can use to specify XML document structure. Section 19.6 presents W3C XML Schema documents, which provide an improved method of specifying XML document structure.
Software Engineering Observation 19.2 XML documents can have many different structures, and for this reason an application cannot be certain whether a particular document it receives is complete, ordered properly, and not missing data. DTDs and schemas (Section 19.6) solve this problem by providing an extensible way to describe XML document structure. Applications should use DTDs or schemas to confirm whether XML documents are valid. 19.2
Software Engineering Observation 19.3 Many organizations and individuals are creating DTDs and schemas for a broad range of applications. These collections—called repositories—are available free for download from the Web (e.g., www.xml.org, www.oasis-open.org). 19.3
Creating a Document Type Definition Figure 19.4 presented a simple business letter marked up with XML. Recall that line 5 of letter.xml references a DTD—letter.dtd (Fig. 19.9). This DTS specifies the business letter’s element types and attributes, and their relationships to one another. A DTD describes the structure of an XML document and enables an XML parser to verify whether an XML document is valid (i.e., whether its elements contain the proper attributes and appear in the proper sequence). DTDs allow users to check document structure and to exchange data in a standardized format. A DTD expresses the set of rules for document structure using an EBNF (Extended Backus-Naur Form) grammar. [Note: EBNF grammars are commonly used to define programming languages. For more information on EBNF grammars, please see en.wikipedia.org/wiki/EBNF or www.garshol.priv.no/download/text/bnf.html.]
Fig. 19.9 | Document Type Definition (DTD) for a business letter.
Common Programming Error 19.8 For documents validated with DTDs, any document that uses elements, attributes or relationships not explicitly defined by a DTD is an invalid document. 19.8
Defining Elements in a DTD The ELEMENT element type declaration in lines 4–5 defines the rules for element letter. In this case, letter contains one or more contact elements, one salutation element, one or more paragraph elements, one closing element and one signature element, in that sequence. The plus sign (+) occurrence indicator specifies that the DTD allows one or more occurrences of an element. Other occurrence indicators include the asterisk (*), which indicates an optional element that can occur zero or more times, and the question mark (?), which indicates an optional element that can occur at most once (i.e., zero or one occurrence). If an element does not have an occurrence indicator, the DTD allows exactly one occurrence. The contact element type declaration (lines 7–8) specifies that a contact element contains child elements name, address1, address2, city, state, zip, phone and flag— in that order. The DTD requires exactly one occurrence of each of these elements. Defining Attributes in a DTD Line 9 uses the ATTLIST attribute-list declaration to define an attribute named type for the contact element. Keyword #IMPLIED specifies that if the parser finds a contact element without a type attribute, the parser can choose an arbitrary value for the attribute or can ignore the attribute. Either way the document will still be valid (if the rest of the doc-
19.5 Document Type Definitions (DTDs)
769
ument is valid)—a missing type attribute will not invalidate the document. Other keywords that can be used in place of #IMPLIED in an ATTLIST declaration include #REQUIRED and #FIXED. Keyword #REQUIRED specifies that the attribute must be present in the element, and keyword #FIXED specifies that the attribute (if present) must have the given fixed value. For example,
indicates that attribute zip (if present in element address) must have the value 01757 for the document to be valid. If the attribute is not present, then the parser, by default, uses the fixed value that the ATTLIST declaration specifies.
Character Data vs. Parsed Character Data Keyword CDATA (line 9) specifies that attribute type contains character data (i.e., a string). A parser will pass such data to an application without modification.
Software Engineering Observation 19.4 DTD syntax does not provide a mechanism for describing an element’s (or attribute’s) data type. For example, a DTD cannot specify that a particular element or attribute can contain only integer data. 19.4
Keyword #PCDATA (line 11) specifies that an element (e.g., name) may contain parsed character data (i.e., data that is processed by an XML parser). Elements with parsed character data cannot contain markup characters, such as less than () or ampersand (&). The document author should replace any markup character in a #PCDATA element with the character’s corresponding character entity reference. For example, the character entity reference < should be used in place of the less-than symbol (). A document author who wishes to use a literal ampersand should use the entity reference & instead—parsed character data can contain ampersands (&) only for inserting entities. See Appendix H for a list of other character entity references.
Common Programming Error 19.9 Using markup characters (e.g., and &) in parsed character data is an error. Use character entity references (e.g., <, > and & instead). 19.9
Defining Empty Elements in a DTD Line 18 defines an empty element named flag. Keyword EMPTY specifies that the element does not contain any data between its start and end tags. Empty elements commonly describe data via attributes. For example, flag’s data appears in its gender attribute (line 19). Line 19 specifies that the gender attribute’s value must be one of the enumerated values (M or F) enclosed in parentheses and delimited by a vertical bar (|) meaning “or.” Note that line 19 also indicates that gender has a default value of M. Well-Formed Documents vs. Valid Documents In Section 19.3, we demonstrated how to use the Microsoft XML Validator to validate an XML document against its specified DTD. The validation revealed that the XML document letter.xml (Fig. 19.4) is well formed and valid—it conforms to letter.dtd (Fig. 19.9). Recall that a well-formed document is syntactically correct (i.e., each start tag
770
Chapter 19
Extensible Markup Language (XML)
has a corresponding end tag, the document contains only one root element, etc.), and a valid document contains the proper elements with the proper attributes in the proper sequence. An XML document cannot be valid unless it is well formed. When a document fails to conform to a DTD or a schema, the Microsoft XML Validator displays an error message. For example, the DTD in Fig. 19.9 indicates that a contact element must contain the child element name. A document that omits this child element is still well formed, but is not valid. In such a scenario, Microsoft XML Validator displays the error message shown in Fig. 19.10.
19.6 W3C XML Schema Documents In this section, we introduce schemas for specifying XML document structure and validating XML documents. Many developers in the XML community believe that DTDs are not flexible enough to meet today’s programming needs. For example, DTDs lack a way of indicating what specific type of data (e.g., numeric, text) an element can contain and DTDs are not themselves XML documents. These and other limitations have led to the development of schemas. Unlike DTDs, schemas do not use EBNF grammar. Instead, schemas use XML syntax and are actually XML documents that programs can manipulate. Like DTDs, schemas are used by validating parsers to validate documents. In this section, we focus on the W3C’s XML Schema vocabulary (note the capital “S” in “Schema”). We use the term XML Schema in the rest of the chapter whenever we refer to W3C’s XML Schema vocabulary. For the latest information on XML Schema, visit www.w3.org/XML/Schema. For tutorials on XML Schema concepts beyond what we present here, visit www.w3schools.com/schema/default.asp. A DTD describes an XML document’s structure, not the content of its elements. For example, 5
contains character data. If the document that contains element quantity references a DTD, an XML parser can validate the document to confirm that this element indeed does contain PCDATA content. However, the parser cannot validate that the content is numeric; DTDs do not provide this capability. So, unfortunately, the parser also considers hello
Fig. 19.10 | XML Validator displaying an error message.
19.6 W3C XML Schema Documents
771
to be valid. An application that uses the XML document containing this markup should test that the data in element quantity is numeric and take appropriate action if it is not. XML Schema enables schema authors to specify that element quantity’s data must be numeric or, even more specifically, an integer. A parser validating the XML document against this schema can determine that 5 conforms and hello does not. An XML document that conforms to a schema document is schema valid, and one that does not conform is schema invalid. Schemas are XML documents and therefore must themselves be valid.
Validating Against an XML Schema Document Figure 19.11 shows a schema-valid XML document named book.xml, and Fig. 19.12 shows the pertinent XML Schema document (book.xsd) that defines the structure for book.xml. By convention, schemas use the .xsd extension. We used an online XSD schema validator provided by Microsoft at apps.gotdotnet.com/xmltools/xsdvalidator
to ensure that the XML document in Fig. 19.11 conforms to the schema in Fig. 19.12. To validate the schema document itself (i.e., book.xsd) and produce the output shown in Fig. 19.12, we used an online XSV (XML Schema Validator) provided by the W3C at www.w3.org/2001/03/webdata/xsv
These tools are free and enforce the W3C’s specifications regarding XML Schemas and schema validation. Section 19.12 lists several online XML Schema validators. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
Visual Basic 2005 How to Program, 3/e Visual C# 2005 How to Program Java How to Program, 6/e C++ How to Program, 5/e Internet and World Wide Web How to Program, 3/e
Fig. 19.11 | Schema-valid XML document describing a list of books.
Fig. 19.12 | XML Schema document for book.xml. Figure 19.11 contains markup describing several Deitel books. The books element (line 5) has the namespace prefix deitel, indicating that the books element is a part of the http://www.deitel.com/booklist namespace. Note that we declare the namespace prefix deitel in line 5.
Creating an XML Schema Document Figure 19.12 presents the XML Schema document that specifies the structure of book.xml (Fig. 19.11). This document defines an XML-based language (i.e., a vocabulary) for writing XML documents about collections of books. The schema defines the elements, attributes and parent-child relationships that such a document can (or must) include. The schema also specifies the type of data that these elements and attributes may contain.
19.6 W3C XML Schema Documents
773
Root element schema (Fig. 19.12, lines 5–23) contains elements that define the structure of an XML document such as book.xml. Line 5 specifies as the default namespace the standard W3C XML Schema namespace URI—http://www.w3.org/2001/XMLSchema. This namespace contains predefined elements (e.g., root element schema) that comprise the XML Schema vocabulary—the language used to write an XML Schema document.
Portability Tip 19.3 W3C XML Schema authors specify URI http://www.w3.org/2001/XMLSchema when referring to the XML Schema namespace. This namespace contains predefined elements that comprise the XML Schema vocabulary. Specifying this URI ensures that validation tools correctly identify XML Schema elements and do not confuse them with those defined by document authors. 19.3
Line 6 binds the URI http://www.deitel.com/booklist to namespace prefix deitel. As we discuss momentarily, the schema uses this namespace to differentiate names created by us from names that are part of the XML Schema namespace. Line 7 also specifies http://www.deitel.com/booklist as the targetNamespace of the schema. This attribute identifies the namespace of the XML vocabulary that this schema defines. Note that the targetNamespace of book.xsd is the same as the namespace referenced in line 5 of book.xml (Fig. 19.11). This is what “connects” the XML document with the schema that defines its structure. When an XML schema validator examines book.xml and book.xsd, it will recognize that book.xml uses elements and attributes from the http:// www.deitel.com/booklist namespace. The validator also will recognize that this namespace is the namespace defined in book.xsd (i.e., the schema’s targetNamespace). Thus the validator knows where to look for the structural rules for the elements and attributes used in book.xml.
Defining an Element in XML Schema In XML Schema, the element tag (line 9) defines an element to be included in an XML document that conforms to the schema. In other words, element specifies the actual elements that can be used to mark up data. Line 9 defines the books element, which we use as the root element in book.xml (Fig. 19.11). Attributes name and type specify the element’s name and data type, respectively. An element’s data type indicates the data that the element may contain. Possible data types include XML Schema-defined types (e.g., string, double) and user-defined types (e.g., BooksType, which is defined in lines 11–16). Figure 19.13 lists several of XML Schema’s many built-in types. For a complete list of built-in types, see Section 3 of the specification found at www.w3.org/TR/xmlschema-2. In this example, books is defined as an element of data type deitel:BooksType (line 9). BooksType is a user-defined type (lines 11–16) in the http://www.deitel.com/ booklist namespace and therefore must have the namespace prefix deitel. It is not an existing XML Schema data type. Two categories of data type exist in XML Schema—simple types and complex types. Simple and complex types differ only in that simple types cannot contain attributes or child elements and complex types can. A user-defined type that contains attributes or child elements must be defined as a complex type. Lines 11–16 use element complexType to define BooksType as a complex type that has a child element named book. The sequence element (lines 12–15) allows you to specify the sequential order in which child elements must appear. The element (lines 13–14) nested within the complexType element indicates that a BooksType element (e.g.,
774
Chapter 19
Extensible Markup Language (XML)
XML Schema Data Type(s)
Description
string
A character string.
boolean
True or false.
true, false
true
decimal
A decimal numeral.
i * (10n),
where i is an integer and n is an integer that is less than or equal to zero.
5, -12, -45.78
float
A floating-point number.
m * (2e),
where m is an integer whose absolute value is less than 224 and e is an integer in the range -149 to 104. Plus three additional numbers: positive infinity, negative infinity and not-a-number (NaN).
0, 12, -109.375, NaN
double
A floating-point number.
m * (2e),
0, 12, -109.375, NaN
long
A whole number.
-9223372036854775808
Ranges or Structures
Examples "hello"
where m is an integer whose absolute value is less than 253 and e is an integer in the range -1075 to 970. Plus three additional numbers: positive infinity, negative infinity and not-a-number (NaN). to
9223372036854775807,
1234567890, -1234567890
inclusive int
A whole number.
-2147483648
to 2147483647,
inclusive
1234567890, -1234567890
short
A whole number.
-32768
date
A date consisting of a year, month and day.
yyyy-mm
with an optional dd and an optional time zone, where yyyy is four digits long and mm and dd are two digits long.
2005-05-10
time
A time consisting of hours, minutes and seconds.
hh:mm:ss
with an optional time zone, where hh, mm and ss are two digits long.
16:30:25-05:00
to 32767, inclusive
12, -345
Fig. 19.13 | Some XML Schema data types. books)
can contain child elements named book of type deitel:SingleBookType (defined in lines 18–22). Attribute minOccurs (line 14), with value 1, specifies that elements of type
19.6 W3C XML Schema Documents
775
BooksType must contain a minimum of one book element. Attribute maxOccurs (line 14), with value unbounded, specifies that elements of type BooksType may have any number of book child elements. Lines 18–22 define the complex type SingleBookType. An element of this type contains a child element named title. Line 20 defines element title to be of simple type string. Recall that elements of a simple type cannot contain attributes or child elements. The schema end tag (, line 23) declares the end of the XML Schema document.
A Closer Look at Types in XML Schema Every element in XML Schema has a type. Types include the built-in types provided by XML Schema (Fig. 19.13) or user-defined types (e.g., SingleBookType in Fig. 19.12). Every simple type defines a restriction on an XML Schema-defined type or a restriction on a user-defined type. Restrictions limit the possible values that an element can hold. Complex types are divided into two groups—those with simple content and those with complex content. Both can contain attributes, but only complex content can contain child elements. Complex types with simple content must extend or restrict some other existing type. Complex types with complex content do not have this limitation. We demonstrate complex types with each kind of content in the next example. The schema document in Fig. 19.14 creates both simple types and complex types. The XML document in Fig. 19.15 (laptop.xml) follows the structure defined in Fig. 19.14 to describe parts of a laptop computer. A document such as laptop.xml that conforms to a schema is known as an XML instance document—the document is an instance (i.e., example) of the schema. Line 5 declares the default namespace to be the standard XML Schema namespace— any elements without a prefix are assumed to be in the XML Schema namespace. Line 6 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
Fig. 19.14 | XML Schema document defining simple and complex types. (Part 1 of 2.)
776 23 24 25 26 27 28 29 30 31 32 33 34
Chapter 19
Extensible Markup Language (XML)
Fig. 19.14 | XML Schema document defining simple and complex types. (Part 2 of 2.) 1 2 3 4 5 6 7 8 9 10 11 12
Intel 17 2.4 256
Fig. 19.15 | XML document using the laptop element defined in computer.xsd. binds the namespace prefix computer to the namespace http://www.deitel.com/ computer. Line 7 identifies this namespace as the targetNamespace—the namespace being defined by the current XML Schema document. To design the XML elements for describing laptop computers, we first create a simple type in lines 9–13 using the simpleType element. We name this simpleType gigahertz because it will be used to describe the clock speed of the processor in gigahertz. Simple types are restrictions of a type typically called a base type. For this simpleType, line 10 declares the base type as decimal, and we restrict the value to be at least 2.1 by using the minInclusive element in line 11. Next, we declare a complexType named CPU that has simpleContent (lines 16–20). Remember that a complex type with simple content can have attributes but not child elements. Also recall that complex types with simple content must extend or restrict some XML Schema type or user-defined type. The extension element with attribute base (line 17) sets the base type to string. In this complexType, we extend the base type string with an attribute. The attribute element (line 18) gives the complexType an attribute of type string named model. Thus an element of type CPU must contain string text (because the base type is string) and may contain a model attribute that is also of type string. Lastly we define type portable, which is a complexType with complex content (lines 23–31). Such types are allowed to have child elements and attributes. The element all
19.7 (Optional) Extensible Stylesheet Language and XSL Transformations
777
(lines 24–29) encloses elements that must each be included once in the corresponding XML instance document. These elements can be included in any order. This complex type holds four elements—processor, monitor, CPUSpeed and RAM. They are given types CPU, int, gigahertz and int, respectively. When using types CPU and gigahertz, we must include the namespace prefix computer, because these user-defined types are part of the computer namespace (http://www.deitel.com/computer)—the namespace defined in the current document (line 7). Also, portable contains an attribute defined in line 30. The attribute element indicates that elements of type portable contain an attribute of type string named manufacturer. Line 33 declares the actual element that uses the three types defined in the schema. The element is called laptop and is of type portable. We must use the namespace prefix computer in front of portable. We have now created an element named laptop that contains child elements processor, monitor, CPUSpeed and RAM, and an attribute manufacturer. Figure 19.15 uses the laptop element defined in the computer.xsd schema. Once again, we used an online XSD schema validator (apps.gotdotnet.com/xmltools/xsdvalidator) to ensure that this XML instance document adheres to the schema’s structural rules. Line 5 declares namespace prefix computer. The laptop element requires this prefix because it is part of the http://www.deitel.com/computer namespace. Line 6 sets the laptop’s manufacturer attribute, and lines 8–11 use the elements defined in the schema to describe the laptop’s characteristics. In this section, we introduced W3C XML Schema documents for defining the structure of XML documents, and we validated XML instance documents against schemas using an online XSD schema validator. Section 19.9 demonstrates programmatically validating XML documents against schemas using .NET Framework classes. This allows you to ensure that a C# program manipulates only valid documents—manipulating an invalid document that is missing required pieces of data could cause errors in the program.
19.7 (Optional) Extensible Stylesheet Language and XSL Transformations Extensible Stylesheet Language (XSL) documents specify how programs are to render XML document data. XSL is a group of three technologies—XSL-FO (XSL Formatting Objects), XPath (XML Path Language) and XSLT (XSL Transformations). XSL-FO is a vocabulary for specifying formatting, and XPath is a string-based language of expressions used by XML and many of its related technologies for effectively and efficiently locating structures and data (such as specific elements and attributes) in XML documents. The third portion of XSL—XSL Transformations (XSLT)—is a technology for transforming XML documents into other documents—i.e., transforming the structure of the XML document data to another structure. XSLT provides elements that define rules for transforming one XML document to produce a different XML document. This is useful when you want to use data in multiple applications or on multiple platforms, each of which may be designed to work with documents written in a particular vocabulary. For example, XSLT allows you to convert a simple XML document to an XHTML (Extensible HyperText Markup Language) document that presents the XML document’s data (or a subset of the data) formatted for display in a Web browser. (See Fig. 19.16 for a sample “before” and “after” view of such a transformation.) XHTML is the W3C technical
778
Chapter 19
Extensible Markup Language (XML)
recommendation that replaces HTML for marking up Web content. For more information on XHTML, see Appendix F, Introduction to XHTML: Part 1 and Appendix G, Introduction to XHTML: Part 2, and visit www.w3.org. Transforming an XML document using XSLT involves two tree structures—the source tree (i.e., the XML document to be transformed) and the result tree (i.e., the XML document to be created). XPath is used to locate parts of the source tree document that match templates defined in an XSL style sheet. When a match occurs (i.e., a node matches a template), the matching template executes and adds its result to the result tree. When there are no more matches, XSLT has transformed the source tree into the result tree. The XSLT does not analyze every node of the source tree; it selectively navigates the source tree using XPath’s select and match attributes. For XSLT to function, the source tree must be properly structured. Schemas, DTDs and validating parsers can validate document structure before using XPath and XSLTs.
A Simple XSL Example Figure 19.16 lists an XML document that describes various sports. The output shows the result of the transformation (specified in the XSLT template of Fig. 19.17) rendered by Internet Explorer 6. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
Cricket More popular among commonwealth nations. Baseball More popular in America. Soccer (Futbol) Most popular sport in the world.
Fig. 19.16 | XML document that describes various sports. (Part 1 of 2.)
19.7 (Optional) Extensible Stylesheet Language and XSL Transformations
779
Fig. 19.16 | XML document that describes various sports. (Part 2 of 2.) To perform transformations, an XSLT processor is required. Popular XSLT processors include Microsoft’s MSXML and the Apache Software Foundation’s Xalan 2 (xml.apache.org). The XML document shown in Fig. 19.16 is transformed into an XHTML document by MSXML when the document is loaded in Internet Explorer. MSXML is both an XML parser and an XSLT processor. Line 2 (Fig. 19.16) is a processing instruction (PI) that references the XSL style sheet sports.xsl (Fig. 19.17). A processing instruction is embedded in an XML document and provides application-specific information to whichever XML processor the application uses. In this particular case, the processing instruction specifies the location of an XSLT document with which to transform the XML document. The characters (line 2, Fig. 19.16) delimit a processing instruction, which consists of a PI target (e.g., xml:stylesheet) and a PI value (e.g., type = "text/xsl" href = "sports.xsl"). The PI value’s type attribute specifies that sports.xsl is a text/xsl file (i.e., a text file containing XSL content). The href attribute specifies the name and location of the style sheet to apply—in this case, sports.xsl in the current directory.
Software Engineering Observation 19.5 XSL enables document authors to separate data presentation (specified in XSL documents) from data description (specified in XML documents). 19.5
Figure 19.17 shows the XSL document for transforming the structured data of the XML document of Fig. 19.16 into an XHTML document for presentation. By convention, XSL documents have the filename extension .xsl. Lines 6–7 begin the XSL style sheet with the stylesheet start tag. Attribute version specifies the XSLT version to which this document conforms. Line 7 binds namespace prefix xsl to the W3C’s XSLT URI (i.e., http://www.w3.org/1999/XSL/Transform). Lines 9–12 use element xsl:output to write an XHTML document type declaration (DOCTYPE) to the result tree (i.e., the XML document to be created). The DOCTYPE identifies XHTML as the type of the resulting document. Attribute method is assigned "xml", which indicates that XML is being output to the result tree. (Recall that XHTML is a type of XML.) Attribute omit-xml-declaration specifies whether the transformation should write the XML declaration to the result tree. In this case, we do not want to omit the XML declaration, so we assign to this attribute the value "no". Attributes doctype-system and doctype-public write the DOCTYPE DTD information to the result tree.
Fig. 19.17 | XSLT that creates elements and attributes in an XHTML document. XSLT uses templates (i.e., xsl:template elements) to describe how to transform particular nodes from the source tree to the result tree. A template is applied to nodes that are specified in the required match attribute. Line 14 uses the match attribute to select the document root (i.e., the conceptual part of the document that contains the root element and everything below it) of the XML source document (i.e., sports.xml). The XPath char-
19.7 (Optional) Extensible Stylesheet Language and XSL Transformations
781
acter / (a forward slash) always selects the document root. Recall that XPath is a stringbased language used to locate parts of an XML document easily. In XPath, a leading forward slash specifies that we are using absolute addressing (i.e., we are starting from the root and defining paths down the source tree). In the XML document of Fig. 19.16, the child nodes of the document root are the two processing instruction nodes (lines 1–2), the two comment nodes (lines 4–5) and the sports element node (lines 7–31). The template in Fig. 19.17, line 14, matches a node (i.e., the root node), so the contents of the template are now added to the result tree. The MSXML processor writes the XHTML in lines 16–29 (Fig. 19.17) to the result tree exactly as it appears in the XSL document. Now the result tree consists of the DOCTYPE definition and the XHTML code from lines 16–29. Lines 33–39 use element xsl:foreach to iterate through the source XML document, searching for game elements. The xsl:for-each element is similar to C#’s foreach statement. Attribute select is an XPath expression that specifies the nodes (called the node set) on which the xsl:for-each operates. Again, the first forward slash means that we are using absolute addressing. The forward slash between sports and game indicates that game is a child node of sports. Thus, the xsl:for-each finds game nodes that are children of the sports node. The XML document sports.xml contains only one sports node, which is also the document root node. After finding the elements that match the selection criteria, the xsl:for-each processes each element with the code in lines 34–38 (these lines produce one row in a table each time they execute) and places the result of lines 34–38 in the result tree. Line 35 uses element value-of to retrieve attribute id’s value and place it in a td element in the result tree. The XPath symbol @ specifies that id is an attribute node of the context node game. Lines 36–37 place the name and paragraph element values in td elements and insert them in the result tree. When an XPath expression has no beginning forward slash, the expression uses relative addressing. Omitting the beginning forward slash tells the xsl:value-of select statements to search for name and paragraph elements that are children of the context node, not the root node. Due to the last XPath expression selection, the current context node is game, which indeed has an id attribute, a name child element and a paragraph child element.
Using XSLT to Sort and Format Data Figure 19.18 presents an XML document (sorting.xml) that marks up information about a book. Note that several elements of the markup describing the book appear out of order (e.g., the element describing Chapter 3 appears before the element describing Chapter 2). We arranged them this way purposely to demonstrate that the XSL style sheet referenced in line 5 (sorting.xsl) can sort the XML file’s data for presentation purposes. 1 2 3 4 5 6 7 8
Deitel's XML Primer
Fig. 19.18 | XML document containing book information. (Part 1 of 2.)
Jane Blue Advanced XML Intermediate XML Parsers and Tools Entities XML Fundamentals
Fig. 19.18 | XML document containing book information. (Part 2 of 2.) Figure 19.19 presents an XSL document (sorting.xsl) for transforming (Fig. 19.18) to XHTML. Recall that an XSL document navigates a source tree and builds a result tree. In this example, the source tree is XML, and the output tree is XHTML. Line 14 of Fig. 19.19 matches the root element of the document in Fig. 19.18. Line 15 outputs an html start tag to the result tree. The element (line 16) specifies that the XSLT processor is to apply the xsl:templates defined in this XSL document to the current node’s (i.e., the document root’s) children. The content from the applied templates is output in the html element that ends at line 17. Lines 21–84 specify a template that matches element book. The template indicates how to format the information contained in book elements of sorting.xml (Fig. 19.18) as XHTML. sorting.xml
1 2 3 4 5 6 7 8 9 10 11 12
Fig. 19.19 | XSL document that transforms sorting.xml into XHTML. (Part 1 of 3.)
19.7 (Optional) Extensible Stylesheet Language and XSL Transformations
Fig. 19.19 | XSL document that transforms sorting.xml into XHTML. (Part 3 of 3.) Lines 23–24 create the title for the XHTML document. We use the book’s ISBN (from attribute isbn) and the contents of element title to create the string that appears in the browser window’s title bar (ISBN 999-99999-9-X - Deitel’s XML Primer ). Line 28 creates a header element that contains the book’s title. Lines 29–31 create a header element that contains the book’s author. Because the context node (i.e., the current node being processed) is book, the XPath expression author/lastName selects the author’s last name, and the expression author/firstName selects the author’s first name.
19.7 (Optional) Extensible Stylesheet Language and XSL Transformations
785
Line 35 selects each element (indicated by an asterisk) that is a child of element frontMatter. Line 38 calls node-set function name to retrieve the current node’s element name (e.g., preface). The current node is the context node specified in the xsl:for-each (line 35). Line 42 retrieves the value of the pages attribute of the current node. Line 47 selects each chapter element. Lines 48–49 use element xsl:sort to sort chapters by number in ascending order. Attribute select selects the value of attribute number in context node chapter. Attribute data-type, with value "number", specifies a numeric sort, and attribute order, with value "ascending", specifies ascending order. Attribute data-type also accepts the value "text" (line 63), and attribute order also accepts the value "descending". Line 56 uses node-set function text to obtain the text between the chapter start and end tags (i.e., the name of the chapter). Line 57 retrieves the value of the pages attribute of the current node. Lines 62–75 perform similar tasks for
each appendix. Lines 79–80 use an XSL variable to store the value of the book’s total page count and output the page count to the result tree. Attribute name specifies the variable’s name (i.e., pagecount), and attribute select assigns a value to the variable. Function sum (line 80) totals the values for all page attribute values. The two slashes between chapters and * indicate a recursive descent—the MSXML processor will search for elements that contain an attribute named pages in all descendant nodes of chapters. The XPath expression //*
selects all the nodes in an XML document. Line 81 retrieves the value of the newly created XSL variable pagecount by placing a dollar sign in front of its name.
Summary of XSL Style Sheet Elements This section’s examples used several predefined XSL elements to perform various operations. Figure 19.20 lists these elements and several other commonly used XSL elements. For more information on these elements and XSL in general, see www.w3.org/Style/XSL. Element
Description
Applies the templates of the XSL document to the children of the current node.
Contains rules to apply when a specified node is matched.
Applies a template to every node selected by the XPath specified by the select attribute.
Has various attributes to define the format (e.g., XML, XHTML), version (e.g., 1.0, 2.0), document type and media type of the output document. This tag is a top-level element—it can be used only as a child element of an xml:stylesheet.
Adds the current node to the output tree.
Fig. 19.20 | XSL style sheet elements. (Part 2 of 2.) This section introduced Extensible Stylesheet Language (XSL) and showed how to create XSL transformations to convert XML documents from one format to another. We showed how to transform XML documents to XHTML documents for display in a Web browser. Recall that these transformations are performed by MSXML, Internet Explorer’s built-in XML parser and XSLT processor. In most business applications, XML documents are transferred between business partners and are transformed to other XML vocabularies programmatically. In Section 19.10, we demonstrate how to perform XSL transformations using the XslCompiledTransform class provided by the .NET Framework.
19.8 (Optional) Document Object Model (DOM) Although an XML document is a text file, retrieving data from the document using traditional sequential file processing techniques is neither practical nor efficient, especially for adding and removing elements dynamically. Upon successfully parsing a document, some XML parsers store document data as tree structures in memory. Figure 19.21 illustrates the tree structure for the root element of the document article.xml discussed in Fig. 19.2. This hierarchical tree structure is called a Document Object Model (DOM) tree, and an XML parser that creates this type of structure is known as a DOM parser. Each element name (e.g., article, date, firstName) is represented by a node. A node that contains other nodes (called child nodes or children) is called a parent node (e.g., author). A parent node can have many children, but a child node can have only one parent node. Nodes that are peers (e.g., firstName and lastName) are called sibling nodes. A node’s descendant nodes include its children, its children’s children and so on. A node’s ancestor nodes include its parent, its parent’s parent and so on. The DOM tree has a single root node, which contains all the other nodes in the document. For example, the root node of the DOM tree that represents article.xml (Fig. 19.2) contains a node for the XML declaration (line 1), two nodes for the comments (lines 2–3) and a node for the XML document’s root element article (line 5). Classes for creating, reading and manipulating XML documents are located in the C# namespace System.Xml. This namespace also contains additional namespaces that provide other XML-related operations.
19.8 (Optional) Document Object Model (DOM)
787
article
title
date
author
firstName
summary
lastName
content
Fig. 19.21 | Tree structure for the document article.xml of Fig. 19.2. Reading an XML Document with an XmlReader In this section, we present several examples that use DOM trees. Our first example, the program in Fig. 19.22, loads the XML document presented in Fig. 19.2 and displays its data in a text box. This example uses class XmlReader to iterate through each node in the XML document. Line 5 is a using declaration for the System.Xml namespace, which contains the XML classes used in this example. Class XmlReader is an abstract class that defines the interface for reading XML documents. We cannot create an XmlReader object directly. Instead, we must invoke XmlReader’s static method Create to obtain an XmlReader reference (line 21). Before doing so, however, we must prepare an XmlReaderSettings object that specifies how we would like the XmlReader to behave (line 20). In this example, we use the default settings of the properties of an XmlReaderSettings object. Later, you will learn how to set certain properties of the XmlReaderSettings class to instruct the XmlReader to perform validation, which it does not do by default. The static method Create receives as arguments the name of the XML document to read and an XmlReaderSettings object. In this example the XML document article.xml (Fig. 19.2) is opened when method 1 2 3 4 5 6 7 8 9 10
// Fig. 19.22: XmlReaderTest.cs // Reading an XML document. using System; using System.Windows.Forms; using System.Xml; namespace XmlReaderTest { public partial class XmlReaderTestForm : Form {
public XmlReaderTestForm() { InitializeComponent(); } // end constructor // read XML document and display its content private void XmlReaderTestForm_Load( object sender, EventArgs e ) { // create the XmlReader object XmlReaderSettings settings = new XmlReaderSettings(); XmlReader reader = XmlReader.Create( "article.xml", settings ); int depth = -1; // tree depth is -1, no indentation while ( reader.Read() ) // display each node's content { switch ( reader.NodeType ) { case XmlNodeType.Element: // XML Element, display its name depth++; // increase tab depth TabOutput( depth ); // insert tabs OutputTextBox.Text += "\r\n"; // if empty element, decrease depth if ( reader.IsEmptyElement ) depth--; break; case XmlNodeType.Comment: // XML Comment, display it TabOutput( depth ); // insert tabs OutputTextBox.Text += "\r\n"; break; case XmlNodeType.Text: // XML Text, display it TabOutput( depth ); // insert tabs OutputTextBox.Text += "\t" + reader.Value + "\r\n"; break; // XML XMLDeclaration, display it case XmlNodeType.XmlDeclaration: TabOutput( depth ); // insert tabs OutputTextBox.Text += "\n"; break; case XmlNodeType.EndElement: // XML EndElement, display it TabOutput( depth ); // insert tabs OutputTextBox.Text += "\r\n"; depth--; // decrement depth break; } // end switch } // end while } // end method XmlReaderTextForm_Load
Fig. 19.22 |
XmlReader
iterating through an XML document. (Part 2 of 3.)
19.8 (Optional) Document Object Model (DOM)
62 63 64 65 66 67 68 69
789
// insert tabs private void TabOutput( int number ) { for ( int i = 0; i < number; i++ ) OutputTextBox.Text += "\t"; } // end method TabOutput } // end class XmlReaderTestForm } // end namespace XmlReaderTest
Fig. 19.22 |
XmlReader
iterating through an XML document. (Part 3 of 3.)
is invoked in line 21. Once the XmlReader is created, the XML document’s contents can be read programmatically. Method Read of XmlReader reads one node from the DOM tree. By calling this method in the while loop condition (line 27), reader Reads all the document nodes. The switch statement (lines 27–58) processes each node. Either the Name property (lines 32, 50 and 55), which contains the node’s name, or the Value property (lines 40 and 44), which contains the node’s data, is formatted and concatenated to the string assigned to the TextBox’s Text property. The XmlReader’s NodeType property specifies whether the node is an element, comment, text, XML declaration or end element. Note that each case specifies a node type using XmlNodeType enumeration constants. For example, XmlNodeType.Element (line 31) indicates the start tag of an element. The displayed output emphasizes the structure of the XML document. Variable depth (line 23) maintains the number of tab characters to indent each element. The depth is incremented each time the program encounters an Element and is decremented each time the program encounters an EndElement or empty element. We use a similar technique in the next example to emphasize the tree structure of the XML document being displayed. Create
Displaying a DOM Tree Graphically in a TreeView Control XmlReaders do not provide features for displaying their content graphically. In this example, we display an XML document’s contents using a TreeView control. We use class TreeNode to represent each node in the tree. Class TreeView and class TreeNode are part
790
Chapter 19
Extensible Markup Language (XML)
of the System.Windows.Forms namespace. TreeNodes are added to the TreeView to emphasize the structure of the XML document. The C# program in Fig. 19.23 demonstrates how to manipulate a DOM tree programmatically to display it graphically in a TreeView control. The GUI for this application contains a TreeView control named xmlTreeView (declared in XmlDom.Designer.cs). The application loads letter.xml (Fig. 19.24) into XmlReader (line 27), then displays the document’s tree structure in the TreeView control. [Note: The version of letter.xml in Fig. 19.24 is nearly identical to the one in Fig. 19.4, except that Fig. 19.24 does not reference a DTD as line 5 of Fig. 19.4 does.] In XmlDomForm’s Load event handler (lines 19–34), lines 23–24 create an XmlReaderSettings object and set its IgnoreWhitespace property to true so that the insignificant 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
// Fig. 19.23: XmlDom.cs // Demonstrates DOM tree manipulation. using System; using System.Windows.Forms; using System.Xml; namespace XmlDom { public partial class XmlDomForm : Form { public XmlDomForm() { InitializeComponent(); } // end constructor private TreeNode tree; // TreeNode reference // initialize instance variables private void XmlDomForm_Load( object sender, EventArgs e ) { // create Xml ReaderSettings and // set the IgnoreWhitespace property XmlReaderSettings settings = new XmlReaderSettings(); settings.IgnoreWhitespace = true; // create XmlReader object XmlReader reader = XmlReader.Create( "letter.xml", settings ); tree = new TreeNode(); // instantiate TreeNode tree.Text = "letter.xml"; // assign name to TreeNode xmlTreeView.Nodes.Add( tree ); // add TreeNode to TreeView BuildTree( reader, tree ); // build node and tree hierarchy } // end method XmlDomForm_Load // construct TreeView based on DOM tree private void BuildTree( XmlReader reader, TreeNode treeNode ) {
Fig. 19.23 | DOM structure of an XML document displayed in a TreeView. (Part 1 of 3.)
// treeNode to add to existing tree TreeNode newNode = new TreeNode(); while ( reader.Read() ) { // build tree based on node type switch ( reader.NodeType ) { // if Text node, add its value to tree case XmlNodeType.Text: newNode.Text = reader.Value; treeNode.Nodes.Add( newNode ); break; case XmlNodeType.EndElement: // if EndElement, move up tree treeNode = treeNode.Parent; break; // if new element, add name and traverse tree case XmlNodeType.Element: // determine if element contains content if ( !reader.IsEmptyElement ) { // assign node text, add newNode as child newNode.Text = reader.Name; treeNode.Nodes.Add( newNode ); // set treeNode to last child treeNode = newNode; } // end if else // do not traverse empty elements { // assign NodeType string to newNode // and add it to tree newNode.Text = reader.NodeType.ToString(); treeNode.Nodes.Add( newNode ); } // end else break; default: // all other types, display node type newNode.Text = reader.NodeType.ToString(); treeNode.Nodes.Add( newNode ); break; } // end switch newNode = new TreeNode(); } // end while // update TreeView control xmlTreeView.ExpandAll(); // expand tree nodes in TreeView xmlTreeView.Refresh(); // force TreeView to update } // end method BuildTree } // end class XmlDomForm } // end namespace XmlDom
Fig. 19.23 | DOM structure of an XML document displayed in a TreeView. (Part 2 of 3.)
792
Chapter 19
Extensible Markup Language (XML)
Fig. 19.23 | DOM structure of an XML document displayed in a TreeView. (Part 3 of 3.) whitespaces in the XML document are ignored. Line 27 then invokes static XmlReader method Create to parse and load letter.xml. Line 29 creates the TreeNode tree (declared in line 16). This TreeNode is used as a graphical representation of a DOM tree node in the TreeView control. Line 31 assigns the XML document’s name (i.e., letter.xml) to tree’s Text property. Line 32 calls method Add to add the new TreeNode to the TreeView’s Nodes collection. Line 33 calls method BuildTree to update the TreeView so that it displays source’s complete DOM tree. Method BuildTree (lines 37–89) receives an XmlReader for reading the XML document and a TreeNode referencing the current location in the tree (i.e., the TreeNode most recently added to the TreeView control). Line 40 declares TreeNode reference newNode, which will be used for adding new nodes to the TreeView. Lines 42–84 iterate through each node in the XML document’s DOM tree. The switch statement in lines 45–81 adds a node to the TreeView, based on the XmlReader’s current node. When a text node is encountered, the Text property of the new TreeNode—newNode—is assigned the current node’s value (line 49). Line 50 adds this TreeNode to treeNode’s node list (i.e., adds the node to the TreeView control). Line 52 matches an EndElement node type. This case moves up the tree to the current node’s parent because the end of an element has been encountered. Line 53 accesses treeNode’s Parent property to retrieve the node’s current parent. Line 57 matches Element node types. Each non-empty Element NodeType (line 60) increases the depth of the tree; thus, we assign the current reader Name to the newNode’s Text property and add the newNode to treeNode’s node list (lines 63–64). Line 67 assigns the newNode’s reference to treeNode to ensure that treeNode refers to the last child TreeNode in the node list. If the current Element node is an empty element (line 69), we assign to the newNode’s Text property the string representation of the NodeType (line 73). Next, the newNode is added to the treeNode node list (line 74). The default case (lines 77–80) assigns the string representation of the node type to the newNode Text property, then adds the newNode to the TreeNode node list. After the entire DOM tree is processed, the TreeNode node list is displayed in the TreeView control (lines 87–88). TreeView method ExpandAll causes all the nodes of the
Jane Doe Box 12345 15 Any Ave. Othertown Otherstate 67890 555-4321 John Doe 123 Main St. Anytown Anystate 12345 555-1234 Dear Sir: It is our privilege to inform you about our new database managed with XML. This new system allows you to reduce the load on your inventory list server by having the client machine perform the work of sorting and filtering the data. Please visit our Web site for availability and pricing. Sincerely, Ms. Doe
Fig. 19.24 | Business letter marked up as XML. tree to be displayed. TreeView method Refresh updates the display to show the newly added TreeNodes. Note that while the application is running, clicking nodes (i.e., the + or – boxes) in the TreeView either expands or collapses them.
Locating Data in XML Documents with XPath Although XmlReader includes methods for reading and modifying node values, it is not the most efficient means of locating data in a DOM tree. The Framework Class Library provides class XPathNavigator in the System.Xml.XPath namespace for iterating through node lists that match search criteria, which are written as XPath expressions. Recall that
794
Chapter 19
Extensible Markup Language (XML)
XPath (XML Path Language) provides a syntax for locating specific nodes in XML documents effectively and efficiently. XPath is a string-based language of expressions used by XML and many of its related technologies (such as XSLT, discussed in Section 19.7). Figure 19.25 uses an XPathNavigator to navigate an XML document and uses a TreeView control and TreeNode objects to display the XML document’s structure. In this example, the TreeNode node list is updated each time the XPathNavigator is positioned to a new node, rather than displaying the entire DOM tree at once. Nodes are added to and deleted from the TreeView to reflect the XPathNavigator’s location in the DOM tree. Figure 19.26 shows the XML document sports.xml that we use in this example. [Note: The versions of sports.xml presented in Fig. 19.26 and Fig. 19.16 are nearly identical. In the current example, we do not want to apply an XSLT, so we omit the processing instruction found in line 2 of Fig. 19.16.] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
// Fig. 19.25: PathNavigator.cs // Demonstrates class XPathNavigator. using System; using System.Windows.Forms; using System.Xml.XPath; // contains XPathNavigator namespace PathNavigator { public partial class PathNavigatorForm : Form { public PathNavigatorForm() { InitializeComponent(); } // end constructor private XPathNavigator xPath; // navigator to traverse document // references document for use by XPathNavigator private XPathDocument document; // references TreeNode list used by TreeView control private TreeNode tree; // initialize variables and TreeView control private void PathNavigatorForm_Load( object sender, EventArgs e ) { // load XML document document = new XPathDocument( "sports.xml" ); xPath = document.CreateNavigator(); // create navigator tree = new TreeNode(); // create root node for TreeNodes
Fig. 19.25 |
tree.Text = xPath.NodeType.ToString(); // #root pathTreeView.Nodes.Add( tree ); // add tree // update TreeView control pathTreeView.ExpandAll(); // expand tree node in TreeView pathTreeView.Refresh(); // force TreeView update XPathNavigator
// move to parent if ( xPath.MoveToParent() ) { tree = tree.Parent; // get number of child nodes, not including sub trees int count = tree.GetNodeCount( false ); // remove all children for ( int i = 0; i < count; i++ ) tree.Nodes.Remove( tree.FirstNode ); // remove child node // update TreeView control pathTreeView.ExpandAll(); // expand node in TreeView pathTreeView.Refresh(); // force TreeView to update pathTreeView.SelectedNode = tree; // highlight root } // end if else // if node has no parent (root node) MessageBox.Show( "Current node has no parent.", "", MessageBoxButtons.OK, MessageBoxIcon.Information ); } // end method parentButton_Click // find next sibling on nextButton_Click event private void nextButton_Click( object sender, EventArgs e ) { // declare and initialize two TreeNodes TreeNode newTreeNode = null; TreeNode newNode = null; // move to next sibling if ( xPath.MoveToNext() ) { newTreeNode = tree.Parent; // get parent node newNode = new TreeNode(); // create new node // decide whether to display current node DetermineType( newNode, xPath ); newTreeNode.Nodes.Add( newNode ); // add to parent node tree = newNode; // set current position for display // update TreeView control pathTreeView.ExpandAll(); // expand node in Tree‘‘‘‘View pathTreeView.Refresh(); // force TreeView to update pathTreeView.SelectedNode = tree; // highlight root } // end if else // node has no additional siblings MessageBox.Show( "Current node is last sibling.", "", MessageBoxButtons.OK, MessageBoxIcon.Information ); } // end method nextButton_Click
Fig. 19.25 |
XPathNavigator
navigating selected nodes. (Part 3 of 5.)
19.8 (Optional) Document Object Model (DOM)
797
142 143 // get previous sibling on previousButton_Click 144 private void previousButton_Click( object sender, EventArgs e ) 145 { 146 TreeNode parentTreeNode = null; 147 148 // move to previous sibling 149 if ( xPath.MoveToPrevious() ) 150 { 151 parentTreeNode = tree.Parent; // get parent node 152 parentTreeNode.Nodes.Remove( tree ); // delete current node 153 tree = parentTreeNode.LastNode; // move to previous node 154 155 // update TreeView control 156 pathTreeView.ExpandAll(); // expand tree node in TreeView 157 pathTreeView.Refresh(); // force TreeView to update 158 pathTreeView.SelectedNode = tree; // highlight root 159 } // end if 160 else // if current node has no previous siblings 161 MessageBox.Show( "Current node is first sibling.", "", 162 MessageBoxButtons.OK, MessageBoxIcon.Information ); 163 } // end method previousButton_Click 164 165 // print values for XPathNodeIterator 166 private void DisplayIterator( XPathNodeIterator iterator ) 167 { 168 selectTextBox.Clear(); 169 170 // prints selected node's values 171 while ( iterator.MoveNext() ) 172 selectTextBox.Text += iterator.Current.Value.Trim() + "\r\n"; 173 } // end method DisplayIterator 174 175 // determine if TreeNode should display current node name or value 176 private void DetermineType( TreeNode node, XPathNavigator xPath ) 177 { 178 switch ( xPath.NodeType ) // determine NodeType 179 { 180 case XPathNodeType.Element: // if Element, get its name // get current node name, and remove whitespaces 181 node.Text = xPath.Name.Trim(); 182 183 break; 184 default: // obtain node values // get current node value and remove whitespaces 185 node.Text = xPath.Value.Trim(); 186 187 break; 188 } // end switch 189 } // end method DetermineType 190 } // end class PathNavigatorForm 191 } // end namespace PathNavigator
Fig. 19.25 |
XPathNavigator
navigating selected nodes. (Part 4 of 5.)
798
Chapter 19
Extensible Markup Language (XML)
(a)
(b)
(c)
(d)
Fig. 19.25 |
XPathNavigator
navigating selected nodes. (Part 5 of 5.)
The program of Fig. 19.25 loads XML document sports.xml (Fig. 19.26) into an object by passing the document’s file name to the XPathDocument constructor (line 28). Method CreateNavigator (line 29) creates and returns an XPathNavigator reference to the XPathDocument’s tree structure. The navigation methods of XPathNavigator are MoveToFirstChild (line 66), MoveToParent (line 92), MoveToNext (line 121) and MoveToPrevious (line 149). Each method performs the action that its name implies. Method MoveToFirstChild moves to XPathDocument
Cricket More popular among commonwealth nations. Baseball More popular in America. Soccer (Futbol) Most popular sport in the world.
Fig. 19.26 | XML document that describes various sports. the first child of the node referenced by the XPathNavigator, MoveToParent moves to the parent node of the node referenced by the XPathNavigator, MoveToNext moves to the next sibling of the node referenced by the XPathNavigator and MoveToPrevious moves to the previous sibling of the node referenced by the XPathNavigator. Each method returns a bool indicating whether the move was successful. In this example, we display a warning in a MessageBox whenever a move operation fails. Furthermore, each method is called in the event handler of the button that matches its name (e.g., clicking the First Child button in Fig. 19.25(a) triggers firstChildButton_Click, which calls MoveToFirstChild). Whenever we move forward using XPathNavigator, as with MoveToFirstChild and MoveToNext, nodes are added to the TreeNode node list. The private method DetermineType (lines 176–189) determines whether to assign the Node’s Name property or Value property to the TreeNode (lines 182 and 186). Whenever MoveToParent is called, all the children of the parent node are removed from the display. Similarly, a call to MoveToPrevious removes the current sibling node. Note that the nodes are removed only from the TreeView, not from the tree representation of the document. The selectButton_Click event handler (lines 42–58) corresponds to the Select button. XPathNavigator method Select (line 49) takes search criteria in the form of either an XPathExpression or a string that represents an XPath expression, and returns as an XPathNodeIterator object any node that matches the search criteria. Figure 19.27
800
Chapter 19
Extensible Markup Language (XML)
XPath Expression
Description
/sports
Matches all sports nodes that are child nodes of the document root node.
/sports/game
Matches all game nodes that are child nodes of sports, which is a child of the document root.
/sports/game/name
Matches all name nodes that are child nodes of game. The game is a child of sports, which is a child of the document root.
/sports/game/paragraph
Matches all paragraph nodes that are child nodes of game. The game is a child of sports, which is a child of the document root.
/sports/game [name='Cricket']
Matches all game nodes that contain a child element whose name is Cricket. The game is a child of sports, which is a child of the document root.
Fig. 19.27 |
XPath
expressions and descriptions.
summarizes the XPath expressions provided by this program’s combo box. We show the result of some of these expressions in Fig. 19.25(b)–(d). Method DisplayIterator (defined in lines 166–173) appends the node values from the given XPathNodeIterator to the selectTextBox. Note that we call string method Trim to remove unnecessary whitespace. Method MoveNext (line 171) advances to the next node, which property Current (line 172) can access.
19.9 (Optional) Schema Validation with Class XmlReader
Recall from Section 19.6 that schemas provide a means for specifying XML document structure and validating XML documents. Such validation helps an application determine whether a particular document it receives is complete, ordered properly and not missing any data. In Section 19.6, we used an online XSD schema validator to verify that an XML document conforms to an XML Schema. In this section, we show how to perform the same type of validation programmatically using classes provided by the .NET Framework.
Validating an XML Document Programmatically Class XmlReader can validate an XML document as it reads and parses the document. In this example, we demonstrate how to activate such validation. The program in Fig. 19.28 validates an XML document that the user chooses—either book.xml (Fig. 19.11) or fail.xml (Fig. 19.29)—against the XML Schema document book.xsd (Fig. 19.12). Line 17 creates XmlSchemaSet variable schemas. An object of class XmlSchemaSet stores a collection of schemas against which an XmlReader can validate. Line 23 assigns a new XmlSchemaSet object to variable schemas, and line 26 calls this object’s Add method to add a schema to the collection. Method Add receives as arguments a namespace URI
19.9 (Optional) Schema Validation with Class XmlReader
801
that identifies the schema (http://www.deitel.com/booklist) and the name and location of the schema file (book.xsd in the current directory).
// Fig. 19.28: ValidationTest.cs // Validating XML documents against schemas. using System; using System.Windows.Forms; using System.Xml; using System.Xml.Schema; // contains XmlSchemaSet class namespace ValidationTest { public partial class ValidationTestForm : Form { public ValidationTestForm() { InitializeComponent(); } // end constructor private XmlSchemaSet schemas; // schemas to validate against private bool valid = true; // validation result // handle validateButton Click event private void validateButton_Click( object sender, EventArgs e ) { schemas = new XmlSchemaSet(); // create the XmlSchemaSet class // add the schema to the collection schemas.Add( "http://www.deitel.com/booklist", "book.xsd" ); // set the validation settings XmlReaderSettings settings = new XmlReaderSettings(); settings.ValidationType = ValidationType.Schema; settings.Schemas = schemas; settings.ValidationEventHandler += ValidationError; // create the XmlReader object XmlReader reader = XmlReader.Create( filesComboBox.Text, settings ); // parse the file while ( reader.Read() ); // empty body if ( valid ) // check validation result { consoleLabel.Text = "Document is valid"; } // end if valid = true; // reset variable reader.Close(); // close reader stream } // end method validateButton_Click
Fig. 19.28 | Schema-validation example. (Part 1 of 2.)
802 49 50 51 52 53 54 55 56 57 58
Chapter 19
Extensible Markup Language (XML)
// event handler for validation error private void ValidationError( object sender , ValidationEventArgs arguments ) { lblConsole.Text = arguments.Message; valid = false; // validation failed } // end method ValidationError } // end class ValidatorTestForm } // end namespace ValidatorTest
Visual Basic 2005 How to Program, 3/e Visual C# 2005 How to Program Java How to Program, 6/e C++ How to Program, 5/e Internet and World Wide Web How to Program, 3/e XML How to Program
Fig. 19.29 | XML file that does not conform to the XML Schema document in Fig. 19.12. (Part 1 of 2.)
19.10 (Optional) XSLT with Class XslCompiledTransform
803
Fig. 19.29 | XML file that does not conform to the XML Schema document in Fig. 19.12. (Part 2 of 2.)
Lines 29–32 create and set the properties of an XmlReaderSettings object. Line 30 sets the XmlReaderSettings object’s ValidationType property to the value ValidationType.Schema, indicating that we want the XmlReader to perform validation with a schema as it reads an XML document. Line 31 sets the XmlReaderSettings object’s Schemas property to schemas. This property sets the schema(s) used to validate the document read by the XmlReader. Line 32 registers method ValidationError with the settings object’s ValidationEventHandler. Method ValidationError (lines 52–56) is called if the document being read is found to be invalid or an error occurs (e.g., the document cannot be found). Failure to register a method with ValidationEventHandler causes an exception (XmlException) to be thrown when the XML document is found to be invalid or missing. After setting the ValidationType property, Schemas property and ValidationEventHandler of the XmlReaderSettings object, we are ready to create a validating XmlReader. Lines 35–36 create an XmlReader that reads the file selected by the user from the cboFiles ComboBox and validates it against the book.xsd schema. Validation is performed node-by-node by calling method Read of the XmlReader object (line 39). Because we set XmlReaderSettings property ValidationType to ValidationType.Schema, each call to Read validates the next node in the document. The loop terminates either when all the nodes have been validated or when a node fails validation.
Detecting an Invalid XML Document The program in Fig. 19.28 validates the XML document book.xml (Fig. 19.12) against the book.xsd (Fig. 19.11) schema successfully. However, when the user selects the XML document of Fig. 19.29, validation fails—the book element in lines 18–21 contains more than one title element. When the program encounters the invalid node, method ValidationError (lines 51–56 of Fig. 19.28) is called, which displays a message explaining why validation failed.
19.10 (Optional) XSLT with Class XslCompiledTransform
Recall from Section 19.7 that XSLT provides elements that define rules for converting one type of XML document to another type of XML document. We showed how to transform XML documents into XHTML documents and displayed these in Internet Explorer. The
804
Chapter 19
Extensible Markup Language (XML)
XSLT processor included in Internet Explorer (i.e., MSXML) performed the transformations.
Performing an XSL Transformation in C# Using the .NET Framework Figure 19.30 applies the style sheet sports.xsl (Fig. 19.17) to sports.xml (Fig. 19.26) programmatically. The result of the transformation is written to an XHTML file on disk and displayed in a text box. Figure 19.30(c) shows the resulting XHTML document (sports.html) when it is viewed in Internet Explorer. Line 5 is a using declaration for the System.Xml.Xsl namespace, which contains class XslCompiledTransform for applying XSLT style sheets to XML documents. Line 17 declares XslCompiledTransform reference transformer. An object of this type serves as an 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
// Fig. 19.30: TransformTest.cs // Applying an XSLT style sheet to an XML document. using System; using System.Windows.Forms; using System.Xml.Xsl; // contains class XslCompiledTransform namespace TransformTest { public partial class TransformTestForm : Form { public TransformTestForm() { InitializeComponent(); } // end constructor // applies the transformation private XslCompiledTransform transformer; // initialize variables private void TransformTestForm_Load( object sender, EventArgs e ) { transformer = new XslCompiledTransform(); // create transformer // load and compile the style sheet transformer.Load( "sports.xsl" ); } // end method TransformTestForm_Load // transform XML data on transformButton_Click event private void transformButton_Click( object sender, EventArgs e ) { // perform the transformation and store the result in new file transformer.Transform( "sports.xml", "sports.html" ); // read and display the XHTML document's text in a Textbox consoleTextBox.Text = System.IO.File.ReadAllText( "sports.html" ); } // end method transformButton_Click } // end class TransformTestForm } // end namespace TransformTest
Fig. 19.30 | XSLT style sheet applied to an XML document. (Part 1 of 2.)
19.10 (Optional) XSLT with Class XslCompiledTransform
(a)
805
(b)
(c)
Fig. 19.30 | XSLT style sheet applied to an XML document. (Part 2 of 2.) XSLT processor (like MSXML in earlier examples) to transform XML data from one format to another. In event handler TransformTestForm_Load (lines 20–26), line 22 creates a new XslCompiledTransform object. Then line 25 calls the XslCompiledTransform object’s Load method, which parses and loads the style sheet that this application uses. This method takes an argument specifying the name and location of the style sheet— sports.xsl (Fig. 19.17) located in the current directory. Event handler transformButton_Click (lines 29–37) calls method Transform of class XslCompiledTransform to apply the style sheet (sports.xsl) to sports.xml (line 32). This method takes two string arguments—the first specifies the XML file to which the style sheet should be applied, and the second specifies the file in which the result of the transformation should be stored on disk. Thus the Transform method call in line 32 transforms sports.xml to XHTML and writes the result to disk as the file sports.html. Figure 19.30(c) shows the new XHTML document rendered in Internet Explorer. Note that the output is identical to that of Fig. 19.16—in the current example, though, the XHTML is stored on disk rather than generated dynamically by MSXML. After applying the transformation, the program displays the content of the newly created sports.html file in the txtConsole TextBox, as shown in Fig. 19.30(b). Lines 35– 36 obtain the text of the file by passing its name to method ReadAllText of the System.IO.File class provided by the FCL to simplify file-processing tasks on the local system.
806
Chapter 19
Extensible Markup Language (XML)
19.11 Wrap-Up In this chapter, we studied Extensible Markup Language and several of its related technologies. We began by discussing some basic XML terminology, introducing the concepts of markup, XML vocabularies and XML parsers (validating and nonvalidating). We then demonstrated how to describe and structure data in XML, illustrating these points with examples marking up an article and a business letter. The chapter discussed the concept of an XML namespace. You learned that each namespace has a unique name that provides a means for document authors to unambiguously refer to elements with the same name (i.e., prevent naming collisions). We presented examples of defining two namespaces in the same document, as well as setting the default namespace for a document. We also discussed how to create DTDs and schemas for specifying and validating the structure of an XML document. We showed how to use various tools to confirm whether XML documents are valid (i.e., conform to a DTD or schema). The chapter also demonstrated how to create and use XSL documents to specify rules for converting XML documents between formats. Specifically, you learned how to format and sort XML data as XHTML for display in a Web browser. The final sections of the chapter presented more advanced uses of XML in C# applications. We demonstrated how to retrieve and display data from an XML document using various classes of the .NET Framework. We illustrated how a Document Object Model (DOM) tree represents each element of an XML document as a node in the tree. The chapter also demonstrated reading data from an XML document using the XmlReader class, displaying DOM trees graphically and locating data in XML documents with XPath. Finally, we showed how to use .NET Framework classes XmlReader and XslCompiledTransform to perform schema validation and XSL transformations, respectively. In Chapter 20, we begin our discussion of databases, which organize data in such a way that the data can be selected and updated quickly. We introduce Structured Query Language (SQL) for writing simple database queries (i.e., searches) and ADO.NET for manipulating information in a database through C#. You will learn how to create XML documents based on data in a database.
19.12 Web Resources www.w3.org/XML
The W3C (World Wide Web Consortium) facilitates the development of common protocols to ensure interoperability on the Web. Its XML page includes information about upcoming events, publications, software, discussion groups and the latest developments in XML. www.xml.org
xml.org is a reference for XML, DTDs, schemas and namespaces. www.w3.org/Style/XSL
This W3C site provides information on XSL, including such topics as XSL development, learning XSL, XSL-enabled tools, the XSL specification, FAQs and XSL history. www.w3.org/TR
This is the W3C technical reports and publications site. It contains links to working drafts, proposed recommendations and other resources.
19.12 Web Resources
807
www.xmlbooks.com
This site provides a list of XML books recommended by Charles Goldfarb, one of the original designers of GML (General Markup Language), from which SGML, HTML and XML were derived. www.xml-zone.com
The DevX XML Zone is a complete resource for XML information. This site includes FAQs, news, articles and links to other XML sites and newsgroups. wdvl.internet.com/Authoring/Languages/XML
The Web Developer's Virtual Library XML site includes tutorials, FAQs, the latest news and extensive links to XML sites and software downloads. www.xml.com
This site provides the latest news and information about XML, conference listings, links to XML Web resources organized by topic, tools and other resources. msdn.microsoft.com/xml/default.aspx
The MSDN XML Development Center features articles on XML, Ask-the-Experts chat sessions, samples, demos, newsgroups and other helpful information. www.oasis-open.org/cover/xml.html
This site includes links to FAQs, online resources, industry initiatives, demos, conferences and tutorials. www-106.ibm.com/developerworks/xml
The IBM developerWorks XML site is a great resource for developers that provides news, tools, a library, case studies and information about XML-related events and standards. www.devx.com/projectcool/door/7051
The Project Cool DevX site includes several tutorials covering introductory through advanced XML topics. www.ucc.ie/xml
This site provides a detailed XML FAQ. Developers can read responses to some popular questions or submit their own questions through the site. www.w3.org/XML/Schema
This W3C site provides information on XML Schema, including links, tools, resources and the XML Schema specification. tools.decisionsoft.com/schemaValidate.html
DecisionSoft provides a free online XML Schema validator. www.sun.com/software/xml/developers/multischema/
The Web page for downloading the Sun Multi-Schema XML Validator (MSV), which validates XML documents against several kinds of schemas. en.wikipedia.org/wiki/EBNF
This site provides detailed information about Extended Backus-Naur Form (EBNF). www.garshol.priv.no/download/text/bnf.html
This site introduces Backus-Naur Form (BNF) and EBNF, and discusses the differences between these notations. www.w3schools.com/schema/default.asp
This site provides an XML Schema tutorial. www.w3.org/2001/03/webdata/xsv
This is an online tool for validating XML Schema documents. apps.gotdotnet.com/xmltools/xsdvalidator
This is an online tool for validating XML documents against an XML Schema. www.w3.org/TR/xmlschema-2
This is part 2 of the W3C XML Schema specification, which defines the data types allowed in an XML Schema.
20 Database, SQL and ADO.NET It is a capital mistake to theorize before one has data. —Arthur Conan Doyle
I
The relational database model.
Now go, write it before them in a table, and note it in a book, that it may be for the time to come for ever and ever.
I
To write basic database queries in SQL.
—Holy Bible, Isaiah 30:8
I
To add data sources to projects.
I
To use the IDE’s drag-and-drop capabilities to display database tables in applications.
I
To use the classes of namespaces System.Data and System.Data.SqlClient to manipulate databases.
I
To use ADO.NET’s disconnected object model to store data from a database in local memory.
I
To create XML documents from data sources.
OBJECTIVES In this chapter you will learn:
Get your facts first, and then you can distort them as much as you please. —Mark Twain
I like two kinds of men: domestic and foreign. —Mae West
Outline
20.1 Introduction
20.1 20.2 20.3 20.4
20.5 20.6
20.7 20.8 20.9 20.10 20.11
809
Introduction Relational Databases Relational Database Overview: Books Database SQL 20.4.1 Basic SELECT Query 20.4.2 WHERE Clause 20.4.3 ORDER BY Clause 20.4.4 Merging Data from Multiple Tables: INNER JOIN 20.4.5 INSERT Statement 20.4.6 UPDATE Statement 20.4.7 DELETE Statement ADO.NET Object Model Programming with ADO.NET: Extracting Information from a Database 20.6.1 Displaying a Database Table in a DataGridView 20.6.2 How Data Binding Works Querying the Books Database Programming with ADO.NET: Address Book Case Study Using a DataSet to Read and Write XML Wrap-Up Web Resources
20.1 Introduction A database is an organized collection of data. Many strategies exist for organizing data to facilitate easy access and manipulation. A database management system (DBMS) provides mechanisms for storing, organizing, retrieving and modifying data for many users. Database management systems allow access to and storage of data independently of the internal representation of the data. Today’s most popular database systems are relational databases. A language called SQL—pronounced “sequel,” or as its individual letters—is the international standard language used almost universally with relational databases to perform queries (i.e., to request information that satisfies given criteria) and to manipulate data. In this book, we pronounce SQL as “sequel.” Some popular relational database management systems (RDBMS) are Microsoft SQL Server, Oracle, Sybase, IBM DB2 and PostgreSQL. We provide URLs for these systems in Section 20.11, Web Resources. MySQL (www.mysql.com) is an increasingly popular open-source RDBMS that can be downloaded and used freely by non-commercial users. You may also be familiar with Microsoft Access—a relational database system that is part of Microsoft Office. In this chapter, we use Microsoft SQL Server 2005 Express— a free version of SQL Server 2005 that is installed when you install Visual C# 2005. We will refer to SQL Server 2005 Express simply as SQL Server from this point forward.
810
Chapter 20
Database, SQL and ADO.NET
A programming language connects to and interacts with a relational database via a database interface—software that facilitates communication between a database management system and a program. C# programs communicate with databases and manipulate their data through ADO.NET. The current version of ADO.NET is 2.0. Section 20.5 presents an overview of ADO.NET’s object model and the relevant namespaces and classes that allow you to work with databases in C#. However, as you will learn in subsequent sections, most of the work required to communicate with a database using ADO.NET 2.0 is performed by the IDE itself. You will work primarily with the IDE’s visual programming tools and wizards, which simplify the process of connecting to and manipulating a database. Throughout this chapter, we refer to ADO.NET 2.0 simply as ADO.NET. This chapter introduces general concepts of relational databases and SQL, then explores ADO.NET and the IDE’s tools for accessing data sources. The examples in Sections 20.6–20.9 demonstrate how to build applications that use databases to store information. In the next two chapters, you will see other practical database applications. Chapter 21, ASP.NET 2.0, Web Forms and Web Controls, presents a Web-based bookstore case study that retrieves user and book information from a database. Chapter 22, Web Services, uses a database to store airline reservation data for a Web service (i.e., a software component that can be accessed remotely over a network).
20.2 Relational Databases A relational database is a logical representation of data that allows the data to be accessed independently of its physical structure. A relational database organizes data in tables. Figure 20.1 illustrates a sample Employees table that might be used in a personnel system. The table stores the attributes of employees. Tables are composed of rows and columns in which values are stored. This table consists of six rows and five columns. The Number column of each row in this table is the table’s primary key—a column (or group of columns) in a table that requires a unique value that cannot be duplicated in other rows. This guarantees that a primary key value can be used to uniquely identify a row. A primary key that is composed of two or more columns is known as a composite key. Good examples of primary key columns in other applications are an employee ID number in a payroll system and a part number in an inventory system—values in each of these columns are guaranteed to be unique. The rows in Fig. 20.1 are displayed in order by primary key. In this case, the rows are listed in increasing (ascending) order, but they could also be listed in decreasing (descending) order or in no particular order at all. As we will demonstrate in an upcoming example, programs can specify ordering criteria when requesting data from a database. Each column represents a different data attribute. Rows are normally unique (by primary key) within a table, but some column values may be duplicated between rows. For example, three different rows in the Employees table’s Department column contain the number 413, indicating that these employees work in the same department. Different database users are often interested in different data and different relationships among the data. Most users require only subsets of the rows and columns. To obtain these subsets, programs use SQL to define queries that select subsets of the data from a table. For example, a program might select data from the Employees table to create a query result that shows where each department is located, in increasing order by Department number (Fig. 20.2). SQL queries are discussed in Section 20.4. In Section 20.7, you will learn how to use the IDE’s Query Builder to create SQL queries.
20.3 Relational Database Overview: Books Database
811
Table Employees
Row
Number
Name
Department
Salary
Location
23603
Jones
413
1100
New Jersey
24568
Kerwin
413
2000
New Jersey
34589
Larson
642
1800
Los Angeles
35761
Myers
611
1400
Orlando
47132
Neumann
413
9000
New Jersey
78321
Stephens
611
8500
Orlando
Primary key
Fig. 20.1 |
Employees
Column
table sample data.
Department
Location
413 611 642
New Jersey Orlando Los Angeles
Fig. 20.2 | Result of selecting distinct Department and Location data from the Employees table.
20.3 Relational Database Overview: Books Database We now overview relational databases in the context of a simple Books database. The database stores information about some recent Deitel publications. First, we overview the tables of the Books database. Then we introduce database concepts, such as how to use SQL to retrieve information from the Books database and to manipulate the data. We provide the database file—Books.mdf—with the examples for this chapter (downloadable from www.deitel.com/books/csharpforprogrammers2). SQL Server database files typically end with the .mdf (“master data file”) filename extension. Section 20.6 explains how to use this file in an application.
Table of the Books Database The database consists of three tables: Authors, AuthorISBN and Titles. The Authors table (described in Fig. 20.3) consists of three columns that maintain each author’s unique ID number, first name and last name, respectively. Figure 20.4 contains the data from the Authors table. We list the rows in order by the table’s primary key—AuthorID. You will learn how to sort data by other criteria (e.g., in alphabetical order by last name) using SQL’s ORDER BY clause in Section 20.4.3. Authors
Table of the Books Database The Titles table (described in Fig. 20.5) consists of four columns that maintain information about each book in the database, including the ISBN, title, edition number and copyright year. Figure 20.6 contains the data from the Titles table. Titles
812
Chapter 20
Database, SQL and ADO.NET
Column
Description
AuthorID
Author’s ID number in the database. In the Books database, this integer column is defined as an identity column, also known as an autoincremented column—for each row inserted in the table, the AuthorID value is increased by 1 automatically to ensure that each row has a unique AuthorID. This is the primary key.
FirstName
Author’s first name (a string).
LastName
Author’s last name (a string).
Fig. 20.3
| Authors
table of the Books database. AuthorID
FirstName
LastName
1
Harvey
Deitel
2
Paul
Deitel
3
Andrew
Goldberg
4
David
Choffnes
Fig. 20.4 | Data from the Authors table of the Books
database.
Column
Description
ISBN
ISBN of the book (a string). The table’s primary key. ISBN is an abbreviation for “International Standard Book Number”—a numbering scheme that publishers worldwide use to give every book a unique identification number.
Title
Title of the book (a string).
EditionNumber
Edition number of the book (an integer).
Copyright
Copyright year of the book (a string).
Fig. 20.5
| Titles
table of the Books database.
ISBN
Title
EditionNumber
Copyright
0131426443
C How to Program
4
2004
0131450913
Internet & World Wide Web How to Program
3
2004
Fig. 20.6 | Data from the Titles table of the Books database. (Part 1 of 2.)
813
20.3 Relational Database Overview: Books Database
ISBN
Title
EditionNumber
Copyright
0131483986
Java How to Program
6
2005
0131525239
Visual C# 2005 How to Program
2
2006
0131828274
Operating Systems
3
2004
0131857576
C++ How to Program
5
2005
0131869000
Visual Basic 2005 How to Program
3
2006
Fig. 20.6 | Data from the Titles table of the Books database. (Part 2 of 2.) Table of the Books Database The AuthorISBN table (described in Fig. 20.7) consists of two columns that maintain ISBNs for each book and their corresponding authors’ ID numbers. This table associates authors with their books. The AuthorID column is a foreign key—a column in this table that matches the primary key column in another table (i.e., AuthorID in the Authors table). The ISBN column is also a foreign key—it matches the primary key column (i.e., ISBN) in the Titles table. Together the AuthorID and ISBN columns in this table form a composite primary key. Every row in this table uniquely matches one author to one book’s ISBN. Figure 20.8 contains the data from the AuthorISBN table of the Books database. AuthorISBN
Column
Description
AuthorID
The author’s ID number, a foreign key to the Authors table.
ISBN
The ISBN for a book, a foreign key to the Titles table.
Fig. 20.7
| AuthorISBN
table of the Books database.
AuthorID
ISBN
AuthorID
ISBN
1
0131869000
2
0131450913
1
0131525239
2
0131426443
1
0131483986
2
0131857576
1
0131857576
2
0131483986
1
0131426443
2
0131525239
1
0131450913
2
0131869000
1
0131828274
3
0131450913
2
0131828274
4
0131828274
Fig. 20.8 | Data from the AuthorISBN table of Books.
814
Chapter 20
Database, SQL and ADO.NET
Foreign Keys Foreign keys can be specified when creating a table. A foreign key helps maintain the Rule of Referential Integrity—every foreign key value must appear as another table’s primary key value. This enables the DBMS to determine whether the AuthorID value for a particular row of the AuthorISBN table is valid. Foreign keys also allow related data in multiple tables to be selected from those tables—this is known as joining the data. (You will learn how to join data using SQL’s INNER JOIN operator in Section 20.4.4.) There is a one-tomany relationship between a primary key and a corresponding foreign key (e.g., one author can write many books). This means that a foreign key can appear many times in its own table, but can appear only once (as the primary key) in another table. For example, the ISBN 0131450913 can appear in several rows of AuthorISBN (because this book has several authors), but can appear only once in Titles, where ISBN is the primary key. Entity-Relationship Diagram for the Books Database Figure 20.9 is an entity-relationship (ER) diagram for the Books database. This diagram shows the tables in the database and the relationships among them. The first compartment in each box contains the table’s name. The names in italic font are primary keys (e.g., AuthorID in the Authors table). A table’s primary key uniquely identifies each row in the table. Every row must have a value in the primary key column, and the value of the key must be unique in the table. This is known as the Rule of Entity Integrity. Note that the names AuthorID and ISBN in the AuthorISBN table are both italic—together these form a composite primary key for the AuthorISBN table.
Common Programming Error 20.1 Not providing a value for every column in a primary key breaks the Rule of Entity Integrity and causes the DBMS to report an error. 20.1
Common Programming Error 20.2 Providing the same value for the primary key in multiple rows breaks the Rule of Entity Integrity and causes the DBMS to report an error. 20.2
The lines connecting the tables in Fig. 20.9 represent the relationships among the tables. Consider the line between the Authors and AuthorISBN tables. On the Authors end of the line, there is a 1, and on the AuthorISBN end, there is an infinity symbol ( ∞ ). This indicates a one-to-many relationship—for each author in the Authors table, there can be an arbitrary number of ISBNs for books written by that author in the AuthorISBN table
Authors AuthorID FirstName
AuthorISBN 1
AuthorID ISBN
LastName
Titles 1
ISBN Title EditionNumber Copyright
Fig. 20.9 | Entity-relationship diagram for the Books database.
20.4 SQL
815
(i.e., an author can write any number of books). Note that the relationship line links the AuthorID column in the Authors table (where AuthorID is the primary key) to the AuthorID column in the AuthorISBN table (where AuthorID is a foreign key)—the line between the tables links the primary key to the matching foreign key.
Common Programming Error 20.3 Providing a foreign-key value that does not appear as a primary-key value in another table breaks the Rule of Referential Integrity and causes the DBMS to report an error. 20.3
The line between the Titles and AuthorISBN tables illustrates a one-to-many relationship—a book can be written by many authors. Note that the line between the tables links the primary key ISBN in table Titles to the corresponding foreign key in table AuthorISBN. The relationships in Fig. 20.9 illustrate that the sole purpose of the AuthorISBN table is to provide a many-to-many relationship between the Authors and Titles tables—an author can write many books, and a book can have many authors.
20.4 SQL We now overview SQL in the context of the Books database. Later in the chapter, you will build C# applications that execute SQL queries and access their results using ADO.NET technology. Though the Visual C# IDE provides visual tools that hide some of the SQL used to manipulate databases, it is nevertheless important to understand SQL basics. Knowing the types of operations you can perform will help you develop more advanced database-intensive applications. Figure 20.10 lists some common SQL keywords used to form complete SQL statements—we discuss these keywords in the next several subsections. Other SQL keywords exist, but they are beyond the scope of this text. For additional information on SQL, please refer to the URLs listed in Section 20.11, Web Resources. SQL keyword
Description
SELECT
Retrieves data from one or more tables.
FROM
Specifies the tables involved in a query. Required in every query.
WHERE
Specifies optional criteria for selection that determine the rows to be retrieved, deleted or updated.
ORDER BY
Specifies optional criteria for ordering rows (e.g., ascending, descending).
INNER JOIN
Specifies optional operator for merging rows from multiple tables.
INSERT
Inserts rows in a specified table.
UPDATE
Updates rows in a specified table.
DELETE
Deletes rows from a specified table.
Fig. 20.10 | Common SQL keywords.
816
Chapter 20
Database, SQL and ADO.NET
20.4.1 Basic SELECT Query Let us consider several SQL queries that retrieve information from database Books. A SQL query “selects” rows and columns from one or more tables in a database. Such selections are performed by queries with the SELECT keyword. The basic form of a SELECT query is SELECT * FROM tableName
in which the asterisk (*) indicates that all the columns from the tableName table should be retrieved. For example, to retrieve all the data in the Authors table, use SELECT * FROM Authors
Note that the rows of the Authors table are not guaranteed to be returned in any particular order. You will learn how to specify criteria for sorting rows in Section 20.4.3. Most programs do not require all the data in a table. To retrieve only specific columns from a table, replace the asterisk (*) with a comma-separated list of the column names. For example, to retrieve only the columns AuthorID and LastName for all the rows in the Authors table, use the query SELECT AuthorID, LastName FROM Authors
This query returns only the data listed in Fig. 20.11.
20.4.2 WHERE Clause When users search a database for rows that satisfy certain selection criteria (formally called predicates), only rows that satisfy the selection criteria are selected. SQL uses the optional WHERE clause in a query to specify the selection criteria for the query. The basic form of a query with selection criteria is SELECT columnName1, columnName2, … FROM tableName WHERE criteria
For example, to select the Title, EditionNumber and Copyright columns from table Titles for which the Copyright date is more recent than 2004, use the query SELECT Title, EditionNumber, Copyright FROM Titles WHERE Copyright > '2004'
Figure 20.12 shows the result of the preceding query. AuthorID
LastName
1
Deitel
2
Deitel
3
Goldberg
4
Choffnes
Fig. 20.11 | AuthorID and LastName data from the Authors table.
20.4 SQL
Title
EditionNumber
Copyright
Java How to Program
6
2005
Visual C# 2005 How to Program
2
2006
C++ How to Program
5
2005
Visual Basic 2005 How to Program
3
2006
817
Fig. 20.12 | Titles with copyright dates after 2004 from table Titles. The WHERE clause criteria can contain the relational operators , =, = (equality), (inequality) and LIKE, as well as the logical operators AND, OR and NOT (discussed in Section 20.4.6). Operator LIKE is used for pattern matching with wildcard characters percent (%) and underscore (_). Pattern matching allows SQL to search for strings that match a given pattern. A pattern that contains a percent character (%) searches for strings that have zero or more characters at the percent character’s position in the pattern. For example, the following query locates the rows of all the authors whose last names start with the letter D:
SELECT AuthorID, FirstName, LastName FROM Authors WHERE LastName LIKE 'D%'
The preceding query selects the two rows shown in Fig. 20.13, because two of the four authors in our database have a last name starting with the letter D (followed by zero or more characters). The % in the WHERE clause’s LIKE pattern indicates that any number of characters can appear after the letter D in the LastName column. Note that the pattern string is surrounded by single-quote characters. An underscore ( _ ) in the pattern string indicates a single wildcard character at that position in the pattern. For example, the following query locates the rows of all the authors whose last names start with any character (specified by _ ), followed by the letter h, followed by any number of additional characters (specified by %): SELECT AuthorID, FirstName, LastName FROM Authors WHERE LastName LIKE '_h%'
The preceding query produces the row shown in Fig. 20.14, because only one author in our database has a last name that contains the letter h as its second letter. AuthorID
FirstName
LastName
1
Harvey
Deitel
2
Paul
Deitel
Fig. 20.13 | Authors from the Authors table whose last names start with D.
818
Chapter 20
Database, SQL and ADO.NET
AuthorID
FirstName
LastName
4
David
Choffnes
Fig. 20.14 | The only author from the Authors table whose last name contains h as the second letter.
20.4.3 ORDER BY Clause The rows in the result of a query can be sorted into ascending or descending order by using the optional ORDER BY clause. The basic form of a query with an ORDER BY clause is SELECT columnName1, columnName2, … FROM tableName ORDER BY column ASC SELECT columnName1, columnName2, … FROM tableName ORDER BY column DESC
where ASC specifies ascending order (lowest to highest), DESC specifies descending order (highest to lowest) and column specifies the column on which the sort is based. For example, to obtain the list of authors in ascending order by last name (Fig. 20.15), use the query SELECT AuthorID, FirstName, LastName FROM Authors ORDER BY LastName ASC
The default sorting order is ascending, so ASC is optional in the preceding query. To obtain the same list of authors in descending order by last name (Fig. 20.16), use the query SELECT AuthorID, FirstName, LastName FROM Authors ORDER BY LastName DESC
Multiple columns can be used for sorting with an ORDER BY clause of the form ORDER BY column1 sortingOrder, column2 sortingOrder, …
where sortingOrder is either ASC or DESC. Note that the sortingOrder does not have to be identical for each column. For example, the query SELECT Title, EditionNumber, Copyright FROM Titles ORDER BY Copyright DESC, Title ASC
AuthorID
FirstName
LastName
4
David
Choffnes
1
Harvey
Deitel
2
Paul
Deitel
3
Andrew
Goldberg
Fig. 20.15 | Authors from table Authors in ascending order by LastName.
20.4 SQL
AuthorID
FirstName
LastName
3
Andrew
Goldberg
1
Harvey
Deitel
2
Paul
Deitel
4
David
Choffnes
819
Fig. 20.16 | Authors from table Authors in descending order by LastName.
returns the rows of the Titles table sorted first in descending order by copyright date, then in ascending order by title (Fig. 20.17). This means that rows with higher Copyright values are returned before rows with lower Copyright values, and any rows that have the same Copyright values are sorted in ascending order by title. The WHERE and ORDER BY clauses can be combined in one query. For example, the query SELECT ISBN, Title, EditionNumber, Copyright FROM Titles WHERE Title LIKE '%How to Program' ORDER BY Title ASC
returns the ISBN, Title, EditionNumber and Copyright of each book in the Titles table that has a Title ending with “How to Program” and sorts them in ascending order by Title. The query results are shown in Fig. 20.18.
20.4.4 Merging Data from Multiple Tables: INNER JOIN Database designers typically normalize databases—i.e., split related data into separate tables to ensure that a database does not store redundant data. For example, the Books database has tables Authors and Titles. We use an AuthorISBN table to store “links” Title
EditionNumber
Copyright
Visual Basic 2005 How to Program
3
2006
Visual C# 2005 How to Program
2
2006
C++ How to Program
5
2005
Java How to Program
6
2005
C How to Program
4
2004
Internet & World Wide Web How to Program
3
2004
Operating Systems
3
2004
Fig. 20.17 | Data from Titles in descending order by Copyright and ascending order by Title.
820
Chapter 20
Database, SQL and ADO.NET
ISBN
Title
EditionNumb er
Copyrigh t
0131426443
C How to Program
4
2004
0131857576
C++ How to Program
5
2005
0131450913
Internet & World Wide Web How to
3
2004
Program 0131483986
Java How to Program
6
2005
0131869000
Visual Basic 2005 How to Program
3
2006
0131525239
Visual C# 2005 How to Program
2
2006
Fig. 20.18 | Books from table Titles whose titles end with How to Program in ascending order by Title.
between authors and titles. If we did not separate this information into individual tables, we would need to include author information with each entry in the Titles table. This would result in the database storing duplicate author information for authors who wrote more than one book. Often, it is desirable to merge data from multiple tables into a single result. This is referred to as joining the tables, and is specified by an INNER JOIN operator in the query. An INNER JOIN merges rows from two tables by testing for matching values in a column that is common to the tables. The basic form of an INNER JOIN is: SELECT columnName1, columnName2, … FROM table1 INNER JOIN table2 ON table1.columnName = table2.columnName
The ON clause of the INNER JOIN specifies the columns from each table that are compared to determine which rows are merged. For example, the following query produces a list of authors accompanied by the ISBNs for books written by each author: SELECT FirstName, LastName, ISBN FROM Authors INNER JOIN AuthorISBN ON Authors.AuthorID = AuthorISBN.AuthorID ORDER BY LastName, FirstName
The query combines the FirstName and LastName columns from table Authors and the ISBN column from table AuthorISBN, sorting the results in ascending order by LastName and FirstName. Note the use of the syntax tableName.columnName in the ON clause. This syntax (called a qualified name) specifies the columns from each table that should be compared to join the tables. The “tableName.” syntax is required if the columns have the same name in both tables. The same syntax can be used in any query to distinguish columns that have the same name in different tables.
Common Programming Error 20.4 In a SQL query, failure to qualify names for columns that have the same name in two or more tables is an error. 20.0
20.4 SQL
821
As always, the query can contain an ORDER BY clause. Figure 20.19 depicts the results of the preceding query, ordered by LastName and FirstName.
20.4.5 INSERT Statement The INSERT statement inserts a row into a table. The basic form of this statement is INSERT INTO tableName ( columnName1, columnName2, …, columnNameN ) VALUES ( value1, value2, …, valueN )
where tableName is the table in which to insert the row. The tableName is followed by a comma-separated list of column names in parentheses (this list is not required if the INSERT operation specifies a value for every column of the table in the correct order). The list of column names is followed by the SQL keyword VALUES and a comma-separated list of values in parentheses. The values specified here must match up with the columns specified after the table name in both order and type (e.g., if columnName1 is supposed to be the FirstName column, then value1 should be a string in single quotes representing the first name). Always explicitly list the columns when inserting rows—if the order of the columns in the table changes, using only VALUES may cause an error. The INSERT statement INSERT INTO Authors ( FirstName, LastName ) VALUES ( 'Sue', 'Smith' )
FirstName
LastName
ISBN
David
Choffnes
0131828274
Harvey
Deitel
0131869000
Harvey
Deitel
0131525239
Harvey
Deitel
0131483986
Harvey
Deitel
0131857576
Harvey
Deitel
0131426443
Harvey
Deitel
0131450913
Harvey
Deitel
0131828274
Paul
Deitel
0131869000
Paul
Deitel
0131525239
Paul
Deitel
0131483986
Paul
Deitel
0131857576
Paul
Deitel
0131426443
Paul
Deitel
0131450913
Paul
Deitel
0131828274
Andrew
Goldberg
0131450913
Fig. 20.19 | Authors and ISBNs for their books in ascending order by LastName and FirstName.
822
Chapter 20
Database, SQL and ADO.NET
inserts a row into the Authors table. The statement indicates that the values 'Sue' and 'Smith' are provided for the FirstName and LastName columns, respectively. We do not specify an AuthorID in this example because AuthorID is an identity column in the Authors table (see Fig. 20.3). For every row added to this table, SQL Server assigns a unique AuthorID value that is the next value in an autoincremented sequence (i.e., 1, 2, 3 and so on). In this case, Sue Smith would be assigned AuthorID number 5. Figure 20.20 shows the Authors table after the INSERT operation. Not every DBMS supports identity or autoincremented columns.
Common Programming Error 20.5 It is an error to specify a value for an identity column.
20.0
Common Programming Error 20.6 SQL uses the single-quote (') character to delimit strings. To specify a string containing a single quote (e.g., O’Malley) in a SQL statement, there must be two single quotes in the position where the single-quote character appears in the string (e.g., 'O''Malley'). The first of the two singlequote characters acts as an escape character for the second. Not escaping single-quote characters in a string that is part of a SQL statement is a syntax error. 20.6
20.4.6 UPDATE Statement An UPDATE statement modifies data in a table. The basic form of the UPDATE statement is UPDATE tableName SET columnName1 = value1, columnName2 = value2, …, columnNameN = valueN WHERE criteria
where tableName is the table to update. The tableName is followed by keyword SET and a comma-separated list of column name-value pairs in the format columnName = value. The optional WHERE clause provides criteria that determine which rows to update. Though not required, the WHERE clause is typically used, unless a change is to be made to every row. The UPDATE statement UPDATE Authors SET LastName = 'Jones' WHERE LastName = 'Smith' AND FirstName = 'Sue'
AuthorID
FirstName
LastName
1
Harvey
Deitel
2
Paul
Deitel
3
Andrew
Goldberg
4
David
Choffnes
5
Sue
Smith
Fig. 20.20 | Table Authors after an INSERT operation.
20.4 SQL
823
updates a row in the Authors table. Keyword AND is a logical operator that, like the C# && operator, returns true if and only if both of its operands are true. Thus, the preceding statement assigns to LastName the value Jones for the row in which LastName is equal to Smith and FirstName is equal to Sue. [Note: If there are multiple rows with the first name “Sue” and the last name “Smith,” this statement modifies all such rows to have the last name “Jones.”] Figure 20.21 shows the Authors table after the UPDATE operation has taken place. SQL also provides other logical operators, such as OR and NOT, which behave like their C# counterparts.
20.4.7 DELETE Statement A DELETE statement removes rows from a table. The basic form of a DELETE statement is DELETE FROM tableName WHERE criteria
where tableName is the table from which to delete. The optional WHERE clause specifies the criteria used to determine which rows to delete. The DELETE statement DELETE FROM Authors WHERE LastName = 'Jones' AND FirstName = 'Sue'
deletes the row for Sue Jones in the Authors table. DELETE statements can delete multiple rows if the rows all meet the criteria in the WHERE clause. Figure 20.22 shows the Authors table after the DELETE operation has taken place.
AuthorID
FirstName
LastName
1
Harvey
Deitel
2
Paul
Deitel
3
Andrew
Goldberg
4
David
Choffnes
5
Sue
Jones
Fig. 20.21 | Table Authors after an UPDATE operation.
AuthorID
FirstName
LastName
1
Harvey
Deitel
2
Paul
Deitel
3
Andrew
Goldberg
4
David
Choffnes
Fig. 20.22 | Table Authors after a DELETE operation.
824
Chapter 20
Database, SQL and ADO.NET
SQL Wrap-Up This concludes our SQL introduction. We demonstrated several commonly used SQL keywords, formed SQL queries that retrieved data from databases and formed other SQL statements that manipulated data in a database. Next, we introduce the ADO.NET object model, which allows C# applications to interact with databases. As you will see, ADO.NET objects manipulate databases using SQL statements like those presented here.
20.5 ADO.NET Object Model The ADO.NET object model provides an API for accessing database systems programmatically. ADO.NET was created for the .NET framework to replace Microsoft’s ActiveX Data Objects™ (ADO) technology. As will be discussed in the next section, the IDE features visual programming tools that simplify the process of using a database in your projects. While you may not need to work directly with many ADO.NET objects to develop simple applications, basic knowledge of how the ADO.NET object model works is important for understanding data access in C#.
Namespaces System.Data, System.Data.OleDb and System.Data.SqlClient Namespace System.Data is the root namespace for the ADO.NET API. The other important ADO.NET namespaces, System.Data.OleDb and System.Data.SqlClient, contain classes that enable programs to connect with and manipulate data sources—locations that contain data, such as a database or an XML file. Namespace System.Data.OleDb contains classes that are designed to work with any data source, whereas System.Data.SqlClient contains classes that are optimized to work with Microsoft SQL Server databases. The chapter examples manipulate SQL Server 2005 Express databases, so we use the classes of namespace System.Data.SqlClient. SQL Server Express 2005 is provided with Visual C# 2005 Express. It can also be downloaded from lab.msdn.microsoft.com/express/ sql/default.aspx. An object of class SqlConnection (namespace System.Data.SqlClient) represents a connection to a data source—specifically a SQL Server database. A SqlConnection object keeps track of the location of the data source and any settings that specify how the data source is to be accessed. A connection is either active (i.e., open and permitting data to be sent to and retrieved from the data source) or closed. An object of class SqlCommand (namespace System.Data.SqlClient) represents a SQL command that a DBMS can execute on a database. A program can use SqlCommand objects to manipulate a data source through a SqlConnection. The program must open the connection to the data source before executing one or more SqlCommands and close the connection once no further access to the data source is required. A connection that remains active for some length of time to permit multiple data operations is known as a persistent connection. Class DataTable (namespace System.Data) represents a table of data. A DataTable contains a collection of DataRows that represent the table’s data. A DataTable also has a collection of DataColumns that describe the columns in a table. DataRow and DataColumn are both located in namespace System.Data. An object of class System.Data.DataSet, which consists of a set of DataTables and the relationships among them, represents a cache of data—data that a program stores temporarily in local memory. The structure of a DataSet mimics the structure of a relational database.
20.6 Programming with ADO.NET: Extracting Information from a Database
825
ADO.NET’s Disconnected Model An advantage of using class DataSet is that it is disconnected—the program does not need a persistent connection to the data source to work with data in a DataSet. Instead, the program connects to the data source to populate the DataSet (i.e., fill the DataSet’s DataTables with data), but disconnects from the data source immediately after retrieving the desired data. The program then accesses and potentially manipulates the data stored in the DataSet. The program operates on this local cache of data, rather than the original data in the data source. If the program makes changes to the data in the DataSet that need to be permanently saved in the data source, the program reconnects to the data source to perform an update then disconnects promptly. Thus the program does not require any active, persistent connection to the data source. An object of class SqlDataAdapter (namespace System.Data.SqlClient) connects to a SQL Server data source and executes SQL statements to both populate a DataSet and update the data source based on the current contents of a DataSet. A SqlDataAdapter maintains a SqlConnection object that it opens and closes as needed to perform these operations using SqlCommands. We demonstrate populating DataSets and updating data sources later in this chapter.
20.6 Programming with ADO.NET: Extracting Information from a Database In this section, we demonstrate how to connect to a database, query the database and display the result of the query. You will notice that there is little code in this section. The IDE provides visual programming tools and wizards that simplify accessing data in your projects. These tools establish database connections and create the ADO.NET objects necessary to view and manipulate the data through GUI controls. The example in this section connects to the SQL Server Books database that we have discussed throughout this chapter. The Books.mdf file that contains the database can be found with the chapter’s examples (www.deitel.com/books/csharpforprogrammers2).
20.6.1 Displaying a Database Table in a DataGridView This example performs a simple query on the Books database that retrieves the entire Authors table and displays the data in a DataGridView (a control from namespace System.Windows.Forms that can display a data source in a GUI—see the output in Fig. 20.32 later in this section). First, we demonstrate how to connect to the Books database and include it as a data source in your project. Once the Books database is established as a data source, you can display the data from the Authors table in a DataGridView simply by dragging and dropping items in the project’s Design view.
Step 1: Creating the Project Create a new Windows Application named DisplayTable. Change the Form name to DisplayTableForm and change the source file name to DisplayTable.cs. Then set the Form’s Text property to Display Table. Step 2: Adding a Data Source to the Project To interact with a data source (e.g., a database), you must add it to the project using the Data Sources window, which lists the data that your project can access. Open the Data
826
Chapter 20
Database, SQL and ADO.NET
Sources window (Fig.
20.23) by selecting Data > Show Data Sources or by clicking the tab to the right of the tab for the Solution Explorer. In the Data Sources window, click Add New Data Source… to open the Data Source Configuration Wizard (Fig. 20.24). This wizard guides you through connecting to a database and choosing the parts of the database you will want to access in your project. Data Sources window
Data menu
Fig. 20.23 | Adding a data source to a project.
Fig. 20.24 | Choosing the data source type in the Data Source Configuration Wizard.
20.6 Programming with ADO.NET: Extracting Information from a Database
827
Step 3: Choosing the Data Source Type to Add to the Project The first screen of the Data Source Configuration Wizard (Fig. 20.24) asks you to choose the data source type you wish to include in the project. Select Database and click Next >. Step 4: Adding a New Database Connection You must next choose the connection that will be used to connect to the database (i.e., the actual source of the data). Click New Connection... to open the Add Connection dialog (Fig. 20.25). If the Data Source is not set to Microsoft SQL Server Database File (SqlClient) , click Change…, select Microsoft SQL Server Database File and click OK. In the Add Connection dialog, click Browse..., locate the Books.mdf database file on your computer, select it and click Open. You can click Test Connection to verify that the IDE can connect to the database through SQL Server. Click OK to create the connection. Step 5: Choosing the Books.mdf Data Connection Now that you have created a connection to the Books.mdf database, you can select and use this connection to access the database. Click Next > to set the connection, then click Yes when asked whether you want to move the database file to your project (Fig. 20.26). Step 6: Saving the Connection String The next screen (Fig. 20.27) asks you whether you want to save the connection string to the application configuration file. A connection string specifies the path to a database file on disk, as well as some additional settings that determine how to access the database. Saving the connection string in a configuration file makes it easy to change the connection settings at a later time. Leave the default selections and click Next > to proceed.
Fig. 20.25 | Adding a new data connection.
828
Chapter 20
Database, SQL and ADO.NET
Fig. 20.26 | Choosing the Books.mdf data connection.
Fig. 20.27 | Saving the connection string to the application configuration file. Step 7: Selecting the Database Objects to Include in Your DataSet The IDE retrieves information about the database you selected and prompts you to select the database objects (i.e., the parts of the database) that you want your project to be able to access (Fig. 20.28). Recall that programs typically access a database’s contents through a cache of the data, which is stored in a DataSet. In response to your selections in this screen, the IDE will generate a class derived from System.Data.DataSet that is designed
20.6 Programming with ADO.NET: Extracting Information from a Database
829
Fig. 20.28 | Choosing the database objects to include in the DataSet. specifically to store data from the Books database. Click the checkbox to the left of Tables to indicate that the custom DataSet should cache (i.e., locally store) the data from all the tables in the Books database—Authors, AuthorISBN and Titles. [Note: You can also expand the Tables node to select specific tables. The other database objects listed do not contain any data in our sample Books database and are beyond the scope of the book.] By default, the IDE names the DataSet BooksDataSet, though it is possible to specify a different name in this screen. Finally, click Finish to complete the process of adding a data source to the project.
Step 8: Viewing the Data Source in the Data Sources Window Notice that a BooksDataSet node now appears in the Data Sources window (Fig. 20.29) with child nodes for each table in the Books database—these nodes represent the DataTables of the BooksDataSet. Expand the Authors node and you will see the table’s columns—the DataSet’s structure mimics that of the actual Books database. Step 9: Viewing the Database in the Solution Explorer Books.mdf is now listed as a node in the Solution Explorer (Fig. 20.30), indicating that the database is now part of this project. In addition, the Solution Explorer now lists a new node named BooksDataSet.xsd. You learned in Chapter 19 that a file with the .xsd extension is an XML Schema document, which specifies the structure of a set of XML documents. The IDE uses an XML Schema document to represent a DataSet’s structure, including the tables that comprise the DataSet and the relationships among them. When you added the Books database as a data source, the IDE created the BooksDataSet.xsd file based on the structure of the Books database. The IDE then generated class BooksDataSet from the schema (i.e., structure) described by the .xsd file.
830
Chapter 20
Database, SQL and ADO.NET
Data Sources window
Fig. 20.29 | Viewing a data source listed in the Data Sources window.
Fig. 20.30 | Viewing a database listed in the Solution Explorer.
20.6 Programming with ADO.NET: Extracting Information from a Database
831
Displaying the Authors Table Now that you have added the Books database as a data source, you can display the data from the database’s Authors table in your program. The IDE provides design tools that allow you to display data from a data source on a Form without writing any code. Simply drag and drop items from the Data Sources window onto a Form, and the IDE generates the GUI controls and code necessary to display the selected data source’s content. To display the Authors table of the Books database, drag the Authors node from the Data Sources window to the Form. Figure 20.31 presents the Design view after we performed this action and resized the controls. The IDE generates two GUI controls that appear on DisplayTableForm—authorsBindingNavigator and authorsDataGridView. The IDE also generates several additional non-visual components that appear in the component tray—the gray region below the Form in Design view. We use the IDE’s default names for these autogenerated components (and others throughout the chapter) to show exactly what the IDE creates. We briefly discuss the authorsBindingNavigator and authorsDataGridView controls here. The next section discusses all of the autogenerated components in detail and explains how the IDE uses these components to connect the GUI controls to the Authors table of the Books database. A DataGridView displays data organized in rows and columns that correspond to the rows and columns of the underlying data source. In this case, the DataGridView displays
authorsBindingNavigator
authorsDataGridView
Component tray
Fig. 20.31 | Design view after dragging the Authors data source node to the Form.
832
Chapter 20
Database, SQL and ADO.NET
the data of the Authors table, so the control has columns named AuthorID, FirstName and LastName. In Design view, the control does not display any rows of actual data below the column headers. The data is retrieved from the database and displayed in the DataGridView only at runtime. Execute the program. When the Form loads, the DataGridView contains four rows of data—one for each row of the Authors table (Fig. 20.32). The strip of buttons below the title bar of the window is a BindingNavigator, which enables users to browse and manipulate data displayed by another GUI control (in this case, a DataGridView) on the Form. A BindingNavigator’s buttons resemble the controls on a CD or DVD player and allow you to move to the first row of data, the preceding row, the next row and the last row. The control also displays the currently selected row number in a text box. You can use this text box to enter the number of a row that you want to select. The authorsBindingNavigator in this example allows you to “navigate” the Authors table displayed in the authorsDataGridView. Clicking the buttons or entering a value in the text box causes the DataGridView to select the appropriate row. An arrow in the DataGridView’s leftmost column indicates the currently selected row. A BindingNavigator also has buttons that allow you to add a new row, delete a row and save changes back to the underlying data source (in this case, the Authors table of the Books database). Clicking the button with the yellow plus icon ( ) adds a new row to the DataGridView. However, simply typing values in the FirstName and LastName columns does not insert a new row in the Authors table. To add the new row to the database on disk, click the Save button (the button with the disk icon, ). Clicking the button with the red X ( ) deletes the currently selected row from the DataGridView. Again, you must click the Save button to make the change in the database. Test these buttons. Execute the program and add a new row, then save the changes and close the program. When you restart the program, you should see that the new row was saved to the database and appears in the DataGridView. Now delete the new row and click the Save button. Close and restart the program to see that the new row no longer exists in the database.
Move to first row Move to next row Move to previous row Move to last row (a)
Delete the current row Add a new row Save changes (b)
Currently selected row
Fig. 20.32 | Displaying the Authors table in a DataGridView.
20.6 Programming with ADO.NET: Extracting Information from a Database
833
20.6.2 How Data Binding Works The technique through which GUI controls are connected to data sources is known as data binding. The IDE allows controls, such as a DataGridView, to be bound to a data source, such as a DataSet that represents a table in a database. Any changes you make through the application to the underlying data source will automatically be reflected in the way the data is presented in the data-bound control (e.g., the DataGridView). Likewise, modifying the data in the data-bound control and saving the changes updates the underlying data source. In the current example, the DataGridView is bound to the DataTable of the BooksDataSet that represents the Authors table in the database. Dragging the Authors node from the Data Sources window to the Form caused the IDE to create this data binding for you, using several autogenerated components (i.e., objects) in the component tray. Figure 20.33 models these objects and their associations, which the following sections examine in detail to explain how data binding works. BooksDataSet
As discussed in Section 20.6.1, adding the Books database to the project enabled the IDE to generate the BooksDataSet. Recall that a DataSet represents a cache of data that mimics the structure of a relational database. You can explore the structure of the BooksDataSet
SQL Server database Books.mdf
Executes SQL statements against authorsTableAdapter Updates database with current data in
Places database query results in Contains
booksDataSet
Authors
Encapsulates authorsBindingSource Applies changes to the data source based on user input through authorsDataGridView
Displays data from
Navigates / manipulates the data source through authorsBindingNavigator
Fig. 20.33 | Data binding architecture used to display the Authors table of the Books database in a GUI.
834
Chapter 20
Database, SQL and ADO.NET
in the Data Sources window. A DataSet’s structure can be determined at execution time or at design time. An untyped DataSet’s structure (i.e., the tables that comprise it and the relationships among them) is determined at execution time based on the result of a specific query. Tables and column values are accessed using indices into collections of DataTables and DataRows, respectively. The type of each piece of data in an untyped DataSet is unknown at design time. BooksDataSet, however, is created by the IDE at design time as a strongly typed DataSet. BooksDataSet (a derived class of DataSet) contains objects of classes derived from DataTable that represent the tables in the Books database. BooksDataSet provides properties corresponding to the objects whose names match those of the underlying tables. For example, booksDataSet.Authors represents a cache of the data in the Authors table. Each DataTable contains a collection of DataRows. Each DataRow contains members whose names and types correspond to those of the columns of the underlying database table. Thus, booksDataSet.Authors[ 0 ].AuthorID refers to the AuthorID of the first row of the Authors table in the Books database. Note that zero-based indices are used to access DataRows in a DataTable. The booksDataSet object in the component tray is an object of the BooksDataSet class. When you indicate that you want to display the contents of the Authors table on the Form, the IDE generates a BooksDataSet object to store the data that Form will display. This is the data to which the DataGridView will be bound. The DataGridView does not display data from the database directly. Instead, it displays the contents of a BooksDataSet object. As we discuss shortly, the AuthorsTableAdapter fills the BooksDataSet object with data retrieved from the database by executing a SQL query. AuthorsTableAdapter
The AuthorsTableAdapter is the component that interacts with the Books database on disk (i.e., the Books.mdf file). When other components need to retrieve data from the database or write data to the database, they invoke the methods of the AuthorsTableAdapter. Class AuthorsTableAdapter is generated by the IDE when you drag a table from the Books database onto the Form. The authorsTableAdapter object in the component tray is an object of this class. The AuthorsTableAdapter is responsible for filling the BooksDataSet with the Authors data from the database—this stores a copy of the Authors table in local memory. As you will soon see, this cached copy can be modified during program execution. Thus, the AuthorsTableAdapter is also responsible for updating the database when the data in the BooksDataSet changes. Class AuthorsTableAdapter encapsulates a SqlDataAdapter object, which contains SqlCommand objects that specify how the SqlDataAdapter selects, inserts, updates and deletes data in the database. Recall from Section 20.5 that a SqlCommand object must have a SqlConnection object through which the SqlCommand can communicate with a database. In this example, the AuthorsTableAdapter sets the Connection property of each of the SqlDataAdapter’s SqlCommand objects, based on the connection string that refers to the Books database. To interact with the database, the AuthorsTableAdapter invokes the methods of its SqlDataAdapter, each of which executes the appropriate SqlCommand object. For example, to fill the BooksDataSet’s Authors table, the AuthorsTableAdapter’s Fill method invokes its SqlDataAdapter’s Fill method, which executes a SqlCommand object representing the SELECT query
20.6 Programming with ADO.NET: Extracting Information from a Database
835
SELECT AuthorID, FirstName, LastName FROM Authors
This query selects all the rows and columns of the Authors table and places them in booksDataSet.Authors. You will see an example of authorsTableAdapter’s Fill method being invoked shortly. authorsBindingSource and authorsDataGridView
The authorsBindingSource object (an object of class BindingSource) identifies a data source that a program can bind to a control and serves as an intermediary between a databound GUI control and its data source. In this example, the IDE uses a BindingSource object to connect the authorsDataGridView to booksDataSet.Authors. To achieve this data binding, the IDE first sets authorsBindingSource’s DataSource property to BooksDataSet. This property specifies the DataSet that contains the data to be bound. The IDE then sets the DataMember property to Authors. This property identifies a specific table within the DataSource. After configuring the authorsBindingSource object, the IDE assigns this object to authorsDataGridView’s DataSource property to indicate what the DataGridView will display. A BindingSource object also manages the interaction between a data-bound GUI control and its underlying data source. If you edit the data displayed in a DataGridView and want to save changes to the data source, your code must invoke the EndEdit method of the BindingSource object. This method applies the changes made to the data through the GUI control (i.e., the pending changes) to the data source bound to that control. Note that this updates only the DataSet—an additional step is required to permanently update the database itself. You will see an example of this shortly, when we present the code generated by the IDE in the DisplayTable.cs file. authorsBindingNavigator
Recall that a BindingNavigator allows you to move through (i.e., navigate) and manipulate (i.e., add or delete rows) data bound to a control on a Form. A BindingNavigator communicates with a BindingSource (specified in the BindingNavigator’s BindingSource property) to carry out these actions in the underlying data source (i.e., the DataSet). The BindingNavigator does not interact with the data-bound control. Instead, it invokes BindingSource methods that cause the data-bound control to update its presentation of the data. For example, when you click the BindingNavigator’s button to add a new row, the BindingNavigator invokes a method of the BindingSource. The BindingSource then adds a new row to its associated DataSet. Once this DataSet is modified, the DataGridView displays the new row, because the DataGridView and the BindingNavigator are bound to the same BindingSource object (and thus the same DataSet).
Examining the Autogenerated Code for DisplayTableForm Figure 20.34 presents the code for DisplayTableForm. Note that you do not need to write any of this code—the IDE generates it when you drag and drop the Authors table from the Data Sources window onto the Form. We modified the autogenerated code to add comments, split long lines for display purposes and remove unnecessary using declarations. The IDE also generates a considerable amount of additional code, such as the code that defines classes BooksDataSet and AuthorsTableAdapter, as well as the designer code that declares the autogenerated objects in the component tray. The additional IDE-gener-
// Fig. 20.34: DisplayTable.cs // Displays data from a database table in a DataGridView. using System; using System.Windows.Forms; namespace DisplayTable { public partial class DisplayTableForm : Form { public DisplayTableForm() { InitializeComponent(); } // end constructor // Click event handler for the Save Button in the // BindingNavigator saves the changes made to the data private void authorsBindingNavigatorSaveItem_Click( object sender, EventArgs e ) { this.Validate(); this.authorsBindingSource.EndEdit(); this.authorsTableAdapter.Update( this.booksDataSet.Authors ); } // end method authorsBindingNavigatorSaveItem_Click // loads data into the booksDataSet.Authors table, // which is then displayed in the DataGridView private void DisplayTableForm_Load( object sender, EventArgs e ) { // TODO: This line of code loads data into the // 'booksDataSet.Authors' table. You can move, or remove it, // as needed. this.authorsTableAdapter.Fill( this.booksDataSet.Authors ); } // end method DisplayTableForm_Load } // end class DisplayTableForm } // end namespace DisplayTable
(a)
(b)
Fig. 20.34 | Auto-generated code for displaying data from a database table in a DataGridView control.
20.7 Querying the Books Database
837
ated code resides in files visible in the Solution Explorer when you select Show All Files. We present only the code in DisplayTable.cs, because it is the only file you’ll need to modify. Lines 17–23 contain the Click event handler for the Save button in the AuthorsBindingNavigator. Recall that you click this button to save changes made to the data in the DataGridView in the underlying data source (i.e., the Authors table of the Books database). Saving the changes is a two-step process: 1. The DataSet associated with the DataGridView (indicated by its BindingSource) must be updated to include any changes made by the user. 2. The database on disk must be updated to match the new contents of the DataSet. Before the event handler saves any changes, line 21 invokes this.Validate() to validate the controls on the Form. If you implement Validating or Validated events for any of Form’s controls, these events enable you to validate user input and potentially indicate errors for invalid data. Line 21 invokes authorsBindingSource’s EndEdit method to ensure that the object’s associated data source (booksDataSet.Authors) is updated with any changes made by the user to the currently selected row in the DataGridView (e.g., adding a row, changing a column value). Any changes to other rows were applied to the DataSet when you selected another row. Line 22 invokes authorsTableAdapter’s Update method to write the modified version of the Authors table (in memory) to the SQL Server database on disk. The Update method executes the SQL statements (encapsulated in SqlCommand objects) necessary to make the database’s Authors table match booksDataSet.Authors. The Load event handler for DisplayTableForm (lines 27–33) executes when the program loads. This event handler fills the in-memory DataSet with data from the SQL Server database on disk. Once the DataSet is filled, the GUI control bound to it can display its data. Line 32 calls authorsTableAdapter’s Fill method to retrieve information from the database, placing this information in the DataSet member provided as an argument. Recall that authorsTableAdapter was generated by the IDE to execute SqlCommands over the connection we created within the Data Source Configuration Wizard . Thus, the Fill method here executes a SELECT statement to retrieve all the rows of the Authors table of the Books database, then places the result of this query in booksDataSet.Authors. Recall that authorsDataGridView’s DataSource property is set to authorsBindingSource (which references booksDataSet.Authors). Thus, after this data source is loaded, the authorsDataGridView automatically displays the data retrieved from the database.
20.7 Querying the Books Database Now that you have seen how to display an entire database table in a DataGridView, we demonstrate how to execute specific SQL SELECT queries on a database and display the results. Although this example only queries the data, the application could be modified easily to execute other SQL statements. Perform the following steps to build the example application, which executes custom queries against the Titles table of the Books database.
Step 1: Creating the Project Create a new Windows Application named DisplayQueryResult. Rename the Form DisplayQueryResultForm and name its source file DisplayQueryResult.cs, then set the Form’s Text property to Display Query Result.
838
Chapter 20
Database, SQL and ADO.NET
Step 2: Adding a Data Source to the Project Perform the steps in Section 20.6.1 to include the Books database as a data source in the project. Step 3: Creating a DataGridView to Display the Titles Table Drag the Titles node from the Data Sources window onto the Form to create a DataGridView that will display the entire contents of the Titles table. Step 4: Adding Custom Queries to the TitlesTableAdapter Recall that invoking a TableAdapter’s Fill method populates the DataSet passed as an argument with the entire contents of the database table that corresponds to that TableAdapter. To populate a DataSet member (i.e., a DataTable) with only a portion of a table (e.g., books with copyright dates of 2006), you must add a method to the TableAdapter that fills the specified DataTable with the results of a custom query. The IDE provides the TableAdapter Query Configuration Wizard to perform this task. To open this wizard, first right click the BooksDataSet.xsd node in the Solution Explorer and choose View Designer. You can also click the Edit DataSet with Designer icon ( ). Either of these actions opens the Dataset Designer (Fig. 20.35), which displays a visual representation of the BooksDataSet (i.e., the tables AuthorISBN, Authors and Titles and the relationships among them). The Dataset Designer lists each table’s columns and the autogenerated TableAdapter that accesses the table. Select the TitlesTableAdapter by clicking its name, then right click the name and select Add Query… to begin the TableAdapter Query Configuration Wizard (Fig. 20.36).
Dataset Designer
TitlesTableAdapter
Fig. 20.35 | Viewing the BooksDataSet in the Dataset Designer.
20.7 Querying the Books Database
839
Step 5: Choosing How the TableAdapter Should Access the Database On the first screen of the wizard (Fig. 20.36), keep the default option Use SQL Statements and click Next. Step 6: Choosing a Query Type On the next screen of the wizard (Fig. 20.37), keep the default option SELECT which returns rows and click Next.
Fig. 20.36 | TableAdapter Query Configuration Wizard to add a query to a TableAdapter.
Fig. 20.37 | Choosing the type of query to be generated for the TableAdapter.
840
Chapter 20
Database, SQL and ADO.NET
Step 7: Specifying a SELECT Statement for the Query The next screen (Fig. 20.38) asks you to enter a query that will be used to retrieve data from the Books database. Note that the default SELECT prefixes Titles with “dbo.”. This prefix stands for “database owner” and indicates that the table Titles belongs to the database owner (i.e., you). In cases where you need to reference a table owned by another user of the system, this prefix would be replaced by the owner’s username. You can modify the SQL statement in the text box here (using the SQL syntax discussed in Section 20.4), or you can click Query Builder… to design and test the query using a visual tool. Step 8: Building a Query with Query Builder Click the Query Builder… button to open the Query Builder (Fig. 20.39). The top portion of the Query Builder window contains a box listing the columns of the Titles table. By default, each column is checked (Fig. 20.39(a)), indicating that each column should be returned by the query. The middle portion of the window contains a table in which each row corresponds to a column in the Titles table. To the right of the column names are columns in which you can enter values or make selections to modify the query. For example, to create a query that selects only books that are copyright 2006, enter the value 2006 in the Filter column of the Copyright row. Note that the Query Builder modifies your input to be “= '2006'” and adds an appropriate WHERE clause to the SELECT statement displayed in the middle of Fig. 20.39(b). Click the Execute Query button to test the query and display the results in the bottom portion of the Query Builder window. For more Query Builder information, see msdn2.microsoft.com/library/ms172013.aspx. Step 9: Closing the Query Builder Click OK to close the Query Builder and return to the TableAdapter Query Configuration Wizard (Fig. 20.40), which now displays the SQL query created in the preceding step. Click Next to continue.
Fig. 20.38 | Specifying a SELECT statement for the query.
20.7 Querying the Books Database
841
(a)
(b)
Fig. 20.39 | Query Builder after adding a WHERE clause by entering a value in the Filter column.
Step 10: Setting the Names of the Autogenerated Methods That Perform the Query After you specify the SQL query, you must name the methods that the IDE will generate to perform the query (Fig. 20.41). Two methods are generated by default—a “Fill method” that fills a DataTable parameter with the query result and a “Get method” that returns a new DataTable filled with the query result. The text boxes to enter names for these meth-
842
Chapter 20
Database, SQL and ADO.NET
Fig. 20.40 | The SELECT statement created by the Query Builder. ods are prepopulated with FillBy and GetDataBy, respectively. Modify these names to FillWithCopyright2006 and GetDataWithCopyright2006, as shown in Fig. 20.41. Finally, click Finish to complete the wizard and return to the Dataset Designer (Fig. 20.42). Note that these methods are now listed in the TitlesTableAdapter section of the box representing the Titles table.
Fig. 20.41 | Specifying names for the methods to be added to the TitlesTableAdapter.
20.7 Querying the Books Database
843
Fig. 20.42 | Dataset Designer after adding Fill and Get methods to the TitlesTableAdapter.
Step 11: Adding an Additional Query Repeat Steps 4–10 to add another query that selects all books whose titles end with the text “How to Program” and sorts the results by title in ascending order (see Section 20.4.3). In the Query Builder, enter LIKE '%How to Program' in the Title row’s Filter column. To specify the sort order, select Ascending in the Sort Type column of the Title row. In the final step of the TableAdapter Query Configuration Wizard , name the Fill and Get methods FillWithHowToProgramBooks and GetDataForHowToProgramBooks, respectively. Step 12: Adding a ComboBox to the Form Return to Design view and add below the DataGridView a ComboBox named queriesComboBox to the Form. Users will use this control to choose a SELECT query to execute, whose result will be displayed in the DataGridView. Add three items to queriesComboBox—one to match each of the three queries that the TitlesTableAdapter can now perform: SELECT ISBN, Title, EditionNumber, Copyright FROM Titles SELECT ISBN, Title, EditionNumber, Copyright FROM Titles WHERE (Copyright = '2006') SELECT ISBN, Title, EditionNumber, Copyright FROM Titles WHERE (Title LIKE '%How to Program') ORDER BY Title
Step 13: Customizing the Form’s Load Event Handler Add a line of code to the autogenerated DisplayQueryResultForm_Load event handler, which sets the initial SelectedIndex of the queriesComboBox to 0. Recall that the Load event handler calls the Fill method by default, which executes the first query (the item in
844
Chapter 20
Database, SQL and ADO.NET
index 0). Thus, setting the SelectedIndex causes the ComboBox to display the query that is initially performed when DisplayQueryResultForm first loads.
Step 14: Programming an Event Handler for the ComboBox Next you must write code that will execute the appropriate query each time the user chooses a different item from queriesComboBox. Double click queriesComboBox in Design view to generate a queriesComboBox_SelectedIndexChanged event handler (lines 42–61) in the DisplayQueryResult.cs file (Fig. 20.43). Then to the event handler add a switch statement that invokes the method of titlesTableAdapter that executes the query associated with the ComboBox’s current selection (lines 47–60). Recall that method Fill (line 50) executes a SELECT query that selects all rows, method FillWithCopyright2006 (lines 53–54) executes a SELECT query that selects all rows in which the copyright year is 2006 and method FillWithHowToProgramBooks (lines 57–58) executes a query that selects all rows that have “How to Program” at the end of their titles and sorts them in ascending order by title. Each method fills BooksDataSet.Titles with only those rows returned by the corresponding query. Thanks to the data binding relationships created by the IDE, refilling booksDataSet.Titles causes the TitlesDataGridView to display the selected query’s result with no additional code. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
// Fig. 20.43: DisplayQueryResult.cs // Displays the result of a user-selected query in a DataGridView. using System; using System.Windows.Forms; namespace DisplayQueryResult { public partial class DisplayQueryResultForm : Form { public DisplayQueryResultForm() { InitializeComponent(); } // end DisplayQueryResultForm constructor // Click event handler for the Save Button in the // BindingNavigator saves the changes made to the data private void titlesBindingNavigatorSaveItem_Click( object sender, EventArgs e ) { this.Validate(); this.titlesBindingSource.EndEdit(); this.titlesTableAdapter.Update( this.booksDataSet.Titles ); } // end method titlesBindingNavigatorSaveItem_Click // loads data into the booksDataSet.Titles table, // which is then displayed in the DataGridView private void DisplayQueryResultForm_Load( object sender, EventArgs e ) {
Fig. 20.43 | Displaying the result of a user-selected query in a DataGridView. (Part 1 of 3.)
// TODO: This line of code loads data into the // 'booksDataSet.Titles' table. You can move, or remove it, // as needed. this.titlesTableAdapter.Fill( this.booksDataSet.Titles ); // set the ComboBox to show the default query that // selects all books from the Titles table queriesComboBox.SelectedIndex = 0; } // end method DisplayQueryResultForm_Load // loads data into the booksDataSet.Titles table based on // user-selected query private void queriesComboBox_SelectedIndexChanged( object sender, EventArgs e ) { // fill the Titles DataTable with // the result of the selected query switch ( queriesComboBox.SelectedIndex ) { case 0: // all books titlesTableAdapter.Fill( booksDataSet.Titles ); break; case 1: // books with copyright year 2006 titlesTableAdapter.FillWithCopyright2006( booksDataSet.Titles ); break; case 2: // How to Program books, sorted by Title titlesTableAdapter.FillWithHowToProgramBooks( booksDataSet.Titles ); break; } // end switch } // end method queriesComboBox_SelectedIndexChanged } // end class DisplayQueryResultForm } // end namespace DisplayQueryResult
(a)
Fig. 20.43 | Displaying the result of a user-selected query in a DataGridView. (Part 2 of 3.)
846
Chapter 20
Database, SQL and ADO.NET
(b)
(c)
Fig. 20.43 | Displaying the result of a user-selected query in a DataGridView. (Part 3 of 3.) Figure 20.43 also displays the output for DisplayQueryResultForm. Figure 20.43(a) depicts the result of retrieving all rows from the Titles table. Figure 20.43(b) demonstrates the second query, which retrieves only rows for books with a 2006 copyright. Finally, Fig. 20.43(c) demonstrates the third query, which selects rows for How to Program books and sorts them in ascending order by title.
20.8 Programming with ADO.NET: Address Book Case Study Our next example implements a simple address book application that enables users to insert rows into, locate rows from and update the SQL Server database AddressBook.mdf . The AddressBook application (Fig. 20.44) provides a GUI through which users can execute SQL statements on the database. However, instead of displaying a database table in a DataGridView, this example presents data from a table one row at a time, using a set of TextBoxes that display the values of each of the row’s columns. A BindingNavigator allows you to control which row of the table is currently in view at any given time. The
20.8 Programming with ADO.NET: Address Book Case Study
847
also allows you to add new rows, delete rows, and save changes to the data in view. Note that lines 17–34 in Fig. 20.44 are similar to the corresponding lines of code in the chapter’s earlier examples. We discuss the application’s additional functionality and the code in lines 38–53 that supports it momentarily. We begin by showing you the steps to create this application. BindingNavigator
// Fig. 20.44: AddressBook.cs // Allows users to manipulate an address book. using System; using System.Windows.Forms; namespace AddressBook { public partial class AddressBookForm : Form { public AddressBookForm() { InitializeComponent(); } // end AddressBookForm constructor // Click event handler for the Save Button in the // BindingNavigator saves the changes made to the data private void addressesBindingNavigatorSaveItem_Click( object sender, EventArgs e ) { this.Validate(); this.addressesBindingSource.EndEdit(); this.addressesTableAdapter.Update( this.addressBookDataSet.Addresses ); } // end method bindingNavigatorSaveItem_Click // loads data into the addressBookDataSet.Addresses table private void AddressBookForm_Load( object sender, EventArgs e ) { // TODO: This line of code loads data into the // 'addressBookDataSet.Addresses' table. You can move, // or remove it, as needed. this.addressesTableAdapter.Fill( this.addressBookDataSet.Addresses ); } // end method AddressBookForm_Load // loads data for the rows with the specified last name // into the addressBookDataSet.Addresses table private void findButton_Click( object sender, EventArgs e ) { // fill the DataSet's DataTable with only rows // containing the user-specified last name addressesTableAdapter.FillByLastName( addressBookDataSet.Addresses, findTextBox.Text ); } // end method findButton_Click
Fig. 20.44 |
AddressBook application that allows you to manipulate entries in an address book
database. (Part 1 of 2.)
848 45 46 47 48 49 50 51 52 53 54 55
Chapter 20
Database, SQL and ADO.NET
// reloads addressBookDataSet.Addresses with all rows private void browseAllButton_Click( object sender, EventArgs e ) { // fill the DataSet's DataTable with all rows in the database addressesTableAdapter.Fill( addressBookDataSet.Addresses ); findTextBox.Text = ""; // clear Find TextBox } // end method browseAllButton_Click } // end class AddressBookForm } // end namespace AddressBook (a)
(b)
(c)
Fig. 20.44 |
AddressBook application that allows you to manipulate entries in an address book
database. (Part 2 of 2.)
Step 1: Adding the Database to the Project As in the preceding examples, you must begin by adding the database to the project. After adding the AddressBook.mdf as a data source, the Data Sources window will list AddressBookDataSet, which contains a table named Addresses. Step 2: Indicating that the IDE Should Create a Set of Labels and TextBoxes to Display Each Row of Data In the earlier sections, you dragged a node from the Data Sources window to the Form to create a DataGridView bound to the data source member represented by that node. The IDE allows you to specify the type of control(s) that it creates when you drag and drop a data source member onto a Form. In Design view, click the Addresses node in the Data
20.8 Programming with ADO.NET: Address Book Case Study
849
window (Fig. 20.45). Note that this node becomes a drop-down list when you select it. Click the down arrow to view the items in the list. The icon to the left of DataGridView will initially be highlighted in blue, because the default control to be bound to a table is a DataGridView (as you saw in the earlier examples). Select the Details option in the drop-down list to indicate that the IDE should create a set of Label–TextBox pairs for each column name–column value pair when you drag and drop the Addresses table onto the Form. (You will see what this looks like in Fig. 20.46.) The drop-down list contains suggestions for controls to display the table’s data, but you can also choose the Customize… option to select other controls that are capable of being bound to a table’s data. Sources
Step 3: Dragging the Addresses Data Source Node to the Form Drag the Addresses node from the Data Sources window to the Form (Fig. 20.46). The IDE creates a series of Labels and TextBoxes because you selected Details in the preceding step. As in the earlier examples, the IDE also creates a BindingNavigator and the other components in the component tray. The IDE sets the text of each Label based on the corresponding column name in the table in the database, and uses regular expressions to insert spaces into multiword column names to make the Labels more readable. Step 4: Making the AddressID TextBox ReadOnly The AddressID column of the Addresses table is an auto-incremented identity column, so users should not be allowed to edit the values in this column. Select the TextBox for the AddressID and set its ReadOnly property to true using the Properties window. Note that you may need to click in an empty part of the Form to deselect the other Labels and TextBoxes before selecting the AddressID TextBox.
Fig. 20.45 | Selecting the control(s) to be created when dragging and dropping a data source member onto the Form.
850
Chapter 20
Database, SQL and ADO.NET
Fig. 20.46 | Displaying a table on a Form using a series of Labels and TextBoxes. Step 5: Running the Application Run the application and experiment with the controls in the BindingNavigator at the top of the window. Like the previous examples, this example fills a DataSet object (specifically an AddressBookDataSet object) with all the rows of a database table (i.e., Addresses). However, only a single row of the DataSet appears at any given time. The CD- or DVDlike buttons of the BindingNavigator allow you to change the currently displayed row (i.e., change the values in each of the TextBoxes). The buttons to add a row, delete a row and save changes also perform their designated tasks. Adding a row clears the TextBoxes and makes a new auto-incremented ID (i.e., 5) appear in the TextBox to the right of Address ID. After entering some data, click the Save button to record the new row in the database. After closing and restarting the application, there should still be five rows. Delete the new row by clicking the appropriate button, then save the changes. Step 6: Adding a Query to the AddressesTableAdapter While the BindingNavigator allows you to browse the address book, it would be more convenient to be able to find a specific entry by last name. To add this functionality to the application, you must add a new query to the AddressesTableAdapter using the TableAdapter Query Configuration Wizard . Click the Edit DataSet with Designer icon ( ) in the Data Sources window. Select the box representing the AddressesTableAdapter. Right click the TableAdapter’s name and select Add Query…. In the TableAdapter Query Configuration Wizard, keep the default option Use SQL Statements and click Next. On the next screen, keep the default option SELECT which returns rows and click Next. Rather than use the Query Builder to form your query (as we did in the preceding example), modify the
20.8 Programming with ADO.NET: Address Book Case Study
851
query directly in the text box in the wizard. Append the clause “WHERE LastName = @lastName” to the end of the default query. Note that @lastName is a parameter that will be replaced by a value when the query is executed. Click Next, then enter FillByLastName and GetDataByLastName as the names for the two methods that the wizard will generate. The query contains a parameter, so each of these methods will take a parameter to set the value of @lastName in the query. You will see how to call the FillByLastName method and specify a value for @lastName shortly. Click Finish to complete the wizard and return to the Dataset Designer (Fig. 20.47). Note that the newly created Fill and Get methods appear under the AddressesTableAdapter and that parameter @lastName is listed to the right of the method names.
Step 7: Adding Controls to Allow Users to Specify a Last Name to Locate Now that you have created a query to locate rows with a specific last name, add controls to allow users to enter a last name and execute this query. Go to Design view (Fig. 20.48) and add to the Form a Label named findLabel, a TextBox named findTextBox and a Button named findButton. Place these controls in a GroupBox named findGroupBox, then set its Text property to Find an entry by last name. Set the Text properties of the Label and Button as shown in Fig. 20.48. Step 8: Programming an Event Handler That Locates the User-Specified Last Name Double click findButton to add a Click event handler for this Button. In the event handler, write the following lines of code (lines 42–43 of Fig. 20.44): addressesTableAdapter.FillByLastName( addressBookDataSet.Addresses, findTextBox.Text );
The
FillByLastName method replaces the current data in addressBookDataSet.Addresses with data for only those rows with the last name entered in findTextBox. Note that when invoking FillByLastName, you must pass the DataTable to be filled, as well as
an argument specifying the last name to find. This argument becomes the value of the
Fig. 20.47 | Dataset Designer for the AddressBookDataSet after adding a query to AddressesTableAdapter.
852
Chapter 20
Database, SQL and ADO.NET
Fig. 20.48 | Design view after adding controls to locate a last name in the address book. parameter in the SELECT statement created in Step 6. Start the application to test the new functionality. Note that when you search for a specific entry (i.e., enter a last name and click Find), the BindingNavigator allows the user to browse only the rows containing the specified last name. This is because the data source bound to the Form’s controls (i.e., addressBookDataSet.Addresses) has changed and now contains only a limited number of rows. @lastName
Step 9: Allowing the User to Return to Browsing All Rows in the Database To allow users to return to browsing all the rows after searching for specific rows, add a Button named browseAllButton below the findGroupBox. Double click browseAllButton to add a Click event handler to the code. Set the Text property of browseAllButton to Browse All Entries in the Properties window. Add a line of code that calls addressesTableAdapter.Fill( addressBookDataSet.Addresses ) to refill the Addresses DataTable with all the rows from the table in the database (line 50 of Fig. 20.44). Also, add a line of code that clears the Text property of findTextBox (line 52). Start the application. Find a specific last name as in the previous step, then click the browseAllButton button to test the new functionality. Data Binding in the AddressBook Application Dragging and dropping the Addresses node from the Data Sources window onto AddressBookForm in this example caused the IDE to generate several components in the component tray. These serve the same purposes as those generated for the earlier examples that use the Books database. In this case, addressBookDataSet is an object of a strongly
20.8 Programming with ADO.NET: Address Book Case Study
853
typed DataSet, AddressBookDataSet, whose structure mimics that of the AddressBook database. addressesBindingSource is a BindingSource object that refers to the Addresses table of the AddressBookDataSet. addressesTableAdapter encapsulates a SqlDataAdapter object configured with SqlCommand objects that execute SQL statements against the AddressBook database. Finally, addressesBindingNavigator is bound to the addressesBindingSource object, thus allowing you to indirectly manipulate the Addresses table of the AddressBookDataSet. In each of the earlier examples using a DataGridView to display all the rows of a database table, the DataGridView’s BindingSource property was set to the corresponding BindingSource object. In this example, you selected Details from the drop-down list for the Addresses table in the Data Sources window, so the values from a single row of the table appear on the Form in a set of TextBoxes. The IDE sets up the data binding in this example by binding each TextBox to a specific column of the Addresses DataTable in the AddressBookDataSet. To do this, the IDE sets the TextBox’s DataBindings.Text property. You can view this property by clicking the plus sign next to (DataBindings) in the Properties window (Fig. 20.49). Clicking the drop-down list for this property allows you to choose a BindingSource object and a property (i.e., column) within the associated data source to bind to the TextBox. Consider the TextBox that displays the FirstName value—named firstNameTextBox by the IDE. This control’s DataBindings.Text property is set to the FirstName property
Fig. 20.49 | Viewing the DataBindings.Text property of a TextBox in the Properties window.
854
Chapter 20
Database, SQL and ADO.NET
of the AddressesBindingSource (which refers to AddressBookDataSet.Addresses). Thus, firstNameTextBox always displays the value of the FirstName column in the currently selected row of addressBookDataSet.Addresses. Each IDE-created TextBox on the Form is configured in a similar manner. Browsing the address book with the AddressesBindingNavigator changes the current position in addressBookDataSet.Addresses and thus changes the values displayed in each TextBox. Regardless of changes to the contents of addressBookDataSet.Addresses, the TextBoxes remain bound to the same properties of the DataTable and always display the appropriate data. Note that the TextBoxes do not display any values if the cached version of Addresses is empty (i.e., if there are no rows in the DataTable because the query that filled the DataTable returned no rows).
20.9 Using a DataSet to Read and Write XML A powerful feature of ADO.NET is its ability to convert data stored in a data source to XML for exchanging data between applications in a portable format. Class DataSet of namespace System.Data provides methods WriteXml, ReadXml and GetXml, which enable developers to create XML documents from data sources and to convert data from XML into data sources.
Writing Data from a Data Source to an XML Document The application of Fig. 20.50 populates a DataSet with statistics about baseball players, then writes the data to an XML document. The application also displays the XML in a TextBox. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
// Fig. 20.50: XMLWriter.cs // Demonstrates generating XML from an ADO.NET DataSet. using System; using System.Windows.Forms; namespace XMLWriter { public partial class XMLWriterForm : Form { public XMLWriterForm() { InitializeComponent(); } // end XMLWriterForm constructor // Click event handler for the Save Button in the // BindingNavigator saves the changes made to the data private void playersBindingNavigatorSaveItem_Click( object sender, EventArgs e ) { this.Validate(); this.playersBindingSource.EndEdit(); this.playersTableAdapter.Update( this.baseballDataSet.Players ); } // end method bindingNavigatorSaveItem_Click
Fig. 20.50 | Writing the XML representation of a DataSet to a file. (Part 1 of 2.)
// loads data into the baseballDataSet.Players table private void XMLWriterForm_Load( object sender, EventArgs e ) { // TODO: This line of code loads data into the // 'baseballDataSet.Players' table. You can move, // or remove it, as needed. this.playersTableAdapter.Fill( this.baseballDataSet.Players ); } // write XML representation of DataSet when Button clicked private void writeButton_Click( object sender, EventArgs e ) { // set the namespace for this DataSet // and the resulting XML document baseballDataSet.Namespace = "http://www.deitel.com/baseball"; // write XML representation of DataSet to a file baseballDataSet.WriteXml( "Players.xml" ); // display XML representation in TextBox outputTextBox.Text += "Writing the following XML:\r\n" + baseballDataSet.GetXml() + "\r\n"; } // end method writeButton_Click } // end class XMLWriterForm } // end namespace XMLWriter
Fig. 20.50 | Writing the XML representation of a DataSet to a file. (Part 2 of 2.) We created this GUI by first adding the Baseball.mdf database (located in the chapter’s examples directory) to the project, then dragging the Players node from the Data Sources window to the Form. This action created the BindingNavigator and DataGridView seen in the output of Fig. 20.50. We then added the Button writeButton and the TextBox outputTextBox. The XML representation of the Players table should not be edited and will span more lines than the TextBox can display at once, so we set output-
856
Chapter 20
Database, SQL and ADO.NET
TextBox’s ReadOnly and MultiLine properties to true and its ScrollBars property to Vertical. Create the event handler for writeButton by double clicking it in Design view. The autogenerated XMLWriterForm_Load event handler (lines 26–32) calls method Fill of class PlayersTableAdapter to populate baseballDataSet with data from the Players table in the Baseball database. Note that the IDE binds the DataGridView to baseballDataSet.Players (through the PlayersBindingSource) to display the informa-
tion to the user. Lines 35–47 define the event handler for the Write to XML button. When the user clicks this button, line 39 sets baseballDataSet’s Namespace property to specify a namespace for the DataSet and any XML documents based on the DataSet (see Section 19.4 to learn about XML namespaces). Line 42 invokes DataSet method WriteXml, which generates an XML representation of the data contained in the DataSet, then writes the XML to the specified file. This file is created in the project’s bin/Debug or bin/Release directory, depending on how you executed the program. Lines 45–46 then display this XML representation, obtained by invoking DataSet method GetXml, which returns a string containing the XML.
Examining an XML Document Generated By DataSet Method WriteXml Figure 20.51 presents the Players.xml document generated by DataSet method WriteXml in Fig. 20.50. Note that the BaseballDataSet root element (line 2) declares the document’s default namespace to be the namespace specified in line 39 of Fig. 20.50. Each Players element represents a record in the Players table. The PlayerID, FirstName, LastName and BattingAverage elements correspond to the columns with these names in the Players database table. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
1 John Doe 0.375 2 Jack Smith 0.223 3 George O'Malley 0.344
Fig. 20.51 | XML document generated from BaseballDataSet in XMLWriter.
20.10 Wrap-Up
857
20.10 Wrap-Up This chapter introduced relational databases, SQL, ADO.NET and the IDE’s visual programming tools for working with databases. You examined the contents of a simple Books database and learned about the relationships among the tables in the database. You then learned basic SQL to retrieve data from, add new data to, and update data in a database. You learned about the classes of namespaces System.Data and System.Data.SqlClient that allow programs to connect to a database, then access and manipulate its data. The chapter also explained ADO.NET’s disconnected model, which enables a program to store data from a database temporarily in local memory as a DataSet. The second part of the chapter focused on using the IDE’s tools and wizards to access and manipulate data sources like a database in C# GUI applications. You learned how to add data sources to projects and how to use the IDE’s drag-and-drop capabilities to display database tables in applications. We showed how the IDE hides from you the SQL used to interact with the database. We also demonstrated adding custom queries to GUI applications so that you can display only those rows of data that meet specific criteria. Finally, you learned how to write data from a data source to an XML file. In the next chapter, we demonstrate how to build Web applications using Microsoft’s ASP.NET technology. We also introduce the concept of a three-tier application, in which an application is divided into three pieces that can reside on the same computer or can be distributed among separate computers across a network such as the Internet. As will be discussed, one of these tiers—the information tier—typically stores data in an RDBMS like SQL Server.
20.11 Web Resources msdn.microsoft.com/sql/
The SQL Server Developer Center provides up-to-date product information, downloads, articles and community forums. lab.msdn.microsoft.com/express/sql/
The home page for SQL Server 2005 Express provides how-to articles, blogs, newsgroups and other valuable resources. msdn2.microsoft.com/library/system.data.aspx
Microsoft’s documentation for the System.Data namespace. msdn.microsoft.com/SQL/sqlreldata/TSQL/default.aspx
Microsoft’s SQL language reference guide. msdn2.microsoft.com/library/ms172013.aspx
Microsoft’s documentation for the Query Builder and other visual database tools. www.w3schools.com/sql/default.asp
The W3C’s SQL tutorial presents basic and advanced SQL features with examples. www.sql.org
This SQL portal provides links to many resources, including SQL syntax, tips, tutorials, books, magazines, discussion groups, companies with SQL services, SQL consultants and free software. www.oracle.com/database/index.html
The home page for Oracle’s database management systems. www.sybase.com
The home page for the Sybase database management system.
858
Chapter 20
Database, SQL and ADO.NET
www-306.ibm.com/software/data/db2/
The home page for IBM’s DB2 database management system. www.postgresql.org
The home page for the PostgreSQL database management system. www.mysql.com
The home page for the MySQL database server.
21 ASP.NET 2.0, Web Forms and Web Controls If any man will draw up his case, and put his name at the foot of the first page, I will give him an immediate reply. Where he compels me to turn over the sheet, he must wait my leisure. —Lord Sandwich
OBJECTIVES In this chapter you will learn: I
Web application development using ASP.NET.
I
To create Web Forms.
I
To create ASP.NET applications consisting of multiple Web Forms.
—Anonymous
I
A fair question should be followed by a deed in silence.
To maintain state information about a user with session tracking and cookies.
I
To use the Web Site Administration Tool to modify Web application configuration settings.
I
To control user access to Web applications using forms authentication and ASP.NET login controls.
I
To use databases in ASP.NET applications.
I
To design a master page and content pages to create a uniform look-and-feel for a Web site.
Rule One: Our client is always right Rule Two: If you think our client is wrong, see Rule One.
—Dante Alighieri
You will come here and get books that will open your eyes, and your ears, and your curiosity, and turn you inside out or outside in. —Ralph Waldo Emerson
Outline
860 21.1 21.2 21.3 21.4
21.5
21.6
21.7
21.8
21.9 21.10
Chapter 21
ASP.NET 2.0, Web Forms and Web Controls
Introduction Simple HTTP Transactions Multitier Application Architecture Creating and Running a Simple Web-Form Example 21.4.1 Examining an ASPX File 21.4.2 Examining a Code-Behind File 21.4.3 Relationship Between an ASPX File and a Code-Behind File 21.4.4 How the Code in an ASP.NET Web Page Executes 21.4.5 Examining the XHTML Generated by an ASP.NET Application 21.4.6 Building an ASP.NET Web Application Web Controls 21.5.1 Text and Graphics Controls 21.5.2 AdRotator Control 21.5.3 Validation Controls Session Tracking 21.6.1 Cookies 21.6.2 Session Tracking with HttpSessionState Case Study: Connecting to a Database in ASP.NET 21.7.1 Building a Web Form That Displays Data from a Database 21.7.2 Modifying the Code-Behind File for the Guestbook Application Case Study: Secure Books Database Application 21.8.1 Examining the Completed Secure Books Database Application 21.8.2 Creating the Secure Books Database Application Wrap-Up Web Resources
21.1 Introduction In previous chapters, we used Windows Forms and Windows controls to develop Windows applications. In this chapter, we introduce Web application development with Microsoft’s ASP.NET 2.0 technology. Web-based applications create Web content for Web browser clients. This Web content includes Extensible HyperText Markup Language (XHTML), client-side scripting, images and binary data. Readers not familiar with XHTML should first read Appendix F, Introduction to XHTML: Part 1, and Appendix G, Introduction to XHTML: Part 2, before studying this chapter. We present several examples that demonstrate Web application development using Web Forms, Web controls (also called ASP.NET server controls) and C# programming. Web Form files have the filename extension .aspx and contain the Web page’s GUI. You customize Web Forms by adding Web controls including labels, text boxes, images, buttons and other GUI components. The Web Form file represents the Web page that is sent to the client browser. From this point onward, we refer to Web Form files as ASPX files.
21.2 Simple HTTP Transactions
861
Every ASPX file created in Visual Studio has a corresponding class written in a .NET language, such as C#. This class contains event handlers, initialization code, utility methods and other supporting code. The file that contains this class is called the codebehind file and provides the ASPX file’s programmatic implementation. To develop the code and GUIs in this chapter, we used Microsoft Visual Web Developer 2005 Express—an IDE designed for developing ASP.NET Web applications. Visual Web Developer and Visual C# 2005 Express share many common features and visual programming tools that simplify building complex applications, such as those that access a database (presented in Sections 21.7–21.8). The full version of Visual Studio 2005 includes the functionality of Visual Web Developer, so the instructions we present for Visual Web Developer also apply to Visual Studio 2005. Note that you must install either Visual Web Developer 2005 Express (available from lab.msdn.microsoft.com/express/ vwd/default.aspx) or a complete version of Visual Studio 2005 to implement the programs in this chapter and Chapter 22, Web Services. The site www.deitel.com/books/ csharpforprogrammers2 provides instructions for running the ASP.NET 2.0 examples presented in this chapter if you do not wish to recreate them.
21.2 Simple HTTP Transactions Web application development requires a basic understanding of networking and the World Wide Web. In this section, we discuss the Hypertext Transfer Protocol (HTTP) and what occurs behind the scenes when a browser displays a Web page. HTTP specifies a set of methods and headers that allow clients and servers to interact and exchange information in a uniform and predictable manner. In its simplest form, a Web page is nothing more than an XHTML document—a plain text file containing markup (i.e., tags) that describse to a Web browser how to display and format the document’s information. For example, the XHTML markup My Web Page
indicates that the browser should display the text between the start tag and the end tag in the browser’s title bar. XHTML documents also can contain hypertext data (usually called hyperlinks), which links to different pages or to other parts of the same page. When the user activates a hyperlink (usually by clicking it with the mouse), the requested Web page loads into the user’s browser window. Any XHTML document available for viewing over the Web has a corresponding Uniform Resource Locator (URL). A URL is an address indicating the location of an Internet resource, such as an XHTML document. The URL contains information that directs a browser to the resource that the user wishes to access. Computers that run Web server software make such resources available. When requesting ASP.NET Web applications, the Web server is usually Microsoft Internet Information Services (IIS). As we discuss shortly, it is also possible to test ASP.NET applications using the ASP.NET Development Server built into Visual Web Developer. Let us examine the components of the URL http://www.deitel.com/books/downloads.html
The http:// indicates that the resource is to be obtained using the HTTP protocol. The middle portion, www.deitel.com, is the server’s fully qualified hostname—the name of
862
Chapter 21
ASP.NET 2.0, Web Forms and Web Controls
the computer on which the resource resides. This computer usually is referred to as the host, because it houses and maintains resources. The hostname www.deitel.com is translated into an IP address (68.236.123.125), which identifies the server in a manner similar to how a telephone number uniquely defines a particular phone line. The hostname is translated into an IP address by a domain name system (DNS) server—a computer that maintains a database of hostnames and their corresponding IP addresses. This translation operation is called a DNS lookup. The remainder of the URL (i.e., /books/downloads.html) specifies both the name of the requested resource (the XHTML document downloads.html) and its path, or location (/books), on the Web server. The path could specify the location of an actual directory on the Web server’s file system. However, for security reasons, the path often specifies the location of a virtual directory. In such systems, the server translates the virtual directory into a real location on the server (or on another computer on the server’s network), thus hiding the true location of the resource. Some resources are created dynamically and do not reside anywhere on the server computer. The hostname in the URL for such a resource specifies the correct server, and the path and resource information identify the location of the resource with which to respond to the client’s request. When given a URL, a Web browser performs a simple HTTP transaction to retrieve and display the Web page found at that address. Figure 21.1 illustrates the transaction in detail. This transaction consists of interaction between the Web browser (the client side) and the Web server application (the server side). In Fig. 21.1, the Web browser sends an HTTP request to the server. The request (in its simplest form) is GET /books/downloads.html HTTP/1.1
The word GET is an HTTP method indicating that the client wishes to obtain a resource from the server. The remainder of the request provides the path name of the resource (an XHTML document) and the protocol’s name and version number (HTTP/1.1). Any server that understands HTTP (version 1.1) can translate this request and respond appropriately. Figure 21.2 depicts the results of a successful request. The server first responds by sending a line of text that indicates the HTTP version, followed by a numeric code and a phrase describing the status of the transaction. For example, HTTP/1.1 200 OK
indicates success, whereas HTTP/1.1 404 Not found
informs the client that the Web server could not locate the requested resource. The server then sends one or more HTTP headers, which provide additional information about the data that will be sent. In this case, the server is sending an XHTML text document, so the HTTP header for this example reads: Content-type: text/html
The information provided in this header specifies the Multipurpose Internet Mail Extensions (MIME) type of the content that the server is transmitting to the browser. MIME is an Internet standard that specifies data formats so that programs can interpret data correctly. For example, the MIME type text/plain indicates that the sent information is text
21.3 Multitier Application Architecture
(a) The GET request is sent from the client to the Web Server.
Web Server
863
(b) After it receives the request, the Web Server searches through its system for the resource.
Client Internet
Fig. 21.1 | Client interacting with Web server. Step 1: The GET request.
Web Server The server responds to the request with an appropriate message and the resource's contents.
Client Internet
Fig. 21.2 | Client interacting with Web server. Step 2: The HTTP response. that can be displayed directly, without any interpretation of the content as XHTML markup. Similarly, the MIME type image/jpeg indicates that the content is a JPEG image. When the browser receives this MIME type, it attempts to display the image. The header or set of headers is followed by a blank line, which indicates to the client that the server is finished sending HTTP headers. The server then sends the contents of the requested XHTML document (downloads.html). The server terminates the connection when the resource transfer is complete. At this point, the client-side browser parses the XHTML markup it has received and renders (or displays) the results.
21.3 Multitier Application Architecture Web-based applications are multitier applications (sometimes referred to as n-tier applications). Multitier applications divide functionality into separate tiers (i.e., logical groupings of functionality). Although tiers can be located on the same computer, the tiers of Web-based applications typically reside on separate computers. Figure 21.3 presents the basic structure of a three-tier Web-based application. The information tier (also called the data tier or the bottom tier) maintains data pertaining to the application. This tier typically stores data in a relational database management system (RDBMS). We discussed RDBMSs in Chapter 20. For example, a retail store
864
Chapter 21
ASP.NET 2.0, Web Forms and Web Controls
Top tier
Middle tier
Bottom tier
User interface
Business logic
Data
Client
ASP.NET
Information
Browser
XHTML
ADO.NET Web server
Database
Fig. 21.3 | Three-tier architecture. might have a database for storing product information, such as descriptions, prices and quantities in stock. The same database also might contain customer information, such as user names, billing addresses and credit card numbers. This tier can contain multiple databases, which together comprise the data needed for our application. The middle tier implements business logic, controller logic and presentation logic to control interactions between the application’s clients and the application’s data. The middle tier acts as an intermediary between data in the information tier and the application’s clients. The middle-tier controller logic processes client requests (such as requests to view a product catalog) and retrieves data from the database. The middle-tier presentation logic then processes data from the information tier and presents the content to the client. Web applications typically present data to clients as XHTML documents. Business logic in the middle tier enforces business rules and ensures that data is reliable before the server application updates the database or presents the data to users. Business rules dictate how clients can and cannot access application data, and how applications process data. For example, a business rule in the middle tier of a retail store’s Web-based application might ensure that all product quantities remain positive. A client request to set a negative quantity in the bottom tier’s product information database would be rejected by the middle tier’s business logic. The client tier, or top tier, is the application’s user interface, which gathers input and displays output. Users interact directly with the application through the user interface, which is typically a Web browser, keyboard and mouse. In response to user actions (e.g., clicking a hyperlink), the client tier interacts with the middle tier to make requests and to retrieve data from the information tier. The client tier then displays the data retrieved from the middle tier to the user. The client tier never directly interacts with the information tier.
21.4 Creating and Running a Simple Web-Form Example Our first example displays the Web server’s time of day in a browser window. When run, this program displays the text A Simple Web Form Example, followed by the Web server’s time. As mentioned previously, the program consists of two related files—an ASPX file (Fig. 21.4) and a C# code-behind file (Fig. 21.5). We first display the markup, code and output, then we carefully guide you through the step-by-step process of creating this program. [Note: The markup in Fig. 21.4 and other ASPX file listings in this chapter is the
21.4 Creating and Running a Simple Web-Form Example
A Simple Web Form Example Current time on the Web server:
Fig. 21.4 | ASPX file that displays the Web server’s time. same as the markup that appears in Visual Web Developer, but we have reformatted the markup for presentation purposes to make the code more readable.] Visual Web Developer generates all the markup shown in Fig. 21.4 when you set the Web page’s title, type text in the Web Form, drag a Label onto the Web Form and set the properties of the page’s text and the Label. We show these steps shortly.
21.4.1 Examining an ASPX File The ASPX file contains other information in addition to XHTML. Lines 1–2 are ASP.NET comments that indicate the figure number, the file name and the purpose of the file. ASP.NET comments begin with . We added these comments to the file. Lines 3–4 use a Page directive (in an ASPX file a directive is delimited by ) to specify information needed by ASP.NET to process this file. The Language attribute of the Page directive specifies the language of the code-behind file as C#; the code-behind file (i.e., the CodeFile) is WebTime.aspx.cs. Note that a code-behind file name usually consists of the full ASPX file name (e.g., WebTime.aspx) followed by the .cs extension. The AutoEventWireup attribute (line 3) determines how Web Form events are handled. When AutoEventWireup is set to true, ASP.NET determines which methods in the class are called in response to an event generated by the Page. For example, ASP.NET will call methods Page_Load and Page_Init in the code-behind file to handle the Page’s Load and Init events respectively. (We discuss these events later in the chapter.)
866
Chapter 21
ASP.NET 2.0, Web Forms and Web Controls
The Inherits attribute (line 4) specifies the class in the code-behind file from which this ASP.NET class inherits—in this case, WebTime. We say more about Inherits momentarily. [Note: We explicitly set the EnableSessionState attribute (line 4) to False. We explain the significance of this attribute later in the chapter. The IDE sometimes generates attribute values (e.g., True and False) and control names (as you will see later in the chapter) that do not adhere to our standard code capitalization conventions (i.e., true and false). However, unlike C# code, ASP.NET markup is not case-sensitive, so using a different case is not problematic. To remain consistent with the code generated by the IDE, we do not modify these values in our code listings or in our accompanying discussions.] For this first ASPX file, we provide a brief discussion of the XHTML markup. We do not discuss the majority of the XHTML contained in subsequent ASPX files. Lines 6–7 contain the document type declaration, which specifies the document element name (HTML) and the PUBLIC Uniform Resource Identifier (URI) for the DTD that defines the XHTML vocabulary. Lines 9–10 contain the and start tags, respectively. XHTML documents have the root element html and mark up information about the document in the head element. Also note that the html element specifies the XML namespace of the document using the xmlns attribute (see Section 19.4). Line 11 sets the title of this Web page. We demonstrate how to set the title through a property in the IDE shortly. Notice the runat attribute in line 10, which is set to "server". This attribute indicates that when a client requests this ASPX file, ASP.NET processes the head element and its nested elements on the server and generates the corresponding XHTML, which is then sent to the client. In this case, the XHTML sent to the client will be identical to the markup in the ASPX file. However, as you will see, ASP.NET can generate complex XHTML markup from simple elements in an ASPX file. Line 13 contains the start tag, which begins the body of the XHTML document; the body contains the main content that the browser displays. The form that contains our XHTML text and controls is defined in lines 14–23. Again, the runat attribute in the form element indicates that this element executes on the server, which generates equivalent XHTML and sends it to the client. Lines 15–22 contain a div element that groups the elements of the form in a block of markup. Line 16 is an h2 heading element that contains text indicating the purpose of the Web page. As we demonstrate shortly, the IDE generates this element in response to typing text directly in the Web Form and selecting the text as a second-level heading. Lines 17–21 contain a p element to mark up content to be displayed as a paragraph in the browser. Lines 18–20 mark up a label Web control. The properties that we set in the Properties window, such as Font-Size and BackColor (i.e., background color), are attributes here. The ID attribute (line 18) assigns a name to the control so that it can be manipulated programmatically in the code-behind file. We set the control’s EnableViewState attribute (line 20) to False. We explain the significance of this attribute later in the chapter. The asp: tag prefix in the declaration of the Label tag (line 18) indicates that the label is an ASP.NET Web control, not an XHTML element. Each Web control maps to a corresponding XHTML element (or group of elements)—when processing a Web control on the server, ASP.NET generates XHTML markup that will be sent to the client to represent that control in a Web browser.
21.4 Creating and Running a Simple Web-Form Example
867
Portability Tip 21.1 The same Web control can map to different XHTML elements, depending on the client browser and the Web control’s property settings. 21.1
In this example, the asp:Label control maps to the XHTML span element (i.e., ASP.NET creates a span element to represent this control in the client’s Web browser). A span element contains text that is displayed in a Web page. This particular element is used because span elements allow formatting styles to be applied to text. Several of the property values that were applied to our label are represented as part of the style attribute of the span element. You will soon see what the generated span element’s markup looks like. The Web control in this example contains the runat="server" attribute–value pair (line 18), because this control must be processed on the server so that the server can translate the control into XHTML that can be rendered in the client browser. If this attribute pair is not present, the asp:Label element is written as text to the client (i.e., the control is not converted into a span element and does not render properly).
21.4.2 Examining a Code-Behind File Figure 21.5 presents the code-behind file. Recall that the ASPX file in Fig. 21.4 references this file in line 3. Line 13 begins the declaration of class WebTime. Recall from Chapter 9 that a class declaration can span multiple source-code files and that the separate portions of the class declaration in each file are known as partial classes. The partial modifier in line 13 of Fig. 21.5 indicates that the code-behind file actually is a partial class. We discuss the remaining portion of this class shortly. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
// Fig. 21.5: WebTime.aspx.cs // Code-behind file for a page that displays the current time. using System; using System.Data; using System.Configuration; using System.Web; using System.Web.Security; using System.Web.UI; using System.Web.UI.WebControls; using System.Web.UI.WebControls.WebParts; using System.Web.UI.HtmlControls; public partial class WebTime : System.Web.UI.Page { // initializes the contents of the page protected void Page_Init( object sender, EventArgs e ) { // display the server's current time in timeLabel timeLabel.Text = string.Format( "{0:D2}:{1:D2}:{2:D2}", DateTime.Now.Hour, DateTime.Now.Minute, DateTime.Now.Second ); } // end method Page_Init } // end class WebTime
Fig. 21.5 | Code-behind file for a page that displays the Web server’s time. (Part 1 of 2.)
868
Chapter 21
ASP.NET 2.0, Web Forms and Web Controls
Fig. 21.5 | Code-behind file for a page that displays the Web server’s time. (Part 2 of 2.) Line 13 indicates that WebTime inherits from class Page in namespace System.Web.UI. This namespace contains classes and controls that assist in building Web-based applications. Class Page provides event handlers and objects necessary for creating Web-based applications. In addition to class Page (from which all Web applications directly or indirectly inherit), System.Web.UI also includes class Control—the base class that provides common functionality for all Web controls. Lines 16–21 define method Page_Init, which handles the page’s Init event. This event—the first event raised after a page is requested—indicates that the page is ready to be initialized. The only initialization required for this page is setting timeLabel’s Text property to the time on the server (i.e., the computer on which this code executes). The statement in lines 19–20 retrieves the current time and formats it as HH:MM:SS. For example, 9 AM is formatted as 09:00:00, and 2:30 PM is formatted as 14:30:00. Notice that the code-behind file can access timeLabel (the ID of the Label in the ASPX file) programmatically, even though the file does not contain a declaration for a variable named timeLabel. You will learn why momentarily.
21.4.3 Relationship Between an ASPX File and a Code-Behind File How are the ASPX and code-behind files used to create the Web page that is sent to the client? First, recall that class WebTime is the base class specified in line 3 of the ASPX file (Fig. 21.4). This class (partially declared in the code-behind file) inherits from Page, which defines the general functionality of a Web page. Partial class WebTime inherits this functionality and defines some of its own (i.e., displaying the current time). The code-behind file contains the code to display the time, whereas the ASPX file contains the code to define the GUI. When a client requests an ASPX file, ASP.NET creates two classes behind the scenes. Recall that the code-behind file contains a partial class named WebTime. The first file ASP.NET generates is another partial class containing the remainder of class WebTime, based on the markup in the ASPX file. For example, WebTime.aspx contains a Label Web control with ID timeLabel, so the generated partial class would contain a declaration for a Label variable named timeLabel. This partial class might look like public partial class WebTime { protected System.Web.UI.WebControls.Label timeLabel; }
21.4 Creating and Running a Simple Web-Form Example
869
Note that a Label is a Web control defined in namespace System.Web.UI.WebControls, which contains Web controls for designing a page’s user interface. Web controls in this namespace derive from class WebControl. When compiled, the preceding partial class declaration containing Web control declarations combines with the code-behind file’s partial class declaration to form the complete WebTime class. This explains why line 19 in method Page_Init of WebTime.aspx.cs (Fig. 21.5) can access timeLabel, which is created in lines 18–20 of WebTime.aspx (Fig. 21.4)—method Page_Init and control timeLabel are actually members of the same class, but defined in separate partial classes. The second class generated by ASP.NET is based on the ASPX file that defines the page’s visual representation. This new class inherits from class WebTime, which defines the page’s logic. The first time the Web page is requested, this class is compiled, and an instance is created. This instance represents our page—it creates the XHTML that is sent to the client. The assembly created from our compiled classes is placed within a subdirectory of C:\WINDOWS\Microsoft.NET\Framework\VersionNumber\ Temporary ASP.NET Files\WebTime
where VersionNumber is the version number of the .NET Framework (e.g., v2.0.50215) installed on your computer.
Performance Tip 21.1 Once an instance of the Web page has been created, multiple clients can use it to access the page—no recompilation is necessary. The project will be recompiled only when you modify the application; changes are detected by the runtime environment, and the project is recompiled to reflect the altered content. 21.1
21.4.4 How the Code in an ASP.NET Web Page Executes Let’s look briefly at how the code for our Web page executes. When an instance of the page is created, the Init event occurs first, invoking method Page_Init. Method Page_Init can contain code needed to initialize objects and other aspects of the page. After Page_Init executes, the Load event occurs, and the Page_Load event handler executes. Although not present in this example, this event is inherited from class Page. You will see examples of the Page_Load event handler later in the chapter. After this event handler finishes executing, the page processes events that are generated by the page’s controls, such as user interactions with the GUI. When the Web Form object is ready for garbage collection, an Unload event occurs, which calls the Page_Unload event handler. This event, too, is inherited from class Page. Page_Unload typically contains code that releases resources used by the page.
21.4.5 Examining the XHTML Generated by an ASP.NET Application Figure 21.6 shows the XHTML generated by ASP.NET when WebTime.aspx (Fig. 21.4) is requested by a client Web browser. To view this XHTML, select View > Source in Internet Explorer. [Note: We added the XHTML comments in lines 1–2 and reformatted the XHTML to conform to our coding conventions.] The contents of this page are similar to those of the ASPX file. Lines 7–9 define a document header comparable to that in Fig. 21.4. Lines 10–28 define the body of the docu-
A Simple Web Form Example Current time on the Web server:
17:13:52
Fig. 21.6 | XHTML response when the browser requests WebTime.aspx. ment. Line 11 begins the form, a mechanism for collecting user information and sending it to the Web server. In this particular program, the user does not submit data to the Web server for processing; however, processing user data is a crucial part of many applications that is facilitated by the form. We demonstrate how to submit data to the server in later examples. XHTML forms can contain visual and nonvisual components. Visual components include clickable buttons and other GUI components with which users interact. Nonvisual components, called hidden inputs, store data, such as e-mail addresses, that the document author specifies. One of these hidden inputs is defined in lines 13–15. We discuss the precise meaning of this hidden input later in the chapter. Attribute method of the form element (line 11) specifies the method by which the Web browser submits the form to the server. The action attribute identifies the name and location of the resource that will be requested when this form is submitted—in this case, WebTime.aspx. Recall that the ASPX file’s form element contained the runat="server" attribute–value pair (line 14 of Fig. 21.4). When the form is processed on the server, the runat attribute is removed. The method and action attributes are added, and the resulting XHTML form is sent to the client browser. In the ASPX file, the form’s Label (i.e., timeLabel) is a Web control. Here, we are viewing the XHTML created by our application, so the form contains a span element
21.4 Creating and Running a Simple Web-Form Example
871
(lines 21–24 of Fig. 21.6) to represent the text in the label. In this particular case, ASP.NET maps the Label Web control to an XHTML span element. The formatting options that were specified as properties of timeLabel, such as the font size and color of the text in the Label, are now specified in the style attribute of the span element. Notice that only those elements in the ASPX file marked with the runat="server" attribute–value pair or specified as Web controls are modified or replaced when the file is processed by the server. The pure XHTML elements, such as the h2 in line 19, are sent to the browser exactly as they appear in the ASPX file.
21.4.6 Building an ASP.NET Web Application Now that we have presented the ASPX file, the code-behind file and the resulting Web page sent to the Web browser, we outline the process by which we created this application. To build the WebTime application, perform the following steps in Visual Web Developer:
Step 1: Creating the Web Application Project Select File > New Web Site... to display the New Web Site dialog (Fig. 21.7). In this dialog, select ASP.NET Web Site in the Templates pane. Below this pane, the New Web Site dialog contains two fields with which you can specify the type and location of the Web application you are creating. If it is not already selected, select HTTP from the drop-down list closest to Location. This indicates that the Web application should be configured to run as an IIS application using HTTP (either on your computer or on a remote computer). We want our project to be located in http://localhost, which is the URL for IIS’s root directory (this URL corresponds to the C:\InetPub\wwwroot directory on your machine). The name localhost indicates that the client and server reside on the same machine. If the Web server were located on a different machine, localhost would be replaced with the appropriate IP address or hostname. By default, Visual Web Developer sets the location where the Web site will be created to http://localhost/WebSite, which we change to http://localhost/WebTime.
Fig. 21.7 | Creating an ASP.NET Web Site in Visual Web Developer.
872
Chapter 21
ASP.NET 2.0, Web Forms and Web Controls
If you do not have access to IIS, you can select File System from the drop-down list next to Location to create the Web application in a folder on your computer. You will be able to test the application using Visual Web Developer’s internal ASP.NET Development Server, but you will not be able to access the application remotely over the Internet. The Language drop-down list in the New Web Site dialog allows you to specify the language (i.e., Visual Basic, Visual C# or Visual J#) in which you will write the codebehind file(s) for the Web application. Change the setting to Visual C#. Click OK to create the Web application project. This action creates the directory C:\Inetpub\wwwroot\ WebTime and makes it accessible through the URL http://localhost/WebTime. This action also creates a WebTime directory in the Visual Studio 2005/Projects directory of your Windows user’s My Documents directory to store the project’s solution files (e.g., WebTime.sln).
Step 2: Examining the Solution Explorer of the Newly Created Project The next several figures describe the new project’s content, beginning with the Solution Explorer shown in Fig. 21.8. Like Visual C# 2005 Express, Visual Web Developer creates several files when a new project is created. An ASPX file (i.e., Web Form) named Default.aspx is created for each new ASP.NET Web Site project. This file is open by default in the Web Forms Designer in Source mode when the project first loads (we discuss this momentarily). As mentioned previously, a code-behind file is included as part of the project. Visual Web Developer creates a code-behind file named Default.aspx.cs. To open the ASPX file’s code-behind file, right click the ASPX file and select View Code or click the View Code button at the top of the Solution Explorer. Alternatively, you can expand the node for the ASPX file to reveal the node for the code-behind file (see Fig. 21.8). You can also choose to list all the files in the project individually (instead of nested) by clicking the Nest Related Files button—this option is turned on by default, so clicking the button toggles the option off. The Properties and Refresh buttons in Visual Web Developer’s Solution Explorer behave like those in Visual C# 2005 Express. Visual Web Developer’s Solution Explorer also contains three additional buttons—View Designer, Copy Web Site and ASP.NET Configuration. The View Designer button allows you to open the Web Form in Design mode, which we discuss shortly. The Copy Web Site button opens a dialog that allows you to move the files in this project to another location, such as a remote Web server. This is useful if you are developing the application on your local computer, but want to make it available to the public from a different location. Finally, the ASP.NET Configuration button takes you to a Web page called the Web Site Administration Tool, where you can manipulate various settings and security options for your application. We discuss this tool in greater detail in Section 21.8. Step 3: Examining the Toolbox in Visual Web Developer Figure 21.9 shows the Toolbox displayed in the IDE when the project loads. Figure 21.9(a) displays the beginning of the Standard list of Web controls, and Fig. 21.9(b) displays the remaining Web controls, as well as the list of Data controls used in ASP.NET. We discuss specific controls in Fig. 21.9 as they are used throughout the chapter. Notice that some controls in the Toolbox are similar to the Windows controls presented earlier in the book.
21.4 Creating and Running a Simple Web-Form Example
873
View Code Nest Related Files
View Designer
Copy Web Site
Refresh Properties
ASP.NET Configuration ASPX file
Code-behind file
Fig. 21.8 | Solution Explorer window for project WebTime.
(a)
(b)
Fig. 21.9 | Toolbox in Visual Web Developer. Step 4: Examining the Web Forms Designer Figure 21.10 shows the Web Forms Designer in Source mode, which appears in the center of the IDE. When the project loads for the first time, the Web Forms Designer displays the auto-generated ASPX file (i.e., Default.aspx) in Source mode, which allows you to view and edit the markup that comprises the Web page. The markup listed in Fig. 21.10 was created by the IDE and serves as a template that we will modify shortly. Clicking the Design button in the lower-left corner of the Web Forms Designer switches to Design
874
Chapter 21
ASP.NET 2.0, Web Forms and Web Controls
mode (Fig. 21.11), which allows you to drag and drop controls from the Toolbox on the Web Form. You can also type at the current cursor location to add text to the Web page. We demonstrate this shortly. In response to such actions, the IDE generates the appropriate markup in the ASPX file. Notice that Design mode indicates the XHTML element where the cursor is currently located. Clicking the Source button returns the Web Forms Designer to Source mode, where you can see the generated markup.
Step 5: Examining the Code-Behind File in the IDE The next figure (Fig. 21.12) displays Default.aspx.cs—the code-behind file generated by Visual Web Developer for Default.aspx. Right click the ASPX file in the Solution Explorer and select View Code to open the code-behind file. When it is first created, this file contains nothing more than a partial class declaration with an empty Page_Load event handler. We will add the Page_Init event handler to this code momentarily.
Source
mode button Design
mode button
Fig. 21.10 | Source mode of the Web Forms Designer.
Cursor
Cursor’s current location
Fig. 21.11 | Design mode of the Web Forms Designer.
21.4 Creating and Running a Simple Web-Form Example
875
Fig. 21.12 | Code-behind file for Default.aspx generated by Visual Web Developer. Step 6: Renaming the ASPX File We have displayed the contents of the default ASPX and code-behind files. We now rename these files. Right click the ASPX file in the Solution Explorer and select Rename. Enter the new file name WebTime.aspx and press Enter. This updates the name of both the ASPX file and the code-behind file. Note that the CodeFile attribute of WebTime.aspx’s Page directive is also updated by the IDE. Step 7: Renaming the Class in the Code-Behind File and Updating the ASPX File Although renaming the ASPX file causes the name of the code-behind file to change, this action does not affect the name of the partial class declared in the code-behind file. Open the code-behind file and change the class name from _Default (line 11 in Fig. 21.12) to WebTime, so the partial class declaration appears as in line 13 of Fig. 21.5. Recall that this class is also referenced by the Page directive of the ASPX file. Using the Web Forms Designer’s Source mode, modify the Inherits attribute of the Page directive in WebTime.aspx, so it appears as in line 4 of Fig. 21.4. The value of the Inherits attribute and the class name in the code-behind file must be identical; otherwise, you’ll get errors when you build the Web application. Step 8: Changing the Title of the Page Before designing the content of the Web Form, we change its title from the default Untitled Page (line 9 of Fig. 21.10) to A Simple Web Form Example. To do so, open the ASPX file in Source mode and modify the text between the start and end tags. Alternatively, you can open the ASPX file in Design mode and modify the Web Form’s Title property in the Properties window. To view the Web Form’s properties, select DOCUMENT from the drop-down list in the Properties window; DOCUMENT is the name used to represent the Web Form in the Properties window.
876
Chapter 21
ASP.NET 2.0, Web Forms and Web Controls
Step 9: Designing the Page Designing a Web Form is as simple as designing a Windows Form. To add controls to the page, you can drag-and-drop them from the Toolbox onto the Web Form in Design mode. Like the Web Form itself, each control is an object that has properties, methods and events. You can set these properties and events visually using the Properties window or programmatically in the code-behind file. However, unlike working with a Windows Form, you can type text directly on a Web Form at the cursor location or insert XHTML elements using menu commands. Controls and other elements are placed sequentially on a Web Form, much like how text and images are placed in a document using word processing software like Microsoft Word. Controls are placed one after another in the order in which you drag-and-drop them onto the Web Form. The cursor indicates the point at which text and XHTML elements will be inserted. If you want to position a control between existing text or controls, you can drop the control at a specific position within the existing elements. You can also rearrange existing controls using drag-and-drop actions. The positions of controls and other elements are relative to the Web Form’s upper-left corner. This type of layout is known as relative positioning. An alternate type of layout is known as absolute positioning, in which controls are located exactly where they are dropped on the Web Form. You can enable absolute positioning in Design mode by selecting Layout > Position > Auto-position Options…., then clicking the first checkbox in the Positioning options pane of the Options dialog that appears.
Portability Tip 21.2 Absolute positioning is discouraged, because pages designed in this manner may not render correctly on computers with different screen resolutions and font sizes. This could cause absolutely positioned elements to overlap each other or display off-screen, requiring the client to scroll to see the full page content. 21.2
In this example, we use one piece of text and one Label. To add the text to the Web Form, click the blank Web Form in Design mode and type Current time on the Web server:. Visual Web Developer is a WYSIWYG (What You See Is What You Get) editor—whenever you make a change to a Web Form in Design mode, the IDE creates the markup (visible in Source mode) necessary to achieve the desired visual effects seen in Design mode. After adding the text to the Web Form, switch to Source mode. You should see that the IDE added this text to the div element that appears in the ASPX file by default. Back in Design mode, highlight the text you added. From the Block Format dropdown list (see Fig. 21.13), choose Heading 2 to format this text as a heading that will appear bold in a font slightly larger than the default. This action causes the IDE to enclose the newly added text in an h2 element. Finally, click to the right of the text and press the Enter key to move the cursor to a new paragraph. This action generates an empty p element in the ASPX file’s markup. The IDE should now look like Fig. 21.13. You can place a Label on a Web Form either by dragging-and-dropping or by double clicking the Toolbox’s Label control. Be sure the cursor is in the newly created paragraph, then add a Label that will be used to display the time. Using the Properties window, set the (ID) property of the Label to timeLabel. We delete timeLabel’s text, because this text is set programmatically in the code-behind file. When a Label does not contain text, the
21.4 Creating and Running a Simple Web-Form Example
877
Block Format drop-down list
Cursor position after inserting a new paragraph by pressing Enter
Fig. 21.13 |
WebTime.aspx
after inserting text and a new paragraph.
name is displayed in square brackets in the Web Forms Designer (Fig. 21.14), but is not displayed at execution time. The label name is a placeholder for design and layout purposes. We set timeLabel’s BackColor, ForeColor and Font-Size properties to Black, Yellow and XX-Large, respectively. To change font properties, expand the Font node in the Properties window, then change each relevant property individually. Once the Label’s properties are set in the Properties window, Visual Web Developer updates the ASPX file’s contents. Figure 21.14 shows the IDE after these properties are set. Next, set the Label’s EnableViewState property to False. Finally, select DOCUMENT from the drop-down list in the Properties window and set the Web Form’s EnableSessionState property to False. We discuss both of these properties later in the chapter.
Step 10: Adding Page Logic Once the user interface has been designed, C# code must be added to the code-behind file. Open WebTime.aspx.cs by double clicking its node in the Solution Explorer. In this example, we add a Page_Init event handler (lines 16–21 of Fig. 21.5) to the code-behind file. Recall that Page_Init handles the Init event and contains code to initialize the page. The statement in lines 19–20 of Fig. 21.5 programmatically sets the text of timeLabel to the current time on the server. Step 11: Running the Program After the Web Form is created, you can view it several ways. First, you can select Debug > Start Without Debugging, which runs the application by opening a browser window. If you created the application on your local IIS server (as we did in this example), the URL shown in the browser will be http://localhost/WebTime/WebTime.aspx (Fig. 21.5), indicating
878
Chapter 21
ASP.NET 2.0, Web Forms and Web Controls
Label
Fig. 21.14
| WebTime.aspx
after adding a Label and setting its properties.
that the Web page (the ASPX file) is located within the virtual directory WebTime on the local IIS Web server. IIS must be running to test the Web site in a browser. IIS can be started by executing inetmgr.exe from Start > Run..., right clicking Default Web Site and selecting Start. [Note: You might need to expand the node representing your computer to display the Default Web Site.] Note that if you created the ASP.NET application on the local file system, the URL shown in the browser will be http://localhost:PortNumber/WebTime/WebTime.aspx, where PortNumber is the number of the randomly assigned port on which Visual Web Developer’s built-in test server runs. The IDE assigns the port number on a per solution basis. This URL indicates that the WebTime project folder is being accessed through the root directory of the test server running at localhost:PortNumber. When you select Debug > Start Without Debugging, a tray icon appears near the bottom-right of your screen next to the computer’s date and time to show that the ASP.NET Development Server is running. The server stops when you exit Visual Web Developer. You also can select Debug > Start Debugging to view the Web page in a Web browser with debugging enabled. Note that you cannot debug a Web site unless debugging is explicitly enabled by the Web.config file—a file that stores configuration settings for an ASP.NET Web application. You will rarely need to manually create or modify Web.config. The first time you select Debug > Start Debugging in a project, a dialog appears and asks whether you want the IDE to generate the necessary Web.config file and add it to the project, then the IDE enters Running mode. You can exit Running mode by selecting Debug > Stop Debugging in Visual Web Developer or by closing the browser window that is displaying the Web site. You also can right click either the Web Forms Designer or the ASPX file name (in the Solution Explorer) and select View In Browser to open a browser window and load the Web page. Right clicking the ASPX file in the Solution Explorer and selecting Browse
21.5 Web Controls
879
also opens the page in a browser, but first allows you to specify the Web browser that should display the page and its screen resolution. Finally, you can run your application by opening a browser window and typing the Web page’s URL in the Address field. When testing an ASP.NET application on the same computer running IIS, type http://localhost/ProjectFolder/PageName.aspx, where ProjectFolder is the folder in which the page resides (usually the name of the project), and PageName is the name of the ASP.NET page. If your application resides on the local file system, you must first start the ASP.NET Development Server by running the application using one of the methods described above. Then you can type the URL (including the PortNumber found in the test server’s tray icon) in the browser to execute the application. Note that all of these methods of running the application compile the project for you. In fact, ASP.NET compiles your Web page whenever it changes between HTTP requests. For example, suppose you browse the page, then modify the ASPX file or add code to the code-behind file. When you reload the page, ASP.NET recompiles the page on the server before returning the HTTP response to the browser. This important new behavior of ASP.NET 2.0 ensures that the client that requests the page always sees the latest version of the page. You can, however, compile a Web page or an entire Web site by selecting Build Page or Build Site, respectively, from the Build menu in Visual Web Developer. If you would like to test your Web application over a network, you may need to change your Windows Firewall settings. For security reasons, Windows Firewall does not allow remote access to a Web server on your local computer by default. To change this, open the Windows Firewall utility in the Windows Control Panel. Click the Advanced tab and select your network connection from the Network Connection Settings list, then click Settings…. On the Services tab of the Advanced Settings dialog, ensure that Web Server (HTTP) is checked. With…
21.5 Web Controls This section introduces some of the Web controls located in the Standard section of the Toolbox (Fig. 21.9). Figure 21.15 summarizes some of the Web controls used in the chapter examples. Web Control
Description
Label
Displays text that the user cannot edit.
TextBox
Gathers user input and displays text.
Button
Triggers an event when clicked.
HyperLink
Displays a hyperlink.
DropDownList
Displays a drop-down list of choices from which a user can select an item.
RadioButtonList
Groups radio buttons.
Image
Displays images (e.g., GIF and JPG).
Fig. 21.15 | Commonly used Web controls .
880
Chapter 21
ASP.NET 2.0, Web Forms and Web Controls
21.5.1 Text and Graphics Controls Figure 21.16 depicts a simple form for gathering user input. This example uses all the controls listed in Fig. 21.15, except Label, which you used in Section 21.4. Note that all the code in Fig. 21.16 was generated by Visual Web Developer in response to actions performed in Design mode. [Note: This example does not contain any functionality—i.e., no action occurs when the user clicks Register. We ask the reader to provide the functionality as an exercise. In successive examples, we demonstrate how to add functionality to many of these Web controls. ] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
Web Controls Demonstration This is a sample registration form.
Please fill in all fields and click Register.
Please fill out the fields below.
Fig. 21.16 | Web Form that demonstrates Web controls. (Part 1 of 3.)
Visual Basic 2005 How to Program 3e Visual C# 2005 How to Program 2e Java How to Program 6e C++ How to Program 5e XML How to Program 1e
Click here to view more information about our books
Which operating system are you using?
Fig. 21.16 | Web Form that demonstrates Web controls. (Part 2 of 3.)
882
Chapter 21
ASP.NET 2.0, Web Forms and Web Controls
95
97 Windows XP 98 Windows 2000 99 Windows NT 100 Linux 101 Other 102 103 104
105
106 108
109 110 111 112
Image control
TextBox control
DropDownList control HyperLink control
RadioButtonList control
Button control
Fig. 21.16 | Web Form that demonstrates Web controls. (Part 3 of 3.)
21.5 Web Controls
883
Before discussing the Web controls used in this ASPX file, we explain the XHTML that creates the layout seen in Fig. 21.16. The page contains an h3 heading element (line 17), followed by a series of additional XHTML blocks. We place most of the Web controls inside p elements (i.e., paragraphs), but we use an XHTML table element (lines 26–60) to organize the Image and TextBox controls in the user information section of the page. In the preceding section, we described how to add heading elements and paragraphs visually without manipulating any XHTML in the ASPX file directly. Visual Web Developer allows you to add a table in a similar manner.
Adding an XHTML Table to a Web Form To create a table with two rows and two columns in Design mode, select the Insert Table command from the Layout menu. In the Insert Table dialog that appears, make sure the Custom radio button is selected. In the Layout group box, change the values of the Rows and Columns combo boxes to 2. By default, the contents of a table cell are aligned vertically in the middle of the cell. We changed the vertical alignment of all cells in the table by clicking the Cell Properties… button, then selecting top from the Vertical align combo box in the resulting dialog. This causes the content of each table cell to align with the top of the cell. Click OK to close the Cell Properties dialog, then click OK to close the Insert Table dialog and create the table. Once a table is created, controls and text can be added to particular cells to create a neatly organized layout. Setting the Color of Text on a Web Form Notice that some of the instructions to the user on the form appear in a teal color. To set the color of a specific piece of text, highlight the text and select Format > Foreground color…. In the Color Picker dialog, click the Named Colors tab and choose a color from the palette shown. Click OK to apply the color. Note that the IDE places the colored text in an XHTML span element (e.g., lines 23–24) and applies the color using the span’s style attribute. Examining Web Controls on a Sample Registration Form Lines 20–21 of Fig. 21.16 define an Image control, which inserts an image into a Web page. The images used in this example are located in the chapter’s examples directory. You can download the examples from www.deitel.com/books/csharpforprogrammers2. Before an image can be displayed on a Web page using an Image Web control, the image must first be added to the project. We added an Images folder to this project (and to each example project in the chapter that uses images) by right clicking the location of the project in the Solution Explorer, selecting Add Folder > Regular Folder and entering the folder name Images. We then added each of the images used in the example to this folder by right clicking the folder, selecting Add Existing Item… and browsing for the files to add. The ImageUrl property (line 21) specifies the location of the image to display in the Image control. To select an image, click the ellipsis next to the ImageUrl property in the Properties window and use the Select Image dialog to browse for the desired image in the project’s Images folder. When the IDE fills in the ImageUrl property based on your selection, it includes a tilde and forward slash (~/) at the beginning of the ImageUrl—this indicates that the Images folder is in the root directory of the project (i.e., http://localhost/ WebControls, whose physical path is C:\Inetpub\wwwroot\WebControls).
884
Chapter 21
ASP.NET 2.0, Web Forms and Web Controls
Lines 26–60 contain the table element created by the steps discussed previously. Each td element contains an Image control and a TextBox control, which allows you to obtain text from the user and display text to the user. For example, lines 32–33 define a TextBox control used to collect the user’s first name. Lines 70–79 define a DropDownList. This control is similar to the ComboBox Windows control. When a user clicks the drop-down list, it expands and displays a list from which the user can make a selection. Each item in the drop-down list is defined by a ListItem element (lines 72–78). After dragging a DropDownList control onto a Web Form, you can add items to it using the ListItem Collection Editor. This process is similar to customizing a ListBox in a Windows application. In Visual Web Developer, you can access the ListItem Collection Editor by clicking the ellipsis next to the Items property of the DropDownList. It can also be accessed using the DropDownList Tasks menu, which is opened by clicking the small arrowhead that appears in the upper-right corner of the control in Design mode (Fig. 21.17). This menu is called a smart tag menu. Visual Web Developer displays smart tag menus for many ASP.NET controls to facilitate performing common tasks. Clicking Edit Items... in the DropDownList Tasks menu opens the ListItem Collection Editor, which allows you to add ListItem elements to the DropDownList. The HyperLink control (lines 82–86 of Fig. 21.16) adds a hyperlink to a Web page. The NavigateUrl property (line 83) of this control specifies the resource (i.e., http:// www.deitel.com) that is requested when a user clicks the hyperlink. Setting the Target property to _blank specifies that the requested Web page should open in a new browser window. By default, HyperLink controls cause pages to open in the same browser window. Lines 96–103 define a RadioButtonList control, which provides a series of radio buttons from which the user can select only one. Like options in a DropDownList, individual radio buttons are defined by ListItem elements. Note that, like the DropDownList Tasks smart tag menu, the RadioButtonList Tasks smart tag menu also provides an Edit Items… link to open the ListItem Collection Editor. The final Web control in Fig. 21.16 is a Button (lines 106–107). Like a Button Windows control, a Button Web control represents a button that triggers an action when clicked. A Button Web control typically maps to an input XHTML element with attribute type set to "button". As stated earlier, clicking the Register button in this example does not do anything.
21.5.2 AdRotator Control Web pages often contain product or service advertisements, which usually consist of images. Although Web site authors want to include as many sponsors as possible, Web pages can display only a limited number of advertisements. To address this problem, ASP.NET provides the AdRotator Web control for displaying advertisements. Using advertisement data located in an XML file, the AdRotator control randomly selects an image to display
Fig. 21.17 | DropDownList Tasks smart tag menu.
21.5 Web Controls
885
and generates a hyperlink to the Web page associated with that image. Browsers that do not support images display alternate text that is specified in the XML document. If a user clicks the image or substituted text, the browser loads the Web page associated with that image.
Demonstrating the AdRotator Web Control Figure 21.18 demonstrates the AdRotator Web control. In this example, the “advertisements” that we rotate are the flags of 10 countries. When a user clicks the displayed flag image, the browser is redirected to a Web page containing information about the country that the flag represents. If a user refreshes the browser or requests the page again, one of the eleven flags is again chosen at random and displayed. The ASPX file in Fig. 21.18 is similar to that in Fig. 21.4. However, instead of XHTML text and a Label, this page contains XHTML text (i.e., the h3 element in line 17) and one AdRotator control named countryRotator (lines 19–21). This page also contains an XmlDataSource control (lines 22–24), which supplies the data to the AdRotator control. The background attribute of the page’s body element (line 14) is set to display the image background.png, located in the project’s Images folder. To specify this file, click the ellipsis provided next to the Background property of DOCUMENT in the Properties window and use the resulting dialog to browse for background.png. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
Flag Rotator AdRotator Example
Fig. 21.18 | Web Form that demonstrates the AdRotator Web control. (Part 1 of 2.)
886
Chapter 21
ASP.NET 2.0, Web Forms and Web Controls
(a)
(b)
AdRotator image AlternateText
displayed in a tooltip
(c)
Fig. 21.18 | Web Form that demonstrates the AdRotator Web control. (Part 2 of 2.) You do not need to add any code to the code-behind file, because the AdRotator control does “all the work.” The output depicts two different requests. Figure 21.18(a) shows the first time the page is requested, when the American flag is shown. In the second request, as shown in Fig. 21.18(b), the French flag is displayed. Figure 21.18(c) depicts the Web page that loads when the French flag is clicked.
Connecting Data to an AdRotator Control An AdRotator control accesses an XML file (presented shortly) to determine what advertisement (i.e., flag) image, hyperlink URL and alternate text to display and include in the page. To connect the AdRotator control to the XML file, we create an XmlDataSource control—one of several ASP.NET data controls (found in the Data section of the Toolbox) that encapsulate data sources and make such data available for Web controls. An XmlDataSource references an XML file containing data that will be used in an ASP.NET application. Later in the chapter, you will learn more about data-bound Web controls, as well as the SqlDataSource control, which retrieves data from a SQL Server database, and the ObjectDataSource control, which encapsulates an object that makes data available.
21.5 Web Controls
887
To build this example, we first add the XML file AdRotatorInformation.xml to the project. Each project created in Visual Web Developer contains an App_Data folder, which is intended to store all the data used by the project. Right click this folder in the Solution Explorer and select Add Existing Item…, then browse for AdRotatorInformation.xml on your computer. (We provide this file in the chapter’s examples directory.) After adding the XML file to the project, drag an AdRotator control from the Toolbox to the Web Form. The AdRotator Tasks smart tag menu will open automatically. From this menu, select from the Choose Data Source drop-down list to start the Data Source Configuration Wizard. Select XML File as the data-source type. This causes the wizard to create an XmlDataSource with the ID specified in the bottom half of the wizard dialog. We set the ID of the control to adXmlDataSource. Click OK in the Data Source Configuration Wizard dialog. The Configure Data Source - adXmlDataSource dialog appears next. In this dialog’s Data File section, click Browse… and, in the Select XML File dialog, locate the XML file you added to the App_Data folder. Click OK to exit this dialog, then click OK to exit the Configure Data Source - adXmlDataSource dialog. After completing these steps, the AdRotator is configured to use the XML file to determine which advertisements to display.
Examining an XML File Containing Advertisement Information XML document AdRotatorInformation.xml (Fig. 21.19)—or any XML document used with an AdRotator control—must contain one Advertisements root element (lines 4– 94). Within that element can be several Ad elements (e.g., lines 5–12), each of which provides information about a different advertisement. Element ImageUrl (line 6) specifies the path (location) of the advertisement’s image, and element NavigateUrl (lines 7–9) specifies the URL for the Web page that loads when a user clicks the advertisement. Note that we reformatted this file for presentation purposes. The actual XML file cannot contain any whitespace before or after the URL in the NavigateUrl element, or the whitespace will be considered part of the URL, and the page will not load properly. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
Images/france.png http://www.odci.gov/cia/publications/factbook/geos/fr.html France Information 1 Images/germany.png http://www.odci.gov/cia/publications/factbook/geos/gm.html
Fig. 21.19 | File containing advertisement information used in AdRotator example. (Part 1 of 3.)
Germany Information 1 Images/italy.png http://www.odci.gov/cia/publications/factbook/geos/it.html Italy Information 1 Images/spain.png http://www.odci.gov/cia/publications/factbook/geos/sp.html Spain Information 1 Images/latvia.png http://www.odci.gov/cia/publications/factbook/geos/lg.html Latvia Information 1 Images/peru.png http://www.odci.gov/cia/publications/factbook/geos/pe.html Peru Information 1 Images/senegal.png http://www.odci.gov/cia/publications/factbook/geos/sg.html Senegal Information 1 Images/sweden.png
Fig. 21.19 | File containing advertisement information used in AdRotator example. (Part 2 of 3.)
http://www.odci.gov/cia/publications/factbook/geos/sw.html Sweden Information 1 Images/thailand.png http://www.odci.gov/cia/publications/factbook/geos/th.html Thailand Information 1 Images/unitedstates.png http://www.odci.gov/cia/publications/factbook/geos/us.html United States Information 1
Fig. 21.19 | File containing advertisement information used in AdRotator example. (Part 3 of 3.) The AlternateText element (line 10) nested in each Ad element contains text that displays in place of the image when the browser cannot locate or render the image for some reason (i.e., the file is missing, or the browser is not capable of displaying it). The AlternateText element’s text is also a tooltip that Internet Explorer displays when a user places the mouse pointer over the image (Fig. 21.18). The Impressions element (line 56) specifies how often a particular image appears, relative to the other images. An advertisement that has a higher Impressions value displays more frequently than an advertisement with a lower value. In our example, the advertisements display with equal probability, because the value of each Impressions is set to 1.
21.5.3 Validation Controls This section introduces a different type of Web control, called a validation control (or validator), which determines whether the data in another Web control is in the proper format. For example, validators could determine whether a user has provided information in a required field or whether a ZIP-code field contains exactly five digits. Validators provide a mechanism for validating user input on the client. When the XHTML for our page is created, the validator is converted into ECMAScript1 that performs the validation. ECMAScript is a scripting language that enhances the functionality and appearance of Web pages. ECMAScript is typically executed on the client. Some clients do not support scripting or disable scripting. However, for security reasons, validation is always performed on the server—whether or not the script executes on the client.
890
Chapter 21
ASP.NET 2.0, Web Forms and Web Controls
Validating Input in a Web Form The example in this section prompts the user to enter a name, e-mail address and phone number. A Web site could use a form like this to collect contact information from site visitors. After the user enters any data, but before the data is sent to the Web server, validators ensure that the user entered a value in each field and that the e-mail address and phone number values are in an acceptable format. In this example, (555) 123-4567, 555-1234567 and 123-4567 are all considered valid phone numbers. Once the data is submitted, the Web server responds by displaying an appropriate message and an XHTML table repeating the submitted information. Note that a real business application would typically store the submitted data in a database or in a file on the server. We simply send the data back to the form to demonstrate that the server received the data. Figure 21.20 presents the ASPX file. Like the Web Form in Fig. 21.16, this Web Form uses a table to organize the page’s contents. Lines 24–25, 36–37 and 56–57 define TextBoxes for retrieving the user’s name, e-mail address and phone number, respectively, and line 75 defines a Submit button. Lines 77–79 create a Label named outputLabel that displays the response from the server when the user successfully submits the form. Notice that outputLabel’s Visible property is initially set to False, so the Label does not appear in the client’s browser when the page loads for the first time. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
Demonstrating Validation Controls Please fill out the following form. All fields are required and must contain valid information.
Name:
Fig. 21.20 | Validators used in a Web Form that retrieves user’s contact information. (Part 1 of 4.) 1.
ECMAScript (commonly known as JavaScript) is a scripting standard developed by ECMA International. Both Netscape’s JavaScript and Microsoft’s JScript implement the ECMAScript standard, but each provides additional features beyond the specification. For information on the current ECMAScript standard, visit www.ecma-international.org/publications/standards/Ecma-262.htm. See www.mozilla.org/js for information on JavaScript and msdn.microsoft.com/library/en-us/ script56/html/js56jsoriJScript.asp for information on JScript.
Fig. 21.20 | Validators used in a Web Form that retrieves user’s contact information. (Part 2 of 4.)
892 75 76 77 78 79 80 81 82 83
Chapter 21
ASP.NET 2.0, Web Forms and Web Controls
(a)
(b)
Fig. 21.20 | Validators used in a Web Form that retrieves user’s contact information. (Part 3 of 4.)
21.5 Web Controls
893
(c)
(d)
Fig. 21.20 | Validators used in a Web Form that retrieves user’s contact information. (Part 4 of 4.) Using RequiredFieldValidator Controls In this example, we use three RequiredFieldValidator controls (found in the Validation section of the Toolbox) to ensure that the name, e-mail address and phone number TextBoxes are not empty when the form is submitted. A RequiredFieldValidator makes an input control a required field. If such a field is empty, validation fails. For example, lines 26–29 define RequiredFieldValidator nameInputValidator, which confirms that nameTextBox is not empty. Line 27 associates nameTextBox with nameInputValidator by setting the validator’s ControlToValidate property to nameTextBox. This indicates that
894
Chapter 21
ASP.NET 2.0, Web Forms and Web Controls
verifies the nameTextBox’s contents. Property ErrorMessage’s text (line 28) is displayed on the Web Form if the validation fails. If the user does not input any data in nameTextBox and attempts to submit the form, the ErrorMessage text is displayed in red. Because we set the control’s Display property to Dynamic (line 29), the validator takes up space on the Web Form only when validation fails—space is allocated dynamically when validation fails, causing the controls below the validator to shift downward to accommodate the ErrorMessage, as seen in Fig. 21.20(a)–Fig. 21.20(c). nameInputValidator
Using RegularExpressionValidator Controls This example also uses RegularExpressionValidator controls to match the e-mail address and phone number entered by the user against regular expressions. (Regular expressions are introduced in Chapter 16.) These controls determine whether the e-mail address and phone number were each entered in a valid format. For example, lines 43–50 create a RegularExpressionValidator named emailFormatValidator. Line 45 sets property ControlToValidate to emailTextBox to indicate that emailFormatValidator verifies the emailTextBox’s contents. A RegularExpressionValidator’s ValidationExpression property specifies the regular expression that validates the ControlToValidate’s contents. Clicking the ellipsis next to property ValidationExpression in the Properties window displays the Regular Expression Editor dialog, which contains a list of Standard expressions for phone numbers, ZIP codes and other formatted information. You can also write your own custom expression. For the emailFormatValidator, we selected the standard expression Internet e-mail address, which uses the validation expression \w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*
This regular expression indicates that an e-mail address is valid if the part of the address before the @ symbol contains one or more word characters (i.e., alphanumeric characters or underscores), followed by zero or more strings comprised of a hyphen, plus sign, period or apostrophe and additional word characters. After the @ symbol, a valid e-mail address must contain one or more groups of word characters potentially separated by hyphens or periods, followed by a required period and another group of one or more word characters potentially separated by hyphens or periods. For example, [email protected], [email protected] and bob'[email protected] are all valid e-mail addresses. If the user enters text in the emailTextBox that does not have the correct format and either clicks in a different text box or attempts to submit the form, the ErrorMessage text is displayed in red. We also use RegularExpressionValidator phoneFormatValidator (lines 63–70) to ensure that the phoneTextBox contains a valid phone number before the form is submitted. In the Regular Expression Editor dialog, we select U.S. phone number, which assigns ((\(\d{3}\) ?)|(\d{3}-))?\d{3}-\d{4}
to the ValidationExpression property. This expression indicates that a phone number can contain a three-digit area code either in parentheses and followed by an optional space or without parentheses and followed by required hyphen. After an optional area code, a phone number must contain three digits, a hyphen and another four digits. For example, (555) 123-4567, 555-123-4567 and 123-4567 are all valid phone numbers.
21.5 Web Controls
895
If all five validators are successful (i.e., each TextBox is filled in, and the e-mail address and phone number provided are valid), clicking the Submit button sends the form’s data to the server. As shown in Fig. 21.20(d), the server then responds by displaying the submitted data in the outputLabel (lines 77–79).
Examining the Code-Behind File for a Web Form That Receives User Input Figure 21.21 depicts the code-behind file for the ASPX file in Fig. 21.20. Notice that this code-behind file does not contain any implementation related to the validators. We say more about this soon.
// Fig. 21.21: Validation.aspx.cs // Code-behind file for the form demonstrating validation controls. using System; using System.Data; using System.Configuration; using System.Web; using System.Web.Security; using System.Web.UI; using System.Web.UI.WebControls; using System.Web.UI.WebControls.WebParts; using System.Web.UI.HtmlControls; public partial class Validation : System.Web.UI.Page { // Page_Load event handler executes when the page is loaded protected void Page_Load( object sender, EventArgs e ) { // if this is not the first time the page is loading // (i.e., the user has already submitted form data) if ( IsPostBack ) { // retrieve the values submitted by the user string name = Request.Form[ "nameTextBox" ]; string email = Request.Form[ "emailTextBox" ]; string phone = Request.Form[ "phoneTextBox" ]; // create a table indicating the submitted values outputLabel.Text += "We received the following information:" + "
" + "
Name:
" + name + "
" + "
E-mail address:
" + email + "
" + "
Phone number:
" + phone + "
" + "
"; outputLabel.Visible = true; // display the output message } // end if } // end method Page_Load } // end class Validation
Fig. 21.21 | Code-behind for a Web Form that obtains a user’s contact information.
896
Chapter 21
ASP.NET 2.0, Web Forms and Web Controls
Web programmers using ASP.NET often design their Web pages so that the current page reloads when the user submits the form; this enables the program to receive input, process it as necessary and display the results in the same page when it is loaded the second time. These pages usually contain a form that when submitted, sends the values of all the controls to the server and causes the current page to be requested again. This event is known as a postback. Line 20 uses the IsPostBack property of class Page to determine whether the page is being loaded due to a postback. The first time that the Web page is requested, IsPostBack is false, and the page displays only the form for user input. When the postback occurs (from the user clicking Submit), IsPostBack is true. Lines 23–25 use the Request object to retrieve the values of nameTextBox, emailTextBox and phoneTextBox from the NameValueCollection Form. When data is posted to the Web server, the XHTML form’s data is accessible to the Web application through the Request object’s Form array. Lines 28–34 append to outputLabel’s Text a line break, an additional message and an XHTML table containing the submitted data so the user knows that the server received the data correctly. In a real business application, the data would be stored in a database or file at this point in the application. Line 36 sets the outputLabel’s Visible property to true, so the user can see the thank you message and submitted data.
Examining the Client-Side XHTML for a Web Form with Validation Figure 21.22 shows the XHTML and ECMAScript sent to the client browser when Validation.aspx loads after the postback. To view this code, select View > Source in Internet Explorer. Lines 25–36, lines 100–171 and lines 180–218 contain the ECMAScript that provides the implementation for the validation controls and for performing the postback. ASP.NET generates this ECMAScript. You do not need to be able to create or even understand ECMAScript—the functionality defined for the controls in our application is converted to working ECMAScript for us. In earlier ASPX files, we explicitly set the EnableViewState attribute of each Web control to False. This attribute determines whether a Web control’s value persists (i.e., is retained) when a postback occurs. By default, this attribute is True, which indicates that the control’s value persists. In Fig. 21.20(d), notice that the values entered by the user still appear in the text boxes after the postback occurs. A hidden input in the XHTML document (lines 14–22 of Fig. 21.22) contains the data of the controls on this page. This element is always named __VIEWSTATE and stores the controls’ data as an encoded string.
Performance Tip 21.2 Setting EnableViewState to False reduces the amount of data passed to the Web server with each request. 21.2
1 2 3 4 5 6
Fig. 21.22 | XHTML and ECMAScript generated by ASP.NET and sent to the browser when Validation.aspx
Fig. 21.22 | XHTML and ECMAScript generated by ASP.NET and sent to the browser when Validation.aspx
is requested. (Part 6 of 6.)
21.6 Session Tracking Originally, critics accused the Internet and e-business of failing to provide the kind of customized service typically experienced in “brick-and-mortar” stores. To address this problem, e-businesses began to establish mechanisms by which they could personalize users’ browsing experiences, tailoring content to individual users while enabling them to bypass irrelevant information. Businesses achieve this level of service by tracking each customer’s movement through the Internet and combining the collected data with information provided by the consumer, including billing information, personal preferences, interests and hobbies.
Personalization Personalization makes it possible for e-businesses to communicate effectively with their customers and also improves users’ ability to locate desired products and services. Companies that provide content of particular interest to users can establish relationships with customers and build on those relationships over time. Furthermore, by targeting consumers with personal offers, recommendations, advertisements, promotions and services, e-businesses create customer loyalty. Web sites can use sophisticated technology to allow visitors to customize home pages to suit their individual needs and preferences. Similarly, online shopping sites often store personal information for customers, tailoring notifications and special offers to their interests. Such services encourage customers to visit sites more frequently and make purchases more regularly. Privacy A trade-off exists, however, between personalized e-business service and protection of privacy. Some consumers embrace the idea of tailored content, but others fear the possible adverse consequences if the info they provide to e-businesses is released or collected by tracking technologies. Consumers and privacy advocates ask: What if the e-business to which we give personal data sells or gives that information to another organization without our knowledge? What if we do not want our actions on the Internet—a supposedly anonymous medium—to be tracked and recorded by unknown parties? What if unauthorized parties gain access to sensitive private data, such as credit-card numbers or medical history?
902
Chapter 21
ASP.NET 2.0, Web Forms and Web Controls
All of these are questions that must be debated and addressed by programmers, consumers, e-businesses and lawmakers alike.
Recognizing Clients To provide personalized services to consumers, e-businesses must be able to recognize clients when they request information from a site. As we have discussed, the request/response system on which the Web operates is facilitated by HTTP. Unfortunately, HTTP is a stateless protocol—it does not support persistent connections that would enable Web servers to maintain state information regarding particular clients. This means that Web servers cannot determine whether a request comes from a particular client or whether the same or different clients generate a series of requests. To circumvent this problem, sites can provide mechanisms by which they identify individual clients. A session represents a unique client on a Web site. If the client leaves a site and then returns later, the client will still be recognized as the same user. To help the server distinguish among clients, each client must identify itself to the server. Tracking individual clients, known as session tracking, can be achieved in a number of ways. One popular technique uses cookies (Section 21.6.1); another uses ASP.NET’s HttpSessionState object (Section 21.6.2). Additional sessiontracking techniques include the use of input form elements of type "hidden" and URL rewriting. Using "hidden" form elements, a Web Form can write session-tracking data into a form in the Web page that it returns to the client in response to a prior request. When the user submits the form in the new Web page, all the form data, including the "hidden" fields, is sent to the form handler on the Web server. When a Web site performs URL rewriting, the Web Form embeds session-tracking information directly in the URLs of hyperlinks that the user clicks to send subsequent requests to the Web server. Note that our previous examples set the Web Form’s EnableSessionState property to False. However, because we wish to use session tracking in the following examples, we keep this property’s default setting—True.
21.6.1 Cookies Cookies provide Web developers with a tool for personalizing Web pages. A cookie is a piece of data stored in a small text file on the user’s computer. A cookie maintains information about the client during and between browser sessions. The first time a user visits the Web site, the user’s computer might receive a cookie; this cookie is then reactivated each time the user revisits that site. The collected information is intended to be an anonymous record containing data that is used to personalize the user’s future visits to the site. For example, cookies in a shopping application might store unique identifiers for users. When a user adds items to an online shopping cart or performs another task resulting in a request to the Web server, the server receives a cookie containing the user’s unique identifier. The server then uses the unique identifier to locate the shopping cart and perform any necessary processing. In addition to identifying users, cookies also can indicate clients’ shopping preferences. When a Web Form receives a request from a client, the Web Form can examine the cookie(s) it sent to the client during previous communications, identify the client’s preferences and immediately display products of interest to the client. Every HTTP-based interaction between a client and a server includes a header containing information either about the request (when the communication is from the client
21.6 Session Tracking
903
to the server) or about the response (when the communication is from the server to the client). When a Web Form receives a request, the header includes information such as the request type (e.g., Get) and any cookies that have been sent previously from the server to be stored on the client machine. When the server formulates its response, the header information contains any cookies the server wants to store on the client computer and other information, such as the MIME type of the response. The expiration date of a cookie determines how long the cookie remains on the client’s computer. If you do not set an expiration date for a cookie, the Web browser maintains the cookie for the duration of the browsing session. Otherwise, the Web browser maintains the cookie until the expiration date occurs. When the browser requests a resource from a Web server, cookies previously sent to the client by that Web server are returned to the Web server as part of the request formulated by the browser. Cookies are deleted when they expire.
Portability Tip 21.3 Clients may disable cookies in their Web browsers to ensure that their privacy is protected. Such clients will experience difficulty using Web applications that depend on cookies to maintain state information. 21.3
Using Cookies to Provide Book Recommendations The next Web application demonstrates the use of cookies. The example contains two pages. In the first page (Figs. 21.23–21.24), users select a favorite programming language from a group of radio buttons and submit the XHTML form to the Web server for processing. The Web server responds by creating a cookie that stores a record of the chosen language, as well as the ISBN number for a book on that topic. The server then returns an XHTML document to the browser, allowing the user either to select another favorite programming language or to view the second page in our application (Figs. 21.25–21.26), which lists recommended books pertaining to the programming language that the user selected previously. When the user clicks the hyperlink, the cookies previously stored on the client are read and used to form the list of book recommendations. The ASPX file in Fig. 21.23 contains five radio buttons (lines 21–27) with the values Visual Basic 2005, Visual C# 2005, C, C++, and Java. Recall that you can set the values of radio buttons via the ListItem Collection Editor, which is opened either by clicking the 1 2 3 4 5 6 7 8 9 10 11 12 13
Cookies
Fig. 21.23 | ASPX file that presents a list of programming languages. (Part 1 of 3.)
Visual Basic 2005 Visual C# 2005 C C++ Java Click here to choose another language Click here to get book recommendations
(a)
Fig. 21.23 | ASPX file that presents a list of programming languages. (Part 2 of 3.)
21.6 Session Tracking
905
(b)
(c)
(d)
Fig. 21.23 | ASPX file that presents a list of programming languages. (Part 3 of 3.) RadioButtonList’s Items
property in the Properties window or by clicking the Edit Items… link in the RadioButtonList Tasks smart tag menu. The user selects a programming language by clicking one of the radio buttons. The page contains a Submit button, which when clicked, creates a cookie containing a record of the selected language. Once created, this cookie is added to the HTTP response header, and a postback occurs. Each time the user chooses a language and clicks Submit, a cookie is written to the client.
906
Chapter 21
ASP.NET 2.0, Web Forms and Web Controls
When the postback occurs, certain controls are hidden and others are displayed. The Label, RadioButtonList and Button used to select a language are hidden. Toward the bottom of the page, a Label and two HyperLinks are displayed. One link requests this page (lines 36–38), and the other requests Recommendations.aspx (lines 41–43). Notice that
clicking the first hyperlink (the one that requests the current page) does not cause a postback to occur. The file Options.aspx is specified in the NavigateUrl property of the hyperlink. When the hyperlink is clicked, this page is requested as a completely new request. Recall that earlier in the chapter, we set NavigateUrl to a remote Web site (http://www.deitel.com). To set this property to a page within the same ASP.NET application, click the ellipsis button next to the NavigateUrl property in the Properties window to open the Select URL dialog. Use this dialog to select a page within your project as the destination for the HyperLink.
Adding and Linking to a New Web Form Setting the NavigateUrl property to a page in the current application requires that the destination page exist already. Thus, to set the NavigateUrl property of the second link (the one that requests the page with book recommendations) to Recommendations.aspx, you must first create this file by right clicking the project location in the Solution Explorer and selecting Add New Item… from the menu that appears. In the Add New Item dialog, select Web Form from the Templates pane and change the name of the file to Recommendations.aspx. Finally, check the box labeled Place code in separate file to indicate that the IDE should create a code-behind file for this ASPX file. Click Add to create the file. (We discuss the contents of this ASPX file and code-behind file shortly.) Once the Recommendations.aspx file exists, you can select it as the NavigateUrl value for a HyperLink in the Select URL dialog. Writing Cookies in a Code-Behind File Figure 21.24 presents the code-behind file for Options.aspx (Fig. 21.23). This file contains the code that writes a cookie to the client machine when the user selects a programming language. The code-behind file also modifies the appearance of the page in response to a postback. 1 2 3 4 5 6 7 8 9 10 11 12 13 14
// Fig. 21.24: Options.aspx.cs // Processes user's selection of a programming language // by displaying links and writing a cookie to the user's machine. using System; using System.Data; using System.Configuration; using System.Web; using System.Web.Security; using System.Web.UI; using System.Web.UI.WebControls; using System.Web.UI.WebControls.WebParts; using System.Web.UI.HtmlControls; public partial class Options : System.Web.UI.Page {
Fig. 21.24 | Code-behind file that writes a cookie to the client. (Part 1 of 3.)
// stores values to represent books as cookies private System.Collections.Hashtable books = new System.Collections.Hashtable(); // initializes the Hashtable of values to be stored as cookies protected void Page_Init( object sender, EventArgs e ) { books.Add( "Visual Basic 2005", "0-13-186900-0" ); books.Add( "Visual C# 2005", "0-13-152523-9" ); books.Add( "C", "0-13-142644-3" ); books.Add( "C++", "0-13-185757-6" ); books.Add( "Java", "0-13-148398-6" ); } // end method Page_Init // if postback, hide form and display links to make additional // selections or view recommendations protected void Page_Load( object sender, EventArgs e ) { if ( IsPostBack ) { // user has submitted information, so display message // and appropriate hyperlinks responseLabel.Visible = true; languageLink.Visible = true; recommendationsLink.Visible = true; // hide other controls used to make language selection promptLabel.Visible = false; languageList.Visible = false; submitButton.Visible = false; // if the user made a selection, display it in responseLabel if ( languageList.SelectedItem != null ) responseLabel.Text += " You selected " + languageList.SelectedItem.Text.ToString(); else responseLabel.Text += " You did not select a language."; } // end if } // end method Page_Load // write a cookie to record the user's selection protected void submitButton_Click( object sender, EventArgs e ) { // if the user made a selection if ( languageList.SelectedItem != null ) { string language = languageList.SelectedItem.ToString(); // get ISBN number of book for the given language string ISBN = books[ language ].ToString(); // create cookie using language-ISBN name-value pair HttpCookie cookie = new HttpCookie( language, ISBN );
Fig. 21.24 | Code-behind file that writes a cookie to the client. (Part 2 of 3.)
908 68 69 70 71 72 73
Chapter 21
ASP.NET 2.0, Web Forms and Web Controls
// add cookie to response to place it on the user's machine Response.Cookies.Add( cookie ); } // end if } // end method submitButton_Click } // end class Options
Fig. 21.24 | Code-behind file that writes a cookie to the client. (Part 3 of 3.) Lines 16–17 create books as a Hashtable (namespace System.Collections)—a data structure that stores key–value pairs. A program uses the key to store and retrieve the associated value in the Hashtable. In this example, the keys are strings containing the programming languages’ names, and the values are strings containing the ISBN numbers for the recommended books. Class Hashtable provides method Add, which takes as arguments a key and a value. A value that is added via method Add is placed in the Hashtable at a location determined by the key. The value for a specific Hashtable entry can be determined by indexing the Hashtable with that value’s key. The expression HashtableName[ keyName ]
returns the value in the key–value pair in which keyName is the key. For example, the expression books[ language ] in line 64 returns the value that corresponds to the key contained in language. Class Hashtable is discussed in detail in Chapter 24, Data Structures. Clicking the Submit button creates a cookie if a language is selected and causes a postback to occur. In the submitButton_Click event handler (lines 56–72), a new cookie object (of type HttpCookie) is created to store the language and its corresponding ISBN number (line 67). This cookie is then Added to the Cookies collection sent as part of the HTTP response header (line 70). The postback causes the condition in the if statement of Page_Load (line 33) to evaluate to true, and lines 37–51 execute. Lines 37–39 reveal the initially hidden controls responseLabel, languageLink and recommendationsLink. Lines 42–44 hide the controls used to obtain the user’s language selection. Line 47 determines whether the user selected a language. If so, that language is displayed in responseLabel (lines 48–49). Otherwise, text indicating that a language was not selected is displayed in responseLabel (line 51).
Displaying Book Recommendations Based on Cookie Values After the postback of Options.aspx, the user may request a book recommendation. The book recommendation hyperlink forwards the user to Recommendations.aspx (Fig. 21.25) to display the recommendations based on the user’s language selections. 1 2 3 4 5 6 7
Fig. 21.25 | ASPX file that displays book recommendations based on cookies. (Part 1 of 2.)
Book Recommendations Click here to choose another language
Fig. 21.25 | ASPX file that displays book recommendations based on cookies. (Part 2 of 2.) Recommendations.aspx contains a Label (lines 16–19), a ListBox (lines 21–22) and a HyperLink (lines 24–27). The Label displays the text Recommendations if the user has selected one or more languages; otherwise, it displays No Recommendations. The ListBox
displays the recommendations created by the code-behind file, which is shown in Fig. 21.26. The HyperLink allows the user to return to Options.aspx to select additional languages.
Code-Behind File That Creates Book Recommendations From Cookies In the code-behind file Recommendations.aspx.cs (Fig. 21.26), method Page_Init (lines 17–40) retrieves the cookies from the client, using the Request object’s Cookies property
// Fig. 21.26: Recommendations.aspx.cs // Creates book recommendations based on cookies. using System; using System.Data; using System.Configuration; using System.Collections; using System.Web; using System.Web.Security; using System.Web.UI; using System.Web.UI.WebControls; using System.Web.UI.WebControls.WebParts; using System.Web.UI.HtmlControls; public partial class Recommendations : System.Web.UI.Page { // read cookies and populate ListBox with any book recommendations protected void Page_Init( object sender, EventArgs e ) { // retrieve client's cookies HttpCookieCollection cookies = Request.Cookies; // if there are cookies, list the appropriate books and ISBN numbers if ( cookies.Count != 0 ) { for ( int i = 0; i < cookies.Count; i++ ) booksListBox.Items.Add( cookies[ i ].Name + " How to Program. ISBN#: " + cookies[ i ].Value ); } // end if else { // if there are no cookies, then no language was chosen, so // display appropriate message and clear and hide booksListBox recommendationsLabel.Text = "No Recommendations"; booksListBox.Items.Clear(); booksListBox.Visible = false; // modify languageLink because no language was selected languageLink.Text = "Click here to choose a language"; } // end else } // end method Page_Init } // end class Recommendations
Fig. 21.26 | Reading cookies from a client to determine book recommendations. (line 20). This returns a collection of type HttpCookieCollection, containing cookies that have previously been written to the client. Cookies can be read by an application only if they were created in the domain in which the application is running—a Web server can never access cookies created outside the domain associated with that server. For example, a cookie created by a Web server in the deitel.com domain cannot be read by a Web server in any other domain. Line 23 determines whether at least one cookie exists. Lines 25–27 add the information in the cookie(s) to the booksListBox. The for statement retrieves the name and value
21.6 Session Tracking
911
of each cookie using i, the statement’s control variable, to determine the current value in the cookie collection. The Name and Value properties of class HttpCookie, which contain the language and corresponding ISBN, respectively, are concatenated with " How to Program. ISBN# " and added to the ListBox. Lines 33–38 execute if no language was selected. We summarize some commonly used HttpCookie properties in Fig. 21.27.
21.6.2 Session Tracking with HttpSessionState C# provides session-tracking capabilities in the Framework Class Library’s HttpSessionState class. To demonstrate basic session-tracking techniques, we modified Fig. 21.26 so that it uses HttpSessionState objects. Figure 21.28 presents the ASPX file, and Fig. 21.29 presents the code-behind file. The ASPX file is similar to that presented in Fig. 21.23, except Fig. 21.28 contains two additional Labels (lines 35–36 and lines 38– 39), which we discuss shortly. Every Web Form includes an HttpSessionState object, which is accessible through property Session of class Page. Throughout this section, we use property Session to manipulate our page’s HttpSessionState object. When the Web page is requested, an HttpSessionState object is created and assigned to the Page’s Session property. As a result, we often refer to property Session as the Session object.
Properties
Description
Domain
Returns a string containing the cookie’s domain (i.e., the domain of the Web server running the application that wrote the cookie). This determines which Web servers can receive the cookie. By default, cookies are sent to the Web server that originally sent the cookie to the client. Changing the Domain property causes the cookie to be returned to a Web server other than the one that originally wrote it.
Expires
Returns a DateTime object indicating when the browser can delete the cookie.
Name
Returns a string containing the cookie’s name.
Path
Returns a string containing the path to a directory on the server (i.e., the Domain) to which the cookie applies. Cookies can be “targeted” to specific directories on the Web server. By default, a cookie is returned only to applications operating in the same directory as the application that sent the cookie or a subdirectory of that directory. Changing the Path property causes the cookie to be returned to a directory other than the one from which it was originally written.
Secure
Returns a bool value indicating whether the cookie should be transmitted through a secure protocol. The value true causes a secure protocol to be used.
Sessions Visual Basic 2005 Visual C# 2005 C C++ Java Click here to choose another language Click here to get book recommendations
Fig. 21.28 | ASPX file that presents a list of programming languages. (Part 1 of 3.)
21.6 Session Tracking
(a)
(b)
(c)
Fig. 21.28 | ASPX file that presents a list of programming languages. (Part 2 of 3.)
913
914
Chapter 21
ASP.NET 2.0, Web Forms and Web Controls
(d)
Fig. 21.28 | ASPX file that presents a list of programming languages. (Part 3 of 3.) Adding Session Items When the user presses Submit on the Web Form, submitButton_Click is invoked in the code-behind file (Fig. 21.29). Method submitButton_Click responds by adding a key– value pair to our Session object, specifying the language chosen and the ISBN number for a book on that language. These key–value pairs are often referred to as session items. Next, a postback occurs. Each time the user clicks Submit, submitButton_Click adds a new session item to the HttpSessionState object. Because much of this example is identical to the last example, we concentrate on the new features.
Software Engineering Observation 21.1 A Web Form should not use instance variables to maintain client state information, because each new request or postback is handled by a new instance of the page. Web Forms should maintain client state information in HttpSessionState objects, because such objects are specific to each client. 21.1
1 2 3 4 5 6 7 8 9 10 11 12 13 14
// Fig. 21.29: Options.aspx.cs // Processes user's selection of a programming language // by displaying links and writing a cookie to the user's machine. using System; using System.Data; using System.Configuration; using System.Web; using System.Web.Security; using System.Web.UI; using System.Web.UI.WebControls; using System.Web.UI.WebControls.WebParts; using System.Web.UI.HtmlControls; public partial class Options : System.Web.UI.Page {
Fig. 21.29 | Creates a session item for each programming language selected by the user on the ASPX page. (Part 1 of 3.)
// stores values to represent books as cookies private System.Collections.Hashtable books = new System.Collections.Hashtable(); //initializes the Hashtable of values to be stored as cookies protected void Page_Init( object sender, EventArgs e ) { books.Add( "Visual Basic 2005", "0-13-186900-0" ); books.Add( "Visual C# 2005", "0-13-152523-9" ); books.Add( "C", "0-13-142644-3" ); books.Add( "C++", "0-13-185757-6" ); books.Add( "Java", "0-13-148398-6" ); } // end method Page_Init // if postback, hide form and display links to make additional // selections or view recommendations protected void Page_Load( object sender, EventArgs e ) { if ( IsPostBack ) { // user has submitted information, so display appropriate labels // and hyperlinks responseLabel.Visible = true; idLabel.Visible = true; timeoutLabel.Visible = true; languageLink.Visible = true; recommendationsLink.Visible = true; // hide other controls used to make language selection promptLabel.Visible = false; languageList.Visible = false; submitButton.Visible = false; // if the user made a selection, display it in responseLabel if ( languageList.SelectedItem != null ) responseLabel.Text += " You selected " + languageList.SelectedItem.Text.ToString(); else responseLabel.Text += " You did not select a language."; // display session ID idLabel.Text = "Your unique session ID is: " + Session.SessionID; // display the timeout timeoutLabel.Text = "Timeout: " + Session.Timeout + " minutes."; } // end if } // end method Page_Load // write a cookie to record the user's selection protected void submitButton_Click( object sender, EventArgs e ) {
Fig. 21.29 | Creates a session item for each programming language selected by the user on the ASPX page. (Part 2 of 3.)
916 66 67 68 69 70 71 72 73 74 75 76 77
Chapter 21
ASP.NET 2.0, Web Forms and Web Controls
// if the user made a selection if ( languageList.SelectedItem != null ) { string language = languageList.SelectedItem.ToString(); // get ISBN number of book for the given language string ISBN = books[ language ].ToString(); Session.Add( language, ISBN ); // add name/value pair to Session } // end if } // end method submitButton_Click } // end class Options
Fig. 21.29 | Creates a session item for each programming language selected by the user on the ASPX page. (Part 3 of 3.)
Like a cookie, an HttpSessionState object can store name–value pairs. These session items are placed in an HttpSessionState object by calling method Add. Line 74 calls Add to place the language and its corresponding recommended book’s ISBN number in the HttpSessionState object. If the application calls method Add to add an attribute that has the same name as an attribute previously stored in a session, the object associated with that attribute is replaced.
Software Engineering Observation 21.2 One of the primary benefits of using HttpSessionState objects (rather than cookies) is that HttpSessionState objects can store any type of object (not just Strings) as attribute values. This provides you with increased flexibility in determining the type of state information to maintain for clients. 21.2
The application handles the postback event (lines 33–60) in method Page_Load. Here, we retrieve information about the current client’s session from the Session object’s properties and display this information in the Web page. The ASP.NET application contains information about the HttpSessionState object for the current client. Property SessionID (line 56) contains the unique session ID—a sequence of random letters and numbers. The first time a client connects to the Web server, a unique session ID is created for that client. When the client makes additional requests, the client’s session ID is compared with the session IDs stored in the Web server’s memory to retrieve the HttpSessionState object for that client. Property Timeout (line 59) specifies the maximum amount of time that an HttpSessionState object can be inactive before it is discarded. Figure 21.30 lists some common HttpSessionState properties. Properties
Description
Count
Specifies the number of key–value pairs in the Session object.
IsNewSession
Indicates whether this is a new session (i.e., whether the session was created during loading of this page).
Fig. 21.30
| HttpSessionState
properties. (Part 1 of 2.)
21.6 Session Tracking
917
Properties
Description
IsReadOnly
Indicates whether the Session object is read-only.
Keys
Returns a collection containing the Session object’s keys.
SessionID
Returns the session’s unique ID.
Timeout
Specifies the maximum number of minutes during which a session can be inactive (i.e., no requests are made) before the session expires. By default, this property is set to 20 minutes.
Fig. 21.30
| HttpSessionState
properties. (Part 1 of 2.)
Displaying Recommendations Based on Session Values As in the cookies example, this application provides a link to Recommendations.aspx (Fig. 21.31), which displays a list of book recommendations based on the user’s language selections. Lines 21–22 define a ListBox Web control that is used to present the recommendations to the user. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
Book Recommendations Click here to choose another language
Fig. 21.31 | Session-based book recommendations displayed in a ListBox. (Part 1 of 2.)
918
Chapter 21
ASP.NET 2.0, Web Forms and Web Controls
Fig. 21.31 | Session-based book recommendations displayed in a ListBox. (Part 2 of 2.) Code-Behind File That Creates Book Recommendations from a Session Figure 21.32 presents the code-behind file for Recommendations.aspx. Event handler Page_Init (lines 17–47) retrieves the session information. If a user has not selected a language on Options.aspx, our Session object’s Count property will be 0. This property provides the number of session items contained in a Session object. If Session object’s Count property is 0 (i.e., no language was selected), then we display the text No Recommendations and update the Text of the HyperLink back to Options.aspx. If the user has chosen a language, the for statement (lines 25–34) iterates through our Session object’s session items, temporarily storing each key name (line 27). The value in a key–value pair is retrieved from the Session object by indexing the Session object with the key name, using the same process by which we retrieved a value from our Hashtable in the preceding section. Line 27 accesses the Keys property of class HttpSessionState, which returns a collection containing all the keys in the session. Line 27 indexes this collection to retrieve the current key. Lines 31–33 concatenate keyName’s value to the string " How to Program. ISBN#: " and the value from the Session object for which keyName is the key. This string is the recommendation that appears in the ListBox. 1 2 3 4 5 6 7 8 9 10 11
// Fig. 21.32: Recommendations.aspx.cs // Creates book recommendations based on a session object. using System; using System.Data; using System.Configuration; using System.Collections; using System.Web; using System.Web.Security; using System.Web.UI; using System.Web.UI.WebControls; using System.Web.UI.WebControls.WebParts;
Fig. 21.32 | Session data used to provide book recommendations to the user. (Part 1 of 2.)
21.7 Case Study: Connecting to a Database in ASP.NET
using System.Web.UI.HtmlControls; public partial class Recommendations : System.Web.UI.Page { // read cookies and populate ListBox with any book recommendations protected void Page_Init( object sender, EventArgs e ) { // stores a key name found in the Session object string keyName; // determine whether Session contains any information if ( Session.Count != 0 ) { for ( int i = 0; i < Session.Count; i++ ) { keyName = Session.Keys[ i ]; // store current key name // use current key to display one // of session's name-value pairs booksListBox.Items.Add( keyName + " How to Program. ISBN#: " + Session[ keyName ].ToString() ); } // end for } // end if else { // if there are no session items, no language was chosen, so // display appropriate message and clear and hide booksListBox recommendationsLabel.Text = "No Recommendations"; booksListBox.Items.Clear(); booksListBox.Visible = false; // modify languageLink because no language was selected languageLink.Text = "Click here to choose a language"; } // end else } // end method Page_Init } // end class Recommendations
Fig. 21.32 | Session data used to provide book recommendations to the user. (Part 2 of 2.)
21.7 Case Study: Connecting to a Database in ASP.NET Many Web sites allow users to provide feedback about the Web site in a guestbook. Typically, users click a link on the Web site’s home page to request the guestbook page. This page usually consists of an XHTML form that contains fields for the user’s name, e-mail address, message/feedback and so on. Data submitted on the guestbook form is then stored in a database located on the Web server’s machine. In this section, we create a guestbook Web Form application. This example’s GUI is slightly more complex than that of earlier examples. It contains a GridView ASP.NET data control, as shown in Fig. 21.33, which displays all the entries in the guestbook in tabular format. We explain how to create and configure this data control shortly. Note that the GridView displays abc in Design mode to indicate string data that will be retrieved from a data source at runtime.
920
Chapter 21
ASP.NET 2.0, Web Forms and Web Controls
GridView
control
Fig. 21.33 | Guestbook application GUI in Design mode. The XHTML form presented to the user consists of a name field, an e-mail address field and a message field. The form also contains a Submit button to send the data to the server and a Clear button to reset each of the fields on the form. The application stores the guestbook information in a SQL Server database called Guestbook.mdf located on the Web server. (We provide this database in the examples directory for this chapter. You can download the examples from www.deitel.com/books/csharpforprogrammers2.) Below the XHTML form, the GridView displays the data (i.e., guestbook entries) in the database’s Messages table.
21.7.1 Building a Web Form That Displays Data from a Database We now explain how to build this GUI and set up the data binding between the GridView control and the database. Many of these steps are similar to those performed in Chapter 20 to access and interact with a database in a Windows application. We present the ASPX file generated from the GUI later in the section, and we discuss the related code-behind file in the next section. To build the guestbook application, perform the following steps:
Step 1: Creating the Project Create an ASP.NET Web Site named Guestbook and rename the ASPX file Guestbook.aspx. Rename the class in the code-behind file Guestbook, and update the Page directive in the ASPX file accordingly. Step 2: Creating the Form for User Input In Design mode for the ASPX file, add the text Please leave a message in our guestbook: formatted as a navy blue h2 header. As discussed in Section 21.5.1, insert an XHTML table with two columns and four rows, configured so that the text in each cell aligns with the top of the cell. Place the appropriate text (see Fig. 21.33) in the top three cells in the table’s left column. Then place TextBoxes named nameTextBox, emailTextBox
21.7 Case Study: Connecting to a Database in ASP.NET
921
and messageTextBox in the top three table cells in the right column. Set messageTextBox to be a multiline TextBox. Finally, add Buttons named submitButton and clearButton to the bottom-right table cell. Set the buttons’ captions to Submit and Clear, respectively. We discuss the event handlers for these buttons when we present the code-behind file.
Step 3: Adding a GridView Control to the Web Form Add a GridView named messagesGridView that will display the guestbook entries. This control appears in the Data section of the Toolbox. The colors for the GridView are specified through the Auto Format... link in the GridView Tasks smart tag menu that opens when you place the GridView on the page. Clicking this link causes an Auto Format dialog to open with several choices. In this example, we chose Simple. We show how to set the GridView’s data source (i.e., where it gets the data to display in its rows and columns) shortly. Step 4: Adding a Database to an ASP.NET Web Application To use a database in an ASP.NET Web application, you must first add it to the project’s App_Data folder. Right click this folder in the Solution Explorer and select Add Existing Item…. Locate the Guestbook.mdf file in the chapter’s examples directory, then click Add. Step 5: Binding the GridView to the Messages Table of the Guestbook Database Now that the database is part of the project, we can configure the GridView to display its data. Open the GridView Tasks smart tag menu, then select from the Choose Data Source drop-down list. In the Data Source Configuration Wizard that appears, select Database. In this example, we use a SqlDataSource control that allows the application to interact with the Guestbook database. Set the ID of the data source to messagesSqlDataSource and click OK to begin the Configure Data Source wizard. In the Choose Your Data Connection screen, select Guestbook.mdf from the drop-down list (Fig. 21.34), then click Next > twice to continue to the Configure the Select Statement screen.
Fig. 21.34 |
Configure Data Source dialog in Visual Web Developer.
922
Chapter 21
ASP.NET 2.0, Web Forms and Web Controls
The Configure the Select Statement screen (Fig. 21.35) allows you to specify which data the SqlDataSource should retrieve from the database. Your choices on this page design a SELECT statement, shown in the bottom pane of the dialog. The Name drop-down list identifies a table in the database. The Guestbook database contains only one table named Messages, which is selected by default. In the Columns pane, click the checkbox marked with an asterisk (*) to indicate that you want to retrieve the data from all the columns in the Message table. Click the Advanced button, then check the box next to Generate UPDATE, INSERT and DELETE statements . This configures the SqlDataSource control to allow us to insert new data into the database. We discuss inserting new guestbook entries based on users’ form submissions shortly. Click OK, then click Next > to continue the Configure Data Source wizard. The next screen of the wizard allows you to test the query that you just designed. Click Test Query to preview the data that will be retrieved by the SqlDataSource (shown in Fig. 21.36). Finally, click Finish to complete the wizard. Notice that a control named messagesSqlDataSource now appears on the Web Form directly below the GridView (Fig. 21.37). This control is represented in Design mode as a gray box containing its type and name. This control will not appear on the Web page—the gray box simply provides a way to manipulate the control visually through Design mode. Also notice that the GridView now has column headers that correspond to the columns in the Messages table and that the rows each contain either a number (which signifies an autoincremented column) or abc (which indicates string data). The actual data from the Guestbook database file will appear in these rows when the ASPX file is executed and viewed in a Web browser.
Step 6: Modifying the Columns of the Data Source Displayed in the GridView It is not necessary for site visitors to see the MessageID column when viewing past guestbook entries—this column is merely a unique primary key required by the Messages table within the database. Thus, we modify the GridView so that this column does not display
Fig. 21.35 | Configuring the SELECT statement used by the SqlDataSource to retrieve data.
21.7 Case Study: Connecting to a Database in ASP.NET
923
Fig. 21.36 | Previewing the data retrieved by the SqlDataSource.
SqlDataSource
control
Fig. 21.37 | Design mode displaying SqlDataSource control for a GridView. on the Web Form. In the GridView Tasks smart tag menu, click Edit Columns. In the resulting Fields dialog (Fig. 21.38), select MessageID in the Selected fields pane, then click the X. This removes the MessageID column from the GridView. Click OK to return to the main IDE window. The GridView should now appear as in Fig. 21.33.
924
Chapter 21
ASP.NET 2.0, Web Forms and Web Controls
Fig. 21.38 | Removing the MessageID column from the GridView. Step 7: Modifying the Way the SqlDataSource Control Inserts Data When you create a SqlDataSource in the manner described here, it is configured to permit INSERT SQL operations against the database table from which it gathers data. You must specify the values to insert either programmatically or through other controls on the Web Form. In this example, we wish to insert the data entered by the user in the nameTextBox, emailTextBox and messageTextBox controls. We also want to insert the current date— we will specify the date to insert programmatically in the code-behind file, which we present shortly. To configure the SqlDataSource to allow such an insertion, click the ellipsis button next to the InsertQuery property of the messagesSqlDataSource control in the Properties window. The Command and Parameter Editor (Fig. 21.39) that appears displays the INSERT command used by the SqlDataSource control. This command contains parameters @Date, @Name, @Email and @Message. You must provide values for these parameters before they are inserted into the database. Each parameter is listed in the Parameters section of the Command and Parameter Editor. Because we will set the Date parameter programmatically, we do not modify it here. For each of the remaining three parameters, select the parameter, then select Control from the Parameter source drop-down list. This indicates that the value of the parameter should be taken from a control. The ControlID drop-down list contains all the controls on the Web Form. Select the appropriate control for each parameter, then click OK. Now the SqlDataSource is configured to insert the user’s name, e-mail address and message in the Messages table of the Guestbook database. We show how to set the date parameter and initiate the insert operation when the user clicks Submit shortly. ASPX File for a Web Form That Interacts with a Database The ASPX file generated by the guestbook GUI (and messagesSqlDataSource control) is shown in Fig. 21.40. This file contains a large amount of generated markup. We discuss only those parts that are new or noteworthy for the current example. Lines 20–58 contain the XHTML and ASP.NET elements that comprise the form that gathers user input. The
21.7 Case Study: Connecting to a Database in ASP.NET
925
Fig. 21.39 | Setting up INSERT parameters based on control values. control appears in lines 61–87. The start tag (lines 61–65) contains properties that set various aspects of the GridView’s appearance and behavior, such as whether grid lines should be displayed between rows and columns. The DataSourceID property identifies the data source that is used to fill the GridView with data at runtime. Lines 66–76 contain nested elements that define the styles used to format the GridView’s rows. The IDE configured these styles based on your selection of the Simple style in the Auto Format dialog for the GridView. GridView
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
Guestbook Please leave a message in our guestbook:
Fig. 21.40 | ASPX file for the guestbook application. (Part 1 of 4.)
Fig. 21.40 | ASPX file for the guestbook application. (Part 3 of 4.)
928
Chapter 21
ASP.NET 2.0, Web Forms and Web Controls
(a)
(b)
Fig. 21.40 | ASPX file for the guestbook application. (Part 4 of 4.) Lines 77–86 define the Columns that appear in the GridView. Each column is represented as a BoundField, because the values in the columns are bound to values retrieved from the data source (i.e., the Messages table of the Guestbook database). The DataField
21.7 Case Study: Connecting to a Database in ASP.NET
929
property of each BoundField identifies the column in the data source to which the column in the GridView is bound. The HeaderText property indicates the text that appears as the column header. By default, this is the name of the column in the data source, but you can change this property as desired. The messagesSqlDataSource is defined by the markup in lines 89–120 in Fig. 21.40. Lines 90–91 contain a ConnectionString property, which indicates the connection through which the SqlDataSource control interacts with the database. The value of this property uses an ASP.NET expression, delimited by , to access the GuestbookConnectionString stored in the ConnectionStrings section of the application’s Web.config configuration file. Recall that we created this connection string earlier in this section using the Configure Data Source wizard. Line 92 defines the SqlDataSource’s SelectCommand property, which contains the SELECT SQL statement used to retrieve the data from the database. As determined by our actions in the Configure Data Source wizard, this statement retrieves the data in all the columns in all the rows of the Messages table. Lines 93–100 define the DeleteCommand, InsertCommand and UpdateCommand properties, which contain the DELETE, INSERT and UPDATE SQL statements, respectively. These were also generated by the Configure Data Source wizard. In this example, we use only the InsertCommand. We discuss invoking this command shortly. Notice that the SQL commands used by the SqlDataSource contain several parameters (prefixed with @). Lines 101–119 contain elements that define the name, the type and, for some parameters, the source of the parameter. Parameters that are set programmatically are defined by Parameter elements containing Name and Type properties. For example, line 112 defines the Date parameter of Type String. This corresponds to the @Date parameter in the InsertCommand (line 97). Parameters that obtain their values from controls are defined by ControlParameter elements. Lines 113–118 contain markup that sets up the relationships between the INSERT parameters and the Web Form’s TextBoxes. We established these relationships in the Command and Parameter Editor (Fig. 21.39). Each ControlParameter contains a ControlID property indicating the control from which the parameter gets its value. The PropertyName specifies the property that contains the actual value to be used as the parameter value. The IDE sets the PropertyName based on the type of control specified by the ControlID (indirectly via the Command and Parameter Editor). In this case, we use only TextBoxes, so the PropertyName of each ControlParameter is Text (e.g., the value of parameter @Name comes from nameTextBox.Text). However, if we were using a DropDownList, for example, the PropertyName would be SelectedValue.
21.7.2 Modifying the Code-Behind File for the Guestbook Application After building the Web Form and configuring the data controls used in this example, double click the Submit and Clear buttons to create their corresponding Click event handlers in the Guestbook.aspx.cs code-behind file (Fig. 21.41). The IDE generates empty event handlers, so we must add the appropriate code to make these buttons work properly. The event handler for clearButton (lines 43–48) clears each TextBox by setting its Text property to an empty string. This resets the form for a new guestbook submission. Lines 17–40 contain the event-handling code for submitButton, which adds the user’s information to the Messages table of the Guestbook database. Recall that we configured messagesSqlDataSource’s INSERT command to use the values of the TextBoxes on
930
Chapter 21
ASP.NET 2.0, Web Forms and Web Controls
the Web Form as the parameter values inserted into the database. We have not yet specified the date value to be inserted, though. Lines 20–22 assign a string representation of 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
// Fig. 21.41: Guestbook.aspx.cs // Code-behind file that defines event handlers for the guestbook. using System; using System.Data; using System.Configuration; using System.Web; using System.Web.Security; using System.Web.UI; using System.Web.UI.WebControls; using System.Web.UI.WebControls.WebParts; using System.Web.UI.HtmlControls; public partial class Guestbook : System.Web.UI.Page { // Submit Button adds a new guestbook entry to the database, // clears the form and displays the updated list of guestbook entries protected void submitButton_Click( object sender, EventArgs e ) { // create a date parameter to store the current date System.Web.UI.WebControls.Parameter date = new System.Web.UI.WebControls.Parameter( "Date", TypeCode.String, DateTime.Now.ToShortDateString() ); // set the @Date parameter to the date parameter messagesSqlDataSource.InsertParameters.RemoveAt( 0 ); messagesSqlDataSource.InsertParameters.Add( date ); // execute an INSERT SQL statement to add a new row to the // Messages table in the Guestbook database that contains the // current date and the user's name, e-mail address and message messagesSqlDataSource.Insert(); // clear the TextBoxes nameTextBox.Text = ""; emailTextBox.Text = ""; messageTextBox.Text = ""; // update the GridView with the new database table contents messagesGridView.DataBind(); } // end method submitButton_Click // Clear Button clears the Web Form's TextBoxes protected void clearButton_Click( object sender, EventArgs e ) { nameTextBox.Text = ""; emailTextBox.Text = ""; messageTextBox.Text = ""; } // end method clearButton_Click } // end class Guestbook
Fig. 21.41 | Code-behind file for the guestbook application.
21.8 Case Study: Secure Books Database Application
931
the current date (e.g., "5/27/05") to a new object of type Parameter. This Parameter object is identified as "Date" and is given the current date as a default value. The SqlDataSource’s InsertParameters collection contains an item named Date, which we Remove in line 25 and replace in line 26 by Adding our date parameter. Invoking SqlDataSource method Insert in line 31 executes the INSERT command against the database, thus adding a row to the Messages table. After the data is inserted into the database, lines 34–36 clear the TextBoxes, and line 39 invokes messagesGridView’s DataBind method to refresh the data that the GridView displays. This causes messagesSqlDataSource (the data source of the GridView) to execute its SELECT command to obtain the Messages table’s newly updated data.
21.8 Case Study: Secure Books Database Application This case study presents a Web application in which a user logs into a secure Web site to view a list of publications by an author of the user’s choosing. The application consists of several ASPX files. Section 21.8.1 presents the working application and explains the purpose of each of its Web pages. Section 21.8.2 provides step-by-step instructions to guide you through building the application and presents the markup in the ASPX files as they are created.
21.8.1 Examining the Completed Secure Books Database Application This example uses a technique known as forms authentication to protect a page so that only users known to the Web site can access it. Such users are known as the site’s members. Authentication is a crucial tool for sites that allow only members to enter the site or a portion of the site. In this application, Web site visitors must log in before they are allowed to view the publications in the Books database. The first page that a user would typically request is Login.aspx (Fig. 21.42). You will soon learn to create this page using a Login control, one of several ASP.NET login controls that help create secure applications using authentication. These controls are found in the Login section of the Toolbox.
Fig. 21.42 |
Login.aspx
page of the secure books database application.
932
Chapter 21
ASP.NET 2.0, Web Forms and Web Controls
The Login.aspx page allows a site visitor to enter an existing user name and password to log into the Web site. A first-time visitor must click the link below the Log In button to create a new user before attempting to log in. Doing so redirects the visitor to CreateNewUser.aspx (Fig. 21.43), which contains a CreateUserWizard control that presents the visitor with a user registration form. We discuss the CreateUserWizard control in detail in Section 21.8.2. In Fig. 21.43, we use the password pa$$word for testing purposes—as you will learn, the CreateUserWizard requires that the password contain special characters for security purposes. Clicking Create User establishes a new user account. Once the new user account is created, the user is automatically logged in and shown a success message (Fig. 21.44).
Fig. 21.43 |
CreateNewUser.aspx
page of the secure book database application.
Fig. 21.44 | Message displayed to indicate that a user account was created successfully.
21.8 Case Study: Secure Books Database Application
933
Clicking the Continue button on the confirmation page sends the user to Books.aspx (Fig. 21.45), which provides a drop-down list of authors and a table containing the ISBNs, titles, edition numbers and copyright years of books in the database. By default, all the books by Harvey Deitel are displayed. Links appear at the bottom of the table that allow you to access additional pages of data. When the user chooses an author, a postback occurs, and the page is updated to display information about books written by the selected author (Fig. 21.46). Note that once the user creates an account and is logged in, Books.aspx displays a welcome message customized for the particular logged-in user. As you will soon see, a LoginName control provides this functionality. After you add this control to the page, ASP.NET handles the details of determining the user name. Clicking the Click here to log out link logs the user out, then sends the user back to Login.aspx (Fig. 21.47). As you will learn, this link is created by a LoginStatus control, which handles the details of logging the user out of the page. To view the book listing again, the user must log in through Login.aspx. The Login control on this page receives the user name and password entered by a visitor. ASP.NET then compares these values with user names and passwords stored in a database on the server. If there is a match, the visitor is authenticated (i.e., the user’s identity is confirmed). We explain the authentication process in detail in Section 21.8.2. When an existing user is successfully authenticated, Login.aspx redirects the user to Books.aspx (Fig. 21.45). If the user’s login attempt fails, an appropriate error message is displayed (Fig. 21.48).
Fig. 21.45 |
Books.aspx
displaying books by Harvey Deitel (by default).
934
Chapter 21
Fig. 21.46 |
ASP.NET 2.0, Web Forms and Web Controls
Books.aspx
displaying books by Andrew Goldberg.
Fig. 21.47 | Logging in using the Login control. Notice that Login.aspx, CreateNewUser.aspx and Books.aspx share the same page header containing the Bug2Bug logo image. Instead of placing this image at the top of each page, we use a master page to achieve this. As we demonstrate shortly, a master page defines common GUI elements that are inherited by each page in a set of content pages. Just as C# classes can inherit instance variables and methods from existing classes, content pages inherit elements from master pages—this is known as visual inheritance.
21.8.2 Creating the Secure Books Database Application Now that you are familiar with how this application behaves, we demonstrate how to create it from scratch. Thanks to the rich set of login and data controls provided by
21.8 Case Study: Secure Books Database Application
935
Fig. 21.48 | Error message displayed for an unsuccessful login attempt using the Login control.
ASP.NET, you will not have to write any code to create this application. In fact, the application does not contain any code-behind files. All of the functionality is specified through properties of controls, many of which are set through wizards and other visual programming tools. ASP.NET hides the details of authenticating users against a database of user names and passwords, displaying appropriate success or error messages and redirecting the user to the correct page based on the authentication results. We now discuss the steps you must perform to create the secure books database application.
Step 1: Creating the Web Site Create a new ASP.NET Web Site at http://localhost/Bug2Bug as described previously. We will explicitly create each of the ASPX files that we need in this application, so delete the IDE-generated Default.aspx file (and its corresponding code-behind file) by selecting Default.aspx in the Solution Explorer and pressing the Delete key. Click OK in the confirmation dialog to delete these files. Step 2: Setting Up the Web Site’s Folders Before building any of the pages in the Web site, we create folders to organize its contents. First, create an Images folder and add the bug2bug.png file to it. This image can be found in the examples directory for this chapter. Next, add the Books.mdf database file (which can also be found in the examples directory) to the project’s App_Data folder. We show how to retrieve data from this database later in the section. Step 3: Configuring the Application’s Security Settings In this application, we want to ensure that only authenticated users are allowed to access Books.aspx (created in Step 9 and Step 10) to view the information in the database. Previously, we created all of our ASPX pages in the Web application’s root directory (e.g., http://localhost/ProjectName). By default, any Web site visitor (regardless of whether the visitor is authenticated) can view pages in the root directory. ASP.NET allows you to
936
Chapter 21
ASP.NET 2.0, Web Forms and Web Controls
restrict access to particular folders of a Web site. We do not want to restrict access to the root of the Web site, however, because all users must be able to view Login.aspx and CreateNewUser.aspx to log in and create user accounts, respectively. Thus, if we want to restrict access to Books.aspx, it must reside in a directory other than the root directory. Create a folder named Secure. Later in the section, we will create Books.aspx in this folder. First, let’s enable forms authentication in our application and configure the Secure folder to restrict access to authenticated users only. Select Website > ASP.NET Configuration to open the Web Site Administration Tool in a Web browser (Fig. 21.49). This tool allows you to configure various options that determine how your application behaves. Click either the Security link or the Security tab to open a Web page in which you can set security options (Fig. 21.50), such as the type of authentication the application should use. In the Users column, click Select authentication type. On the resulting page (Fig. 21.51), select the radio button next to From the internet to indicate that users will log in via a form on the Web site in which the user can enter a username and password (i.e., the application will use forms authentication). The default setting—From a local network—relies on users’ Windows user names and passwords for authentication purposes. Click the Done button to save this change. Now that forms authentication is enabled, the Users column on the main page of the Web Site Administration Tool (Fig. 21.52) provides links to create and manage users. As you saw in Section 21.8.1, our application provides the CreateNewUser.aspx page in which users can create their own accounts. Thus, while it is possible to create users through the Web Site Administration Tool, we do not do so here. Even though no users exist at the moment, we configure the Secure folder to grant access only to authenticated users (i.e., deny access to all unauthenticated users). Click the Create access rules link in the Access Rules column of the Web Site Administration Tool
Fig. 21.49 | Web Site Administration Tool for configuring a Web application.
21.8 Case Study: Secure Books Database Application
937
Fig. 21.50 | Security page of the Web Site Administration Tool.
Fig. 21.51 | Choosing the type of authentication used by an ASP.NET Web application.
938
Chapter 21
ASP.NET 2.0, Web Forms and Web Controls
(Fig. 21.52) to view the Add New Access Rule page (Fig. 21.53). This page is used to create an access rule—a rule that grants or denies access to a particular Web application directory for a specific user or group of users. Click the Secure directory in the left column of the page to identify the directory to which our access rule applies. In the middle column, select the radio button marked Anonymous users to specify that the rule applies to users who have not been authenticated. Finally, select Deny in the right column, labeled Permission, then click OK. This rule indicates that anonymous users (i.e., users who have not identified themselves by logging in) should be denied access to any pages in the Secure directory (e.g., Books.aspx). By default, anonymous users who attempt to load a page in the Secure directory are redirected to the Login.aspx page so that they can identify themselves. Note that because we did not set up any access rules for the Bug2Bug root directory, anonymous users may still access pages there (e.g., Login.aspx, CreateNewUser.aspx). We create these pages momentarily.
Step 4: Examining the Auto-Generated Web.config Files We have now configured the application to use forms authentication and created an access rule to ensure that only authenticated users can access the Secure folder. Before creating the Web site’s content, we examine how the changes made through the Web Site Administration Tool appear in the IDE. Recall that Web.config is an XML file used for application configuration, such as enabling debugging or storing database connection strings. Visual Web Developer generates two Web.config files in response to our actions using the
Fig. 21.52 | Main page of the Web Site Administration Tool after enabling forms authentication.
21.8 Case Study: Secure Books Database Application
939
Fig. 21.53 | Add New Access Rule page used to configure directory access. Web Site Administration Tool—one
in the application’s root directory and one in the Sefolder. [Note: You may need to click the Refresh button in the Solution Explorer to see these files.] In an ASP.NET application, a page’s configuration settings are determined by the current directory’s Web.config file. The settings in this file take precedence over the settings in the root directory’s Web.config file. After setting the authentication type for the Web application, the IDE generates a Web.config file at http://localhost/Bug2Bug/Web.config, which contains an authentication element cure
This element appears in the root directory’s Web.config file, so the setting applies to the entire Web site. The value "Forms" of the mode attribute specifies that we want to use forms authentication. Had we left the authentication type set to From a local network in the Web Site Administration Tool, the mode attribute would be set to "Windows". Note that "Forms" is the default mode in a Web.config file generated for another purpose, such as saving a connection string. After creating the access rule for the Secure folder, the IDE generates a second Web.config file in that folder. This file contains an authorization element that indicates who is; and who is not, authorized to access this folder over the Web. In this application,
940
Chapter 21
ASP.NET 2.0, Web Forms and Web Controls
we want to allow only authenticated users to access the contents of the Secure folder, so the authorization element appears as
Rather than grant permission to each individual authenticated user, we deny access to those who are not authenticated (i.e., those who have not logged in). The deny element inside the authorization element specifies the users to whom we wish to deny access. When the users attribute’s value is set to "?", all anonymous (i.e., unauthenticated) users are denied access to the folder. Thus, an unauthenticated user will not be able to load http://localhost/Bug2Bug/Secure/Books.aspx. Instead, such a user will be redirected to the Login.aspx page—when a user is denied access to a part of a site, ASP.NET by default sends the user to a page named Login.aspx in the application’s root directory.
Step 5: Creating a Master Page Now that you have established the application’s security settings, you can create the application’s Web pages. We begin with the master page, which defines the elements we want to appear on each page. A master page is like a base class in a visual inheritance hierarchy, and content pages are like derived classes. The master page contains placeholders for custom content created in each content page. The content pages visually inherit the master page’s content, then add content in place of the master page’s placeholders. For example, you might want to include a navigation bar (i.e., a series of buttons for navigating a Web site) on every page of a site. If the site encompasses a large number of pages, adding markup to create the navigation bar for each page can be time consuming. Moreover, if you subsequently modify the navigation bar, every page on the site that uses it must be updated. By creating a master page, you can specify the navigation bar markup in one file and have it appear on all the content pages, with only a few lines of markup. If the navigation bar changes, only the master page changes—any content pages that use it are updated the next time the page is requested. In this example, we want the Bug2Bug logo to appear as a header at the top of every page, so we will place an Image control in the master page. Each subsequent page we create will be a content page based on this master page and thus will include the header. To create a master page, right click the location of the Web site in the Solution Explorer and select Add New Item…. In the Add New Item dialog, select Master Page from the template list and specify Bug2Bug.master as the filename. Master pages have the filename extension .master and, like Web Forms, can optionally use a code-behind file to define additional functionality. In this example, we do not need to specify any code for the master page, so leave the box labeled Place code in a separate file unchecked. Click Add to create the page. The IDE opens the master page in Source mode (Fig. 21.54) when the file is first created. [Note: We added a line break in the DOCTYPE element for presentation purposes.] The markup for a master page is almost identical to that of a Web Form. One difference is that a master page contains a Master directive (line 1 in Fig. 21.54), which specifies that this file defines a master page using the indicated Language for any code. Because we chose not to use a code-behind file, the master page also contains a script element (lines 6–8). Code that would usually be placed in a code-behind file can be placed in a script element.
21.8 Case Study: Secure Books Database Application
941
Fig. 21.54\ | Master page in Source mode. However, we remove the script element from this page, because we do not need to write any additional code. After deleting this block of markup, set the title of the page to Bug2Bug. Finally, notice that the master page contains a ContentPlaceHolder control in lines 17–18. This control serves as a placeholder for content that will be defined by a content page. You will see how to define content to replace the ContentPlaceHolder shortly. At this point, you can edit the master page in Design mode (Fig. 21.55) as if it were an ASPX file. Notice that the ContentPlaceHolder control appears as a large rectangle with a gray bar indicating the control’s type and ID. Using the Properties window, change the ID of this control to bodyContent.
Fig. 21.55 | Master page in Design mode.
942
Chapter 21
ASP.NET 2.0, Web Forms and Web Controls
To create a header in the master page that will appear at the top of each content page, we insert a table into the master page. Place the cursor to the left of the ContentPlaceHolder and select Layout > Insert Table. In the Insert Table dialog, click the Template radio button, then select Header from the drop-down list of available table templates. Click OK to create a table that fills the page and contains two rows. Drag and drop the ContentPlaceHolder into the bottom table cell. Change the valign property of this cell to top, so the ContentPlaceHolder vertically aligns with the top of the cell. Next, set the Height of the top table cell to 130. Add to this cell an Image control named headerImage with its ImageUrl property set to the bug2bug.png file in the project’s Images folder. (You can also simply drag the image from the Solution Explorer into the top cell.) Figure 21.56 shows the markup and Design view of the completed master page. As you will see in Step 6, a content page based on this master page displays the logo image defined here, as well as the content designed for that specific page (in place of the ContentPlaceHolder). 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
Bug2Bug
Fig. 21.56 |
Bug2Bug.master page that defines a logo image header for all pages in the secure book database application. (Part 1 of 2.)
21.8 Case Study: Secure Books Database Application
943
Fig. 21.56 |
Bug2Bug.master page that defines a logo image header for all pages in the secure book database application. (Part 2 of 2.)
Step 6: Creating a Content Page We now create a content page based on Bug2Bug.master. We begin by building CreateNewUser.aspx. To create this file, right click the master page in the Solution Explorer and select Add Content Page. This action causes a Default.aspx file, configured to use the master page, to be added to the project. Rename this file CreateNewUser.aspx, then open it in Source mode (Fig. 21.57). Note that this file contains a Page directive with a Language property, a MasterPageFile property and a Title property. The Page directive indicates the MasterPageFile on which the content page builds. In this case, the MasterPageFile property is set to "~/Bug2Bug.master" to indicate that the current file builds on the master page we just created. The Title property specifies the title that will be displayed in the Web browser’s title bar when the content page is loaded. This value, which we set to Create a New User, replaces the value (i.e., Bug2Bug) set in the title element of the master page. Because CreateNewUser.aspx’s Page directive specifies Bug2Bug.master as the page’s MasterPageFile, the content page implicitly contains the contents of the master page,
Fig. 21.57 | Content page CreateNewUser.aspx in Source mode.
944
Chapter 21
ASP.NET 2.0, Web Forms and Web Controls
such as the DOCTYPE, html and body elements. The content page file does not duplicate the XHTML elements found in the master page. Instead, the content page contains a Content control (lines 4–6 in Fig. 21.57), in which we will place page-specific content that will replace the master page’s ContentPlaceHolder when the content page is requested. The ContentPlaceHolderID property of the Content control identifies the ContentPlaceHolder in the master page that the control should replace—in this case, bodyContent. The relationship between a content page and its master page is more evident in Design mode (Fig. 21.58). The shaded region contains the contents of the master page Bug2Bug.master as they will appear in CreateNewUser.aspx when rendered in a Web browser. The only editable part of this page is the Content control, which appears in place of the master page’s ContentPlaceHolder.
Step 7: Adding a CreateUserWizard Control to a Content Page Recall from Section 21.8.1 that CreateNewUser.aspx is the page in our Web site that allows first-time visitors to create user accounts. To provide this functionality, we use a CreateUserWizard control. Place the cursor inside the Content control in Design mode and double click CreateUserWizard in the Login section of the Toolbox to add it to the page at the current cursor position. You can also drag-and-drop the control onto the page. To change the CreateUserWizard’s appearance, open the CreateUserWizard Tasks smart tag menu, and click Auto Format. Select the Professional color scheme. As discussed previously, a CreateUserWizard provides a registration form that site visitors can use to create a user account. ASP.NET handles the details of creating a SQL Server database (named ASPNETDB.MDF and located in the App_Data folder) to store the user names, passwords and other account information of the application’s users. ASP.NET also enforces a default set of requirements for filling out the form. Each field on the form is required, the password must contain at least seven characters, including at least one nonalphanumeric character, and the two passwords entered must match. The form also asks for a security question and answer that can be used to identify a user in case the account’s password needs to be reset or recovered.
Fig. 21.58 | Content page CreateNewUser.aspx in Design mode.
21.8 Case Study: Secure Books Database Application
945
After the user fills in the form’s fields and clicks the Create User button to submit the account information, ASP.NET verifies that all the form’s requirements were fulfilled and attempts to create the user account. If an error occurs (e.g., the user name already exists), the CreateUserWizard displays a message below the form. If the account is created successfully, the form is replaced by a confirmation message and a button that allows the user to continue. You can view this confirmation message in Design mode by selecting Complete from the Step drop-down list in the CreateUserWizard Tasks smart tag menu. When a user account is created, ASP.NET automatically logs the user into the site (we say more about the login process shortly). At this point, the user is authenticated and allowed to access the Secure folder. After we create Books.aspx later in this section, we set the CreateUserWizard’s ContinueDestinationPageUrl property to ~/Secure/ Books.aspx to indicate that the user should be redirected to Books.aspx after clicking the Continue button on the confirmation page. Figure 21.59 presents the completed CreateNewUser.aspx file (reformatted for readability). Inside the Content control, the CreateUserWizard control is defined by the markup in lines 9–40. The start tag (lines 9–12) contains several properties that specify formatting styles for the control, as well as the ContinueDestinationPageUrl property, which you will set later in the chapter. Lines 14–32 contain elements that define additional styles used to format specific parts of the control. Finally, lines 34–39 specify the wizard’s two steps—CreateUserWizardStep and CompleteWizardStep—in a WizardSteps element. CreateUserWizardStep and CompleteWizardStep are classes that encapsulate the details of creating a user and issuing a confirmation message. The sample outputs in Fig. 21.59(a) and Fig. 21.59(b) demonstrate successfully creating a user account with CreateNewUser.aspx. We use the password pa$$word for testing purposes. This password satisfies the minimum length and special character requirement imposed by ASP.NET, but in a real application, you should use a password that is more difficult for someone to guess. Figure 21.59(c) illustrates the error message that appears when you attempt to create a second user account with the same user name—ASP.NET requires that each user name be unique. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
Fig. 21.59 | of 3.)
CreateNewUser.aspx
content page that provides a user registration form. (Part 1
content page that provides a user registration form. (Part 2
21.8 Case Study: Secure Books Database Application
947
(c)
Fig. 21.59 |
CreateNewUser.aspx
content page that provides a user registration form. (Part 3
of 3.)
Step 8: Creating a Login Page Recall from Section 21.8.1 that Login.aspx is the page in our Web site that allows returning visitors to log into their user accounts. To create this functionality, add another content page named Login.aspx and set its title to Login. In Design mode, drag a Login control (located in the Login section of the Toolbox) to the page’s Content control. Open the Auto Format dialog from the Login Tasks smart tag menu and set the control’s color scheme to Professional. Next, configure the Login control to display a link to the page for creating new users. Set the Login control’s CreateUserUrl property to CreateNewUser.aspx by clicking the ellipsis button to the right of this property and selecting the CreateNewUser.aspx file in the resulting dialog. Then set the CreateUserText property to Click here to create a new user. These property values cause a link to appear in the Login control. Finally, we change the value of the Login control’s DisplayRememberMe property to False. By default, the control displays a checkbox and the text Remember me next time. This can be used to allow a user to remain authenticated beyond a single browser session on the user’s current computer. However, we want to require that users log in each time they visit the site, so we disable this option. The Login control encapsulates the details of logging a user into a Web application (i.e., authenticating a user). When a user enters a user name and password, then clicks the Log In button, ASP.NET determines whether the information provided match those of an account in the membership database (i.e., ASPNETDB.MDF created by ASP.NET). If they match, the user is authenticated (i.e., the user’s identity is confirmed), and the browser is redirected to the page specified by the Login control’s DestinationPageUrl property. We
948
Chapter 21
ASP.NET 2.0, Web Forms and Web Controls
set this property to the Books.aspx page after creating it in the next section. If the user’s identity cannot be confirmed (i.e., the user is not authenticated), the Login control displays an error message (see Fig. 21.60), and the user can attempt to log in again. Figure 21.60 presents the completed Login.aspx file. Note that, as in CreateNewUser.aspx, the Page directive indicates that this content page inherits content from Bug2Bug.master. In the Content control that replaces the master page’s ContentPlaceHolder with ID bodyContent, lines 8–22 create a Login control. Note the CreateUser1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
(a)
Fig. 21.60 |
(b)
Login.aspx
content page using a Login control.
21.8 Case Study: Secure Books Database Application
949
and CreateUserUrl properties (lines 10–11) that we set using the Properties window. Line 12 in the start tag for the Login control contains the DestinationPageUrl (you will set this property in the next step) and the DisplayRememberMe property, which we set to False. The elements in lines 15–21 define various formatting styles applied to parts of the control. Note that all of the functionality related to actually logging the user in or displaying error messages is completely hidden from you. When a user enters the user name and password of an existing user account, ASP.NET authenticates the user and writes to the client an encrypted cookie containing information about the authenticated user. Encrypted data is data translated into a code that only the sender and receiver can understand—thereby keeping it private. The encrypted cookie contains a string user name and a bool value that specifies whether this cookie should persist (i.e., remain on the client’s computer) beyond the current session. Our application authenticates the user only for the current session. Text
Step 9: Creating a Content Page That Only Authenticated Users Can Access A user who has been authenticated will be redirected to Books.aspx. We now create the Books.aspx file in the Secure folder—the folder for which we set an access rule denying access to anonymous users. If an unauthenticated user requests this file, the user will be redirected to Login.aspx. From there, the user can either log in or a create a new account, both of which will authenticate the user, thus allowing the user to return to Books.aspx. To create Books.aspx, right click the Secure folder in the Solution Explorer and select Add New Item…. In the resulting dialog, select Web Form and specify the file name Books.aspx. Check the box Select Master Page to indicate that this Web Form should be created as a content page that references a master page, then click Add. In the Select a Master Page dialog, select Bug2Bug.master and click OK. The IDE creates the file and opens it in Source mode. Change the Title property of the Page directive to Book Information. Step 10: Customizing the Secure Page To customize the Books.aspx page for a particular user, we add a welcome message containing a LoginName control, which displays the current authenticated user name. Open Books.aspx in Design mode. In the Content control, type Welcome followed by a comma and a space. Then drag a LoginName control from the Toolbox onto the page. When this page executes on the server, the text [UserName] that appears in this control in Design mode will be replaced by the current user name. In Source mode, type an exclamation point (!) directly after the LoginName control (with no spaces in between). [Note: If you add the exclamation point in Design mode, the IDE may insert extra spaces or a line break between this character and the preceding control. Entering the ! in Source mode ensures that it appears adjacent to the user’s name.] Next, we add a LoginStatus control, which will allow the user to log out of the Web site when finished viewing the listing of books in the database. A LoginStatus control renders on a Web page in one of two ways—by default, if the user is not authenticated, the control displays a hyperlink with the text Login; if the user is authenticated, the control displays a hyperlink with the text Logout. Each link performs the stated action. Add a LoginStatus control to the page by dragging it from the Toolbox onto the page. In this example, any user who reaches this page must already be authenticated, so the control will always render as a Logout link. The LoginStatus Tasks smart tag menu allows you switch
950
Chapter 21
ASP.NET 2.0, Web Forms and Web Controls
between the control’s Views. Select the Logged In view to see the Logout link. To change the actual text of this link, modify the control’s LogoutText property to Click here to log out. Next, set the LogoutAction property to RedirectToLoginPage.
Step 11: Connecting the CreateUserWizard and Login Controls to the Secure Page Now that we have created Books.aspx, we can specify that this is the page to which the CreateUserWizard and Login controls redirect users after they are authenticated. Open CreateNewUser.aspx in Design mode and set the CreateUserWizard control’s ContinueDestinationPageUrl property to Books.aspx. Next, open Login.aspx and select Books.aspx as the DestinationPageUrl of the Login control. At this point, you can run the Web application by selecting Debug > Start Without Debugging. First, create a user account on CreateNewUser.aspx, then notice how the LoginName and LoginStatus controls appear on Books.aspx. Next, log out of the site and log back in using Login.aspx. Step 12: Generating a DataSet Based on the Books.mdf Database We now begin to add the content (i.e., book information) to the secure page Books.aspx. This page will provide a DropDownList containing authors’ names and a GridView displaying information about books written by the author selected in the DropDownList. A user will select an author from the DropDownList to cause the GridView to display information about only the books written by the selected author. As you will see, we create this functionality entirely in Design mode without writing any code. To work with the Books database, we use an approach slightly different than in the preceding case study in which we accessed the Guestbook database using a SqlDataSource control. Here we use an ObjectDataSource control, which encapsulates an object that provides access to a data source. Recall that in Chapter 20, we accessed the Books database in a Windows application using TableAdapters configured to communicate with the database file. These TableAdapters placed a cached copy of the database’s data in a DataSet, which the application then accessed. We use a similar approach in this example. An ObjectDataSource can encapsulate a TableAdapter and use its methods to access the data in the database. This helps separate the data-access logic from the presentation logic. As you will see shortly, the SQL statements used to retrieve data do not appear in the ASPX page when using an ObjectDataSource. The first step in accessing data using an ObjectDataSource is to create a DataSet that contains the data from the Books database required by the application. In Visual C# 2005 Express, this occurs automatically when you add a data source to a project. In Visual Web Developer, however, you must explicitly generate the DataSet. Right click the project’s location in the Solution Explorer and select Add New Item…. In the resulting dialog, select DataSet and specify BooksDataSet.xsd as the file name, then click Add. A dialog will appear that asks you whether the DataSet should be placed in an App_Code folder—a folder whose contents are compiled and made available to all parts of the project. Click Yes for the IDE to create this folder to store BooksDataSet.xsd. Step 13: Creating and Configuring an AuthorsTableAdapter Once the DataSet is added, the Dataset Designer will appear, and the TableAdapter Configuration Wizard will open. Recall from Chapter 20 that this wizard allows you to configure a TableAdapter for filling a DataTable in a DataSet with data from a database. The
21.8 Case Study: Secure Books Database Application
951
Books.aspx page requires two sets of data—a list of authors that will be displayed in the page’s DropDownList (created shortly) and a list of books written by a specific author. We
focus on the first set of data here—the authors. Thus, we use the TableAdapter Configuration Wizard first to configure an AuthorsTableAdapter. In the next step, we will configure a TitlesTableAdapter. In the TableAdapter Configuration Wizard, select Books.mdf from the drop-down list. Then click Next > twice to save the connection string in the application’s Web.config file and move to the Choose a Command Type screen. In the wizard’s Choose a Command Type screen, select Use SQL statements and click Next >. The next screen allows you to enter a SELECT statement for retrieving data from the database, which will then be placed in an Authors DataTable within the BooksDataSet. Enter the SQL statement SELECT AuthorID, FirstName + ' ' + LastName AS Name FROM Authors
in the text box on the Enter a SQL Statement screen. This query selects the AuthorID of each row. This query’s result will also contain a column named Name that is created by concatenating each row’s FirstName and LastName, separated by a space. The AS SQL keyword allows you to generate a column in a query result—called an alias—that contains the result of a SQL expression (e.g., FirstName + ' ' + LastName). You will soon see how we use the result of this query to populate the DropDownList with items containing the authors’ full names. After entering the SQL statement, click the Advanced Options… button and uncheck Generate Insert, Update and Delete statements , since this application does not need to modify the database’s contents. Click OK to close the Advanced Options dialog. Click Next > to advance to the Choose Methods to Generate screen. Leave the default names and click Finish. Notice that the DataSet Designer (Fig. 21.61) now displays a DataTable named Authors with AuthorID and Name members, and Fill and GetData methods.
Step 14: Creating and Configuring a TitlesTableAdapter Books.aspx needs to access a list of books by a specific author and a list of authors. Thus we must create a TitlesTableAdapter that will retrieve the desired information from the database’s Titles table. Right click the Dataset Designer and from the menu that appears, select Add > TableAdapter… to launch the TableAdapter Configuration Wizard. Make sure the BooksConnectionString is selected as the connection in the wizard’s first screen, then click Next >. Choose Use SQL statements and click Next >.
Fig. 21.61 |
Authors DataTable
in the Dataset Designer.
952
Chapter 21
ASP.NET 2.0, Web Forms and Web Controls
In the Enter a SQL Statement screen, open the Advanced Options dialog and uncheck Generate Insert, Update and Delete statements , then click OK. Our application allows users to filter the books displayed by the author’s name, so we need to build a query that takes an AuthorID as a parameter and returns the rows in the Titles table for books written by that author. To build this complex query, click the Query Builder… button. In the Add Table dialog that appears, select AuthorISBN and click Add. Then Add the Titles table, too. Our query will require access to data in both of these tables. Click Close to exit the Add Table dialog. In the top pane of the Query Builder window (Fig. 21.62), check the box marked * (All Columns) in the Titles table. Next, in the middle pane, add a row with Column set to AuthorISBN.AuthorID. Uncheck the Output box, because we do not want the AuthorID to appear in our query result. Add an @authorID parameter in the Filter column of the newly added row. The SQL statement generated by these actions retrieves information about all books written by the author specified by parameter @authorID. The statement first merges the data from the AuthorISBN and Titles tables. The INNER JOIN clause specifies that the ISBN columns of each table are compared to determine which rows are merged. The INNER JOIN results in a temporary table containing the columns of both tables. The outer portion of the SQL statement selects the book information from this temporary table for a specific author (i.e., all rows in which the AuthorID column is equal to @authorID). Click OK to exit the Query Builder, then in the TableAdapter Configuration Wizard, click Next >. On the Choose Methods to Generate screen, enter FillByAuthorID and GetDataByAuthorID as the names of the two methods to be generated for the TitlesTableAdapter. Click Finish to exit the wizard. You should now see a Titles DataTable in the Dataset Designer (Fig. 21.63).
Fig. 21.62 | Query Builder for designing a query that selects books written by a particular author.
21.8 Case Study: Secure Books Database Application
953
Fig. 21.63 | Dataset Designer after adding the TitlesTableAdapter. Step 15: Adding a DropDownList Containing Authors’ First and Last Names Now that we have created a BooksDataSet and configured the necessary TableAdapters, we add controls to Books.aspx that will display the data on the Web page. We first add the DropDownList from which users can select an author. Open Books.aspx in Design mode, then add the text Author: and a DropDownList control named authorsDropDownList in the page’s Content control, below the existing content. The DropDownList initially displays the text [Unbound]. We now bind the list to a data source, so the list displays the author information placed in the BooksDataSet by the AuthorsTableAdapter. In the DropDownList Tasks smart tag menu, click Choose Data Source… to start the Data Source Configuration Wizard. Select from the Select a data source dropdown list in the first screen of the wizard. Doing so opens the Choose a Data Source Type screen. Select Object and set the ID to authorsObjectDataSource, then click OK. An ObjectDataSource accesses data through another object, often called a business object. Recall from Section 21.3 that the middle tier of a three-tier application contains business logic that controls the way an application’s top tier user interface (in this case, Books.aspx) accesses the bottom tier’s data (in this case, the Books.mdf database file). Thus, a business object represents the middle tier of an application and mediates interactions between the other two tiers. In an ASP.NET Web application, a TableAdapter typically serves as the business object that retrieves the data from the bottom-tier database and makes it available to the top-tier user interface through a DataSet. In the Choose a Business Object screen of the Configure Data Source wizard (Fig. 21.64), select BooksDataSetTableAdapters.AuthorsTableAdapter. [Note: You may need to save the project to see the AuthorsTableAdapter.] BooksDataSetTableAdapters is a namespace declared by the IDE when you create BooksDataSet. Click Next > to continue. The Define Data Methods screen (Fig. 21.65) allows you to specify which method of the business object (in this case, AuthorsTableAdapter) should be used to obtain the data accessed through the ObjectDataSource. You can choose only methods that return data, so the only choice provided is the GetData method, which returns an AuthorsDataTable. Click Finish to close the Configure Data Source wizard and return to the Data Source Configuration Wizard for the DropDownList (Fig. 21.66). The newly created data source (i.e., authorsObjectDataSource) should be selected in the top drop-down list. The other two drop-down lists on this screen allow you to configure how the DropDownList control uses the data from the data source. Set Name as the data field to display and AuthorID as the data field to use as the value. Thus, when authorsDropDownList is rendered in a Web browser, the list items will display the names of the authors, but the underlying values associated with each item will be the AuthorIDs of the authors. Finally, click OK to bind the DropDownList to the specified data.
954
Chapter 21
ASP.NET 2.0, Web Forms and Web Controls
Fig. 21.64 | Choosing a business object for an ObjectDataSource.
Fig. 21.65 | Choosing a data method of a business object for use with an ObjectDataSource.
The last step in configuring the DropDownList on Books.aspx is to set the control’s property to True. This property indicates that a postback occurs each time the user selects an item in the DropDownList. As you will see shortly, this causes the page’s GridView (created in the next step) to display new data. AutoPostBack
Step 16: Creating a GridView to Display the Selected Author’s Books We now add a GridView to Books.aspx for displaying the book information by the author selected in the authorsDropDownList. Add a GridView named titlesGridView below the other controls in the page’s Content control.
21.8 Case Study: Secure Books Database Application
955
Fig. 21.66 | Choosing a data source for a DropDownList. To bind the GridView to data from the Books database, select from the Choose Data Source drop-down list in the GridView Tasks smart tag menu. When the Data Source Configuration Wizard opens, select Object and set the ID of the data source to titlesObjectDataSource, then click OK. In the Choose a Business Object screen, select the BooksDataSetTableAdapters.TitlesTableAdapter from the drop-down list to indicate the object that will be used to access the data. Click Next >. In the Define Data Methods screen, leave the default selection of GetDataByAuthorID as the method that will be invoked to obtain the data for display in the GridView. Click Next >. Recall that TitlesTableAdapter method GetDataByAuthorID requires a parameter to indicate the AuthorID for which data should be retrieved. The Define Parameters screen (Fig. 21.67) allows you to specify where to obtain the value of the @authorID parameter in the SQL statement executed by GetDataByAuthorID. Select Control from the Parameter source drop-down list. Select authorsDropDownList as the ControlID (i.e., the ID of the parameter source control). Next, enter 1 as the DefaultValue, so books by Harvey Deitel (who has AuthorID 1 in the database) display when the page first loads (i.e., before the user has made any selections using the authorsDropDownList). Finally, click Finish to exit the wizard. The GridView is now configured to display the data retrieved by TitlesTableAdapter.GetDataByAuthorID, using the value of the current selection in authorsDropDownList as the parameter. Thus, when the user selects a new author and a postback occurs, the GridView displays a new set of data. Now that the GridView is tied to a data source, we modify several of the control’s properties to adjust its appearance and behavior. Set the GridView’s CellPadding property to 5, set the BackColor of the AlternatingRowStyle to LightYellow, and set the BackColor of the HeaderStyle to LightGreen. Change the Width of the control to 600px to accommodate long data values.
956
Chapter 21
ASP.NET 2.0, Web Forms and Web Controls
Fig. 21.67 | Choosing the data source for a parameter in a business object’s data method. Next, in the GridView Tasks smart tag menu, check Enable Sorting. This causes the column headings in the GridView to turn into hyperlinks that allow users to sort the data in the GridView. For example, clicking the Titles heading in the Web browser will cause the displayed data to appear sorted in alphabetical order. Clicking this heading a second time will cause the data to be sorted in reverse alphabetical order. ASP.NET hides from you the details required to achieve this functionality. Finally, in the GridView Tasks smart tag menu, check Enable Paging. This causes the GridView to split across multiple pages. The user can click the numbered links at the bottom of the GridView control to display a different page of data. GridView’s PageSize property determines the number of entries per page. Set the PageSize property to 4 using the Properties window so that the GridView displays only four books per page. This technique for displaying data makes the site more readable and enables pages to load more quickly (because less data is displayed at one time). Note that, as with sorting data in a GridView, you do not need to add any code to achieve paging functionality. Fig. 21.68 displays the completed Books.aspx file in Design mode.
Step 17: Examining the Markup in Books.aspx Figure 21.69 presents the markup in Books.aspx (reformatted for readability). Aside from the exclamation point in line 9, which we added manually in Source mode, all the remaining markup was generated by the IDE in response to the actions we performed in Design mode. The Content control (lines 6–55) defines page-specific content that will replace the ContentPlaceHolder named bodyContent. Recall that this control is located in the master page specified in line 3. Line 9 creates the LoginName control, which displays the authenticated user’s name when the page is requested and viewed in a browser. Lines 10–12 create the LoginStatus control. Recall that this control is configured to redirect the user to the login page after logging out (i.e., clicking the hyperlink with the LogoutText).
21.8 Case Study: Secure Books Database Application
957
Fig. 21.68 | Completed Books.aspx in Design mode. Lines 15–18 define the DropDownList that displays the names of the authors in the database. Line 16 contains the control’s AutoPostBack property, which indicates that changing the selected item in the list causes a postback to occur. The DataSourceID property in line 16 specifies that the DropDownList’s items are created based on the data obtained through the authorsObjectDataSource (defined in lines 20–23). Line 21 specifies that this ObjectDataSource accesses the Books database by calling method GetData of the BooksDataSet’s AuthorsTableAdapter (line 22). Lines 25–43 create the GridView that displays information about the books written by the selected author. The start tag (lines 25–28) indicates that paging (with a page size of 4) and sorting are enabled in the GridView. The AutoGenerateColumns property indicates whether the columns in the GridView are generated at runtime based on the fields in the data source. This property is set to False, because the IDE-generated Columns element (lines 30–39) already specifies the columns for the GridView using BoundFields. Lines 45–54 define the ObjectDataSource used to fill the GridView with data. Recall that we configured titlesObjectDataSource to use method GetDataByAuthorID of the BooksDataSet’s TitlesTableAdapter for this purpose. The ControlParameter in lines 50–52 specifies that the value of method GetDataByAuthorID’s parameter comes from the SelectedValue property of the authorsDropDownList. Figure 21.69(a) depicts the default appearance of Books.aspx in a Web browser. Because the DefaultValue property (line 51) of the ControlParameter for the titlesObjectDataSource is set to 1, books by the author with AuthorID 1 (i.e., Harvey Deitel) are displayed when the page first loads. Note that the GridView displays paging links below Books
Fig. 21.69 | Markup for the completed Books.aspx file. (Part 1 of 3.)
21.8 Case Study: Secure Books Database Application
53 54 55
(a)
(b)
Fig. 21.69 | Markup for the completed Books.aspx file. (Part 2 of 3.)
959
960
Chapter 21
ASP.NET 2.0, Web Forms and Web Controls
(c)
Fig. 21.69 | Markup for the completed Books.aspx file. (Part 3 of 3.) the data, because the number of rows of data returned by GetDataByAuthorID is greater than the page size. Figure 21.69(b) shows the GridView after clicking the 2 link to view the second page of data. Figure 21.69(c) presents Books.aspx after the user selects a different author from the authorsDropDownList. The data fits on one page, so the GridView does not display paging links.
21.9 Wrap-Up In this chapter, we introduced Web application development using ASP.NET and Visual Web Developer 2005 Express. We began by discussing the simple HTTP transactions that take place when you request and receive a Web page through a Web browser. You then learned about the three tiers (i.e., the client or top tier, the business logic or middle tier and the information or bottom tier) that comprise most Web applications. We then explained the role of ASPX files (i.e., Web Form files) and code-behind files, and the relationship between them. We discussed how ASP.NET compiles and executes Web applications so that they can be displayed as XHTML in a Web browser. You also learned how to build an ASP.NET Web application using the Visual Web Developer IDE. The chapter demonstrated several common ASP.NET Web controls used for displaying text and images on a Web Form. You learned how to use an AdRotator control to display randomly selected images. We also discussed validation controls, which allow you to ensure that user input on a Web page satisfies certain requirements. We discussed the benefits of maintaining state information about a user across multiple pages of a Web site. We then demonstrated how you can include such functionality in a Web application using either cookies or session tracking with HttpSessionState objects. Finally, the chapter presented two case studies on building ASP.NET applications that interact with databases. First, we showed how to build a guestbook application that
21.10 Web Resources
961
allows users to submit comments about a Web site. You learned how to save the user input in a SQL Server database and how to display past submissions on the Web page. The second case study demonstrated how to build an application that requires users to log in before accessing information from the Books database discussed in Chapter 20. You used the Web Site Administration Tool to configure the application to use forms authentication and prevent anonymous users from accessing the book information. This case study explained how to use the Login, CreateUserWizard, LoginName and LoginStatus controls to simplify user authentication. You also learned to create a uniform lookand-feel for a Web site using one master page and several content pages. In the next chapter, we continue our coverage of ASP.NET technology with an introduction to Web services, which allow methods on one machine to call methods on other machines via common data formats and protocols, such as XML and HTTP. You will learn how Web services promote software reusability and interoperability across multiple computers on a network such as the Internet.
21.10 Web Resources beta.asp.net
This official Microsoft site overviews ASP.NET and provides a link for downloading Visual Web Developer. This site includes ASP.NET articles, links to useful ASP.NET resources and lists of books on Web development with ASP.NET. beta.asp.net/QuickStartv20/aspnet/
The ASP.NET QuickStart Tutorial from Microsoft provides code samples and discussion of fundamental ASP.NET topics. beta.asp.net/guidedtour2/
This guided tour of Visual Web Developer 2005 Express introduces key features in the IDE used to develop ASP.NET Web applications. www.15seconds.com
This site offers ASP.NET news, articles, code samples, FAQs and links to valuable community resources, such as an ASP.NET message board and a mailing list. aspalliance.com
This community site contains ASP.NET articles, tutorials and examples. aspadvice.com
This site provides access to many e-mail lists where anyone can ask and respond to questions about ASP.NET and related technologies. www.asp101.com/aspdotnet/
This site overviews ASP.NET and includes articles, code examples, a discussion board and links to ASP.NET resources. The code samples build on many of the techniques presented in the chapter, such as session tracking and connecting to a database. www.411asp.net
This resource site provides programmers with ASP.NET tutorials and code samples. The community pages allow programmers to ask questions, answer questions and post messages. www.123aspx.com
This site offers a directory of links to ASP.NET resources. The site also includes daily and weekly newsletters.
22 Web Services A client is to me a mere unit, a factor in a problem. —Sir Arthur Conan Doyle
OBJECTIVES In this chapter you will learn: I
What a Web service is.
I
How to create Web services.
I
The important part that XML and the XML-based Simple Object Access Protocol play in enabling Web services.
I
The elements that comprise Web services, such as service descriptions and discovery files.
I
How to create a client that uses a Web service.
I
How to use Web services with Windows applications and Web applications.
I
How to use session tracking in Web services to maintain state information for the client.
I
How to pass user-defined types to a Web service.
...if the simplest things of nature have a message that you understand, rejoice, for your soul is alive. —Eleonora Duse
Protocol is everything. —Francoise Giuliani
They also serve who only stand and wait. —John Milton
Outline
22.1 Introduction
963
22.1 Introduction 22.2 .NET Web Services Basics 22.2.1 Creating a Web Service in Visual Web Developer 22.2.2 Discovering Web Services 22.2.3 Determining a Web Service’s Functionality 22.2.4 Testing a Web Service’s Methods 22.2.5 Building a Client to Use a Web Service 22.3 Simple Object Access Protocol (SOAP) 22.4 Publishing and Consuming Web Services 22.4.1 Defining the HugeInteger Web Service 22.4.2 Building a Web Service in Visual Web Developer 22.4.3 Deploying the HugeInteger Web Service 22.4.4 Creating a Client to Consume the HugeInteger Web Service 22.4.5 Consuming the HugeInteger Web Service 22.5 Session Tracking in Web Services 22.5.1 Creating a Blackjack Web Service 22.5.2 Consuming the Blackjack Web Service 22.6 Using Web Forms and Web Services 22.6.1 Adding Data Components to a Web Service 22.6.2 Creating a Web Form to Interact with the Airline Reservation Web Service 22.7 User-Defined Types in Web Services 22.8 Wrap-Up 22.9 Web Resources
22.1 Introduction This chapter introduces Web services, which promote software reusability in distributed systems where applications execute across multiple computers on a network. A Web service is a class that allows its methods to be called by methods on other machines via common data formats and protocols, such as XML (see Chapter 19) and HTTP. In .NET, the over-the-network method calls are commonly implemented through the Simple Object Access Protocol (SOAP), an XML-based protocol describing how to mark up requests and responses so that they can be transferred via protocols such as HTTP. Using SOAP, applications represent and transmit data in a standardized XML-based format. Microsoft is encouraging software vendors and e-businesses to deploy Web services. As increasing numbers of organizations worldwide have connected to the Internet, the concept of applications that call methods across a network has become more practical. Web services represent the next step in object-oriented programming—rather than developing software from a small number of class libraries provided at one location, programmers can access Web service class libraries distributed worldwide.
964
Chapter 22
Web Services
Performance Tip 22.1 Web services are not the best solution for certain performance-intensive applications, because applications that invoke Web services experience network delays. Also, data transfers are typically larger because data is transmitted in text-based XML formats. 22.1
Web services facilitate collaboration and allow businesses to grow. By purchasing Web services and using extensive free Web services that are relevant to their businesses, companies can spend less time developing new applications. E-businesses can use Web services to provide their customers with enhanced shopping experiences. Consider an online music store. The store’s Web site provides links to information about various CDs, enabling users to purchase the CDs or to learn about the artists. Another company that sells concert tickets provides a Web service that displays upcoming concert dates for various artists, then allows users to buy tickets. By consuming the concert-ticket Web service on its site, the online music store can provide an additional service to its customers and increase its site traffic. The company that sells concert tickets also benefits from the business relationship by selling more tickets and possibly by receiving revenue from the online music store for the use of its Web service. Many Web services are provided at no charge. For example, Amazon and Google offer free Web services that you can use in your own applications to access the information they provide. Visual Web Developer and the .NET Framework provide a simple, user-friendly way to create Web services. In this chapter, we show how to use these tools to create, deploy and use Web services. For each example, we provide the code for the Web service, then present an application that uses the Web service. Our first examples analyze Web services and how they work in Visual Web Developer. Then we demonstrate Web services that use more sophisticated features, such as session tracking (discussed in Chapter 21) and manipulating objects of user-defined types. As in Chapter 21, we distinguish between Visual C# 2005 Express and Visual Web Developer 2005 Express in this chapter. We create Web services in Visual Web Developer, and we create client applications that use these Web services using both Visual C# 2005 and Visual Web Developer 2005. The full version of Visual Studio 2005 includes the functionality of both Express editions.
22.2 .NET Web Services Basics A Web service is a software component stored on one machine that can be accessed by an application (or other software component) on another machine over a network. The machine on which the Web service resides is referred to as a remote machine. The application (i.e., the client) that accesses the Web service sends a method call over a network to the remote machine, which processes the call and returns a response over the network to the application. This kind of distributed computing benefits various systems. For example, an application without direct access to certain data on another system might be able to retrieve this data via a Web service. Similarly, an application lacking the processing power necessary to perform specific computations could use a Web service to take advantage of another system’s superior resources. A Web service is typically implemented as a class. In previous chapters, we included a class in a project either by defining the class in the project or by adding a reference to a
22.2 .NET Web Services Basics
965
compiled DLL. All the pieces of an application resided on one machine. When a client uses a Web service, the class (and its compiled DLL) is stored on a remote machine—a compiled version of the Web service class is not placed in the current application’s directory. We discuss what happens shortly. Requests to and responses from Web services created with Visual Web Developer are typically transmitted via SOAP. So any client capable of generating and processing SOAP messages can interact with a Web service, regardless of the language in which the Web service is written. We say more about SOAP in Section 22.3. It is possible for Web services to limit access to authorized clients. See the Web Resources at the end of the chapter for links to information on standard mechanisms and protocols addressing Web service security concerns. Web services have important implications for business-to-business (B2B) transactions. They enable businesses to conduct transactions via standardized, widely available Web services rather than relying on proprietary applications. Web services and SOAP are platform and language independent, so companies can collaborate via Web services without worrying about the compatibility of their hardware, software and communications technologies. Companies such as Amazon, Google, eBay and many others are using Web services to their advantage. To read case studies of Web services used in business, visit msdn.microsoft.com/webservices/understanding/casestudies/default.aspx.
22.2.1 Creating a Web Service in Visual Web Developer To create a Web service in Visual Web Developer, you first create a project of type ASP.NET Web Service. Visual Web Developer then generates the following: •
files to contain the Web service code (which implements the Web service)
•
an ASMX file (which provides access to the Web service)
•
a DISCO file (which potential clients use to discover the Web service)
Figure 22.1 displays the files that comprise a Web service. When you create an ASP.NET Web Service application in Visual Web Developer, the IDE typically generates several additional files. We show only those files that are specific to Web services applications. We discuss these files in Section 22.2.2. Visual Web Developer generates code files for the Web service class and any other code that is part of the Web service implementation. In the Web service class, you define the methods that your Web service makes available to client applications. Like ASP.NET Web applications, ASP.NET Web services can be tested using Visual Web Developer’s built-in test server. However, to make an ASP.NET Web service publicly accessible to clients outside Visual Web Developer, you must deploy the Web service to a Web server such as an Internet Information Services (IIS) Web server. Methods in a Web service are invoked through a Remote Procedure Call (RPC). These methods, which are marked with the WebMethod attribute, are often referred to as Web service methods or simply Web methods—we refer to them as Web methods from this point forward. Declaring a method with attribute WebMethod makes the method accessible to other classes through RPCs and is known as exposing a Web method. We discuss the details of exposing Web methods in Section 22.4.
966
Chapter 22
Web Services
Potential Clients
Web Service
ASMX file (Provides access to WSDL and .disco files)
DISCO file
Web Service Code
Fig. 22.1 | Web service components.
22.2.2 Discovering Web Services Once you implement a Web service, compile it and deploy it on a Web server (discussed in Section 22.4), a client application can consume (i.e., use) the Web service. However, clients must be able to find the Web service and learn about its capabilities. Discovery of Web services (DISCO) is a Microsoft-specific technology used to locate Web services on a server. Four types of DISCO files facilitate the discovery process: .disco files, .vsdisco files, .discomap files and .map files. DISCO files consist of XML markup that describes for clients the location of Web services. A .disco file is accessed via a Web service’s ASMX page and contains markup specifying references to the documents that define various Web services. The resulting data that is returned from accessing a .disco file is placed in the .discomap file. A .vsdisco file is placed in a Web service’s application directory and behaves in a slightly different manner. When a potential client requests a .vsdisco file, XML markup describing the locations of Web services is generated dynamically, then returned to the client. First, the .NET Framework searches for Web services in the directory in which the .vsdisco file is located, as well as that directory’s subdirectories. The .NET Framework then generates XML (using the same syntax as that of a .disco file) that contains references to all the Web services found in this search. Note that a .vsdisco file does not store the markup generated in response to a request. Instead, the .vsdisco file on disk contains configuration settings that specify the .vsdisco file’s behavior. For example, developers can specify in the .vsdisco file certain directories that should not be searched when a client requests a .vsdisco file. Although a developer can open a .vsdisco file in a text editor and examine its contents, this is rarely necessary—a .vsdisco file is intended to be requested (i.e., viewed in a browser) by clients over the Web. Every time this occurs, new markup is generated and displayed.
22.2 .NET Web Services Basics
967
Using .vsdisco files benefits developers in several ways. These files contain only a small amount of data and provide up-to-date information about a server’s available Web services. However, .vsdisco files generate more overhead (i.e., require more processing) than .disco files do, because a search must be performed every time a .vsdisco file is accessed. Thus, some developers find it more beneficial to update .disco files manually. Many systems use both types of files. As we discuss shortly, Web services created using ASP.NET contain the functionality to generate a .disco file when it is requested. This .disco file contains references only to files in the current Web service. Thus, a developer typically places a .vsdisco file at the root of a server; when accessed, this file locates the .disco files for Web services anywhere on the system and uses the markup found in these .disco files to return information about the entire system.
22.2.3 Determining a Web Service’s Functionality After locating a Web service, the client must determine the Web service’s functionality and how to use it. For this purpose, Web services normally contain a service description. This is an XML document that conforms to the Web Service Description Language (WSDL)—an XML vocabulary that defines the methods a Web service makes available and how clients interact with them. The WSDL document also specifies lower-level information that clients might need, such as the required formats for requests and responses. WSDL documents are not meant to be read by developers; rather, WSDL documents are meant to be read by applications, so they know how to interact with the Web services described in the documents. Visual Web Developer generates an ASMX file when a Web service is constructed. Files with the .asmx filename extension are ASP.NET Web service files and are executed by ASP.NET on a Web server (e.g., IIS). When viewed in a Web browser, an ASMX file presents Web method descriptions and links to test pages that allow users to execute sample calls to these methods. We explain these test pages in greater detail later in this section. The ASMX file also specifies the Web service’s implementation class, and optionally the code-behind file in which the Web service is defined and the assemblies referenced by the Web service. When the Web server receives a request for the Web service, it accesses the ASMX file, which, in turn, invokes the Web service implementation. To view more technical information about the Web service, developers can access the WSDL file (which is generated by ASP.NET). We show how to do this shortly. The ASMX page in Fig. 22.2 displays information about the HugeInteger Web service that we create in Section 22.4. This Web service is designed to perform calculations with integers that contain a maximum of 100 digits. Most programming languages cannot easily perform calculations using integers this large. The Web service provides client applications with methods that take two “huge integers” and determine their sum, their difference, which one is larger or smaller and whether the two numbers are equal. Note that the top of the page provides a link to the Web service’s Service Description. ASP.NET generates the WSDL service description from the code you write to define the Web service. Client programs use a Web service’s service description to validate Web method calls when the client programs are compiled. ASP.NET generates WSDL information dynamically rather than creating an actual WSDL file. If a client requests the Web service’s WSDL description (either by appending ?WSDL to the ASMX file’s URL or by clicking the Service Description link), ASP.NET generates the WSDL description, then returns it to the client for display in the Web browser.
968
Chapter 22
Web Services
Link to the service description
Links to the Web service’s methods
Fig. 22.2 | ASMX file rendered in a Web browser. Generating the WSDL description dynamically ensures that clients receive the most current information about the Web service. It is common for an XML document (such as a WSDL description) to be created dynamically and not saved to disk. When a user clicks the Service Description link at the top of the ASMX page in Fig. 22.2, the browser displays the generated WSDL document containing the service description for our HugeInteger Web service (Fig. 22.3).
22.2.4 Testing a Web Service’s Methods Below the Service Description link, the ASMX page shown in Fig. 22.2 lists the methods that the Web service offers. Clicking any method name requests a test page that describes the method (Fig. 22.4). The test page allows users to test the method by entering parameter values and clicking the Invoke button. (We discuss the process of testing a Web method shortly.) Below the Invoke button, the page displays sample request-and-response messages using SOAP and HTTP POST. These protocols are two options for sending and receiving messages in Web services. The protocol that transmits request-and-response messages is also known as the Web service’s wire format or wire protocol, because it defines how information is sent “along the wire.” SOAP is the more commonly used wire format, because SOAP messages can be sent using several transport protocols, whereas HTTP POST must use HTTP. When you test a Web service via an ASMX page (as in Fig. 22.4), the ASMX page uses HTTP POST to test the Web service methods. Later in this chapter, when we use Web services in our C# programs, we employ SOAP—the default protocol for .NET Web services. Figure 22.4 depicts the test page for the HugeInteger Web method Bigger. From this page, users can test the method by entering values in the first: and second: fields, then clicking Invoke. The method executes, and a new Web browser window opens, displaying an XML document that contains the result (Fig. 22.5).
22.2 .NET Web Services Basics
Fig. 22.3 | Service description for our HugeInteger Web service.
Fig. 22.4 | Invoking a Web method from a Web browser.
969
970
Chapter 22
Web Services
Fig. 22.5 | Results of invoking a Web method from a Web browser.
Error-Prevention Tip 22.1 Using the ASMX page of a Web service to test and debug methods can help you make the Web service more reliable and robust. 22.1
22.2.5 Building a Client to Use a Web Service Now that we have discussed the different files that comprise a .NET Web service, let’s examine the parts of a .NET Web service client (Fig. 22.6). A .NET client can be any type of .NET application, such as a Windows application, a console application or a Web application. You can enable a client application to consume a Web service by adding a Web reference to the client. This process adds files to the client application that allow the client to access the Web service. This section discusses Visual C# 2005, but the discussion also applies to Visual Web Developer. To add a Web reference in Visual C# 2005, right click the project name in the Solution Explorer and select Add Web Reference…. In the resulting dialog, specify the Web service to consume. Visual C# 2005 then adds an appropriate Web reference to the client application. We demonstrate adding Web references in more detail in Section 22.4.
Client Client code (Web service consumer)
Web reference
Proxy class
WSDL copy
Fig. 22.6 | .NET Web service client after a Web reference has been added.
22.3 Simple Object Access Protocol (SOAP)
971
When you specify the Web service you want to consume, Visual C# 2005 accesses the Web service’s WSDL information and copies it into a WSDL file that is stored in the client project’s Web References folder. This file is visible when you instruct Visual C# 2005 to Show All Files. [Note: A copy of the WSDL file provides the client application with local access to the Web service’s description. To ensure that the WSDL file is up-to-date, Visual C# 2005 provides an Update Web Reference option (available by right clicking the Web reference in the Solution Explorer), which updates the files in the Web References folder.] The WSDL information is used to create a proxy class, which handles all the “plumbing” required for Web method calls (i.e., the networking details and the formation of SOAP messages). Whenever the client application calls a Web method, the application actually calls a corresponding method in the proxy class. This method has the same name and parameters as the Web method that is being called, but formats the call to be sent as a request in a SOAP message. The Web service receives this request as a SOAP message, executes the method call and sends back the result as another SOAP message. When the client application receives the SOAP message containing the response, the proxy class deserializes it and returns the results as the return value of the Web method that was called. Figure 22.7 depicts the interactions among the client code, proxy class and Web service. The .NET environment hides most of these details from you. Many aspects of Web service creation and consumption—such as generating WSDL files, proxy classes and DISCO files—are handled by Visual Web Developer, Visual C# 2005 and ASP.NET. Although developers are relieved of the tedious process of creating these files, they can still modify the files if necessary. This is required only when developing advanced Web services—none of our examples require modifications to these files.
22.3 Simple Object Access Protocol (SOAP) The Simple Object Access Protocol (SOAP) is a platform-independent protocol that uses XML to make remote procedure calls, typically over HTTP. Each request and response is packaged in a SOAP message—an XML message containing the information that a Web service requires to process the message. SOAP messages are written in XML so that they are human readable and platform independent. Most firewalls—security barriers that restrict communication among networks—do not restrict HTTP traffic. Thus, XML and HTTP enable computers on different platforms to send and receive SOAP messages with few limitations. Web services also use SOAP for the extensive set of types it supports. The wire format used to transmit requests and responses must support all types passed between the appli-
Client
Client code
Proxy class
Internet
Web Service
Fig. 22.7 | Interaction between a Web service client and a Web service.
972
Chapter 22
Web Services
cations. SOAP types include the primitive types (e.g., Integer), as well as DateTime, XmlNode and others. SOAP can also transmit arrays of all these types. In addition, DataSets can be serialized into SOAP. In Section 22.7, you will see that you can transmit userdefined types in SOAP messages. When a program invokes a Web method, the request and all relevant information are packaged in a SOAP message and sent to the server on which the Web service resides. When the Web service receives this SOAP message, it begins to process the contents (contained in a SOAP envelope), which specify the method that the client wishes to execute and any arguments the client is passing to that method. This process of interpreting a SOAP message’s contents is known as parsing a SOAP message. After the Web service receives and parses a request, the proper method is called with the specified arguments (if there are any), and the response is sent back to the client in another SOAP message. The client parses the response to retrieve the result of the method call. The SOAP request in Fig. 22.8 was taken from the test page for the HugeInteger Web service’s Bigger method (Fig. 22.4). Visual C# 2005 creates such a message when a client wishes to execute the HugeInteger Web service’s Bigger method. If the client is a Web application, Visual Web Developer creates the SOAP message. The message in Fig. 22.8 contains placeholders (length in line 4 and string in lines 16–17) representing values specific to a particular call to Bigger. If this were a real SOAP request, elements first and second (lines 16–17) would each contain an actual value passed from the client to the Web service, rather than the placeholder string. For example, if this envelope were transmitting the request from Fig. 22.4, element first and element second would contain the numbers displayed in the figure, and placeholder length (line 4) would contain the length of the SOAP message. Most programmers do not manipulate SOAP messages directly, but instead allow the .NET framework to handle the transmission details.
Fig. 22.8 | SOAP request message for the HugeInteger Web service.
22.4 Publishing and Consuming Web Services
973
22.4 Publishing and Consuming Web Services This section presents several examples of creating (also known as publishing) and using (also known as consuming) Web services. Recall that an application that consumes a Web service actually consists of two parts—a proxy class representing the Web service and a client application that accesses the Web service via an instance of the proxy class. The instance of the proxy class passes a Web method’s arguments from the client application to the Web service. When the Web method completes its task, the instance of the proxy class receives the result and parses it for the client application. Visual C# 2005 and Visual Web Developer create these proxy classes for you. We demonstrate this momentarily.
22.4.1 Defining the HugeInteger Web Service Figure 22.9 presents the code-behind file for the HugeInteger Web service that you will build in Section 22.4.2. When creating Web services in Visual Web Developer, you work almost exclusively in the code-behind file. As we mentioned earlier, this Web service is designed to perform calculations with integers that have a maximum of 100 digits. long variables cannot handle integers of this size (i.e., an overflow occurs). The Web service provides methods that take two “huge integers” (represented as strings) and determine their sum, their difference, which one is larger or smaller and whether the two numbers are equal. You can think of these methods as services available to programmers of other applications via the Web (hence the term Web services). Any programmer can access this Web service, use the methods and thus avoid writing 172 lines of code. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
// Fig. 22.9: HugeInteger.cs // HugeInteger Web service performs operations on large integers. using System; using System.Web; using System.Web.Services; using System.Web.Services.Protocols; [ WebService( Namespace = "http://www.deitel.com/", Description = "A Web service that provides methods for" + " manipulating large integer values" ) ] [ WebServiceBinding( ConformsTo = WsiProfiles.BasicProfile1_1 ) ] public class HugeInteger : System.Web.Services.WebService { private const int MAXIMUM = 100; // maximum number of digits public int[] number; // array representing the huge integer
Fig. 22.9
// default constructor public HugeInteger() { number = new int[ MAXIMUM ]; } // end default constructor // indexer that accepts an integer parameter public int this[ int index ] { | HugeInteger
get { return number[ index ]; } // end get set { number[ index ] = value; } // end set } // end indexer // returns string representation of HugeInteger public override string ToString() { string returnString = ""; foreach ( int i in number ) returnString = i + returnString; return returnString; } // end method ToString // creates HugeInteger based on argument public static HugeInteger FromString( string value ) { // create temporary HugeInteger to be returned by the method HugeInteger parsedInteger = new HugeInteger(); for ( int i = 0 ; i < value.Length; i++ ) parsedInteger[ i ] = Int32.Parse( value[ value.Length - i - 1 ].ToString() ); return parsedInteger; } // end method FromString // WebMethod that adds integers represented by the string arguments [ WebMethod( Description = "Adds two huge integers." ) ] public string Add( string first, string second ) { int carry = 0; HugeInteger operand1 = HugeInteger.FromString( first ); HugeInteger operand2 = HugeInteger.FromString( second ); HugeInteger result = new HugeInteger(); // stores result of addition // perform addition algorithm for each digit for ( int i = 0; i < MAXIMUM; i++ ) { // add two digits in same column, // result is their sum plus carry from // previous operation modulo 10 result[ i ] = ( operand1[ i ] + operand2[ i ] + carry ) % 10;
// set carry to remainder of dividing sums of two digits by 10 carry = ( operand1[ i ] + operand2[ i ] + carry ) / 10; } // end for return result.ToString(); } // end method Add // WebMethod that subtracts integers // represented by the string arguments [ WebMethod( Description = "Subtracts two huge integers." ) ] public string Subtract( string first, string second ) { HugeInteger operand1 = HugeInteger.FromString( first ); HugeInteger operand2 = HugeInteger.FromString( second ); HugeInteger result = new HugeInteger(); // subtract bottom digit from top digit for ( int i = 0; i < MAXIMUM; i++ ) { // if top digit is smaller than bottom digit we need to borrow if ( operand1[ i ] < operand2[ i ] ) Borrow( operand1, i ); // subtract bottom from top result[ i ] = operand1[ i ] - operand2[ i ]; } // end for return result.ToString(); } // end method Subtract // borrow 1 from next digit private void Borrow( HugeInteger hugeInteger, int place ) { // if no place to borrow from, signal problem if ( place >= MAXIMUM - 1 ) throw new ArgumentException(); // otherwise if next digit is zero, borrow from column to left else if ( hugeInteger[ place + 1 ] == 0 ) Borrow( hugeInteger, place + 1 ); // add ten to current place because we borrowed and subtract // one from previous digit--this is the digit we borrowed from hugeInteger[ place ] += 10; hugeInteger[ place + 1 ]--; } // end method Borrow // WebMethod that returns true if first integer is bigger than second [ WebMethod( Description = "Determines whether the first integer is " + "larger than the second integer." ) ] public bool Bigger( string first, string second ) { char[] zeros = { '0' }; | HugeInteger
Web service. (Part 3 of 4.)
976
Chapter 22
Web Services
132 133 try 134 { 135 // if elimination of all zeros from result 136 // of subtraction is an empty string, 137 // numbers are equal, so return false, otherwise return true 138 if ( Subtract( first, second ).Trim( zeros ) == "" ) 139 return false; 140 else 141 return true; 142 } // end try 143 // if ArgumentException occurs, 144 // first number was smaller, so return false 145 catch ( ArgumentException exception ) 146 { 147 return false; 148 } // end catch 149 } // end method Bigger 150 151 // WebMethod returns true if first integer is smaller than second [ WebMethod( Description = "Determines whether the first integer " + 152 "is smaller than the second integer." ) ] 153 154 public bool Smaller( string first, string second ) 155 { 156 // if second is bigger than first, then first is smaller than second 157 return Bigger( second, first ); 158 } // end method Smaller 159 160 // WebMethod that returns true if two integers are equal [ WebMethod( Description = "Determines whether the first integer " + 161 "is equal to the second integer." ) ] 162 163 public bool EqualTo( string first, string second ) 164 { 165 // if either first is bigger than second, 166 // or first is smaller than second, they are not equal 167 if ( Bigger( first, second ) || Smaller( first, second ) ) 168 return false; 169 else 170 return true; 171 } // end method EqualTo 172 } // end class HugeInteger
Fig. 22.9
| HugeInteger
Web service. (Part 4 of 4.)
Lines 8–10 contain a WebService attribute. Attaching this attribute to a Web service class declaration allows you to specify the Web service’s namespace and description. Like an XML namespace (see Section 19.4), a Web service’s namespace is used by client applications to differentiate that Web service from others available on the Web. Line 8 assigns http://www.deitel.com as the Web service’s namespace using the WebService attribute’s Namespace property. Lines 9–10 use the WebService attribute’s Description property to describe the Web service’s purpose—this appears in the ASMX page (Fig. 22.2). Visual Web Developer places line 11 in all newly created Web services. This line indicates that the Web service conforms to the Basic Profile 1.1 (BP 1.1) developed by the Web
22.4 Publishing and Consuming Web Services
977
Services Interoperability Organization (WS-I), a group dedicated to promoting interoperability among Web services developed on different platforms with different programming languages. BP 1.1 is a document that defines best practices for various aspects of Web service creation and consumption (www.WS-I.org). As we discussed in Section 22.2, the .NET environment hides many of these details from you. Setting the WebServiceBinding attribute’s ConformsTo property to WsiProfiles.BasicProfile1_1 instructs Visual Web Developer to perform its “behind-the-scenes” work, such as generating WSDL and ASMX files, in conformance with the guidelines laid out in BP 1.1. For more information on Web services interoperabilty and the Basic Profile 1.1, visit the WS-I Web site at www.ws-i.org. By default, each new Web service class created in Visual Web Developer inherits from class System.Web.Services.WebService (line 12). Although a Web service need not derive from class WebService, this class provides members that are useful in determining information about the client and the Web service itself. Several methods in class HugeInteger are tagged with the WebMethod attribute (lines 62, 88, 127, 152 and 161), which exposes a method so that it can be called remotely. When this attribute is absent, the method is not accessible to clients that consume the Web service. Note that this attribute, like the WebService attribute, contains a Description property that allows the ASMX page to display information about the method (see these descriptions shown in Fig. 22.2).
Common Programming Error 22.1 Failing to expose a method as a Web method by declaring it with the WebMethod attribute prevents clients of the Web service from accessing the method. 22.1
Portability Tip 22.1 Specify a namespace for each Web service so that it can be uniquely identified by clients. In general, you should use your company’s domain name as the Web service’s namespace, since company domain names are guaranteed to be unique. 22.1
Portability Tip 22.2 Specify descriptions for a Web service and its Web methods so that the Web service’s clients can view information about the service in the service’s ASMX page. 22.2
Common Programming Error 22.2 No method with the WebMethod attribute can be declared static—for a client to access a Web method, an instance of that Web service must exist. 22.2
Lines 24–35 define an indexer, which enables us to access any digit in a HugeInteger. Lines 62–84 and 88–107 define Web methods Add and Subtract, which perform addition and subtraction, respectively. Method Borrow (lines 110–124) handles the case in which the digit that we are currently examining in the left operand is smaller than the corresponding digit in the right operand. For instance, when we subtract 19 from 32, we usually examine the numbers in the operands digit-by-digit, starting from the right. The number 2 is smaller than 9, so we add 10 to 2 (resulting in 12). After borrowing, we can subtract 9 from 12, resulting in 3 for the rightmost digit in the solution. We then subtract 1 from the 3 in 32—the next digit to the left (i.e., the digit we borrowed from). This leaves a 2 in the tens place. The corresponding digit in the other operand is now the 1 in 19. Subtracting 1 from 2 yields 1, making the corresponding digit in the result 1. The final result, when the digits are put together, is 13. Method Borrow is the method that adds 10
978
Chapter 22
Web Services
to the appropriate digits and subtracts 1 from the digits to the left. This is a utility method that is not intended to be called remotely, so it is not qualified with attribute WebMethod. Recall that Fig. 22.2 presented a screen capture of the ASMX page HugeInteger.asmx for which the code-behind file HugeInteger.cs (Fig. 22.9) defines Web methods. A client application can invoke only the five methods listed in the screen capture in Fig. 22.2 (i.e., the methods qualified with the WebMethod attribute in Fig. 22.9).
22.4.2 Building a Web Service in Visual Web Developer We now show you how to create the HugeInteger Web service. In the following steps, you will create an ASP.NET Web Service project that executes on your computer’s local IIS Web server. To create the HugeInteger Web service in Visual Web Developer, perform the following steps:
Step 1: Creating the Project To begin, we must create a project of type ASP.NET Web Service. Select File > New Web Site… to display the New Web Site dialog (Fig. 22.10). Select ASP.NET Web Service in the Templates pane. Select HTTP from the Location drop-down list to indicate that the files should be placed on a Web server. By default, Visual Web Developer indicates that it will place the files on the local machine’s IIS Web server in a virtual directory named WebSite (http://localhost/WebSite). Replace the name WebSite with HugeInteger for this example. Next, select Visual C# from the Language drop-down list to indicate that you will use Visual C# to build this Web service. Visual Web Developer places the Web service project’s solution file (.sln) in the Projects subfolder within the current Windows user’s My Documents\Visual Studio 2005 folder. If you do not have access to an IIS Web server to build and test the examples in this chapter, you can select File System from the Location drop-down list. In this case, Visual Web Developer will place your Web service’s files on your local hard disk. You will then be able to test the Web service using Visual Web Developer’s built-in Web server.
Fig. 22.10 | Creating an ASP.NET Web Service in Visual Web Developer.
22.4 Publishing and Consuming Web Services
979
Step 2: Examining the Newly Created Project When the project is created, the code-behind file Service.cs, which contains code for a simple Web service (Fig. 22.11) is displayed by default. If the code-behind file is not open, it can be opened by double clicking the file in the App_Code directory listed in the Solution Explorer. Visual Web Developer includes four using declarations that are helpful for developing Web services (lines 1–4). By default, a new code-behind file defines a class named Service that is marked with the WebService and WebServiceBinding attributes (lines 6– 7). The class contains a sample Web method named HelloWorld (lines 14–17). This method is a placeholder that you will replace with your own method(s). Step 3: Modifying and Renaming the Code-Behind File To create the HugeInteger Web service developed in this section, modify Service.cs by replacing all of the sample code provided by Visual Web Developer with all of the code from the HugeInteger code-behind file (Fig. 22.9). Then rename the file HugeInteger.cs (by right clicking the file in the Solution Explorer and choosing Rename). Step 4: Examining the ASMX File The Solution Explorer lists one file—Service.asmx—in addition to the code-behind file. Recall from Fig. 22.2 that a Web service’s ASMX page, when accessed through a Web browser, displays information about the Web service’s methods and provides access to the Web service’s WSDL information. However, if you open the ASMX file on disk, you will see that it actually contains only
to indicate the programming language in which the Web service’s code-behind file is written, the code-behind file’s location and the class that defines the Web service. When you
Fig. 22.11 | Code view of a Web service.
980
Chapter 22
Web Services
request the ASMX page through IIS, ASP.NET uses this information to generate the content displayed in the Web browser (i.e., the list of Web methods and their descriptions).
Step 5: Modifying the ASMX File Whenever you change the name of the code-behind file or the name of the class that defines the Web service, you must modify the ASMX file accordingly. Thus, after defining class HugeInteger in the code-behind file HugeInteger.cs, modify the ASMX file to contain the lines
Error-Prevention Tip 22.2 Update the Web service’s ASMX file appropriately whenever the name of a Web service’s codebehind file or the class name changes. Visual Web Developer creates the ASMX file, but does not automatically update it when you make changes to other files in the project. 22.2
Step 6: Renaming the ASMX File The final step in creating the HugeInteger Web service is to rename the ASMX file HugeInteger.asmx.
22.4.3 Deploying the HugeInteger Web Service The Web service is already deployed because we created the HugeInteger Web service directly on our computer’s local IIS server. You can choose Build Web Site from the Build menu to ensure that the Web service compiles without errors. You can also test the Web service directly from Visual Web Developer by selecting Start Without Debugging from the Debug menu. This opens a browser window that contains the ASMX page shown in Fig. 22.2. Clicking the link for a particular HugeInteger Web service method displays a Web page like the one in Fig. 22.4 that allows you to test the method. Note that you can also access the Web service’s ASMX page from your computer by typing the following URL in a Web browser http://localhost/HugeInteger/HugeInteger.asmx
Accessing the HugeInteger Web Service’s ASMX Page from Another Computer Eventually, you will want other clients to be able to access and use your Web service. If you deploy the Web service on an IIS Web server, a client can connect to that server to access the Web service with a URL of the form http://host/HugeInteger/HugeInteger.asmx
where host is the hostname or IP address of the Web server. To access the Web service from another computer in your company’s or school’s local area network, you can replace host with the actual name of the computer on which IIS is running. If you have the Windows XP Service Pack 2 operating system on the computer running IIS, that computer may not allow requests from other computers by default. If you wish to allow other computers to connect to your computer’s Web server, perform the following steps:
22.4 Publishing and Consuming Web Services
981
1. Select Start > Control Panel to open your system’s Control Panel window, then double click Windows Firewall to view the Windows Firewall settings dialog. 2. In the Windows Firewall settings dialog, click the Advanced tab, select Local Area Connection (or your network connection’s name, if it is different) in the Network Connection Settings list box and click the Settings… button to display the Advanced Settings dialog. 3. In the Advanced Settings dialog, ensure that the checkbox for Web Server (HTTP) is checked to allow clients on other computers to submit requests to your computer’s Web server. 4. Click OK in the Advanced Settings dialog, then click OK in the Windows Firewall settings dialog.
Accessing the HugeInteger Web Service’s ASMX Page When the Web Service Executes in Visual Web Developer’s Built-In Web Server Recall from Step 1 of Section 22.4.2 that if you do not have access to an IIS server to deploy and test your Web service, you can create the Web service on your computer’s hard disk and use Visual Web Developer’s built-in Web server to test the Web service. In this case, when you select Start Without Debugging from the Debug menu, Visual Web Developer executes its built-in Web server, then opens a Web browser containing the Web service’s ASMX page so that you can test the Web service. Web servers typically receive requests on port 80. To ensure that Visual Web Developer’s built-in Web server does not conflict with another Web server running on your local computer, Visual Web Developer’s Web server receives requests on a randomly selected port number. When a Web server receives requests on a port number other than port 80, the port number must be specified as part of the request. In this case, the URL to access the HugeInteger Web service’s ASMX page would be of the form http://host:portNumber/HugeInteger/HugeInteger.asmx
where host is the hostname or IP address of the computer on which Visual Web Developer’s built-in Web server is running and portNumber is the specific port on which the Web server receives requests. You can see this port number in your Web browser’s Address field when you test the Web service from Visual Web Developer. Unfortunately, Web services executed using Visual Web Developer’s built-in server cannot be accessed over a network.
22.4.4 Creating a Client to Consume the HugeInteger Web Service Now that we have defined and deployed our Web service, we demonstrate how to consume it from a client application. In this section, you will create a Windows application as the client using Visual C# 2005. After creating the client application, you will add a proxy class to the project that allows the client to access the Web service. Recall that the proxy class (or proxy) is generated from the Web service’s WSDL file and enables the client to call Web methods over the Internet. The proxy class handles all the details of communicating with the Web service. The proxy class is hidden from you by default—you can view it in the Solution Explorer by clicking the Show All Files button. The proxy class’s purpose is to make clients think that they are calling the Web methods directly.
982
Chapter 22
Web Services
This example demonstrates how to create a Web service client and generate a proxy class that allows the client to access the HugeInteger Web service. You will begin by creating a project and adding a Web reference to it. When you add the Web reference, Visual C# 2005 will generate the appropriate proxy class. You will then create an instance of the proxy class and use it to call the Web service’s methods. First, create a Windows application in Visual C# 2005, then perform the following steps:
Step 1: Opening the Add Web Reference Dialog Right click the project name in the Solution Explorer and select (Fig. 22.12).
Add Web Reference…
Step 2: Locating Web Services on Your Computer In the Add Web Reference dialog that appears (Fig. 22.13), click Web services on the local machine to locate Web references stored on the IIS Web server on your local computer (http://localhost). This server’s files are located at C:\Inetpub\wwwroot by default. Note that the Add Web Reference dialog allows you to search for Web services in several different locations. Many companies that provide Web services simply distribute the exact URLs at which their Web services can be accessed. For this reason, the Add Web Reference dialog also allows you to enter the specific URL of a Web service in the URL field. Step 3: Choosing the Web Service to Reference Select the HugeInteger Web service from the list of available Web services (Fig. 22.14). Step 4: Adding the Web Reference Add the Web reference by clicking the Add Reference button (Fig. 22.15). Step 5: Viewing the Web Reference in the Solution Explorer The Solution Explorer (Fig. 22.16) should now contain a Web References folder with a node named after the domain name where the Web service is located. In this case, the
Fig. 22.12 | Adding a Web service reference to a project.
22.4 Publishing and Consuming Web Services
983
Fig. 22.13 | Add Web Reference dialog.
Fig. 22.14 | Web services located on localhost. name is
localhost because we are using the local Web server. When we reference class HugeInteger in the client application, we will do so through the localhost namespace.
Notes on Creating a Client to Consume a Web Service The steps we just presented also apply to adding Web references to Web applications created in Visual Web Developer. We present a Web application that consumes a Web service in Section 22.6.
984
Chapter 22
Web Services
Fig. 22.15 | Web reference selection and description.
Fig. 22.16 | Solution Explorer after adding a Web reference to a project. When creating a client to consume a Web service, add the Web reference first so that Visual C# 2005 (or Visual Web Developer) can recognize an object of the Web service proxy class. Once you add the Web reference to the client, it can access the Web service through an object of the proxy class. The proxy class (named HugeInteger) is located in namespace localhost, so you must use localhost.HugeInteger to reference this class. Although you must create an object of the proxy class to access the Web service, you do not need access to the proxy class’s code. As we show in Section 22.4.5, you can invoke the proxy object’s methods as if it were an object of the Web service class. The steps that we described in this section work well if you know the appropriate Web service reference. However, what if you are trying to locate a new Web service? Two common technologies facilitate this process—Universal Description, Discovery and Integration (UDDI) and Discovery of Web services (DISCO). We discussed DISCO in Section 22.2. UDDI is an ongoing project for developing a set of specifications that define how Web services should be published so that programmers searching for Web services can
22.4 Publishing and Consuming Web Services
985
find them. Microsoft and its partners are working on this project to help programmers locate Web services that conform to certain specifications, allowing developers to find Web services through search engines similar to Yahoo!® and Google™. You can learn more about UDDI and view a demonstration by visiting www.uddi.org and uddi.microsoft.com. These sites contain search tools that make finding Web services convenient.
22.4.5 Consuming the HugeInteger Web Service The Windows Form in Fig. 22.17 uses the HugeInteger Web service to perform computations with positive integers up to 100 digits long. Line 22 declares variable remoteInteger of type localhost.HugeInteger. This variable is used in each of the application’s event handlers to call methods of the HugeInteger Web service. The proxy object is created and assigned to this variable at line 31 in the Form’s Load event handler. Lines 52–53, 66–67, 95–96, 116–117 and 135–136 in the various button event handlers invoke methods of the Web service. Note that each call is made on the local proxy object, which then communicates with the Web service on the client’s behalf. If you downloaded the example from www.deitel.com/books/csharpforprogrammers2, you might need to regenerate the proxy by removing the Web reference, then adding it again. To do so, right click localhost in the Web References folder in the Solution Explorer and select option Delete. Then follow the instructions in the preceding section to add the Web reference to the project. The user inputs two integers, each up to 100 digits long. Clicking a button causes the application to invoke a Web method to perform the appropriate task and return the result. Note that client application UsingHugeIntegerService cannot perform operations using 100-digit numbers directly. Instead the application creates string representations of these 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
// Fig. 22.17: UsingHugeIntegerService.cs // Using the HugeInteger Web Service. using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; using System.Web.Services.Protocols; namespace UsingHugeIntegerWebService { public partial class UsingHugeIntegerServiceForm : Form { public UsingHugeIntegerServiceForm() { InitializeComponent(); } // end constructor // declare a reference to Web service private localhost.HugeInteger remoteInteger;
Fig. 22.17 | Using the HugeInteger Web service. (Part 1 of 5.)
private char[] zeros = { '0' }; // character to trim from strings // instantiates object to interact with Web service private void UsingHugeIntegerServiceForm_Load( object sender, EventArgs e ) { // instantiate remoteInteger remoteInteger = new localhost.HugeInteger(); } // end method UsingHugeIntegerServiceForm_Load // adds two numbers input by user private void addButton_Click( object sender, EventArgs e ) { // make sure numbers do not exceed 100 digits and that both // are not 100 digits long, which would result in overflow if ( firstTextBox.Text.Length > 100 || secondTextBox.Text.Length > 100 || ( firstTextBox.Text.Length == 100 && secondTextBox.Text.Length == 100) ) { MessageBox.Show( "HugeIntegers must not be more " + "than 100 digits\r\nBoth integers cannot be " + "of length 100: this causes an overflow", "Error", MessageBoxButtons.OK, MessageBoxIcon.Information ); return; } // end if // perform addition resultLabel.Text = remoteInteger.Add( firstTextBox.Text, secondTextBox.Text ).TrimStart( zeros ); } // end method addButton_Click // subtracts two numbers input by user private void subtractButton_Click( object sender, EventArgs e ) { // make sure HugeIntegers do not exceed 100 digits if ( SizeCheck( firstTextBox, secondTextBox ) ) return; // perform subtraction try { string result = remoteInteger.Subtract( firstTextBox.Text, secondTextBox.Text ).TrimStart( zeros ); if ( result == "" ) resultLabel.Text = "0"; else resultLabel.Text = result; } // end try
Fig. 22.17 | Using the HugeInteger Web service. (Part 2 of 5.)
// if WebMethod throws an exception, // then first argument was smaller than second catch ( SoapException exception ) { MessageBox.Show( "First argument was smaller than the second" ); } // end catch } // end method subtractButton_Click // determines whether first number // input by user is larger than second private void largerButton_Click( object sender, EventArgs e ) { // make sure HugeIntegers do not exceed 100 digits if ( SizeCheck( firstTextBox, secondTextBox ) ) return; // call Web-service method to determine if // first integer is larger than the second if ( remoteInteger.Bigger( firstTextBox.Text, secondTextBox.Text ) ) resultLabel.Text = firstTextBox.Text.TrimStart( zeros ) + " is larger than " + secondTextBox.Text.TrimStart( zeros ); else resultLabel.Text = firstTextBox.Text.TrimStart( zeros ) + " is not larger than " + secondTextBox.Text.TrimStart( zeros ); } // end method largerButton_Click // determines whether first number // input by user is smaller than second private void smallerButton_Click( object sender, EventArgs e ) { // make sure HugeIntegers do not exceed 100 digits if ( SizeCheck( firstTextBox, secondTextBox ) ) return; // call Web-service method to determine if // first integer is smaller than second if ( remoteInteger.Smaller( firstTextBox.Text, secondTextBox.Text ) ) resultLabel.Text = firstTextBox.Text.TrimStart( zeros ) + " is smaller than " + secondTextBox.Text.TrimStart( zeros ); else resultLabel.Text = firstTextBox.Text.TrimStart( zeros ) + " is not smaller than " + secondTextBox.Text.TrimStart( zeros ); } // end method smallerButton_Click // determines whether two numbers input by user are equal private void equalButton_Click( object sender, EventArgs e )
Fig. 22.17 | Using the HugeInteger Web service. (Part 3 of 5.)
988
Chapter 22
Web Services
129 { 130 // make sure HugeIntegers do not exceed 100 digits 131 if ( SizeCheck( firstTextBox, secondTextBox ) ) 132 return; 133 134 // call Web-service method to determine if integers are equal if ( remoteInteger.EqualTo( firstTextBox.Text, 135 secondTextBox.Text ) ) 136 137 resultLabel.Text = firstTextBox.Text.TrimStart( zeros ) + 138 " is equal to " + secondTextBox.Text.TrimStart( zeros ); 139 else 140 resultLabel.Text = firstTextBox.Text.TrimStart( zeros ) + 141 " is not equal to " + 142 secondTextBox.Text.TrimStart( zeros ); 143 } // end method equalButton_Click 144 145 // determines whether numbers input by user are too big 146 private bool SizeCheck( TextBox first, TextBox second ) 147 { 148 // display an error message if either number has too many digits 149 if ( ( first.Text.Length > 100 ) || 150 ( second.Text.Length > 100 ) ) 151 { 152 MessageBox.Show( "HugeIntegers must be less than 100 digits" , 153 "Error", MessageBoxButtons.OK, MessageBoxIcon.Information); 154 return true; 155 } // end if 156 157 return false; 158 } // end method SizeCheck 159 } // end class UsingHugeIntegerServiceForm 160 } // end namespace UsingHugeIntegerWebService
Fig. 22.17 | Using the HugeInteger Web service. (Part 4 of 5.)
22.5 Session Tracking in Web Services
989
Fig. 22.17 | Using the HugeInteger Web service. (Part 5 of 5.) numbers and passes them as arguments to Web methods that handle such tasks for the client. It then uses the return value of each operation to display an appropriate message. Note that the application eliminates leading zeros in the numbers before displaying them by calling string method TrimStart. Like string method Trim (discussed in Chapter 16), TrimStart removes all occurrences of characters specified by a char array (line 24) from the beginning of a string.
22.5 Session Tracking in Web Services In Chapter 21, we described the advantages of maintaining information about users to personalize their experiences. In particular, we discussed session tracking using cookies and HttpSessionState objects. We will now incorporate session tracking into a Web service. Suppose a client application needs to call several methods from the same Web service, possibly several times each. In such a case, it can be beneficial for the Web service to maintain state information for the client. Session tracking eliminates the need for information about the client to be passed between the client and the Web service multiple times. For example, a Web service providing access to local restaurant reviews would benefit from storing the client user’s street address. Once the user’s address is stored in a session variable, Web methods can return personalized, localized results without requiring that the address be passed in each method call. This not only improves performance, but also requires less effort on the part of the programmer—less information is passed in each method call.
990
Chapter 22
Web Services
22.5.1 Creating a Blackjack Web Service Storing session information can provide client programmers with a more intuitive Web service. Our next example is a Web service that assists programmers in developing a blackjack card game (Fig. 22.18). The Web service provides Web methods to deal a card and to evaluate a hand of cards. After presenting the Web service, we use it to serve as the dealer for a game of blackjack (Fig. 22.19). The blackjack Web service uses a session variable to maintain a unique deck of cards for each client application. Several clients can use the service at the same time, but Web method calls made by a specific client use only the deck stored in that client’s session. Our example uses a simple subset of casino blackjack rules: Two cards each are dealt to the dealer and the player. The player’s cards are dealt face up. Only the first of the dealer’s cards is dealt face up. Each card has a value. A card numbered 2 through 10 is worth its face value. Jacks, queens and kings each count as 10. Aces can count as 1 or 11—whichever value is more beneficial to the player (as we will soon see). If the sum of the player’s two initial cards is 21 (i.e., the player was dealt a card valued at 10 and an ace, which counts as 11 in this situation), the player has “blackjack” and immediately wins the game. Otherwise, the player can begin taking additional cards one at a time. These cards are dealt face up, and the player decides when to stop taking cards. If the player “busts” (i.e., the sum of the player’s cards exceeds 21), the game is over, and the player loses. When the player is satisfied with the current set of cards, the player “stays” (i.e., stops taking cards), and the dealer’s hidden card is revealed. If the dealer’s total is 16 or less, the dealer must take another card; otherwise, the dealer must stay. The dealer must continue to take cards until the sum of the dealer’s cards is greater than or equal to 17. If the dealer exceeds 21, the player wins. Otherwise, the hand with the higher point total wins. If the dealer and the player have the same point total, the game is a “push” (i.e., a tie), and no one wins.
The Web service (Fig. 22.18) provides methods to deal a card and to determine the point value of a hand. We represent each card as a string consisting of a digit (e.g., 1–13) representing the card’s face (e.g., ace through king), followed by a space and a digit (e.g., 0–3) representing the card’s suit (e.g., clubs, diamonds, hearts or spades). For example, the jack of hearts is represented as "11 2", and the two of clubs is represented as "2 0". After deploying the Web service, we create a Windows application that uses the BlackjackService’s Web methods to implement a game of blackjack. To create and deploy this Web service follow the steps presented in Sections 22.4.2–22.4.3 for the HugeInteger service.
1 2 3 4 5 6 7 8 9 10 11
// Fig. 22.18: BlackjackService.cs // Blackjack Web Service deals and counts cards. using System; using System.Web; using System.Web.Services; using System.Web.Services.Protocols; using System.Collections; [ WebService( Namespace = "http://www.deitel.com/", Description = "A Web service that deals and counts cards for the game Blackjack" ) ] [ WebServiceBinding( ConformsTo = WsiProfiles.BasicProfile1_1 ) ]
public class BlackjackService { // deals card that has not [ WebMethod( EnableSession Description="Deal a new public string DealCard() { string card = "2 2";
991
: System.Web.Services.WebService yet been dealt = true, card from the deck." ) ]
// get client's deck ArrayList deck = ( ArrayList )( Session[ "deck" ] ); card = Convert.ToString( deck[ 0 ] ); deck.RemoveAt( 0 ); return card; } // end method DealCard // creates and shuffles a deck of cards [ WebMethod( EnableSession = true, Description="Create and shuffle a deck of cards." ) ] public void Shuffle() { object temporary; // holds card temporarily during swapping Random randomObject = new Random(); // generates random numbers int newIndex; // index of randomly selected card ArrayList deck = new ArrayList(); // stores deck of cards (strings) // generate all possible cards for ( int i = 1; i button, then click Yes when you are asked whether you would like to add the file to your project and modify the connection. Click Next > to save the connection string in the application configuration file. Step 3: Open the Query Builder and Add the Seats Table from Tickets.mdf Now we must specify how the TableAdapter will access the database. In this example, we will use SQL statements, so choose Use SQL Statements, then click Next >. Click Query Builder… to display the Query Builder and Add Table dialogs. Before building a SQL query, we must specify the table(s) to use in the query. The Tickets.mdf database contains only one table, named Seats. Select this table from the Tables tab and click Add. Click Close to close the Add Table dialog. Step 4: Configure a SELECT Query to Obtain Available Seats Now let’s create a query which selects seats that are not already reserved and that match a particular type and class. Begin by selecting Number from the Seats table at the top of the Query Builder dialog. Next, we must specify the criteria for selecting seats. In the middle of the Query Builder dialog, click the cell below Number in the Column column and select Taken. In the Filter column of this row, type 0 (i.e., false) to indicate that we should select
1006
Chapter 22
Web Services
only seat numbers that are not taken. In the next row, select Type in the Column column and specify @type as the Filter to indicate that the filter value will be specified as an argument to the method that implements this query. In the next row, select Class in the Column column and specify @class as the Filter to indicate that this filter value also will be specified as a method argument. Uncheck the checkboxes in the Output column for the Taken, Type and Class rows. The Query Builder dialog should now appear as shown in Fig. 22.21. Click OK to close the Query Builder dialog. Click the Next > button to choose the methods to generate. For the method name under Fill a DataTable, type FillByTypeAndClass. For the method name under Return a DataTable, type GetDataByTypeAndClass. Click the Finish button to generate these methods.
Step 5: Add Another Query to the SeatsTableAdapter for the TicketsDataSet The last two steps we need to perform create an UPDATE query that reserves a seat. In the design area for the TicketsDataSet, click SeatsTableAdapter to select it, then right click it and select Add Query… to display the TableAdapter Query Configuration Wizard . Select Use SQL Statements and click the Next > button. Select Update as the query type and click the Next > button. Delete the existing UPDATE query. Click Query Builder… to display the Query Builder and Add Table dialogs. Then add the Seats table as we did in Step 3 and click Close to return to the Query Builder dialog.
Fig. 22.21 | QueryBuilder dialog specifying a SELECT query that selects seats that are not already reserved and that match a particular type and class.
22.6 Using Web Forms and Web Services
1007
Step 6: Configure an UPDATE Statement to Reserve a Seat In the Query Builder dialog, select the Taken column from the Seats table at the top of the dialog. In the middle of the dialog, place the value 1 (i.e., true) in the New Value column for the Taken row. In the row below Taken, select Number, uncheck the checkbox in the Set column and specify @number as the Filter value to indicate that the seat number will be specified as an argument to the method that implements this query. The Query Builder dialog should now appear as shown in Fig. 22.22. Click OK in the Query Builder dialog to return to the TableAdapter Query Configuration Wizard . Then click the Next > button to choose the name of the method that will perform the UPDATE query. Name the method UpdateSeatAsTaken, then click Finish to close the TableAdapter Query Configuration Wizard. At this point, you can use the ReservationService.asmx page to test the Web service’s Reserve method. To do so, select Start Without Debugging from the Debug menu. In Section 22.6.2, we build a Web form to consume this Web service.
22.6.2 Creating a Web Form to Interact with the Airline Reservation Web Service Figure 22.23 presents the ASPX listing for a Web Form through which users can select seat types. This page allows users to reserve a seat on the basis of its class (Economy or First) and location (Aisle, Middle or Window) in a row of seats. The page then uses the airline reservation Web service to carry out users’ requests. If the database request is not successful, the user is instructed to modify the request and try again. This page defines two DropDownList objects and a Button. One DropDownList (lines 22–27) displays all the seat types from which users can select. The second (lines 29–32)
Fig. 22.22 | QueryBuilder dialog specifying an UPDATE statement that reserves a seat.
Ticket Reservation Aisle Middle Window Economy First
Fig. 22.23 | ASPX file that takes reservation information. provides choices for the class type. Users click the Button named reserveButton (lines 34–36) to submit requests after making selections from the DropDownLists. The page also defines an initially blank Label named errorLabel (lines 38–39), which displays an appropriate message if no seat matching the user’s selection is available. The code-behind file (Fig. 22.24) attaches an event handler to reserveButton. Lines 16–17 of Fig. 22.24 create a ReservationService object. (Recall that you must add a Web reference to this Web service.) When the user clicks Reserve (Fig. 22.25), the
22.6 Using Web Forms and Web Services
1009
event handler (lines 20–42 of Fig. 22.24) executes, and the page reloads. The event handler calls the Web service’s Reserve method and passes to it the selected seat and class type as arguments (lines 23–24). If Reserve returns true, the application displays a message thanking the user for making a reservation (line 34); otherwise, errorLabel notifies the user that the type of seat requested is not available and instructs the user to try again (lines 39–40). Use the techniques presented in Chapter 21 to build this ASP.NET Web Form. reserveButton_Click
// Fig. 22.24: ReservationClient.aspx.cs // ReservationClient code behind file. using System; using System.Data; using System.Configuration; using System.Web; using System.Web.Security; using System.Web.UI; using System.Web.UI.WebControls; using System.Web.UI.WebControls.WebParts; using System.Web.UI.HtmlControls; public partial class ReservationClient : System.Web.UI.Page { // object of proxy type used to connect to Reservation Web service private localhost.ReservationService ticketAgent = new localhost.ReservationService(); // attempt to reserve the selected type of seat protected void reserveButton_Click( object sender, EventArgs e ) { // if WebMethod returned true, signal success if ( ticketAgent.Reserve( seatList.SelectedItem.Text, classList.SelectedItem.Text ) ) { // hide other controls instructionsLabel.Visible = false; seatList.Visible = false; classList.Visible = false; reserveButton.Visible = false; errorLabel.Visible = false; // display message indicating success Response.Write( "Your reservation has been made. Thank you." ); } // end if else // WebMethod returned false, so signal failure { // display message in the initially blank errorLabel errorLabel.Text = "This type of seat is not available. " + "Please modify your request and try again."; } // end else } // end method reserveButton_Click } // end class ReservationClient
Fig. 22.24 | Code-behind file for the reservation page.
1010
Chapter 22
Web Services
a) Selecting a seat.
b) Seat reserved successfully.
c) Attempting to reserve another seat.
d) No seats match the requested type and class.
Fig. 22.25 | Ticket reservation Web Form sample execution.
22.7 User-Defined Types in Web Services
1011
22.7 User-Defined Types in Web Services The Web methods we have demonstrated so far all received and returned simple type values. It is also possible to process user-defined types—known as custom types—in a Web service. These types can be passed to or returned from Web methods. Web service clients also can use these user-defined types, because the proxy class created for the client contains the type definitions. This section presents an EquationGenerator Web service that generates random arithmetic equations of type Equation. The client is a math-tutoring application that inputs information about the mathematical question that the user wishes to attempt (addition, subtraction or multiplication) and the skill level of the user (1 specifies equations using one-digit numbers, 2 specifies equations involving two-digit numbers and 3 specifies equations containing three-digit numbers). The Web service then generates an equation consisting of random numbers with the proper number of digits. The client application receives the Equation and displays the sample question to the user in a Windows Form.
Serialization of User-Defined Types We mentioned earlier that all types passed to and from Web services must be supported by SOAP. How, then, can SOAP support a type that is not even created yet? Custom types that are sent to or from a Web service are serialized, enabling them to be passed in XML format. This process is referred to as XML serialization. Requirements for User-Defined Types Used with Web Methods Classes that are used to specify return types and parameter types for Web methods must meet several requirements: 1. They must provide a public default or parameterless constructor. When a Web service or Web service consumer receives an XML serialized object, the .NET Framework must be able to call this constructor as part of the process of deserializing the object (i.e., converting it back to a C# object). 2. Properties and instance variables that should be serialized in XML format must be declared public. (Note that the public properties can be used to provide access to private instance variables.) 3. Properties that should be serialized must provide both get and set accessors (even if they have empty bodies). Read-only properties are not serialized. Any data that is not serialized simply receives its default value (or the value provided by the default or parameterless constructor) when an object of the class is deserialized.
Common Programming Error 22.3 Failure to define a default or parameterless public constructor for a type being passed to or returned from a Web method is a runtime error. 22.3
Common Programming Error 22.4 Defining only the get or set accessor of a property for a user-defined type being passed to or returned from a Web method results in a property that is inaccessible to the client. 22.4
1012
Chapter 22
Web Services
Software Engineering Observation 22.1 Clients of a Web service can access only the service’s public members. The programmer can provide public properties to allow access to private data. 22.1
Defining Class Equation We define class Equation in Fig. 22.26. Lines 28–46 define a constructor that takes three arguments—two ints representing the left and right operands and a string that represents the arithmetic operation to perform. The constructor sets the leftOperand, rightOperand and operationType instance variables, then calculates the appropriate result. The parameterless constructor (lines 21–25) calls the three-argument constructor (lines 28–46) and passes some default values. We do not use the parameterless constructor explicitly, but the XML serialization mechanism uses it when objects of this class are deserialized. Because we provide a constructor with parameters, we must explicitly define the parameterless constructor in this class so that objects of the class can be passed to or returned from Web methods. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
// Fig. 22.26: Equation.cs // Class Equation that contains information about an equation. using System; using System.Data; using System.Configuration; using System.Web; using System.Web.Security; using System.Web.UI; using System.Web.UI.WebControls; using System.Web.UI.WebControls.WebParts; using System.Web.UI.HtmlControls; public class Equation { private int leftOperand; // number to the left of the operator private int rightOperand; // number to the right of the operator private int resultValue; // result of the operation private string operationType; // type of the operation // required default constructor public Equation() : this( 0, 0, "+" ) { // empty body } // end default constructor // three-argument constructor for class Equation public Equation( int leftValue, int rightValue, string type ) { leftOperand = leftValue; rightOperand = rightValue; operationType = type; switch ( operationType ) // perform appropriate operation {
Fig. 22.26 | Class that stores equation information. (Part 1 of 3.)
case "+": // addition resultValue = leftOperand + rightOperand; break; case "-": // subtraction resultValue = leftOperand - rightOperand; break; case "*": // multiplication resultValue = leftOperand * rightOperand; break; } // end switch } // end three-argument constructor // return string representation of the Equation object public override string ToString() { return leftOperand.ToString() + " " + operationType + " " + rightOperand.ToString() + " = " + resultValue.ToString(); } // end method ToString // property that returns a string representing left-hand side public string LeftHandSide { get { return leftOperand.ToString() + " " + operationType + " " + rightOperand.ToString(); } // end get set // required set accessor { // empty body } // end set } // end property LeftHandSide // property that returns a string representing right-hand side public string RightHandSide { get { return resultValue.ToString(); } // end get set // required set accessor { // empty body } // end set } // end property RightHandSide // property to access the left operand public int Left { get {
Fig. 22.26 | Class that stores equation information. (Part 2 of 3.)
1014
Chapter 22
Web Services
89 return leftOperand; 90 } // end get 91 92 set 93 { 94 leftOperand = value; 95 } // end set 96 } // end property Left 97 98 // property to access the right operand 99 public int Right 100 { 101 get 102 { 103 return rightOperand; 104 } // end get 105 106 set 107 { 108 rightOperand = value; 109 } // end set 110 } // end property Right 111 112 // property to access the result of applying 113 // an operation to the left and right operands 114 public int Result 115 { 116 get 117 { 118 return resultValue; 119 } // end get 120 121 set 122 { 123 resultValue = value; 124 } // end set 125 } // end property Result 126 127 // property to access the operation 128 public string Operation 129 { 130 get 131 { 132 return operationType; 133 } // end get 134 135 set 136 { 137 operationType = value; 138 } // end set 139 } // end property Operation 140 } // end class Equation
Fig. 22.26 | Class that stores equation information. (Part 3 of 3.)
22.7 User-Defined Types in Web Services
1015
Class Equation defines properties LeftHandSide (lines 56–68), RightHandSide (lines 71–82), Left (lines 85–96), Right (lines 99–110), Result (lines 114–125) and Operation (lines 128–139). The client of the Web service does not need to modify the values of properties LeftHandSide and RightHandSide. However, recall that a property can be serialized only if it has both a get and a set accessor—this is true even if the set accessor has an empty body. LeftHandSide (lines 56–68) returns a string representing everything to the left of the equals (=) sign in the equation, and RightHandSide (lines 71–82) returns a string representing everything to the right of the equals (=) sign. Left (lines 85–96) returns the int to the left of the operator (known as the left operand), and Right (lines 99–110) returns the int to the right of the operator (known as the right operand). Result (lines 114–125) returns the solution to the equation, and Operation (lines 128–139) returns the operator in the equation. The client in this case study does not use the RightHandSide property, but we included it in case future clients choose to use it.
Creating the EquationGenerator Web Service Figure 22.27 presents the EquationGenerator Web service, which creates random, customized Equations. This Web service contains only method GenerateEquation (lines 16– 32), which takes two parameters—a string representing the mathematical operation (addition, subtraction or multiplication) and an int representing the difficulty level. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
// Fig. 22.27: Generator.cs // Web Service to generate random equations based on a specified // operation and difficulty level. using System; using System.Web; using System.Web.Services; using System.Web.Services.Protocols; [ WebService( Namespace = "http://www.deitel.com/", Description = "Web service that generates a math equation." ) ] [ WebServiceBinding( ConformsTo = WsiProfiles.BasicProfile1_1 ) ] public class Generator : System.Web.Services.WebService { // Method to generate a math equation [ WebMethod( Description = "Method to generate a math equation." ) ] public Equation GenerateEquation( string operation, int level ) { // find maximum and minimum number to be used int maximum = Convert.ToInt32( Math.Pow( 10, level ) ); int minimum = Convert.ToInt32( Math.Pow( 10, level - 1 ) ); // object to generate random numbers Random randomObject = new Random(); // create equation consisting of two random // numbers between minimum and maximum parameters Equation equation = new Equation( randomObject.Next( minimum, maximum ), randomObject.Next( minimum, maximum ), operation );
Fig. 22.27 | Web service that generates random equations. (Part 1 of 2.)
1016 30 31 32 33
Chapter 22
Web Services
return equation; } // end method GenerateEquation } // end class Generator
Fig. 22.27 | Web service that generates random equations. (Part 2 of 2.) Testing the EquationGenerator Web Service Figure 22.28 shows the result of testing the EquationGenerator Web service. Note that the return value from our Web method is XML encoded. However, this example differs from previous ones in that the XML specifies the values for all public properties and data of the object that is being returned. The return object has been serialized in XML. Our proxy class takes this return value and deserializes it into an object of class Equation, then passes it to the client. Note that an Equation object is not being passed between the Web service and the client. Rather, the information in the object is being sent as XML-encoded data. Clients a) Invoking the GenerateEquation
method to create a subtraction equation with two-digit numbers.
b) XML encoded results of invoking the GenerateEquation method to create a subtraction equation with two-digit numbers.
Fig. 22.28 | Returning an XML-serialized object from a Web method.
22.7 User-Defined Types in Web Services
1017
created using .NET will take the information and create a new Equation object. Clients created on other platforms, however, may use the information differently. Readers creating clients on other platforms should check the Web services documentation for the specific platform they are using, to see how their clients may process custom types. Let’s examine Web method GenerateEquation more closely. Lines 19–20 of Fig. 22.27 define the upper and lower bounds for the random numbers that the method uses to generate an Equation. To set these limits, the program first calls static method Pow of class Math—this method raises its first argument to the power of its second argument. To calculate the value of maximum (the upper bound for any randomly generated numbers used to form an Equation), the program raises 10 to the power of the specified level argument (line 19). If level is 1, maximum is 10; if level is 2, maximum is 100; and if level is 3, maximum is 1000. Variable minimum’s value is determined by raising 10 to a power one less than level (line 20). This calculates the smallest number with level digits. If level is 1, minimum is 1; if level is 2, minimum is 10; and if level is 3, minimum is 100. Lines 27–29 create a new Equation object. The program calls Random method Next, which returns an int that is greater than or equal to the specified lower bound, but less than the specified upper bound. This method generates a left operand value that is greater than or equal to minimum but less than maximum (i.e., a number with level digits). The right operand is another random number with the same characteristics. Line 29 passes the string operation received by GenerateEquation to the Equation constructor. Line 31 returns the new Equation object to the client.
Consuming the EquationGenerator Web Service The MathTutor application (Fig. 22.29) uses the EquationGenerator Web service. The application calls the Web service’s GenerateEquation method to create an Equation object. The tutor then displays the left-hand side of the Equation and waits for user input. This example accesses classes Generator and Equation from the localhost namespace— both are placed in this namespace by default when the proxy is generated. We declare variables of these types at lines 22–23. Line 23 also creates the Generator proxy. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
// Fig. 22.29: MathTutor.cs // Math tutoring program using Web service to generate random equations. using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; namespace MathTutor { public partial class MathTutorForm : Form { public MathTutorForm() {
Fig. 22.29 | Math-tutoring application. (Part 1 of 4.)
InitializeComponent(); } // end constructor private private private private
string operation = "+"; int level = 1; localhost.Equation equation; localhost.Generator generator = new localhost.Generator();
// generates new equation when user clicks button private void generateButton_Click( object sender, EventArgs e ) { // generate equation using current operation and level equation = generator.GenerateEquation( operation, level ); // display left-hand side of equation questionLabel.Text = equation.LeftHandSide; okButton.Enabled = true; answerTextBox.Enabled = true; } // end method generateButton_Click // check user's answer private void okButton_Click( object sender, EventArgs e ) { // determine correct result from Equation object int answer = equation.Result; if ( answerTextBox.Text == "" ) return; // get user's answer int userAnswer = Int32.Parse( answerTextBox.Text ); // determine whether user's answer is correct if ( answer == userAnswer ) { questionLabel.Text = ""; // clear question answerTextBox.Text = ""; // clear answer okButton.Enabled = false; // disable OK button MessageBox.Show( "Correct! Good job!" ); } // end if else MessageBox.Show( "Incorrect. Try again." ); } // end method okButton_Click // set difficulty level to 1 private void levelOneRadioButton_CheckedChanged( object sender, EventArgs e ) { level = 1; } // end method levelOneRadioButton_CheckedChanged
Fig. 22.29 | Math-tutoring application. (Part 2 of 4.)
22.7 User-Defined Types in Web Services
1019
69 // set difficulty level to 2 70 private void levelTwoRadioButton_CheckedChanged( object sender, 71 EventArgs e ) 72 { 73 level = 2; 74 } // end method levelTwoRadioButton_CheckedChanged 75 76 // set difficulty level to 3 77 private void levelThreeRadioButton_CheckedChanged( object sender, 78 EventArgs e ) 79 { 80 level = 3; 81 } // end method levelThreeRadioButton_CheckedChanged 82 83 // set the operation to addition 84 private void additionRadioButton_CheckedChanged( object sender, 85 EventArgs e ) 86 { 87 operation = "+"; 88 generateButton.Text = 89 "Generate " + additionRadioButton.Text + " Example"; 90 } // end method additionRadioButton_CheckedChanged 91 92 // set the operation to subtraction 93 private void subtractionRadioButton_CheckedChanged( object sender, 94 EventArgs e ) 95 { 96 operation = "-"; 97 generateButton.Text = "Generate " + 98 subtractionRadioButton.Text + " Example"; 99 } // end method subtractionRadioButton_CheckedChanged 100 101 // set the operation to multiplication 102 private void multiplicationRadioButton_CheckedChanged( 103 object sender, EventArgs e ) 104 { 105 operation = "*"; 106 generateButton.Text = "Generate " + 107 multiplicationRadioButton.Text + " Example"; 108 } // end method multiplicationRadioButton_CheckedChanged 109 } // end class MathTutorForm 110 } // end namespace MathTutor a) Generating a level 1 addition equation.
Fig. 22.29 | Math-tutoring application. (Part 3 of 4.)
1020
Chapter 22
Web Services
b) Answering the equation incorrectly.
c) Answering the equation correctly.
d) Generating a level 2 multiplication equation.
Fig. 22.29 | Math-tutoring application. (Part 4 of 4.) The math-tutoring application displays an equation and waits for the user to enter an answer. The default setting for the difficulty level is 1, but the user can change this by choosing a level from the RadioButtons in the GroupBox labeled Difficulty. Clicking any of the levels invokes the corresponding RadioButton’s CheckedChanged event handler (lines 63–81), which sets integer level to the level selected by the user. Although the default setting for the question type is Addition, the user also can change this by selecting one of the RadioButtons in the GroupBox labeled Operation. Doing so invokes the corresponding operation’s event handlers in lines 84–108, which assigns to string operation the symbol corresponding to the user’s selection. Each event handler also updates the Text property of the Generate button to match the newly selected operation. Event handler generateButton_Click (lines 26–36) invokes EquationGenerator method GenerateEquation (line 29). After receiving an Equation object from the Web service, the handler displays the left-hand side of the equation in questionLabel (line 32) and enables OkButton so that the user can enter an answer. When the user clicks OK, OkButton_Click (lines 39–60) checks whether the user provided the correct answer.
22.8 Wrap-Up
1021
22.8 Wrap-Up This chapter introduced ASP.NET Web services—a technology that enables users to request and receive data via the Internet and promotes software reusability in distributed systems. You learned that a Web service is a class that allows client machines to call the Web service’s methods remotely via common data formats and protocols, such as XML, HTTP and SOAP. We discussed several benefits of this kind of distributed computing—e.g., clients can access certain data on remote machines, and clients lacking the processing power necessary to perform specific computations can leverage remote machines’ resources. We explained how Visual C# 2005, Visual Web Developer 2005 and the .NET Framework facilitate the creation and consumption of Web services. You learned how to define Web services and Web methods, as well as how to consume them from both Windows applications and ASP.NET Web applications. After explaining the mechanics of Web services through our HugeInteger example, we demonstrated more sophisticated Web services that use session tracking and user-defined types. In the next chapter, we discuss the low-level details of computer networking. We show how to implement servers and clients that communicate with one another, how to send and receive data via sockets (which make such transmissions as simple as writing to and reading from files, respectively), and how to create a multithreaded server for playing a networked version of the popular game Tic-Tac-Toe.
22.9 Web Resources In addition to the Web resources shown here, you should also refer to the ASP.NET-related Web resources provided at the end of Chapter 21. msdn.microsoft.com/webservices
The Microsoft Web Services Developer Center includes .NET Web services technology specifications and white papers as well as XML/SOAP articles, columns and links. www.webservices.org
This site provides industry-related news, articles, resources and links on Web services. www-130.ibm.com/developerworks/webservices
IBM’s site for service-oriented architecture (SOA) and Web services includes articles, downloads, demos and discussion forums regarding Web services technology. www.w3.org/TR/wsdl
This site provides extensive documentation on WSDL, including a thorough discussion of Web services–related technologies such as XML, SOAP, HTTP and MIME types in the context of WSDL. www.w3.org/TR/soap
This site provides extensive documentation on SOAP messages, using SOAP with HTTP and SOAP security issues. www.uddi.com
The Universal Description, Discovery and Integration site provides discussions, specifications, white papers and general information on UDDI. www.ws-i.org
The Web Services Interoperability Organization’s Web site provides detailed information regarding efforts to build Web services based on standards that promote interoperability and true platform independence. webservices.xml.com/security
This site contains articles about Web services security issues and standard security protocols.
23 Networking: Streams-Based Sockets and Datagrams OBJECTIVES In this chapter you will learn: I
To implement networking applications that use sockets and datagrams.
I
To implement clients and servers that communicate with one another.
I
To implement network-based collaborative applications.
I
To construct a multithreaded server.
I
To use the WebBrowser control to add Web browsing capabilities to any application.
I
To use .NET remoting to enable an application executing on one computer to invoke methods from an application executing on a different computer.
If the presence of electricity can be made visible in any part of a circuit, I see no reason why intelligence may not be transmitted instantaneously by electricity. —Samuel F. B. Morse
Protocol is everything. —Francois Giuliani
What networks of railroads, highways and canals were in another age, the networks of telecommunications, information and computerization … are today. —Bruno Kreisky
The port is near, the bells I hear, the people all exulting. —Walt Whitman
Introduction Connection-Oriented vs. Connectionless Communication Protocols for Transporting Data Establishing a Simple TCP Server (Using Stream Sockets) Establishing a Simple TCP Client (Using Stream Sockets) Client/Server Interaction with Stream-Socket Connections Connectionless Client/Server Interaction with Datagrams Client/Server Tic-Tac-Toe Using a Multithreaded Server WebBrowser Control .NET Remoting Wrap-Up
23.1 Introduction There is much excitement about the Internet and the Web. The Internet ties the information world together. The Web makes the Internet easy to use and gives it the flair and sizzle of multimedia. Organizations see the Internet and the Web as crucial to their informationsystems strategies. The .NET FCL provides a number of built-in networking capabilities that make it easy to develop Internet- and Web-based applications. Programs can search the world for information and collaborate with programs running on other computers internationally, nationally or just within an organization. In Chapter 21, ASP.NET 2.0, Web Forms and Web Controls, and Chapter 22, Web Services, we began our presentation of C#’s networking and distributed-computing capabilities. We discussed ASP.NET, Web Forms and Web services—high-level networking technologies that enable programmers to develop distributed applications. In this chapter, we focus on the underlying networking technologies that support C#’s ASP.NET and Web services capabilities. This chapter begins with an overview of the communication techniques and technologies used to transmit data over the Internet. Next, we present the basic concepts of establishing a connection between two applications using streams of data that are similar to File I/O. This connection-oriented approach enables programs to communicate with one another as easily as writing to and reading from files on disk. Then we present a simple chat application that uses these techniques to send messages between a client and a server. The chapter continues with a presentation and an example of connectionless techniques for transmitting data between applications that is less reliable than establishing a connection between applications, but much more efficient. Such techniques are typically used in applications such as streaming audio and video over the Internet. Next, we present an example of a client-server Tic-Tac-Toe game that demonstrates how to create a simple multithreaded server. Then this chapter demonstrates the new WebBrowser control for adding Web browsing capabilities to any application. The chapter completes with a brief introduction to .NET remoting which, like Web services (Chapter 22) enable distributed computing over networks.
1024
Chapter 23
Networking: Streams-Based Sockets and Datagrams
23.2 Connection-Oriented vs. Connectionless Communication There are two primary approaches to communicating between applications—connection oriented and connectionless. Connection-oriented communications are similar to the telephone system, in which a connection is established and held for the length of the session. Connectionless services are similar to the postal service, in which two letters mailed from the same place and to the same destination may actually take two dramatically different paths through the system and even arrive at different times, or not at all. In a connection-oriented approach, computers send each other control information—through a technique called handshaking—to initiate an end-to-end connection. The Internet is an unreliable network, which means that data sent across the Internet may be damaged or lost. Data is sent in packets, which contain pieces of the data along with information that helps the Internet route the packets to the proper destination. The Internet does not guarantee anything about the packets sent; they could arrive corrupted or out of order, as duplicates or not at all. The Internet makes only a “best effort” to deliver packets. A connection-oriented approach ensures reliable communications on unreliable networks, guaranteeing that sent packets will arrive at the intended receiver undamaged and be reassembled in the correct sequence. In a connectionless approach, the two computers do not handshake before transmission, and reliability is not guaranteed—data sent may never reach the intended recipient. A connectionless approach, however, avoids the overhead associated with handshaking and enforcing reliability—less information often needs to be passed between the hosts.
23.3 Protocols for Transporting Data There are many protocols for communicating between applications. Protocols are sets of rules that govern how two entities should interact. In this chapter, we focus on Transmission Control Protocol (TCP) and User Datagram Protocol (UDP). .NET’s TCP and UDP networking capabilities are defined in the System.Net.Sockets namespace. Transmission Control Protocol (TCP) is a connection-oriented communication protocol which guarantees that sent packets will arrive at the intended receiver undamaged and in the correct sequence. TCP allows protocols like HTTP (Chapter 21) to send information across a network as simply and reliably as writing to a file on a local computer. If packets of information don’t arrive at the recipient, TCP ensures that the packets are sent again. If the packets arrive out of order, TCP reassembles them in the correct order transparently to the receiving application. If duplicate packets arrive, TCP discards them. Applications that do not require TCP’s reliable end-to-end transmission guaranty typically use the connectionless User Datagram Protocol (UDP). UDP incurs the minimum overhead necessary to communicate between applications. UDP makes no guarantees that packets, called datagrams, will reach their destination or arrive in their original order. There are benefits to using UDP over TCP. UDP has little overhead because UDP datagrams do not need to carry the information that TCP packets carry to ensure reliability. UDP also reduces network traffic relative to TCP due to the absence of handshaking, retransmissions, etc. Unreliable communication is acceptable in many situations. First, reliability is not necessary for some applications, so the overhead imposed by a protocol that guarantees
23.4 Establishing a Simple TCP Server (Using Stream Sockets)
1025
reliability can be avoided. Second, some applications, such as streaming audio and video, can tolerate occasional datagram loss. This usually results in a small pause (or “hiccup”) in the audio or video being played. If the same application were run over TCP, a lost segment could cause a significant pause, since the protocol would wait until the lost segment was retransmitted and delivered correctly before continuing. Finally, applications that need to implement their own reliability mechanisms different from those provided by TCP can build such mechanisms over UDP.
23.4 Establishing a Simple TCP Server (Using Stream Sockets) Typically, with TCP, a server “waits” for a connection request from a client. Often, the server program contains a control statement or block of code that executes continuously until the server receives a request. On receiving a request, the server establishes a connection to the client. The server then uses this connection—managed by an object of class Socket—to handle future requests from that client and to send data to the client. Since programs that communicate via TCP process the data they send and receive as streams of bytes, programmers sometimes refer to Sockets as “stream Sockets.” Establishing a simple server with TCP and stream sockets requires five steps. First, create an object of class TcpListener of namespace System.Net.Sockets. This class represents a TCP stream socket through which a server can listen for requests. Creating a new TcpListener, as in TcpListener server = new TcpListener( ipAddress, port );
binds (assigns) the server application to the specified port number. A port number is a numeric identifier that an application uses to identify itself at a given network address, also known as an Internet Protocol Address (IP Address). IP addresses identify computers on the Internet. In fact, Web-site names, such as www.deitel.com, are aliases for IP addresses. An IP address is represented by an object of class IPAddress of namespace System.Net. Any application that performs networking identifies itself via an IP address/ port number pair—no two applications can have the same port number at a given IP address. Explicitly binding a socket to a connection port (using method Bind of class Socket) is usually unnecessary, because class TcpListener and other classes discussed in this chapter do it automatically, along with other socket-initialization operations.
Software Engineering Observation 23.1 Port numbers can have values between 0 and 65535. Many operating systems reserve port numbers below 1024 for system services (such as e-mail and Web servers). Applications must be granted special privileges to use these reserved port numbers. 23.1
To receive requests, TcpListener first must listen for them. The second step in the connection process is to call TcpListener’s Start method, which causes the TcpListener object to begin listening for connection requests. The server listens indefinitely for a request—i.e., the execution of the server-side application waits until some client attempts to connect with it. The server creates a connection to the client when it receives a connection request. An object of class Socket (namespace System.Net.Sockets) manages a con-
1026
Chapter 23
Networking: Streams-Based Sockets and Datagrams
nection to a client. Method AcceptSocket of class TcpListener accepts a connection request. This method returns a Socket object upon connection, as in the statement Socket connection = server.AcceptSocket();
When the server receives a request, AcceptSocket calls method Accept of the TcpListener’s underlying Socket to make the connection. This is an example of how networking complexity is hidden from the programmer. You simply place the preceding statement in a serverside program—the classes of namespace System.Net.Sockets handle the details of accepting requests and establishing connections. The third step establishes the streams used for communication with the client. In this step, we create a NetworkStream object that uses the Socket object representing the connection to perform the actual sending and receiving of data. In our forthcoming example, we use this NetworkStream object to create a BinaryWriter and a BinaryReader that will be used to send information to and receive information from the client, respectively. Step four is the processing phase, in which the server and client communicate using the connection established in the third step. In this phase, the client uses BinaryWriter method Write and BinaryReader method ReadString to perform the appropriate communications. The fifth step is the connection-termination phase. When the client and server have finished communicating, the server calls method Close of the BinaryReader, BinaryWriter, NetworkStream and Socket to terminate the connection. The server can then return to step two to wait for the next connection request. Note that the documentation for class Socket recommends that you call method Shutdown before method Close to ensure that all data is sent and received before the Socket closes. One problem associated with the server scheme described in this section is that step four blocks other requests while processing the connected client’s request, so no other client can connect with the server while the code that defines the processing phase is executing. The most common technique for addressing this problem is to use multithreaded servers, which place the processing-phase code in a separate thread. For each connection request the server receives, it creates a Thread to process the connection, leaving its TcpListener (or Socket) free to receive other connections. We demonstrate a multithreaded server in Section 23.8.
Software Engineering Observation 23.2 Multithreaded servers can efficiently manage simultaneous connections with multiple clients. This architecture is precisely what popular UNIX and Windows network servers use. 23.2
Software Engineering Observation 23.3 A multithreaded server can be implemented to create a thread that manages network I/O across a Socket object returned by method AcceptSocket. A multithreaded server also can be implemented to maintain a pool of threads that manage network I/O across newly created Sockets. 23.3
Performance Tip 23.1 In high-performance systems with abundant memory, a multithreaded server can be implemented to create a pool of threads. These threads can be assigned quickly to handle network I/O across multiple Sockets. Thus, when a connection is received, the server does not incur the overhead of thread creation. 23.1
23.5 Establishing a Simple TCP Client (Using Stream Sockets)
1027
23.5 Establishing a Simple TCP Client (Using Stream Sockets) There are four steps to creating a simple TCP client. First, we create an object of class TcpClient (namespace System.Net.Sockets) to connect to the server. The connection is established by calling TcpClient method Connect. One overloaded version of this method takes two arguments—the server’s IP address and its port number—as in: TcpClient client = new TcpClient(); client.Connect( serverAddress, serverPort );
The serverPort is an int that represents the port number to which the server application is bound to listen for connection requests. The serverAddress can be either an IPAddress instance (that encapsulates the server’s IP address) or a string that specifies the server’s hostname or IP address. Method Connect also has an overloaded version to which you can pass an IPEndPoint object that represents an IP address/port number pair. TcpClient method Connect calls Socket method Connect to establish the connection. If the connection is successful, TcpClient method Connect returns a positive integer; otherwise, it returns 0. In step two, the TcpClient uses its GetStream method to get a NetworkStream so that it can write to and read from the server. We then use the NetworkStream object to create a BinaryWriter and a BinaryReader that will be used to send information to and receive information from the server, respectively. The third step is the processing phase, in which the client and the server communicate. In this phase of our example, the client uses BinaryWriter method Write and BinaryReader method ReadString to perform the appropriate communications. Using a process similar to that used by servers, a client can employ threads to prevent blocking of communication with other servers while processing data from one connection. After the transmission is complete, step four requires the client to close the connection by calling method Close on each of BinaryReader, BinaryWriter, NetworkStream and TcpClient. This closes each of the streams and the TcpClient’s Socket to terminate the connection with the server. At this point, a new connection can be established through method Connect, as we have described.
23.6 Client/Server Interaction with Stream-Socket Connections The applications in Fig. 23.1 and Fig. 23.2 use the classes and techniques discussed in the previous two sections to construct a simple client/server chat application. The server waits for a client’s request to make a connection. When a client application connects to the server, the server application sends an array of bytes to the client, indicating that the connection was successful. The client then displays a message notifying the user that a connection has been established. Both the client and the server applications contain TextBoxes that enable users to type messages and send them to the other application. When either the client or the server sends the message “TERMINATE,” the connection between the client and the server terminates. The server then waits for another client to request a connection. Figure 23.1 and Fig. 23.2 provide the code for classes Server and Client, respectively. Figure 23.2 also contains screen captures displaying the execution between the client and the server.
1028
Chapter 23
Networking: Streams-Based Sockets and Datagrams
Class We begin by discussing class ChatServerForm (Fig. 23.1). In the constructor, line 27 creates a Thread that will accept connections from clients. The ThreadStart delegate object that is passed as the constructor’s argument specifies which method the Thread should execute. Line 28 starts the Thread, which uses the ThreadStart delegate to invoke method RunServer (lines 104–179). This method initializes the server to receive connection requests and process connections. Line 115 instantiates a TcpListener object to listen for a connection request from a client at port 50000 (Step 1). Line 118 then calls TcpListener method Start, which causes the TcpListener to begin waiting for requests (Step 2). ChatServerForm
// Fig. 23.1: ChatServer.cs // Set up a server that will receive a connection from a client, send a // string to the client, chat with the client and close the connection. using System; using System.Windows.Forms; using System.Threading; using System.Net; using System.Net.Sockets; using System.IO; public partial class ChatServerForm : Form { public ChatServerForm() { InitializeComponent(); } // end constructor private private private private private
Socket connection; // Socket for accepting a connection Thread readThread; // Thread for processing incoming messages NetworkStream socketStream; // network data stream BinaryWriter writer; // facilitates writing to the stream BinaryReader reader; // facilitates reading from the stream
// initialize thread for reading private void ChatServerForm_Load( object sender, EventArgs e ) { readThread = new Thread( new ThreadStart( RunServer ) ); readThread.Start(); } // end method CharServerForm_Load // close all threads associated with this application private void ChatServerForm_FormClosing( object sender, FormClosingEventArgs e ) { System.Environment.Exit( System.Environment.ExitCode ); } // end method CharServerForm_FormClosing // delegate that allows method DisplayMessage to be called // in the thread that creates and maintains the GUI private delegate void DisplayDelegate( string message );
Fig. 23.1 | Server portion of a client/server stream-socket connection. (Part 1 of 4.)
23.6 Client/Server Interaction with Stream-Socket Connections
// method DisplayMessage sets displayTextBox's Text property // in a thread-safe manner private void DisplayMessage( string message ) { // if modifying displayTextBox is not thread safe if ( displayTextBox.InvokeRequired ) { // use inherited method Invoke to execute DisplayMessage // via a delegate Invoke( new DisplayDelegate( DisplayMessage ), new object[] { message } ); } // end if else // OK to modify displayTextBox in current thread displayTextBox.Text += message; } // end method DisplayMessage // delegate that allows method DisableInput to be called // in the thread that creates and maintains the GUI private delegate void DisableInputDelegate( bool value ); // method DisableInput sets inputTextBox's ReadOnly property // in a thread-safe manner private void DisableInput( bool value ) { // if modifying inputTextBox is not thread safe if ( inputTextBox.InvokeRequired ) { // use inherited method Invoke to execute DisableInput // via a delegate Invoke( new DisableInputDelegate( DisableInput ), new object[] { value } ); } // end if else // OK to modify inputTextBox in current thread inputTextBox.ReadOnly = value; } // end method DisableInput // send the text typed at the server to the client private void inputTextBox_KeyDown( object sender, KeyEventArgs e ) { // send the text to the client try { if ( e.KeyCode == Keys.Enter && inputTextBox.ReadOnly == false ) { writer.Write( "SERVER>>> " + inputTextBox.Text ); displayTextBox.Text += "\r\nSERVER>>> " + inputTextBox.Text; // if the user at the server signaled termination // sever the connection to the client if ( inputTextBox.Text == "TERMINATE" ) connection.Close();
Fig. 23.1 | Server portion of a client/server stream-socket connection. (Part 2 of 4.)
inputTextBox.Clear(); // clear the user’s input } // end if } // end try catch ( SocketException ) { displayTextBox.Text += "\nError writing object"; } // end catch } // end method inputTextBox_KeyDown // allows a client to connect; displays text the client sends public void RunServer() { TcpListener listener; int counter = 1; // wait for a client connection and display the text // that the client sends try { // Step 1: create TcpListener IPAddress local = IPAddress.Parse( "127.0.0.1" ); listener = new TcpListener( local, 50000 ); // Step 2: TcpListener waits for connection request listener.Start(); // Step 3: establish connection upon client request while ( true ) { DisplayMessage( "Waiting for connection\r\n" ); // accept an incoming connection connection = listener.AcceptSocket(); // create NetworkStream object associated with socket socketStream = new NetworkStream( connection ); // create objects for transferring data across stream writer = new BinaryWriter( socketStream ); reader = new BinaryReader( socketStream ); DisplayMessage( "Connection " + counter + " received.\r\n" ); // inform client that connection was successfull writer.Write( "SERVER>>> Connection successful" ); DisableInput( false ); // enable inputTextBox string theReply = ""; // Step 4: read string data sent from client do {
Fig. 23.1 | Server portion of a client/server stream-socket connection. (Part 3 of 4.)
23.6 Client/Server Interaction with Stream-Socket Connections
1031
147 try 148 { 149 // read the string sent to the server 150 theReply = reader.ReadString(); 151 152 // display the message 153 DisplayMessage( "\r\n" + theReply ); 154 } // end try 155 catch ( Exception ) 156 { 157 // handle exception if error reading data 158 break; 159 } // end catch 160 } while ( theReply != "CLIENT>>> TERMINATE" && 161 connection.Connected ); 162 163 DisplayMessage( "\r\nUser terminated connection\r\n" ); 164 // Step 5: close connection 165 writer.Close(); 166 reader.Close(); 167 socketStream.Close(); 168 connection.Close(); 169 170 171 DisableInput( true ); // disable InputTextBox 172 counter++; 173 } // end while 174 } // end try 175 catch ( Exception error ) 176 { 177 MessageBox.Show( error.ToString() ); 178 } // end catch 179 } // end method RunServer 180 } // end class ChatServerForm
Fig. 23.1 | Server portion of a client/server stream-socket connection. (Part 4 of 4.) Accepting the Connection and Establishing the Streams Lines 121–173 declare an infinite loop that begins by establishing the connection requested by the client (Step 3). Line 126 calls method AcceptSocket of the TcpListener object, which returns a Socket upon successful connection. The thread in which method AcceptSocket is called blocks (i.e., stops executing) until a connection is established. The returned Socket object manages the connection. Line 129 passes this Socket object as an argument to the constructor of a NetworkStream object, which provides access to streams across a network. In this example, the NetworkStream object uses the streams of the specified Socket. Lines 132–133 create instances of the BinaryWriter and BinaryReader classes for writing and reading data. We pass the NetworkStream object as an argument to each constructor—BinaryWriter can write bytes to the NetworkStream, and BinaryReader can read bytes from NetworkStream. Line 135 calls DisplayMessage, indicating that a connection was received. Next, we send a message to the client indicating that the connection was received. BinaryWriter method Write has many overloaded versions that write
1032
Chapter 23
Networking: Streams-Based Sockets and Datagrams
data of various types to a stream. Line 138 uses method Write to send to the client a string notifying the user of a successful connection. This completes Step 3.
Receiving Messages from the Client Next, we begin the processing phase (Step 4). Lines 145–161 declare a do...while statement that executes until the server receives a message indicating connection termination (i.e., CLIENT>>> TERMINATE). Line 150 uses BinaryReader method ReadString to read a string from the stream. Method ReadString blocks until a string is read. This is the reason that we execute method RunServer in a separate Thread (created at lines 27–28, when the Form loads). This Thread ensures that our Server application’s user can continue to interact with the GUI to send messages to the client, even when this thread is blocked while awaiting a message from the client. Modifying GUI Controls from Separate Threads Windows Form controls are not thread safe—a control that is modified from multiple threads is not guaranteed to be modified correctly. The Visual Studio 2005 Documentation1 recommends that only the thread which created the GUI should modify the controls. Class Control provides method Invoke to help ensure this. Invoke takes two arguments—a delegate representing a method that will modify the GUI and an array of objects representing the parameters of the method. At some point after Invoke is called, the thread that originally created the GUI will (when it’s not executing any other code) execute the method represented by the delegate, passing the contents of the object array as the method’s arguments. Line 40 declares a delegate type named DisplayDelegate, which represents methods that take a string argument and do not return a value. Method DisplayMessage (lines 44–56) meets those requirements—it receives a string parameter named message and does not return a value. The if statement in line 47 tests displayTextBox’s InvokeRequired property (inherited from class Control), which returns true if the current thread is not allowed to modify this control directly and returns false otherwise. If the current thread executing method DisplayMessage is not the thread that created the GUI, then the if condition evaluates to true and lines 51–52 call method Invoke, passing to it a new DisplayDelegate representing the method DisplayMessage itself and a new object array consisting of the string argument message. This causes the thread that created the GUI to call method DisplayMessage again at a later time with the same string argument as the original call. When that call occurs from the thread that created the GUI, the method is allowed to modify displayTextBox directly, so the else body (line 55) executes and appends message to displayTextBox’s Text property. Lines 60–76 provide a delegate definition, DisableInputDelegate, and a method, DisableInput, to allow any thread to modify the ReadOnly property of inputTextBox using the same techniques. A thread calls DisableInput with a bool argument (true to disable; false to enable). If DisableInput is not allowed to modify the control from the current thread, DisableInput calls method Invoke. This causes the thread that created the GUI to call DisableInput at a later time and set inputTextBox.ReadOnly to the value of the bool argument. 1.
The MSDN article “How to: Make Cross-Thread Calls to Windows Forms Controls” can be found at msdn2.microsoft.com/library/ms171728(en-us,vs.80).aspx.
23.6 Client/Server Interaction with Stream-Socket Connections
1033
Terminating the Connection with the Client When the chat is complete, lines 166–169 close the BinaryWriter, BinaryReader, NetworkStream and Socket (Step 5) by invoking their respective Close methods. The server then waits for another client connection request by returning to the beginning of the while loop (line 121). Sending Messages to the Client When the server application’s user enters a string in the TextBox and presses the Enter key, event handler inputTextBox_KeyDown (lines 79–101) reads the string and sends it via method Write of class BinaryWriter. If a user terminates the server application, line 92 calls method Close of the Socket object to close the connection. Terminating the Server Application Lines 32–36 define event handler ChatServerForm_FormClosing for the FormClosing event. The event closes the application and calls method Exit of class Environment with parameter ExitCode to terminate all threads. Method Exit of class Environment closes all threads associated with the application. Class Figure 23.2 lists the code for the ChatClientForm class. Like the ChatServerForm object, the ChatClientForm object creates a Thread (lines 26–27) in its constructor to handle all incoming messages. ChatClientForm method RunClient (lines 96–151) connects to the ChatServerForm, receives data from the ChatServerForm and sends data to the ChatServerForm. Lines 106–107 instantiate a TcpClient object, then call its Connect method to establish a connection (Step 1). The first argument to method Connect is the name of the server—in our case, the server’s name is "localhost", meaning that the server is located on the same machine as the client. The localhost is also known as the loopback IP address and is equivalent to the IP address 127.0.0.1. This value sends the data transmission back to the sender’s IP address. [Note: We chose to demonstrate the client/server relationship by connecting between programs that are executing on the same computer (localhost). Normally, this argument would contain the Internet address of another computer.] The second argument to method Connect is the server port number. This number must match the port number at which the server waits for connections. ChatClientForm
1 2 3 4 5 6 7 8 9 10 11
// Fig. 23.2: ChatClient.cs // Set up a client that will send information to and // read information from a server. using System; using System.Windows.Forms; using System.Threading; using System.Net.Sockets; using System.IO; public partial class ChatClientForm : Form {
Fig. 23.2 | Client portion of a client/server stream-socket connection. (Part 1 of 5.)
public ChatClientForm() { InitializeComponent(); } // end constructor private private private private private
NetworkStream output; // stream for receiving data BinaryWriter writer; // facilitates writing to the stream BinaryReader reader; // facilitates reading from the stream Thread readThread; // Thread for processing incoming messages string message = "";
// initialize thread for reading private void ChatClientForm_Load( object sender, EventArgs e ) { readThread = new Thread( new ThreadStart( RunClient ) ); readThread.Start(); } // end method ChatClientForm_Load // close all threads associated with this application private void ChatClientForm_FormClosing( object sender, FormClosingEventArgs e ) { System.Environment.Exit( System.Environment.ExitCode ); } // end method ChatClientForm_FormClosing // delegate that allows method DisplayMessage to be called // in the thread that creates and maintains the GUI private delegate void DisplayDelegate( string message ); // method DisplayMessage sets displayTextBox's Text property // in a thread-safe manner private void DisplayMessage( string message ) { // if modifying displayTextBox is not thread safe if ( displayTextBox.InvokeRequired ) { // use inherited method Invoke to execute DisplayMessage // via a delegate Invoke( new DisplayDelegate( DisplayMessage ), new object[] { message } ); } // end if else // OK to modify displayTextBox in current thread displayTextBox.Text += message; } // end method DisplayMessage // delegate that allows method DisableInput to be called // in the thread that creates and maintains the GUI private delegate void DisableInputDelegate( bool value ); // method DisableInput sets inputTextBox's ReadOnly property // in a thread-safe manner private void DisableInput( bool value ) {
Fig. 23.2 | Client portion of a client/server stream-socket connection. (Part 2 of 5.)
23.6 Client/Server Interaction with Stream-Socket Connections
// if modifying inputTextBox is not thread safe if ( inputTextBox.InvokeRequired ) { // use inherited method Invoke to execute DisableInput // via a delegate Invoke( new DisableInputDelegate( DisableInput ), new object[] { value } ); } // end if else // OK to modify inputTextBox in current thread inputTextBox.ReadOnly = value; } // end method DisableInput // sends text the user typed to server private void inputTextBox_KeyDown( object sender, KeyEventArgs e ) { try { if ( e.KeyCode == Keys.Enter && inputTextBox.ReadOnly == false ) { writer.Write( "CLIENT>>> " + inputTextBox.Text ); displayTextBox.Text += "\r\nCLIENT>>> " + inputTextBox.Text; inputTextBox.Clear(); } // end if } // end try catch ( SocketException ) { displayTextBox.Text += "\nError writing object"; } // end catch } // end method inputTextBox_KeyDown // connect to server and display server-generated text public void RunClient() { TcpClient client; // instantiate TcpClient for sending data to server try { DisplayMessage( "Attempting connection\r\n" ); // Step 1: create TcpClient and connect to server client = new TcpClient(); client.Connect( "127.0.0.1", 50000 ); // Step 2: get NetworkStream associated with TcpClient output = client.GetStream(); // create objects for writing and reading across stream writer = new BinaryWriter( output ); reader = new BinaryReader( output ); DisplayMessage( "\r\nGot I/O streams\r\n" ); DisableInput( false ); // enable inputTextBox
Fig. 23.2 | Client portion of a client/server stream-socket connection. (Part 3 of 5.)
1036
Chapter 23
Networking: Streams-Based Sockets and Datagrams
118 119 // loop until server signals termination 120 do 121 { 122 // Step 3: processing phase 123 try 124 { // read message from server 125 message = reader.ReadString(); 126 DisplayMessage( "\r\n" + message ); 127 128 } // end try 129 catch ( Exception ) 130 { 131 // handle exception if error in reading server data 132 System.Environment.Exit( System.Environment.ExitCode ); 133 } // end catch 134 } while ( message != "SERVER>>> TERMINATE" ); 135 // Step 4: close connection 136 writer.Close(); 137 reader.Close(); 138 output.Close(); 139 client.Close(); 140 141 142 Application.Exit(); 143 } // end try 144 catch ( Exception error ) 145 { 146 // handle exception if error in establishing connection 147 MessageBox.Show( error.ToString(), "Connection Error", 148 MessageBoxButtons.OK, MessageBoxIcon.Error ); 149 System.Environment.Exit( System.Environment.ExitCode ); 150 } // end catch 151 } // end method RunClient 152 } // end class ChatClientForm
(a)
(b)
Fig. 23.2 | Client portion of a client/server stream-socket connection. (Part 4 of 5.)
23.6 Client/Server Interaction with Stream-Socket Connections
(c)
(d)
(e)
(f)
1037
(g)
Fig. 23.2 | Client portion of a client/server stream-socket connection. (Part 5 of 5.) The ChatClientForm uses a NetworkStream to send data to and receive data from the server. The client obtains the NetworkStream in line 110 through a call to TcpClient method GetStream (Step 2). The do...while statement in lines 120–134 loops until the client receives the connection-termination message (SERVER>>> TERMINATE). Line 126 uses BinaryReader method ReadString to obtain the next message from the server (Step 3).
1038
Chapter 23
Networking: Streams-Based Sockets and Datagrams
Line 127 displays the message, and lines 137–140 close the BinaryWriter, BinaryReader, NetworkStream and TcpClient objects (Step 4). Lines 39–75 declare DisplayDelegate, DisplayMessage, DisableInputDelegate and DisableInput just as in lines 40–76 of Fig. 23.1. These once again are used to ensure that the GUI is modified by only the thread that created the GUI controls. When the user of the client application enters a string in the TextBox and presses the Enter key, event handler inputTextBox_KeyDown (lines 78–93) reads the string from the TextBox and sends it via BinaryWriter method Write. Notice here that the ChatServerForm receives a connection, processes it, closes it and waits for the next one. In a real-world application, a server would likely receive a connection, set up the connection to be processed as a separate thread of execution and wait for new connections. The separate threads that process existing connections could then continue to execute while the server concentrates on new connection requests.
23.7 Connectionless Client/Server Interaction with Datagrams Up to this point, we have discussed connection-oriented, streams-based transmissions using the TCP protocol to ensure that the packets of data are transmitted reliably. Now, we consider connectionless transmission using datagrams and UDP. Connectionless transmission via datagrams resembles the method by which the postal service carries and delivers mail. Connectionless transmission bundles and sends information in packets called datagrams, which can be thought of as similar to letters you send through the mail. If a large message will not fit in one envelope, that message is broken into separate message pieces and placed in separate, sequentially numbered envelopes. All the letters are mailed at once. The letters might arrive in order, out of order or not at all. The person at the receiving end reassembles the message pieces in sequential order before attempting to interpret the message. If the message is small enough to fit in one envelope, the sequencing problem is eliminated, but it is still possible that the message will never arrive. (Unlike with postal mail, duplicate of datagrams could reach receiving computers.) C# provides the UdpClient class for connectionless transmission. Like TcpListener and TcpClient, UdpClient uses methods from class Socket. The UdpClient methods Send and Receive transmit data with Socket’s SendTo method and read data with Socket’s ReceiveFrom method, respectively. The programs in Fig. 23.3 and Fig. 23.4 use datagrams to send packets of information between client and server applications. In the PacketClient application, the user types a message into a TextBox and presses Enter. The client converts the message to a byte array and sends it to the server. The server receives the packet and displays the packet’s information, then echoes, or returns, the packet to the client. When the client receives the packet, the client displays the packet’s information. In this example, the implementations of the PacketClientForm and PacketServerForm classes are similar. PacketServerForm Class
The code in Fig. 23.3 defines the PacketServerForm for this application. Line 23 in the Load event handler for class PacketServerForm creates an instance of the UdpClient class that receives data at port 50000. This initializes the underlying Socket for communica-
23.7 Connectionless Client/Server Interaction with Datagrams
1039
tions. Line 24 creates an instance of class IPEndPoint to hold the IP address and port number of the client(s) that transmit to PacketServerForm. The first argument to the IPEndPoint constructor is an IPAddress object; the second argument is the port number of the endpoint. These values are both 0, because we need only instantiate an empty IPEndPoint object. The IP addresses and port numbers of clients are copied into the IPEndPoint when datagrams are received from clients. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
// Fig. 23.3: PacketServer.cs // Set up a server that will receive packets from a // client and send the packets back to the client. using System; using System.Windows.Forms; using System.Net; using System.Net.Sockets; using System.Threading; public partial class PacketServerForm : Form { public PacketServerForm() { InitializeComponent(); } // end constructor private UdpClient client; private IPEndPoint receivePoint; // initialize variables and thread for receiving packets private void PacketServerForm_Load( object sender, EventArgs e ) { client = new UdpClient( 50000 ); receivePoint = new IPEndPoint( new IPAddress( 0 ), 0 ); Thread readThread = new Thread( new ThreadStart( WaitForPackets ) ); readThread.Start(); } // end method PacketServerForm_Load // shut down the server private void PacketServerForm_FormClosing( object sender, FormClosingEventArgs e ) { System.Environment.Exit( System.Environment.ExitCode ); } // end method PacketServerForm_FormClosing // delegate that allows method DisplayMessage to be called // in the thread that creates and maintains the GUI private delegate void DisplayDelegate( string message ); // method DisplayMessage sets displayTextBox's Text property // in a thread-safe manner private void DisplayMessage( string message ) {
Fig. 23.3 | Server-side portion of connectionless client/server computing. (Part 1 of 2.)
// if modifying displayTextBox is not thread safe if ( displayTextBox.InvokeRequired ) { // use inherited method Invoke to execute DisplayMessage // via a delegate Invoke( new DisplayDelegate( DisplayMessage ), new object[] { message } ); } // end if else // OK to modify displayTextBox in current thread displayTextBox.Text += message; } // end method DisplayMessage // wait for a packet to arrive public void WaitForPackets() { while ( true ) { // set up packet byte[] data = client.Receive( ref receivePoint ); DisplayMessage( "\r\nPacket received:" + "\r\nLength: " + data.Length + "\r\nContaining: " + System.Text.Encoding.ASCII.GetString( data ) ); // echo information from packet back to client DisplayMessage( "\r\n\r\nEcho data back to client..." ); client.Send( data, data.Length, receivePoint ); DisplayMessage( "\r\nPacket sent\r\n" ); } // end while } // end method WaitForPackets } // end class PacketServerForm
Fig. 23.3 | Server-side portion of connectionless client/server computing. (Part 2 of 2.) Lines 39–55 define DisplayDelegate and DisplayMessage, allowing any thread to modify displayTextBox’s Text property. PacketServerForm method WaitForPackets (lines 58–74) executes an infinite loop while waiting for data to arrive at the PacketServerForm. When information arrives, UdpClient method Receive (line 63) receives a byte array from the client. We pass to
23.7 Connectionless Client/Server Interaction with Datagrams
1041
Receive the IPEndPoint object created in the constructor—this provides the method with an IPEndPoint to which the program copies the client’s IP address and port number. This program will compile and run without an exception even if the reference to the IPEndPoint object is null, because method Receive initializes the IPEndPoint if it is null. Lines 64–67 update the PacketServerForm’s display to include the packet’s information and content. Line 71 echoes the data back to the client, using UdpClient method Send. This version of Send takes three arguments—the byte array to send, an int representing the array’s length and the IPEndPoint to which to send the data. We use array data returned by method Receive as the data, the length of array data as the length and the IPEndPoint passed to method Receive as the data’s destination. The IP address and port number of the client that sent the data are stored in receivePoint, so merely passing receivePoint to Send allows PacketServerForm to respond to the client.
PacketClientForm Class
Class PacketClientForm (Fig. 23.4) works similarly to class PacketServerForm, except that the Client object sends packets only when the user types a message in a TextBox and presses the Enter key. When this occurs, the program calls event handler inputTextBox_KeyDown (lines 58–75). Line 68 converts the string that the user entered in the TextBox to a byte array. Line 71 calls UdpClient method Send to send the byte array to the PacketServerForm that is located on localhost (i.e., the same machine). We specify the port as 50000, which we know to be PacketServerForm’s port. Lines 39–55 define DisplayDelegate and DisplayMessage, allowing any thread to modify displayTextBox’s Text property. Line 24 instantiates a UdpClient object to receive packets at port 50001—we choose port 50001 because the PacketServerForm already occupies port 50000. Method WaitForPackets of class PacketClientForm (lines 78–90) uses an infinite loop to wait for these packets. UdpClient method Receive blocks until a packet of data is received (line 83). The blocking performed by method Receive does not prevent class PacketClientForm from performing other services (e.g., handling user input), because a separate thread runs method WaitForPackets. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
// Fig. 23.4: PacketClient.cs // Set up a client that sends packets to a server and receives // packets from a server. using System; using System.Windows.Forms; using System.Net; using System.Net.Sockets; using System.Threading; public partial class PacketClientForm : Form { public PacketClientForm() { InitializeComponent(); } // end constructor
Fig. 23.4 | Client portion of connectionless client/server computing. (Part 1 of 3.)
private UdpClient client; private IPEndPoint receivePoint; // initialize variables and thread for receiving packets private void PacketClientForm_Load( object sender, EventArgs e ) { receivePoint = new IPEndPoint( new IPAddress( 0 ), 0 ); client = new UdpClient( 50001 ); Thread thread = new Thread( new ThreadStart( WaitForPackets ) ); thread.Start(); } // end method PacketClientForm_Load // shut down the client private void PacketClientForm_FormClosing( object sender, FormClosingEventArgs e ) { System.Environment.Exit( System.Environment.ExitCode ); } // end method PacketClientForm_FormClosing // delegate that allows method DisplayMessage to be called // in the thread that creates and maintains the GUI private delegate void DisplayDelegate( string message ); // method DisplayMessage sets displayTextBox's Text property // in a thread-safe manner private void DisplayMessage( string message ) { // if modifying displayTextBox is not thread safe if ( displayTextBox.InvokeRequired ) { // use inherited method Invoke to execute DisplayMessage // via a delegate Invoke( new DisplayDelegate( DisplayMessage ), new object[] { message } ); } // end if else // OK to modify displayTextBox in current thread displayTextBox.Text += message; } // end method DisplayMessage // send a packet private void inputTextBox_KeyDown( object sender, KeyEventArgs e ) { if ( e.KeyCode == Keys.Enter ) { // create packet (datagram) as string string packet = inputTextBox.Text; displayTextBox.Text += "\r\nSending packet containing: " + packet; // convert packet to byte array byte[] data = System.Text.Encoding.ASCII.GetBytes( packet );
Fig. 23.4 | Client portion of connectionless client/server computing. (Part 2 of 3.)
23.8 Client/Server Tic-Tac-Toe Using a Multithreaded Server
// send packet to server on port 50000 client.Send( data, data.Length, "127.0.0.1", 50000 ); displayTextBox.Text += "\r\nPacket sent\r\n"; inputTextBox.Clear(); } // end if } // end method inputTextBox_KeyDown // wait for packets to arrive public void WaitForPackets() { while ( true ) { // receive byte array from server byte[] data = client.Receive( ref receivePoint ); // output packet data to TextBox DisplayMessage( "\r\nPacket received:" + "\r\nLength: " + data.Length + "\r\nContaining: " + System.Text.Encoding.ASCII.GetString( data ) + "\r\n" ); } // end while } // end method WaitForPackets } // end class PacketClientForm (a) Packet Client window before sending a packet to the server
(b) Packet Client window after sending a packet to the server and receiving it back
Fig. 23.4 | Client portion of connectionless client/server computing. (Part 3 of 3.) When a packet arrives, lines 86–88 display its contents in the TextBox. The user can type information in the PacketClientForm window’s TextBox and press the Enter key at any time, even while a packet is being received. The event handler for the TextBox processes the event and sends the data to the server.
23.8 Client/Server Tic-Tac-Toe Using a Multithreaded Server In this section, we present a networked version of the popular game Tic-Tac-Toe, implemented with stream sockets and client/server techniques. The program consists of a Tic-
1044
Chapter 23
Networking: Streams-Based Sockets and Datagrams
TacToeServer application (Fig. 23.5) and a TicTacToeClient application (Fig. 23.6). The TicTacToeServer allows two TicTacToeClient instances to connect to the server and
play Tic-Tac-Toe against each other. We depict the output in Fig. 23.6. When the server receives a client connection, lines 78–87 of Fig. 23.5 create instances of class Player to process each client in a separate thread of execution. This enables the server to handle requests from both clients. The server assigns value "X" to the first client that connects (player X makes the first move), then assigns value "O" to the second client. Throughout the game, the server maintains information regarding the status of the board so that the server can validate players’ requested moves. However, neither the server nor the client can establish whether a player has won the game—in this application, method GameOver (lines 139–143) always returns false. Each Client maintains its own GUI version of the TicTac-Toe board to display the game. The clients can place marks only in empty squares on the board. Class Square (Fig. 23.7) is used to define squares on the Tic-Tac-Toe board. TicTacToeServerForm Class TicTacToeServerForm (Fig. 23.5) uses its Load event handler (lines 27–37) to create a byte array to store the moves the players have made (line 29). The program creates an array of two references to Player objects (line 30) and an array of two references to Thread
objects (line 31). Each element in both arrays corresponds to a Tic-Tac-Toe player. Variable currentPlayer is set to 0 (line 32), which corresponds to player "X". In our program, player "X" makes the first move. Lines 35–36 create and start Thread getPlayers, which the TicTacToeServerForm uses to accept connections so that the current Thread does not block while awaiting players.
// Fig. 23.5: TicTacToeServer.cs // This class maintains a game of Tic-Tac-Toe for two // client applications. using System; using System.Windows.Forms; using System.Net; using System.Net.Sockets; using System.Threading; using System.IO; public partial class TicTacToeServerForm : Form { public TicTacToeServerForm() { InitializeComponent(); } // end constructor private private private private private
byte[] board; // the local representation of the game board Player[] players; // two Player objects Thread[] playerThreads; // Threads for client interaction TcpListener listener; // listen for client connection int currentPlayer; // keep track of whose turn it is
Fig. 23.5 | Server side of client/server Tic-Tac-Toe program. (Part 1 of 6.)
23.8 Client/Server Tic-Tac-Toe Using a Multithreaded Server
private Thread getPlayers; // Thread for acquiring client connections internal bool disconnected = false; // true if the server closes // initialize variables and thread for receiving clients private void TicTacToeServerForm_Load( object sender, EventArgs e ) { board = new byte[ 9 ]; players = new Player[ 2 ]; playerThreads = new Thread[ 2 ]; currentPlayer = 0; // accept connections on a different thread getPlayers = new Thread( new ThreadStart( SetUp ) ); getPlayers.Start(); } // end method TicTacToeServerForm_Load // notify Players to stop Running private void TicTacToeServerForm_FormClosing( object sender, FormClosingEventArgs e ) { disconnected = true; System.Environment.Exit( System.Environment.ExitCode ); } // end method TicTacToeServerForm_FormClosing // delegate that allows method DisplayMessage to be called // in the thread that creates and maintains the GUI private delegate void DisplayDelegate( string message ); // method DisplayMessage sets displayTextBox's Text property // in a thread-safe manner internal void DisplayMessage( string message ) { // if modifying displayTextBox is not thread safe if ( displayTextBox.InvokeRequired ) { // use inherited method Invoke to execute DisplayMessage // via a delegate Invoke( new DisplayDelegate( DisplayMessage ), new object[] { message } ); } // end if else // OK to modify displayTextBox in current thread displayTextBox.Text += message; } // end method DisplayMessage // accepts connections from 2 players public void SetUp() { DisplayMessage( "Waiting for players...\r\n" ); // set up Socket listener = new TcpListener( IPAddress.Parse( "127.0.0.1" ), 50000 ); listener.Start();
Fig. 23.5 | Server side of client/server Tic-Tac-Toe program. (Part 2 of 6.)
// accept first player and start a player thread players[ 0 ] = new Player( listener.AcceptSocket(), this, 0 ); playerThreads[ 0 ] = new Thread( new ThreadStart( players[ 0 ].Run ) ); playerThreads[ 0 ].Start(); // accept second player and start another player thread players[ 1 ] = new Player( listener.AcceptSocket(), this, 1 ); playerThreads[ 1 ] = new Thread( new ThreadStart( players[ 1 ].Run ) ); playerThreads[ 1 ].Start(); // let the first player know that the other player has connected lock ( players[ 0 ] ) { players[ 0 ].threadSuspended = false; Monitor.Pulse( players[ 0 ] ); } // end lock } // end method SetUp // determine if a move is valid public bool ValidMove( int location, int player ) { // prevent another thread from making a move lock ( this ) { // while it is not the current player's turn, wait while ( player != currentPlayer ) Monitor.Wait( this ); // if the desired square is not occupied if ( !IsOccupied( location ) ) { // set the board to contain the current player's mark board[ location ] = ( byte ) ( currentPlayer == 0 ? 'X' : 'O' ); // set the currentPlayer to be the other player currentPlayer = ( currentPlayer + 1 ) % 2; // notify the other player of the move players[ currentPlayer ].OtherPlayerMoved( location ); // alert the other player that it's time to move Monitor.Pulse( this ); return true; } // end if else return false; } // end lock } // end method ValidMove
Fig. 23.5 | Server side of client/server Tic-Tac-Toe program. (Part 3 of 6.)
23.8 Client/Server Tic-Tac-Toe Using a Multithreaded Server
// determines whether the specified square is occupied public bool IsOccupied( int location ) { if ( board[ location ] == 'X' || board[ location ] == 'O' ) return true; else return false; } // end method IsOccupied // determines if the game is over public bool GameOver() { // place code here to test for a winner of the game return false; } // end method GameOver } // end class TicTacToeServerForm // class Player represents a tic-tac-toe player public class Player { internal Socket connection; // Socket for accepting private NetworkStream socketStream; // network data private TicTacToeServerForm server; // reference to private BinaryWriter writer; // facilitates writing private BinaryReader reader; // facilitates reading private int number; // player number private char mark; // player’s mark on the board internal bool threadSuspended = true; // if waiting
a connection stream server to the stream from the stream
for other player
// constructor requiring Socket, TicTacToeServerForm and int // objects as arguments public Player( Socket socket, TicTacToeServerForm serverValue, int newNumber ) { mark = (newNumber == 0 ? 'X' : 'O'); connection = socket; server = serverValue; number = newNumber; // create NetworkStream object for Socket socketStream = new NetworkStream( connection ); // create Streams for reading/writing bytes writer = new BinaryWriter( socketStream ); reader = new BinaryReader( socketStream ); } // end constructor // signal other player of move public void OtherPlayerMoved( int location ) { // signal that opponent moved writer.Write( "Opponent moved." );
Fig. 23.5 | Server side of client/server Tic-Tac-Toe program. (Part 4 of 6.)
writer.Write( location ); // send location of move } // end method OtherPlayerMoved // allows the players to make moves and receive moves // from the other player public void Run() { bool done = false; // display on the server that a connection was made server.DisplayMessage( "Player " + ( number == 0 ? 'X' : 'O' ) + " connected\r\n" ); // send the current player's mark to the client writer.Write( mark ); // if number equals 0 then this player is X, // otherwise O must wait for X's first move writer.Write( "Player " + ( number == 0 ? "X connected.\r\n" : "O connected, please wait.\r\n" ) ); // X must wait for another player to arrive if ( mark == 'X' ) { writer.Write( "Waiting for another player." ); // wait for notification from server that another // player has connected lock ( this ) { while ( threadSuspended ) Monitor.Wait( this ); } // end lock writer.Write( "Other player connected. Your move." ); } // end if // play game while ( !done ) { // wait for data to become available while ( connection.Available == 0 ) { Thread.Sleep( 1000 ); if ( server.disconnected ) return; } // end while // receive data int location = reader.ReadInt32();
Fig. 23.5 | Server side of client/server Tic-Tac-Toe program. (Part 5 of 6.)
23.8 Client/Server Tic-Tac-Toe Using a Multithreaded Server
1049
233 // if the move is valid, display the move on the 234 // server and signal that the move is valid 235 if ( server.ValidMove( location, number ) ) 236 { 237 server.DisplayMessage( "loc: " + location + "\r\n" ); writer.Write( "Valid move." ); 238 239 } // end if 240 else // signal that the move is invalid writer.Write( "Invalid move, try again." ); 241 242 243 // if game is over, set done to true to exit while loop 244 if ( server.GameOver() ) 245 done = true; 246 } // end while loop 247 // close the socket connection 248 writer.Close(); 249 reader.Close(); 250 socketStream.Close(); 251 connection.Close(); 252 253 } // end method Run 254 } // end class Player
Fig. 23.5 | Server side of client/server Tic-Tac-Toe program. (Part 6 of 6.) Lines 49–65 define DisplayDelegate and DisplayMessage, allowing any thread to modify displayTextBox’s Text property. This time, the DisplayMessage method is declared as internal, so it can be called inside a method of class Player through a TicTacToeServerForm reference. Thread getPlayers executes method SetUp (lines 68–95), which creates a TcpListener object to listen for requests on port 50000 (lines 73–75). This object then listens for connection requests from the first and second players. Lines 78 and 84 instantiate Player objects representing the players, and lines 79–81 and 85–87 create two Threads that execute the Run methods of each Player object. The Player constructor (Fig. 23.5, lines 160–174) receives as arguments a reference to the Socket object (i.e., the connection to the client), a reference to the TicTacToeServerForm object and an int indicating the player number (from which the constructor infers the mark, "X" or "O" used by that player). In this case study, TicTacToeServerForm calls method Run (lines 186–253) after instantiating a Player object. Lines 191–200 notify the server of a successful connection and send to the client the char that the client will place on the board when making a move. If Run is executing for Player "X", lines 205–215 execute, causing Player "X" to wait for a second player to connect. Lines 211–212 define a while statement that suspends the Player "X" Thread until the server signals that Player "O" has connected. The server notifies the Player of the connection by setting the Player’s threadSuspended variable to false (line 92). When threadSuspended becomes false, Player exits the while statement at lines 211–212. Method Run executes the while statement at lines 219–24), enabling the user to play the game. Each iteration of this statement waits for the client to send an int specifying where on the board to place the "X" or "O"—the Player then places the mark on the
1050
Chapter 23
Networking: Streams-Based Sockets and Datagrams
board, if the specified mark location is valid (e.g., that location does not already contain a mark). Note that the while statement continues execution only if bool variable done is false. This variable is set to true by event handler TicTacToeServerForm_FormClosing of class TicTacToeServerForm, which is invoked when the server closes the connection. Line 222 of Fig. 23.5 begins a while statement that loops until Socket property Available indicates that there is information to receive from the Socket (or until the server disconnects from the client). If there is no information, the Thread goes to sleep for one second. On awakening, the Thread uses property Disconnected to check whether server variable disconnected is true (line 226). If the value is true, the Thread exits the method (thus terminating the Thread); otherwise, the Thread loops again. However, if property Available indicates that there is data to receive, the while statement of lines 222–228 terminates, enabling the information to be processed. This information contains an int representing the location in which the client wants to place a mark. Line 231 calls method ReadInt32 of the BinaryReader object (which reads from the NetworkStream created with the Socket) to read this int. Line 235 then passes the int to TicTacToeServerForm method ValidMove. If this method validates the move, the Player places the mark in the desired location. Method ValidMove (lines 98–127) sends to the client a message indicating whether the move was valid. Locations on the board correspond to numbers from 0–8 (0–2 for the top row, 3–5 for the middle and 6–8 for the bottom). All statements in method ValidMove are enclosed in a lock statement that allows only one move to be attempted at a time. This prevents two players from modifying the game’s state information simultaneously. If the Player attempting to validate a move is not the current player (i.e., the one allowed to make a move), that Player is placed in a Wait state until it is that Player’s turn to move. If the user attempts to place a mark on a location that already contains a mark, method ValidMove returns false. However, if the user has selected an unoccupied location (line 108), lines 111–112 place the mark on the local representation of the board. Line 118 notifies the other Player that a move has been made, and line 121 invokes the Pulse method so that the waiting Player can validate a move. The method then returns true to indicate that the move is valid. When a TicTacToeClientForm application (Fig. 23.6) executes, it creates a TextBox to display messages from the server and the Tic-Tac-Toe board representation. The board is created out of nine Square objects (Fig. 23.7) that contain Panels on which the user can click, indicating the position on the board in which to place a mark. The TicTacToeClientForm’s Load event handler (lines 30–58) opens a connection to the server (line 50) and obtains a reference to the connection’s associated NetworkStream object from TcpClient (line 51). Lines 56–57 start a thread to read messages sent from the server to the client. The server passes messages (for example, whether each move is valid) to method ProcessMessage (lines 179–215). If the message indicates that a move is valid (line 184), the client sets its Mark to the current square (the square that the user clicked) and repaints the board. If the message indicates that a move is invalid (line 190), the client notifies the user to click a different square. If the message indicates that the opponent made a move (line 197), line 200 reads an int from the server specifying where on the board the client should place the opponent’s Mark. TicTacToeClientForm includes a delegate/method pair for allowing threads to modify idLabel’s Text property (lines 97–113), as well as DisplayDelegate and DisplayMesage for modifying displayTextBox’s Text property (lines 77–93).
23.8 Client/Server Tic-Tac-Toe Using a Multithreaded Server
// Fig. 23.6: TicTacToeClient.cs // Client for the TicTacToe program. using System; using System.Drawing; using System.Windows.Forms; using System.Net.Sockets; using System.Threading; using System.IO; public partial class TicTacToeClientForm : Form { public TicTacToeClientForm() { InitializeComponent(); } // end constructor private private private private private private private private private private private
Square[ , ] board; // local representation of the game board Square currentSquare; // the Square that this player chose Thread outputThread; // Thread for receiving data from server TcpClient connection; // client to establish connection NetworkStream stream; // network data stream BinaryWriter writer; // facilitates writing to the stream BinaryReader reader; // facilitates reading from the stream char myMark; // player's mark on the board bool myTurn; // is it this player's turn? SolidBrush brush; // brush for drawing X's and O's bool done = false; // true when game is over
// initialize variables and thread for connecting to server private void TicTacToeClientForm_Load( object sender, EventArgs e ) { board = new Square[ 3, 3 ]; // create board[ 0, board[ 0, board[ 0, board[ 1, board[ 1, board[ 1, board[ 2, board[ 2, board[ 2,
9 0 1 2 0 1 2 0 1 2
Square objects and place them on the board ] = new Square( board0Panel, ' ', 0 ); ] = new Square( board1Panel, ' ', 1 ); ] = new Square( board2Panel, ' ', 2 ); ] = new Square( board3Panel, ' ', 3 ); ] = new Square( board4Panel, ' ', 4 ); ] = new Square( board5Panel, ' ', 5 ); ] = new Square( board6Panel, ' ', 6 ); ] = new Square( board7Panel, ' ', 7 ); ] = new Square( board8Panel, ' ', 8 );
// create a SolidBrush for writing on the Squares brush = new SolidBrush( Color.Black ); // make connection to server and get the associated // network stream connection = new TcpClient( "127.0.0.1", 50000 ); stream = connection.GetStream(); writer = new BinaryWriter( stream ); reader = new BinaryReader( stream );
Fig. 23.6 | Client side of client/server Tic-Tac-Toe program. (Part 1 of 7.)
// start a new thread for sending and receiving messages outputThread = new Thread( new ThreadStart( Run ) ); outputThread.Start(); } // end method TicTacToeClientForm_Load // repaint the Squares private void TicTacToeClientForm_Paint( object sender, PaintEventArgs e ) { PaintSquares(); } // end method TicTacToeClientForm_Load // game is over private void TicTacToeClientForm_FormClosing( object sender, FormClosingEventArgs e ) { done = true; System.Environment.Exit( System.Environment.ExitCode ); } // end TicTacToeClientForm_FormClosing // delegate that allows method DisplayMessage to be called // in the thread that creates and maintains the GUI private delegate void DisplayDelegate( string message ); // method DisplayMessage sets displayTextBox's Text property // in a thread-safe manner private void DisplayMessage( string message ) { // if modifying displayTextBox is not thread safe if ( displayTextBox.InvokeRequired ) { // use inherited method Invoke to execute DisplayMessage // via a delegate Invoke( new DisplayDelegate( DisplayMessage ), new object[] { message } ); } // end if else // OK to modify displayTextBox in current thread displayTextBox.Text += message; } // end method DisplayMessage // delegate that allows method ChangeIdLabel to be called // in the thread that creates and maintains the GUI private delegate void ChangeIdLabelDelegate( string message ); // method ChangeIdLabel sets displayTextBox's Text property // in a thread-safe manner private void ChangeIdLabel( string label ) { // if modifying idLabel is not thread safe if ( idLabel.InvokeRequired ) {
Fig. 23.6 | Client side of client/server Tic-Tac-Toe program. (Part 2 of 7.)
23.8 Client/Server Tic-Tac-Toe Using a Multithreaded Server
// use inherited method Invoke to execute ChangeIdLabel // via a delegate Invoke( new ChangeIdLabelDelegate( ChangeIdLabel ), new object[] { label } ); } // end if else // OK to modify idLabel in current thread idLabel.Text = label; } // end method ChangeIdLabel // draws the mark of each square public void PaintSquares() { Graphics g; // draw the appropriate mark on each panel for ( int row = 0; row < 3; row++ ) { for ( int column = 0; column < 3; column++ ) { // get the Graphics for each Panel g = board[ row, column ].SquarePanel.CreateGraphics(); // draw the appropriate letter on the panel g.DrawString( board[ row, column ].Mark.ToString(), board0Panel.Font, brush, 10, 8 ); } // end for } // end for } // end method PaintSquares // send location of the clicked square to server private void square_MouseUp( object sender, System.Windows.Forms.MouseEventArgs e ) { // for each square check if that square was clicked for ( int row = 0; row < 3; row++ ) { for ( int column = 0; column < 3; column++ ) { if ( board[ row, column ].SquarePanel == sender ) { CurrentSquare = board[ row, column ]; // send the move to the server SendClickedSquare( board[ row, column ].Location ); } // end if } // end for } // end for } // end method square_MouseUp // control thread that allows continuous update of the // TextBox display public void Run() {
Fig. 23.6 | Client side of client/server Tic-Tac-Toe program. (Part 3 of 7.)
// first get players's mark (X or O) myMark = reader.ReadChar(); ChangeIdLabel( "You are player \"" + myMark + "\"" ); myTurn = ( myMark == 'X' ? true : false ); // process incoming messages try { // receive messages sent to client while ( !done ) ProcessMessage( reader.ReadString() ); } // end try catch ( IOException ) { MessageBox.Show( "Server is down, game over", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error ); } // end catch } // end method Run // process messages sent to client public void ProcessMessage( string message ) { // if the move the player sent to the server is valid // update the display, set that square's mark to be // the mark of the current player and repaint the board if ( message == "Valid move." ) { DisplayMessage( "Valid move, please wait.\r\n" ); currentSquare.Mark = myMark; PaintSquares(); } // end if else if ( message == "Invalid move, try again." ) { // if the move is invalid, display that and it is now // this player's turn again DisplayMessage( message + "\r\n" ); myTurn = true; } // end else if else if ( message == "Opponent moved." ) { // if opponent moved, find location of their move int location = reader.ReadInt32(); // set that square to have the opponents mark and // repaint the board board[ location / 3, location % 3 ].Mark = ( myMark == 'X' ? 'O' : 'X' ); PaintSquares(); DisplayMessage( "Opponent moved.
Your turn.\r\n" );
Fig. 23.6 | Client side of client/server Tic-Tac-Toe program. (Part 4 of 7.)
23.8 Client/Server Tic-Tac-Toe Using a Multithreaded Server
210 // it is now this player's turn myTurn = true; 211 212 } // end else if 213 else DisplayMessage( message + "\r\n" ); // display message 214 215 } // end method ProcessMessage 216 217 // sends the server the number of the clicked square 218 public void SendClickedSquare( int location ) 219 { 220 // if it is the current player's move right now if ( myTurn ) 221 222 { // send the location of the move to the server 223 writer.Write( location ); 224 225 226 // it is now the other player's turn myTurn = false; 227 228 } // end if 229 } // end method SendClickedSquare 230 231 // write-only property for the current square 232 public Square CurrentSquare 233 { 234 set 235 { 236 currentSquare = value; 237 } // end set 238 } // end property CurrentSquare 239 } // end class TicTacToeClientForm
At the start of the game. (a)
(b)
Fig. 23.6 | Client side of client/server Tic-Tac-Toe program. (Part 5 of 7.)
1055
1056
Chapter 23
Networking: Streams-Based Sockets and Datagrams
After Player X makes the first move. (c)
(d)
After Player O makes the second move. (e)
(f)
After Player X makes the final move. (g)
(h)
Fig. 23.6 | Client side of client/server Tic-Tac-Toe program. (Part 6 of 7.)
23.8 Client/Server Tic-Tac-Toe Using a Multithreaded Server
The Tie Tac Toe Server’s output from the client interactions (i) server output after (a,b) server output after (c,d) server output after (e,f) server output after (g,h)
// Fig. 23.7: Square.cs // A Square on the TicTacToe board. using System.Windows.Forms; // the representation of a square in a tic-tac-toe grid public class Square { private Panel panel; // GUI Panel that represents this Square private char mark; // player’s mark on this Square (if any) private int location; // location on the board of this Square // constructor public Square( Panel newPanel, char newMark, int newLocation ) { panel = newPanel; mark = newMark; location = newLocation; } // end constructor // property SquarePanel; the panel which the square represents public Panel SquarePanel { get { return panel; } // end get } // end property SquarePanel // property Mark; the mark on the square public char Mark {
get { return mark; } // end get set { mark = value; } // end set } // end property Mark // property Location; the square's location on the board public int Location { get { return location; } // end get } // end property Location } // end class Square
Fig. 23.7 | Class Square. (Part 2 of 2.)
23.9 WebBrowser Control With FCL 2.0, Microsoft introduced the WebBrowser control, which enables applications to incorporate Web browsing capabilities. The control provides methods for navigating Web pages and maintains its own history of Web sites visited. It also generates events as the user interacts with the content displayed in the control, so your application can respond to events such as the user clicking the links displayed in the content. Figure 23.8 demonstrates the WebBrowser control’s capabilities. Class BrowserForm provides the basic functionality of a Web browser, allowing the user to navigate to a URL, to move backward and forward through the history of visited sites and to reload the current Web page. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
// Fig. 23.8: Browser.cs // WebBrowser control example. using System; using System.Windows.Forms; public partial class BrowserForm : Form { public BrowserForm() { InitializeComponent(); } // end constructor // navigate back one page private void backButton_Click( object sender, EventArgs e ) {
webBrowser.GoBack(); } // end method backButton_Click // navigate forward one page private void forwardButton_Click( object sender, EventArgs e ) { webBrowser.GoForward(); } // end method forwardButton_Click // stop loading the current page private void stopButton_Click( object sender, EventArgs e ) { webBrowser.Stop(); } // end method stopButton_Click // reload the current page private void reloadButton_Click( object sender, EventArgs e ) { webBrowser.Refresh(); } // end method reloadButton_Click // navigate to the user's home page private void homeButton_Click( object sender, EventArgs e ) { webBrowser.GoHome(); } // end method homeButton_Click // if the user pressed enter, navigate to the specified URL private void navigationTextBox_KeyDown( object sender, KeyEventArgs e ) { if ( e.KeyCode == Keys.Enter ) webBrowser.Navigate( navigationTextBox.Text ); } // end method navigationTextBox_KeyDown // enable stopButton while the current page is loading private void webBrowser_Navigating( object sender, WebBrowserNavigatingEventArgs e ) { stopButton.Enabled = true; } // end method webBrowser_Navigating // update the status text private void webBrowser_StatusTextChanged( object sender, EventArgs e ) { statusTextBox.Text = webBrowser.StatusText; } // end method webBrowser_StatusTextChanged // update the ProgressBar for how much of the page has been loaded private void webBrowser_ProgressChanged( object sender, WebBrowserProgressChangedEventArgs e ) {
pageProgressBar.Value = ( int ) ( ( 100 * e.CurrentProgress ) / e.MaximumProgress ); } // end method webBrowser_ProgressChanged // update the web browser's controls appropriately private void webBrowser_DocumentCompleted( object sender, WebBrowserDocumentCompletedEventArgs e ) { // set the text in navigationTextBox to the current page's URL navigationTextBox.Text = webBrowser.Url.ToString(); // enable or disable backButton and forwardButton backButton.Enabled = webBrowser.CanGoBack; forwardButton.Enabled = webBrowser.CanGoForward; // disable stopButton stopButton.Enabled = false; // clear the pageProgressBar pageProgressBar.Value = 0; } // end method webBrowser_DocumentCompleted // update the title of the Browser private void webBrowser_DocumentTitleChanged( object sender, EventArgs e ) { this.Text = webBrowser.DocumentTitle + " - Browser"; } // end method webBrowser_DocumentTitleChanged } // end class BrowserForm
Fig. 23.8 |
WebBrowser
control example. (Part 3 of 3.)
Lines 14–41 define five Click event handlers, one for each of the five navigation Buttons that appear at the top of the Form. Each event handler calls a corresponding WebBrowser method. WebBrowser method GoBack (line 16) causes the control to navigate back to the previous page in the navigation history. Method GoForward (line 22) causes the control to navigate forward to the next page in the navigation history. Method Stop (line 28)
23.10 .NET Remoting
1061
causes the control to stop loading the current page. Method Refresh (line 34) causes the control to reload the current page. Method GoHome (line 40) causes the control to navigate to the user’s home page, as defined under Internet Explorer’s settings (under Tools > Internet Options… in the Home page section). The TextBox to the right of the navigation buttons allows the user to enter the URL of a Web site to browse. When the user types each keystroke in the TextBox, the event handler at lines 44–49 executes. If the key pressed was Enter, line 48 calls WebBrowser method Navigate to retrieve the document at the specified URL. The WebBrowser control generates a Navigating event when the WebBrowser starts loading a new page. When this occurs, the event handler at lines 52–56 executes, and line 55 enables stopButton so that the user can cancel the loading of the Web page. Typically, a user can see the status of a loading Web page at the bottom of the browser window. For this reason, we include a TextBox control (named statusTextBox) and a ProgressBar control (named pageProgressBar) at the bottom of our Form. The WebBrowser control generates a StatusTextChanged event when the WebBrowser’s StatusText property changes. The event handler for this event (lines 59–63) assigns the new contents of the control’s StatusText property to statusTextBox’s Text property (line 62), so the user can monitor the WebBrowser’s status messages. The control generates a ProgressChanged event when the WebBrowser control’s page-loading progress is updated. The ProgressChanged event handler (lines 66–71) updates pageProgressBar’s Value (lines 69–70) to reflect how much of the current document has been loaded. When the WebBrowser finishes loading a document, the control generates a DocumentCompleted event. This executes the event handler at lines 74–89. Line 78 updates the contents of navigationTextBox so that it shows the URL of the currently loaded page (WebBrowser property Url). This is particularly important if the user browses to another Web page by clicking a link in the existing page. Lines 81–82 use properties CanGoBack and CanGoForward to determine whether the back and forward buttons should be enabled or disabled. Since the document is now loaded, line 85 disables stopButton. Line 88 sets pageProgressBar’s Value to 0 to indicate that no content is currently being loaded. Lines 92–96 define an event handler for the DocumentTitleChanged event, which occurs when a new document is loaded in the WebBrowser control. Line 95 sets BrowserForm’s Text property (which is displayed in the Form’s title bar) to the WebBrowser’s current DocumentTitle.
23.10 .NET Remoting The .NET framework provides a distributed computing technology called .NET remoting that allows a program to access objects on another machine over a network. .NET remoting is similar in concept to RMI (remote method invocation) in Java and RPC (remote procedure call) in procedural programming languages. .NET remoting is also similar to Web services (Chapter 22) with a few key differences. With Web services, a client application communicates with a Web service that is hosted by a Web server. The client and the Web service can be written in any language, as long as they can transmit messages in SOAP. With .NET remoting, a client application communicates with a server application, both of which must be written in .NET languages. Using .NET remoting, a client and a server can communicate via method calls and objects can be transmitted between applications—a process known as marshaling the objects.
1062
Chapter 23
Networking: Streams-Based Sockets and Datagrams
Channels The client and the server are able to communicate with one another through channels. Channels typically use either the HTTP protocol or the TCP protocol to transmit messages. The advantage of an HTTP channel is that firewalls usually permit HTTP connections by default, while they normally block unfamiliar TCP connections. The advantage of a TCP channel is better performance than an HTTP channel. In a .NET remoting application, the client and the server each create a channel, and both channels must use the same protocol to communicate with one another. In our example, we use HTTP channels. Marshaling There are two ways to marshal an object—by value and by reference. Marshal by value requires that the object be serializable—that is, capable of being represented as a formatted message that can be sent between applications through a channel. The receiving end of the channel deserializes the object to obtain a copy of the original object. To enable an object to be serialized and deserialized, its class must either be declared with attribute [ Serializable ] or must implement interface ISerializable. Marshal by reference requires that the object’s class extend class MarshalByRefObject of namespace System. An object that is marshaled by reference is referred to as a remote object, and its class is referred to as a remote class. When an object is marshaled by reference, the object itself is not transmitted. Instead, two proxy objects are created—a transparent proxy and a real proxy. The transparent proxy provides all the public services of a remote object. Typically, a client calls the methods and properties of the transparent proxy as if it were the remote object. The transparent proxy then calls the Invoke method of the real proxy. This sends the appropriate message from the client channel to the server channel. The server receives this message and performs the specified method call or accesses the specified property on the actual object, which resides on the server. In our example, we marshal a remote object by reference. Weather Information Application Using .NET Remoting We now present a .NET remoting example that downloads the Traveler’s Forecast weather information from the National Weather Service Web site: http://iwin.nws.noaa.gov/iwin/us/traveler.html
[Note: As we developed this example, the National Weather Service indicated that the information provided on the Traveler’s Forecast Web page would be provided by a Web service in the near future. The information we use in this example depends directly on the format of the Traveler’s Forecast Web page. If you have trouble running this example, please refer to the FAQ page at www.deitel.com/faq.html. This potential problem demonstrates a benefit of using Web services or .NET remoting to implement distributed computing applications that may change in the future. Separating the server part of the application that depends on the format of an outside data source from the client part of the application allows the server implementation to be updated without requiring any changes to the client.] Our .NET remoting application consists of five components: 1. Serializable class CityWeather, which represents the weather report for one city.
23.10 .NET Remoting
1063
2. Interface Report, which declares a property Reports that the marshaled object provides to a client application to obtain a collection of CityWeather objects. 3. Remote class ReportInfo, which extends class MarshalByRefObject, implements interface Report and will be instantiated only on the server. 4. A
WeatherServer application that sets up a server channel and makes the ReportInfo class available at a particular URI (uniform resource identifier).
5. A WeatherClient application that sets up a client channel and requests a ReportInfo object from the WeatherServer to retrieve the day’s weather report.
Class CityWeather Class CityWeather (Fig. 23.9) contains weather information for one city. Class CityWeather is declared in namespace Weather (line 5) for reusability and will be published in the Weather.dll class library file that both the server application and the client application must reference. For this reason, you should place this class (and interface Report from Fig. 23.10) in a class library project. Class CityWeather is declared with attribute Serializable (line 7), which indicates that an object of class CityWeather can be marshaled by value. This is necessary because CityWeather objects will be returned by the ReportInfo object’s Reports property, and the return values of the methods and properties declared by a remote class must themselves be marshaled by value from the server to the client. (The argument values in method calls will also be marshaled by value from the client to the server.) So when the client calls a get accessor that returns CityWeather objects, the server channel will serialize the CityWeather objects in a message that the client channel can deserialize to create copies of the original CityWeather objects. Class CityWeather also implements interface IComparable (line 8) so that an ArrayList of CityWeather objects can be sorted alphabetically. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
// Fig. 23.9: CityWeather.cs // Class representing the weather information for one city. using System; namespace Weather { [ Serializable ] public class CityWeather : IComparable { private string cityName; private string description; private string temperature; public CityWeather( string city, string information, string degrees ) { cityName = city; description = information; temperature = degrees; } // end constructor
// read-only property that gets city's name public string CityName { get { return cityName; } // end get } // end property CityName // read-only property that gets city's weather description public string Description { get { return description; } // end get } // end property Description // read-only property that gets city's temperature public string Temperature { get { return temperature; } // end get } // end property Temperature // implementation of CompareTo method for alphabetizing public int CompareTo( object other ) { return string.Compare( CityName, ( ( CityWeather ) other ).CityName ); } // end method Compare // return string representation of this CityWeather object // (used to display the weather report on the server console) public override string ToString() { return cityName + " | " + temperature + " | " + description; } // end method ToString } // end class CityWeather } // end namespace Weather
Fig. 23.9 | Class CityWeather. (Part 2 of 2.) CityWeather contains three instance variables (lines 10–12) for storing the city’s name, high/low temperatures and weather condition description. The CityWeather constructor (lines 14–20) initializes the three instance variables. Lines 23–47 declare three read-only properties that allow the values of the three instance variables to be retrieved. CityWeather implements IComparable, so it must declare a method called CompareTo that takes an object reference and returns an int (lines 50–54). Also, we want to alphabetize CityWeather objects by their cityNames, so CompareTo calls string method Compare with the cityNames of the two CityWeather objects. Class CityWeather also overrides the
23.10 .NET Remoting
1065
method to display information for this city (lines 58–61). Method ToString is used by the server application to display the weather information retrieved from the Traveler’s Forecast Web page in the console. ToString
Interface Report Figure 23.10 shows the code for interface Report. Interface Report is also declared in namespace Weather (line 7) and will be included with class CityWeather in the Weather.dll class library file, so it can be used in both the client and server applications. Report declares a read-only property (lines 11–14) with a get accessor that returns an ArrayList of CityWeather objects. The client application will use this property to retrieve the information in the weather report—each city’s name, high/low temperature and weather condition. Class ReportInfo Remote class ReportInfo (Fig. 23.11) implements interface Report (line 10) of namespace Weather (specified by the using directive in line 8). ReportInfo also extends base class MarshalByRefObject. Class ReportInfo is part of the remote WeatherServer application and will not be directly available to the client application. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
// Fig. 23.10: Report.cs // Interface that defines a property for getting // the information in a weather report. using System; using System.Collections; namespace Weather { public interface Report { ArrayList Reports { get; } // end property Reports } // end interface Report } // end namespace Weather
// Fig. 23.11: ReportInfo.cs // Class that implements interface Report, retrieves // and returns data on weather using System; using System.Collections; using System.IO; using System.Net; using Weather;
Fig. 23.11 | Class ReportInfo, which implements interface Report, is marshaled by reference. (Part 1 of 3.)
public class ReportInfo : MarshalByRefObject, Report { private ArrayList cityList; // cities, temperatures, descriptions public ReportInfo() { cityList = new ArrayList(); // create WebClient to get access to Web page WebClient myClient = new WebClient(); // get StreamReader for response so we can read page StreamReader input = new StreamReader( myClient.OpenRead( "http://iwin.nws.noaa.gov/iwin/us/traveler.html" ) ); string separator1 = "TAV12"; // indicates first batch of cities string separator2 = "TAV13"; // indicates second batch of cities // locate separator1 in Web page while ( !input.ReadLine().StartsWith( separator1 ) ); // do nothing ReadCities( input ); // read the first batch of cities // locate separator2 in Web page while ( !input.ReadLine().StartsWith( separator2 ) ); // do nothing ReadCities( input ); // read the second batch of cities cityList.Sort(); // sort list of cities by alphabetical order input.Close(); // close StreamReader to NWS server // display the data on the server side Console.WriteLine( "Data from NWS Web site:" ); foreach ( CityWeather city in cityList ) { Console.WriteLine( city ); } // end foreach } // end constructor // utility method that reads a batch of cities private void ReadCities( StreamReader input ) { // day format and night format string dayFormat = "CITY WEA HI/LO WEA HI/LO"; string nightFormat = "CITY WEA LO/HI WEA LO/HI"; string inputLine = ""; // locate header that begins weather information do {
Fig. 23.11 | Class ReportInfo, which implements interface Report, is marshaled by reference. (Part 2 of 3.)
inputLine = input.ReadLine(); } while ( !inputLine.Equals( dayFormat ) && !inputLine.Equals( nightFormat ) ); inputLine = input.ReadLine(); // get first city's data // while there are more cities to read while ( inputLine.Length > 28 ) { // create CityWeather object for city CityWeather weather = new CityWeather( inputLine.Substring( 0, 16 ), inputLine.Substring( 16, 7 ), inputLine.Substring( 23, 7 ) ); cityList.Add( weather ); // add to ArrayList inputLine = input.ReadLine(); // get next city's data } // end while } // end method ReadCities // property for getting the cities’ weather reports public ArrayList Reports { get { return cityList; } // end get } // end property Reports } // end class ReportInfo
Fig. 23.11 | Class ReportInfo, which implements interface Report, is marshaled by reference. (Part 3 of 3.)
Lines 14–46 declare the ReportInfo constructor. Line 19 creates a WebClient (namespace System.Net) object to interact with a data source that is specified by a URL— in this case, the URL for the NWS Traveler’s Forecast page (http://iwin.nws.noaa.gov/ iwin/us/traveler.html). Lines 22–23 call WebClient method OpenRead, which returns a Stream that the program can use to read data containing the weather information from the specified URL. This Stream is used to create a StreamReader object, so the program can read the Web page’s HTML markup line-by-line. The section of the Web page in which we are interested consists of two batchs of cities—Albany through Reno, and Salt Lake City through Washington, D.C. The first batch occurs in a section that starts with the string "TAV12", while the second batch occurs in a section that starts with the string "TAV13". We declare variables separator1 and separator2 to store these strings. Line 29 reads the HTML markup one line at a time until "TAV12" is encountered. Once "TAV12" is reached, the program calls utility method ReadCities to read a batch of cities into ArrayList cityList. Next, line 33 reads the HTML markup one line at a time until "TAV13" is encountered, and line 34 makes another call to method ReadCities to read the second batch of cities. Line 36 calls method Sort of class ArrayList to sort the CityWeather objects in alphabetical order by city name. Line
1068
Chapter 23
Networking: Streams-Based Sockets and Datagrams
37 closes the StreamReader connection to the Web site. Lines 42–45 output the weather information for each city to the server application’s console display. Lines 49–79 declare utility method ReadCities, which takes a StreamReader object and reads the information for each city, creates a CityWeather object for it and places the CityWeather object in cityList. The do...while statement (lines 59–63) continues to read the page one line at a time until it finds the header line that begins the weather forecast table. This line starts with either dayFormat (lines 52–53), indicating the header for the daytime information, or nightFormat (lines 54–55), indicating the header for the nighttime information. Because the line could be in either format based on the time of day, the loop-continuation condition checks for both. Line 65 reads the next line from the Web page, which is the first line containing temperature information. The while statement (lines 68–78) creates a new CityWeather object to represent the current city. It parses the string containing the current weather data, separating the city name, the weather condition and the temperature. The CityWeather object is added to cityList. Then the next line from the page is read and stored in inputLine for the next iteration. This process continues while the length of the string read from the Web page is greater than 28 (the lines containing weather data are all longer than 28 characters). The first line shorter than this signals the end of that forecast section in the Web page. Read-only property Reports (lines 82–88) implements the Report interface’s Reports property to return cityList. The client application will remotely call this property to retrieve the day’s weather report.
Class WeatherServer Figure 23.12 contains the code for the server application. The using directives in lines 5– 7 specify .NET remoting namespaces System.Runtime.Remoting, System.Runtime.Remoting.Channels and System.Runtime.Remoting.Channels.Http. The first two namespaces are required for .NET remoting, and the third is required for HTTP channels. Namespace System.Runtime.Remoting.Channels.Http requires the project to reference the System.Runtime.Remoting assembly, which can be found under the .NET tab in the Add References menu. The using directive at line 8 specifies namespace Weather, which contains interface Report. Remember to add a reference to Weather.dll in this project. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
// Fig. 23.12: WeatherServer.cs // Server application that uses .NET remoting to send // weather report information to a client using System; using System.Runtime.Remoting; using System.Runtime.Remoting.Channels; using System.Runtime.Remoting.Channels.Http; using Weather; class WeatherServer { static void Main( string[] args ) { // establish HTTP channel HttpChannel channel = new HttpChannel( 50000 );
Fig. 23.12 | Class WeatherServer exposes remote class ReportInfo. (Part 1 of 2.)
23.10 .NET Remoting
16 17 18 19 20 21 22 23 24 25 26
1069
ChannelServices.RegisterChannel( channel, false ); // register ReportInfo class RemotingConfiguration.RegisterWellKnownServiceType( typeof( ReportInfo ), "Report", WellKnownObjectMode.Singleton ); Console.WriteLine( "Press Enter to terminate server." ); Console.ReadLine(); } // end Main } // end class WeatherServer
Fig. 23.12 | Class WeatherServer exposes remote class ReportInfo. (Part 2 of 2.) Lines 15–16 in Main register an HTTP channel on the current machine at port 50000. This is the port number that clients will use to connect to the WeatherServer remotely. The argument false at line 16 indicates that we do not wish to enable security, which is beyond the scope of this introduction. Lines 19–21 register the ReportInfo class type at the “Report” URI as a Singleton remote class. If a remote class is registered as Singleton, one remote object will be created when the first client requests that remote class, and that remote object will service all clients. The alternative mode is SingleCall, where one remote object is created for each individual remote method call to the remote class. [Note: A Singleton remote object does not have an infinite lifetime; it will be garbage collected after being idle for 5 minutes. A new Singleton remote object will be created if another client requests one later.] The ReportInfo remote class is now available to clients at the URI “http://IPAddress:50000/Report” where IPAddress is the IP address of the computer on which the server is running. The channel remains open as long as the server application continues running, so line 24 waits for the user running the server application to press Enter before terminating the application.
Class WeatherClientForm WeatherClientForm (Fig. 23.13) is a Windows application that uses .NET remoting to retrieve weather information from WeatherServer and displays the information in a graphical, easy-to-read manner. The GUI contains 43 Labels—one for each city in the Traveler’s Forecast. All the Labels are placed in a Panel with a vertical scrollbar. Each Label displays 1 2 3 4 5 6 7 8 9 10
// Fig. 23.13: WeatherClient.cs // Client that uses .NET remoting to retrieve a weather report. using System; using System.Collections; using System.Drawing; using System.Windows.Forms; using System.Runtime.Remoting; using System.Runtime.Remoting.Channels; using System.Runtime.Remoting.Channels.Http; using Weather;
Fig. 23.13 | The Weather Client accesses a ReportInfo object remotely and displays the weather report. (Part 1 of 3.)
public partial class WeatherClientForm : Form { public WeatherClientForm() { InitializeComponent(); } // end constructor // retrieve weather data private void WeatherClientForm_Load( object sender, EventArgs e ) { // setup HTTP channel, does not need to provide a port number HttpChannel channel = new HttpChannel(); ChannelServices.RegisterChannel( channel, false ); // obtain a proxy for an object that implements interface Report Report info = ( Report ) RemotingServices.Connect( typeof( Report ), "http://localhost:50000/Report" ); // retrieve an ArrayList of CityWeather objects ArrayList cities = info.Reports; // create array and populate it with every Label Label[] cityLabels = new Label[ 43 ]; int labelCounter = 0; foreach ( Control control in displayPanel.Controls ) { if ( control is Label ) { cityLabels[ labelCounter ] = ( Label ) control; ++labelCounter; // increment Label counter } // end if } // end foreach // create Hashtable and populate with all weather conditions Hashtable weather = new Hashtable(); weather.Add( "SUNNY", "sunny" ); weather.Add( "PTCLDY", "pcloudy" ); weather.Add( "CLOUDY", "mcloudy" ); weather.Add( "MOCLDY", "mcloudy" ); weather.Add( "TSTRMS", "rain" ); weather.Add( "RAIN", "rain" ); weather.Add( "SNOW", "snow" ); weather.Add( "VRYHOT", "vryhot" ); weather.Add( "FAIR", "fair" ); weather.Add( "RNSNOW", "rnsnow" ); weather.Add( "SHWRS", "showers" ); weather.Add( "WINDY", "windy" ); weather.Add( "NOINFO", "noinfo" ); weather.Add( "MISG", "noinfo" );
Fig. 23.13 | The Weather Client accesses a ReportInfo object remotely and displays the weather report. (Part 2 of 3.)
// create the font for the text output Font font = new Font( "Courier New", 8, FontStyle.Bold ); // for every city for ( int i = 0; i < cities.Count; i++ ) { // use array cityLabels to find the next Label Label currentCity = cityLabels[ i ]; // use ArrayList cities to find the next CityWeather object CityWeather city = ( CityWeather ) cities[ i ]; // set current Label's image to image // corresponding to the city's weather condition // find correct image name in Hashtable weather currentCity.Image = new Bitmap( @"images\" + weather[ city.Description.Trim() ] + ".png" ); currentCity.Font = font; // set font of Label currentCity.ForeColor = Color.White; // set text color of Label // set Label's text to city name and temperature currentCity.Text = "\r\n " + city.CityName + city.Temperature; } // end for } // end method WeatherClientForm_Load } // end class WeatherClientForm
Fig. 23.13 | The Weather Client accesses a ReportInfo object remotely and displays the weather report. (Part 3 of 3.)
1072
Chapter 23
Networking: Streams-Based Sockets and Datagrams
the weather information for one city. Lines 7–9 are using directives for the namespaces that are required to perform .NET remoting. For this project, you must add references to the System.Runtime.Remoting assembly and the Weather.dll file we created earlier. Method WeatherClientForm_Load (lines 20–92) retrieves the weather information when this Windows application loads. Line 23 creates an HTTP channel without specifying a port number. This causes the HttpChannel constructor to choose any available port number on the client computer. A specific port number is not necessary because this application does not have its own clients that need to know the port number in advance. Line 24 registers the channel on the client computer. This will allow the server to send information back to the client. Lines 27–28 declare a Report variable and assign to it a proxy for a Report object instantiated by the server. This proxy allows the client to remotely call ReportInfo’s properties by redirecting method calls to the server. RemotingServices method Connect connects to the server and returns a reference to the proxy for the Report object. Note that we are executing the client and the server on the same computer, so we use localhost in the URL that represents the server application. To connect to a WeatherServer on a different computer, you must modify this URL accordingly. Line 31 retrieves the ArrayList of CityWeather objects generated by the ReportInfo constructor (lines 14–46 of Fig. 23.11). Variable cities now refers to an ArrayList of CityWeather objects that contains the information taken from the Traveler’s Forecast Web page. Because the application presents weather data for so many cities, we must establish a way to organize the information in the Labels and to ensure that each weather description is accompanied by an appropriate image. The program uses an array to store all the Labels, and a Hashtable (discussed further in Chapter 26, Collections) to store weather descriptions and the names of their corresponding images. A Hashtable stores key–value pairs, in which both the key and the value can be any type of object. Method Add adds key–value pairs to a Hashtable. The class also provides an indexer to return the value for a particular key in the Hashtable. Line 34 creates an array of Label references, and lines 35–44 place the Labels we created in the designer in the array so that they can be accessed programmatically to display weather information for individual cities. Line 47 creates Hashtable object weather to store pairs of weather conditions and the names for images associated with those conditions. Note that a given weather description name does not necessarily correspond to the name of the PNG file containing the correct image. For example, both “TSTRMS” and “RAIN” weather conditions use the rain.png image file. Lines 73–91 set each Label so that it contains a city name, the current temperature in the city and an image corresponding to the weather conditions for that city. Line 76 retrieves the Label that will display the weather information for the next city. Line 79 uses ArrayList cities to retrieve the CityWeather object that contains the weather information for the city. Lines 84–85 set the Label’s image to the PNG image that corresponds to the city’s weather conditions. This is done by eliminating any spaces in the description string by calling string method Trim and retrieving the name of the PNG image from the weather Hashtable. Lines 86–87 set Label properties to achieve the visual effect seen in the sample output. Line 90 sets the Text property to display the city’s name and high/ low temperatures. [Note: To preserve the layout of the client application’s window, we set the MaximumSize and MinimumSize properties of the Windows Form to the same value so that the user cannot resize the window.]
23.11 Wrap-Up
1073
Web Resources for .NET Remoting This section provided a basic introduction to .NET remoting. There is much more to this powerful .NET framework capability. The following Web sites provide additional information for readers who wish to investigate these capabilities further. In addition, searching for “.NET remoting” with most search engines yields many additional resources. msdn.microsoft.com/library/en-us/cpguide/html/cpconaccessingobjectsinotherapplicationdomainsusingnetremoting.asp
The .NET Framework Developer’s Guide on the MSDN Web site provides detailed information on .NET remoting, including articles that include choosing between ASP.NET and .NET remoting, an overview of .NET remoting, advanced .NET remoting techniques and .NET remoting examples. msdn.microsoft.com/library/en-us/dndotnet/html/introremoting.asp
This site offers a general overview of .NET remoting capabilties. search.microsoft.com/search/results.aspx?qu=.net+remoting
This microsoft.com search provides links to many .NET remoting articles and resources.
23.11 Wrap-Up In this chapter, we presented both connection-oriented and connectionless networking techniques. You learned that the Internet is an “unreliable” network that simply transmits packets of data. We discussed two protocols for transmitting packets over the Internet— Transmission Control Protocol (TCP) and User Datagram Protocol (UDP). You learned that TCP is a connection-oriented communication protocol that guarantees that sent packets will arrive at the intended receiver undamaged and in the correct sequence. You also learned that UDP is typically used in performance-oriented applications because it incurs minimum overhead for communicating between applications. We presented some of C#’s capabilities for implementing communications with TCP and UDP. We showed how to create a simple client/server chat application using stream sockets. We then showed how to send datagrams between a client and a server. You also saw a multithreaded TicTac-Toe server that allowed two clients to connect simultaneously to the server and play Tic-Tac-Toe against one another. We presented the new WebBrowser control, which allows you to add Web browsing capabilities to your Windows applications. Finally, we demonstrated .NET remoting, a technology that allows a client application to remotely access the properties and methods of an object instantiated by a server application. In Chapter 24, Data Structures, you will learn about dynamic data structures that can grow or shrink at execution time.
24 DataStructures Much that I bound, I could not free; Much that I freed returned to me.
OBJECTIVES In this chapter you will learn: I
To form linked data structures using references, selfreferential classes and recursion.
I
How boxing and unboxing enable simple-type values to be used where objects are expected in a program.
I
To create and manipulate dynamic data structures, such as linked lists, queues, stacks and binary trees.
I
Various important applications of linked data structures.
I
To create reusable data structures with classes, inheritance and composition.
—Lee Wilson Dodd
‘Will you walk a little faster?’ said a whiting to a snail, ‘There’s a porpoise close behind us, and he’s treading on my tail.’ —Lewis Carroll
There is always room at the top. —Daniel Webster
Push on—keep moving. —Thomas Morton
I think that I shall never see A poem lovely as a tree. —Joyce Kilmer
Outline
24.1 Introduction
1075
24.1 25.2 24.3 24.4 24.5 24.6 24.7
Introduction Simple-Type structs, Boxing and Unboxing Self-Referential Classes Linked Lists Stacks Queues Trees 24.7.1 Binary Search Tree of Integer Values 24.7.2 Binary Search Tree of IComparable Objects 24.8 Wrap-Up
24.1 Introduction This chapter begins our three-chapter treatment of data structures. The data structures that we have studied thus far have had fixed sizes, such as one- and two-dimensional arrays. This chapter introduces dynamic data structures that grow and shrink at execution time. Linked lists are collections of data items “lined up in a row”—users can make insertions and deletions anywhere in a linked list. Stacks are important in compilers and operating systems; insertions and deletions are made at only one end—its top. Queues represent waiting lines; insertions are made at the back (also referred to as the tail) of a queue, and deletions are made from the front (also referred to as the head) of a queue. Binary trees facilitate high-speed searching and sorting of data, efficient elimination of duplicate data items, representation of file system directories and compilation of expressions into machine language. These data structures have many other interesting applications as well. We will discuss each of these major types of data structures and implement programs that create and manipulate them. We use classes, inheritance and composition to create and package these data structures for reusability and maintainability. In Chapter 25, we introduce generics, which allow you to declare data structures that can be automatically adapted to contain data of any type. In Chapter 26, Collections, we discuss C#’s predefined classes that implement various data structures. The chapter examples are practical programs that will be useful in more advanced courses and in industrial applications. The programs focus on reference manipulation. The exercises offer a rich collection of useful applications.
24.2 Simple-Type structs, Boxing and Unboxing The data structures we discuss in this chapter store object references. However, as you will soon see, we are able to store both simple- and reference-type values in these data structures. This section discusses the mechanisms that enable simple-type values to be manipulated as objects.
Simple-Type structs Each simple type (see Appendix L, Simple Types) has a corresponding struct in namespace System that declares the simple type. These structs are called Boolean, Byte,
1076
Chapter 24
Data Structures
SByte, Char, Decimal, Double, Single, Int32, UInt32, Int64, UInt64, Int16 and UInt16. Types declared with keyword struct are implicitly value types. Simple types are actually aliases for their corresponding structs, so a variable of a simple type can be declared using either the keyword for that simple type or the struct name—e.g., int and Int32 are interchangeable. The methods related to a simple type are located in the corresponding struct (e.g., method Parse, which converts a string to an int value, is located in struct Int32). Refer to the documentation for the corresponding struct type to see the methods available for manipulating values of that type.
Boxing and Unboxing Conversions All simple-type structs inherit from class ValueType in namespace System. Class ValueType inherits from class object. Thus, any simple-type value can be assigned to an object variable; this is referred to as a boxing conversion. In a boxing conversion, the simple-type value is copied into an object so that the simple-type value can be manipulated as an object. Boxing conversions can be performed either explicitly or implicitly as shown in the following statements: int i = 5; // create an int value object object1 = ( object ) i; // explicitly box the int value object object2 = i; // implicitly box the int value
After executing the preceding code, both object1 and object2 refer to two different objects that contain a copy of the integer value in int variable i. An unboxing conversion can be used to explicitly convert an object reference to a simple value as shown in the following statement: int int1 = ( int ) object1; // explicitly unbox the int value
Explicitly attempting to unbox an object reference that does not refer to the correct simple value type causes an InvalidCastException. In Chapter 25, Generics, and Chapter 26, Collections, we discuss C#’s generics and generic collections. As you will see, generics eliminate the overhead of boxing and unboxing conversions by enabling us to create and use collections of specific value types.
24.3 Self-Referential Classes A self-referential class contains a reference member that refers to an object of the same class type. For example, the class declaration in Fig. 24.1 defines the shell of a self-referential class named Node. This type has two private instance variables—integer data and Node reference next. Member next references an object of type Node, an object of the same type as the one being declared here—hence, the term “self-referential class.” Member next is referred to as a link (i.e., next can be used to “tie” an object of type Node to another object of the same type). Class Node also has two properties—one for instance variable data (named Data) and another for instance variable next (named Next). Self-referential objects can be linked together to form useful data structures, such as lists, queues, stacks and trees. Figure 24.2 illustrates two self-referential objects linked together to form a linked list. A backslash (representing a null reference) is placed in the
// Fig. 24.1: Fig24_01.cs // A self-referential class. class Node { private int data; // store integer data private Node next; // store reference to next Node public Node( int dataValue ) { // constructor body } // end constructor public int Data { get { // get body } // end get set { // set body } // end set } // end property Data public Node Next { get { // get body } // end get set { // set body } // end set } // end property Next } // end class Node
Fig. 24.1 | Self-referential Node class declaration.
15
10
Fig. 24.2 | Self-referential class objects linked together. link member of the second self-referential object to indicate that the link does not refer to another object. The backslash is for illustration purposes; it does not correspond to the backslash character in C#. A null reference normally indicates the end of a data structure.
Common Programming Error 24.1 Not setting the link in the last node of a list to null is a logic error.
24.1
1078
Chapter 24
Data Structures
Creating and maintaining dynamic data structures requires dynamic memory allocation—a program’s ability to obtain more memory space at execution time to hold new nodes and to release space no longer needed. As you learned in Section 9.9, C# programs do not explicitly release dynamically allocated memory—rather, C# performs automatic garbage collection. The new operator is essential to dynamic memory allocation. Operator new takes as an operand the type of the object being dynamically allocated and returns a reference to an object of that type. For example, the statement Node nodeToAdd = new Node( 10 );
allocates the appropriate amount of memory to store a Node and stores a reference to this object in nodeToAdd. If no memory is available, new throws an OutOfMemoryException. The constructor argument 10 specifies the Node object’s data. The following sections discuss lists, stacks, queues and trees. These data structures are created and maintained with dynamic memory allocation and self-referential classes.
Good Programming Practice 24.1 When creating a large number of objects, test for an OutOfMemoryException. Perform appropriate error processing if the requested memory is not allocated. 24.1
24.4 Linked Lists A linked list is a linear collection (i.e., a sequence) of self-referential class objects, called nodes, connected by reference links—hence, the term “linked” list. A program accesses a linked list via a reference to the first node of the list. Each subsequent node is accessed via the link-reference member stored in the previous node. By convention, the link reference in the last node of a list is set to null to mark the end of the list. Data is stored in a linked list dynamically—that is, each node is created as necessary. A node can contain data of any type, including references to objects of other classes. Stacks and queues are also linear data structures—in fact, they are constrained versions of linked lists. Trees are non-linear data structures. Lists of data can be stored in arrays, but linked lists provide several advantages. A linked list is appropriate when the number of data elements to be represented in the data structure is unpredictable. Unlike a linked list, the size of a conventional C# array cannot be altered, because the array size is fixed at creation time. Conventional arrays can become full, but linked lists become full only when the system has insufficient memory to satisfy dynamic memory allocation requests.
Performance Tip 24.1 An array can be declared to contain more elements than the number of items expected, possibly wasting memory. Linked lists provide better memory utilization in these situations, because they can grow and shrink at execution time. 24.1
Performance Tip 24.2 After locating the insertion point for a new item in a sorted linked list, inserting an element in the list is fast—only two references have to be modified. All existing nodes remain at their current locations in memory. 24.2
24.4 Linked Lists
1079
Programmers can maintain linked lists in sorted order simply by inserting each new element at the proper point in the list (locating the proper insertion point does take time). They do not need to move existing list elements.
Performance Tip 24.3 The elements of an array are stored contiguously in memory to allow immediate access to any array element—the address of any element can be calculated directly from its index. Linked lists do not afford such immediate access to their elements—an element can be accessed only by traversing the list from the front. 24.3
Normally linked-list nodes are not stored contiguously in memory. Rather, the nodes are logically contiguous. Figure 24.3 illustrates a linked list with several nodes.
Performance Tip 24.4 Using linked data structures and dynamic memory allocation (instead of arrays) for data structures that grow and shrink at execution time can save memory. Keep in mind, however, that reference links occupy space, and dynamic memory allocation incurs the overhead of method calls. 24.4
Linked List Implementation The program of Figs. 24.4 and 24.5 uses an object of class List to manipulate a list of miscellaneous object types. The Main method of class ListTest (Fig. 24.5) creates a list of objects, inserts objects at the beginning of the list using List method InsertAtFront, inserts objects at the end of the list using List method InsertAtBack, deletes objects from the front of the list using List method RemoveFromFront and deletes objects from the end of the list using List method RemoveFromBack. After each insertion and deletion operation, the program invokes List method Print to display the current list contents. If an attempt is made to remove an item from an empty list, an EmptyListException occurs. A detailed discussion of the program follows.
Performance Tip 24.5 Insertion and deletion in a sorted array can be time consuming—all the elements following the inserted or deleted element must be shifted appropriately. 24.5
The program consists of four classes—ListNode (Fig. 24.4, lines 8–49), List (lines 52–165), EmptyListException (lines 168–174) and ListTest (Fig. 24.5). The classes in Fig. 24.4 create a linked-list library (defined in namespace LinkedListLibrary) that can be reused throughout this chapter. You should place the code of Fig. 24.4 in its own class library project as we described in Section 9.14. firstPtr
H
lastPtr
D
Fig. 24.3 | Linked list graphical representation.
...
Q
1080
Chapter 24
Data Structures
Encapsulated in each List object is a linked list of ListNode objects. Class ListNode (Fig. 24.4, lines 8–49) contains two instance variables—data and next. Member data can refer to any object. [Note: Typically, a data structure will contain data of only one type, or data of any type derived from one base type. In this example, we use data of various types derived from object to demonstrate that our List class can store data of any type. Member next stores a reference to the next ListNode object in the linked list. The ListNode constructors (lines 15–18 and 22–26) enable us to initialize a ListNode that will be placed at the end of a List or before a specific ListNode in a List, respectively. A List accesses the ListNode member variables via properties Next (lines 29–39) and Data (lines 42–48), respectively. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
// Fig. 24.4: LinkedListLibrary.cs // Class ListNode and class List declarations. using System; namespace LinkedListLibrary { // class to represent one node in a list class ListNode { private object data; // stores data for this node private ListNode next; // stores a reference to the next node
Fig. 24.4 |
// constructor to create ListNode that refers to dataValue // and is last node in list public ListNode( object dataValue ) : this( dataValue, null ) { } // end default constructor // constructor to create ListNode that refers to dataValue // and refers to next ListNode in List public ListNode( object dataValue, ListNode nextNode ) { data = dataValue; next = nextNode; } // end constructor // property Next public ListNode Next { get { return next; } // end get set { next = value; } // end set } // end property Next ListNode, List
// property Data public object Data { get { return data; } // end get } // end property Data } // end class ListNode // class List declaration public class List { private ListNode firstNode; private ListNode lastNode; private string name; // string like "list" to display
Fig. 24.4 |
// construct empty List with specified name public List( string listName ) { name = listName; firstNode = lastNode = null; } // end constructor // construct empty List with "list" as its name public List() : this( "list" ) { } // end constructor default // Insert object at front of List. If List is empty, // firstNode and lastNode will refer to same object. // Otherwise, firstNode refers to new node. public void InsertAtFront( object insertItem ) { if ( IsEmpty() ) firstNode = lastNode = new ListNode( insertItem ); else firstNode = new ListNode( insertItem, firstNode ); } // end method InsertAtFront // Insert object at end of List. If List is empty, // firstNode and lastNode will refer to same object. // Otherwise, lastNode's Next property refers to new node. public void InsertAtBack( object insertItem ) { if ( IsEmpty() ) firstNode = lastNode = new ListNode( insertItem ); else lastNode = lastNode.Next = new ListNode( insertItem ); } // end method InsertAtBack ListNode, List
// remove first node from List public object RemoveFromFront() { if ( IsEmpty() ) throw new EmptyListException( name ); object removeItem = firstNode.Data; // retrieve data // reset firstNode and lastNode references if ( firstNode == lastNode ) firstNode = lastNode = null; else firstNode = firstNode.Next; return removeItem; // return removed data } // end method RemoveFromFront // remove last node from List public object RemoveFromBack() { if ( IsEmpty() ) throw new EmptyListException( name ); object removeItem = lastNode.Data; // retrieve data // reset firstNode and lastNode references if ( firstNode == lastNode ) firstNode = lastNode = null; else { ListNode current = firstNode; // loop while current node is not lastNode while ( current.Next != lastNode ) current = current.Next; // move to next node // current is new lastNode lastNode = current; current.Next = null; } // end else return removeItem; // return removed data } // end method RemoveFromBack // return true if List is empty public bool IsEmpty() { return firstNode == null; } // end method IsEmpty
ListNode, List
and EmptyListException classes. (Part 3 of 4.)
24.4 Linked Lists
1083
143 // output List contents 144 public void Print() 145 { 146 if ( IsEmpty() ) 147 { 148 Console.WriteLine( "Empty " + name ); 149 return; 150 } // end if 151 152 Console.Write( "The " + name + " is: " ); 153 154 ListNode current = firstNode; 155 156 // output current node data while not at end of list 157 while ( current != null ) 158 { 159 Console.Write( current.Data + " " ); 160 current = current.Next; 161 } // end while 162 163 Console.WriteLine( "\n" ); 164 } // end method Print 165 } // end class List 166 167 // class EmptyListException declaration 168 public class EmptyListException : ApplicationException 169 { 170 public EmptyListException( string name ) 171 : base( "The " + name + " is empty" ) 172 { 173 } // end constructor 174 } // end class EmptyListException 175 } // end namespace LinkedListLibrary
Fig. 24.4 |
ListNode, List
and EmptyListException classes. (Part 4 of 4.)
Class List (lines 52–165) contains private instance variables firstNode (a reference to the first ListNode in a List) and lastNode (a reference to the last ListNode in a List). The constructors (lines 59–63 and 66–69) initialize both references to null and enable us to specify the List’s name for output purposes. InsertAtFront (lines 74–80), InsertAtBack (lines 85–91), RemoveFromFront (lines 94–108) and RemoveFromBack (lines 111– 135) are the primary methods of class List. Method IsEmpty (lines 138–141) is a predicate method that determines whether the list is empty (i.e., the reference to the first node of the list is null). Predicate methods typically test a condition and do not modify the object on which they are called. If the list is empty, method IsEmpty returns true; otherwise, it returns false. Method Print (lines 144–164) displays the list’s contents. A detailed discussion of class List’s methods follows Fig. 24.5. Class EmptyListException (lines 168–174) defines an exception class that we use to indicate illegal operations on an empty List. Class ListTest (Fig. 24.5) uses the linked-list library to create and manipulate a linked list. [Note: In the project containing Fig. 24.5, you must add a reference to the class library containing the classes in Fig. 24.4. If you use our existing example, you may need
1084
Chapter 24
Data Structures
to update this reference.] Line 11 creates a new List object and assigns it to variable list. Lines 14–17 create data to add to the list. Lines 20–27 use List insertion methods to insert these values and use List method Print to output the contents of list after each insertion. Note that the values of the simple-type variables are implicitly boxed in lines 20, 22 and 24 where object references are expected. The code inside the try block (lines 33– 50) removes objects via List deletion methods, outputs each removed object and outputs list after every deletion. If there is an attempt to remove an object from an empty list, the catch at lines 51–54 catches the EmptyListException and displays an error message. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
// Fig. 24.5: ListTest.cs // Testing class List. using System; using LinkedListLibrary; // class to test List class functionality class ListTest { static void Main( string[] args ) { List list = new List(); // create List container // create data to store in List bool aBoolean = true; char aCharacter = '$'; int anInteger = 34567; string aString = "hello"; // use List insert methods list.InsertAtFront( aBoolean ); list.Print(); list.InsertAtFront( aCharacter ); list.Print(); list.InsertAtBack( anInteger ); list.Print(); list.InsertAtBack( aString ); list.Print(); // use List remove methods object removedObject; // remove data from list and print after each removal try { removedObject = list.RemoveFromFront(); Console.WriteLine( removedObject + " removed" ); list.Print(); removedObject = list.RemoveFromFront(); Console.WriteLine( removedObject + " removed" ); list.Print();
Fig. 24.5 | Linked list demonstration. (Part 1 of 2.)
removedObject = list.RemoveFromBack(); Console.WriteLine( removedObject + " removed" ); list.Print(); removedObject = list.RemoveFromBack(); Console.WriteLine( removedObject + " removed" ); list.Print(); } // end try catch ( EmptyListException emptyListException ) { Console.Error.WriteLine( "\n" + emptyListException ); } // end catch } // end method Main } // end class ListTest
The list is: True The list is: $ True The list is: $ True 34567 The list is: $ True 34567 hello $ removed The list is: True 34567 hello True removed The list is: 34567 hello hello removed The list is: 34567 34567 removed Empty list
Fig. 24.5 | Linked list demonstration. (Part 2 of 2.) Method InsertAtFront Over the next several pages, we discuss each of the methods of class List in detail. Method InsertAtFront (Fig. 24.4, lines 74–80) places a new node at the front of the list. The method consists of three steps: 1. Call IsEmpty to determine whether the list is empty (line 76). 2. If the list is empty, set both firstNode and lastNode to refer to a new ListNode initialized with insertItem (line 77). The ListNode constructor at lines 15–18 of Fig. 24.4 calls the ListNode constructor at lines 22–26, which sets instance variable data to refer to the object passed as the first argument and sets the next reference to null. 3. If the list is not empty, the new node is “linked” into the list by setting firstNode to refer to a new ListNode object initialized with insertItem and firstNode (line 79). When the ListNode constructor (lines 22–26) executes, it sets instance variable data to refer to the object passed as the first argument and performs the insertion by setting the next reference to the ListNode passed as the second argument.
1086
Chapter 24
Data Structures
In Fig. 24.6, part (a) shows a list and a new node during the InsertAtFront operation and before the new node is linked into the list. The dashed lines and arrows in part (b) illustrate Step 3 of the InsertAtFront operation, which enables the node containing 12 to become the new list front.
Method InsertAtBack Method InsertAtBack (Fig. 24.4, lines 85–91) places a new node at the back of the list. The method consists of three steps: 1. Call IsEmpty to determine whether the list is empty (line 87). 2. If the list is empty, set both firstNode and lastNode to refer to a new ListNode initialized with insertItem (line 88). The ListNode constructor at lines 15–18 calls the ListNode constructor at lines 22–26, which sets instance variable data to refer to the object passed as the first argument and sets the next reference to null. 3. If the list is not empty, link the new node into the list by setting lastNode and lastNode.next to refer to a new ListNode object initialized with insertItem (line 90). When the ListNode constructor (lines 15–18) executes, it calls the constructor at lines 22–26, which sets instance variable data to refer to the object passed as an argument and sets the next reference to null. In Fig. 24.7, part (a) shows a list and a new node during the InsertAtBack operation; before the new node has been linked into the list. The dashed lines and arrows in part (b) illustrate Step 3 of method InsertAtBack, which enables a new node to be added to the end of a list that is not empty. Method RemoveFromFront Method RemoveFromFront (Fig. 24.4, lines 94–108) removes the front node of the list and returns a reference to the removed data. The method throws an EmptyListException (line 97) if the programmer tries to remove a node from an empty list. Otherwise, the method
(a) firstNode 7
11
new ListNode 12
(b) firstNode 7
new ListNode 12
Fig. 24.6 |
InsertAtFront
operation.
11
24.4 Linked Lists
(a)
firstNode
lastNode
12
(b)
7
firstNode
lastNode
12
Fig. 24.7 |
InsertAtBack
11
7
11
1087
new ListNode
5
new ListNode
5
operation.
returns a reference to the removed data. After determining that a List is not empty, the method consists of four steps to remove the first node: 1. Assign
firstNode.Data moveItem (line 99).
(the data being removed from the list) to variable
re-
2. If the objects to which firstNode and lastNode refer are the same object, the list has only one element, so the method sets firstNode and lastNode to null (line 103) to remove the node from the list (leaving the list empty). 3. If the list has more than one node, the method leaves reference lastNode as is and assigns firstNode.Next to firstNode (line 105). Thus, firstNode references the node that was previously the second node in the List. 4. Return the removeItem reference (line 107). In Fig. 24.8, part (a) illustrates a list before a removal operation. The dashed lines and arrows in part (b) show the reference manipulations.
Method RemoveFromBack Method RemoveFromBack (Fig. 24.4, lines 111–135) removes the last node of a list and returns a reference to the removed data. The method throws an EmptyListException (line 114) if the program attempts to remove a node from an empty list. The method consists of several steps: 1. Assign lastNode.Data (the data being removed from the list) to variable removeItem (line 116). 2. If firstNode and lastNode refer to the same object (line 119), the list has only one element, so the method sets firstNode and lastNode to null (line 120) to remove that node from the list (leaving the list empty).
1088
Chapter 24
(a)
Data Structures
firstNode
12
(b)
lastNode
7
11
firstNode
12
5
lastNode
7
11
5
lastNode
Fig. 24.8 |
RemoveFromFront
operation.
3. If the list has more than one node, create ListNode variable current and assign it firstNode (line 123). 4. Now “walk the list” with current until it references the node before the last node. The while loop (lines 126–127) assigns current.Next to current as long as current.Next is not equal to lastNode. 5. After locating the second-to-last node, assign current to lastNode (line 130) to update which node is last in the list. 6. Set current.Next to null (line 131) to remove the last node from the list and terminate the list at the current node. 7. Return the removeItem reference (line 134). In Fig. 24.9, part (a) illustrates a list before a removal operation. The dashed lines and arrows in part (b) show the reference manipulations.
Method Print Method Print (Fig. 24.4, lines 144–164) first determines whether the list is empty (line 146). If so, Print displays a string consisting of the string "Empty " and the list’s name, then returns control to the calling method. Otherwise, Print outputs the data in the list. The method prints a string consisting of the string "The ", the list’s name and the string " is: ". Then line 154 creates ListNode variable current and initializes it with firstNode. While current is not null, there are more items in the list. Therefore, the method displays current.Data (line 159), then assigns current.Next to current (line 160) to move to the next node in the list.
24.4 Linked Lists
(a)
firstNode
current
lastNode
12
(b)
7
firstNode
Fig. 24.9 |
11
current
12
rent
1089
5
lastNode
7
11
5
lastNode
RemoveFromBack
operation.
Linear and Circular Singly Linked and Doubly Linked Lists The kind of linked list we have been discussing is a singly linked list—the list begins with a reference to the first node, and each node contains a reference to the next node “in sequence.” This list terminates with a node whose reference member has the value null. A singly linked list may be traversed in only one direction. A circular, singly linked list (Fig. 24.10) begins with a reference to the first node, and each node contains a reference to the next node. The “last node” does not contain a null reference; rather, the reference in the last node points back to the first node, thus closing the “circle.” A doubly linked list (Fig. 24.11) allows traversals both forward and backward. Such a list is often implemented with two “start references”—one that refers to the first element of the list to allow front-to-back traversal of the list and one that refers to the last element firstNode
12
7
Fig. 24.10 | Circular, singly linked list.
11
5
1090
Chapter 24
Data Structures
lastNode
firstNode
12
7
11
5
Fig. 24.11 | Doubly linked list. to allow back-to-front traversal. Each node has both a forward reference to the next node in the list and a backward reference to the previous node in the list. If your list contains an alphabetized telephone directory, for example, a search for someone whose name begins with a letter near the front of the alphabet might begin from the front of the list. A search for someone whose name begins with a letter near the end of the alphabet might begin from the back of the list. In a circular, doubly linked list (Fig. 24.12), the forward reference of the last node refers to the first node, and the backward reference of the first node refers to the last node, thus closing the “circle.”
24.5 Stacks A stack is a constrained version of a linked list—a stack receives new nodes and releases nodes only at the top. For this reason, a stack is referred to as a last-in-first-out (LIFO) data structure. The primary operations to manipulate a stack are push and pop. Operation push adds a new node to the top of the stack. Operation pop removes a node from the top of the stack and returns the data item from the popped node. Stacks have many interesting applications. For example, when a program calls a method, the called method must know how to return to its caller, so the return address is pushed onto the method call stack. If a series of method calls occurs, the successive return values are pushed onto the stack in last-in, first-out order so that each method can return to its caller. Stacks support recursive method calls in the same manner that they support conventional non-recursive method calls. lastNode
firstNode
12
7
Fig. 24.12 | Circular, doubly linked list.
11
5
24.5 Stacks
1091
The System.Collections namespace contains class Stack for implementing and manipulating stacks that can grow and shrink during program execution. Chapters 25 and 26 discuss class Stack. In our next example, we take advantage of the close relationship between lists and stacks to implement a stack class by reusing a list class. We demonstrate two different forms of reusability. First, we implement the stack class by inheriting from class List of Fig. 24.4. Then we implement an identically performing stack class through composition by including a List object as a private member of a stack class.
Stack Class That Inherits from List The program of Figs. 24.13 and 24.14 creates a stack class by inheriting from class List of Fig. 24.4 (line 9). We want the stack to have methods Push, Pop, IsEmpty and Print. Essentially, these are the methods InsertAtFront, RemoveFromFront, IsEmpty and Print of class List. Of course, class List contains other methods (such as InsertAtBack and RemoveFromBack) that we would rather not make accessible through the public interface of the stack. It is important to remember that all methods in the public interface of class List are also public methods of the derived class StackInheritance (Fig. 24.13). 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
// Fig. 24.13: StackInheritanceLibrary.cs // Implementing a stack by inheriting from class List. using System; using LinkedListLibrary; namespace StackInheritanceLibrary { // class StackInheritance inherits class List's capabilities public class StackInheritance : List { // pass name "stack" to List constructor public StackInheritance() : base( "stack" ) { } // end constructor // place dataValue at top of stack by inserting // dataValue at front of linked list public void Push( object dataValue ) { InsertAtFront(dataValue); } // end method Push // remove item from top of stack by removing // item at front of linked list public object Pop() { return RemoveFromFront(); } // end method Pop } // end class StackInheritance } // end namespace StackInheritanceLibrary
Fig. 24.13
| StackInheritance
extends class List.
1092
Chapter 24
Data Structures
The implementation of each StackInheritance method calls the appropriate List method—method Push calls InsertAtFront, method Pop calls RemoveFromFront. Class StackInheritance does not define methods IsEmpty and Print, because StackInheritance inherits these methods from class List into StackInheritance’s public interface. Note that class StackInheritance uses namespace LinkedListLibrary (Fig. 24.4); thus, the class library that defines StackInheritance must have a reference to the LinkedListLibrary class library. StackInheritanceTest’s Main method (Fig. 24.14) uses class StackInheritance to create a stack of objects called stack (line 12). Lines 15–18 define four values that will be pushed onto the stack and popped off the stack. The program pushes onto the stack (lines 21, 23, 25 and 27) a bool containing true, a char containing '$', an int containing 34567 and a string containing "hello". An infinite while loop (lines 33–38) pops the elements from the stack. When the stack is empty, method Pop throws an EmptyListException, and the program displays the exception’s stack trace, which shows the programexecution stack at the time the exception occurred. The program uses method Print (inherited by StackInheritance from class List) to output the contents of the stack after each operation. Note that class StackInheritanceTest uses namespace LinkedListLibrary (Fig. 24.4) and namespace StackInheritanceLibrary (Fig. 24.13); thus, the solution for class StackInheritanceTest must have references to both class libraries. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
// Fig. 24.14: StackInheritanceTest.cs // Testing class StackInheritance. using System; using StackInheritanceLibrary; using LinkedListLibrary; // demonstrate functionality of class StackInheritance class StackInheritanceTest { static void Main( string[] args ) { StackInheritance stack = new StackInheritance(); // create objects to store in the stack bool aBoolean = true; char aCharacter = '$'; int anInteger = 34567; string aString = "hello"; // use method Push to add items to stack stack.Push( aBoolean ); stack.Print(); stack.Push( aCharacter ); stack.Print(); stack.Push( anInteger ); stack.Print(); stack.Push( aString ); stack.Print();
Fig. 24.14 | Using class StackInheritance. (Part 1 of 2.)
// remove items from stack try { while ( true ) { object removedObject = stack.Pop(); Console.WriteLine( removedObject + " popped" ); stack.Print(); } // end while } // end try catch ( EmptyListException emptyListException ) { // if exception occurs, print stack trace Console.Error.WriteLine( emptyListException.StackTrace ); } // end catch } // end Main } // end class StackInheritanceTest
The stack is: True The stack is: $ True The stack is: 34567 $ True The stack is: hello 34567 $ True hello popped The stack is: 34567 $ True 34567 popped The stack is: $ True $ popped The stack is: True True popped Empty stack at LinkedListLibrary.List.RemoveFromFront() at StackInheritanceLibrary.StackInheritance.Pop() at StackInheritanceTest.StackInheritanceTest.Main(String[] args) in C:\examples\ch25\Fig25_14\StackInheritanceTest\ StackInheritanceTest.cs:line 35
Fig. 24.14 | Using class StackInheritance. (Part 2 of 2.) Stack Class That Contains a Reference to a List Another way to implement a stack class is by reusing a list class through composition. The class in Fig. 24.15 uses a private object of class List (line 11) in the declaration of class StackComposition. Composition enables us to hide the methods of class List that should not be in our stack’s public interface by providing public interface methods only to the required List methods. This class implements each stack method by delegating its work to an appropriate List method. StackComposition’s methods call List methods InsertAtFront, RemoveFromFront, IsEmpty and Print. In this example, we do not show class StackCompositionTest, because the only difference in this example is that we change the
// Fig. 24.15: StackCompositionLibrary.cs // StackComposition declaration with composed List object. using System; using LinkedListLibrary; namespace StackCompositionLibrary { // class StackComposition encapsulates List's capabilities public class StackComposition { private List stack; // construct empty stack public StackComposition() { stack = new List( "stack" ); } // end constructor // add object to stack public void Push( object dataValue ) { stack.InsertAtFront( dataValue ); } // end method Push // remove object from stack public object Pop() { return stack.RemoveFromFront(); } // end method Pop // determine whether stack is empty public bool IsEmpty() { return stack.IsEmpty(); } // end method IsEmpty // output stack contents public void Print() { stack.Print(); } // end method Print } // end class StackComposition } // end namespace StackCompositionLibrary
Fig. 24.15
| StackComposition
class encapsulates functionality of class List.
name of the stack class from StackInheritance to StackComposition. If you execute the application from the code, you will see that the output is identical.
24.6 Queues Another commonly used data structure is the queue. A queue is similar to a checkout line in a supermarket—the cashier services the person at the beginning of the line first. Other
24.6 Queues
1095
customers enter the line only at the end and wait for service. Queue nodes are removed only from the head (or front) of the queue and are inserted only at the tail (or end). For this reason, a queue is a first-in, first-out (FIFO) data structure. The insert and remove operations are known as enqueue and dequeue. Queues have many uses in computer systems. Most computers have only a single processor, so only one application at a time can be serviced. Each application requiring processor time is placed in a queue. The application at the front of the queue is the next to receive service. Each application gradually advances to the front as the applications before it receive service. Queues are also used to support print spooling. For example, a single printer might be shared by all users of a network. Many users can send print jobs to the printer, even when the printer is already busy. These print jobs are placed in a queue until the printer becomes available. A program called a spooler manages the queue to ensure that as each print job completes, the next print job is sent to the printer. Information packets also wait in queues in computer networks. Each time a packet arrives at a network node, it must be routed to the next node along the path to the packet’s final destination. The routing node routes one packet at a time, so additional packets are enqueued until the router can route them. A file server in a computer network handles file-access requests from many clients throughout the network. Servers have a limited capacity to service requests from clients. When that capacity is exceeded, client requests wait in queues.
Queue Class That Inherits from List The program of Figs. 24.16 and 24.17 creates a queue class by inheriting from a list class. We want the QueueInheritance class (Fig. 24.16) to have methods Enqueue, Dequeue, IsEmpty and Print. Essentially, these are the methods InsertAtBack, RemoveFromFront, IsEmpty and Print of class List. Of course, the list class contains other methods (such as InsertAtFront and RemoveFromBack) that we would rather not make accessible through the public interface to the queue class. Remember that all methods in the public interface of the List class are also public methods of the derived class QueueInheritance. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
// Fig. 24.16: QueueInheritanceLibrary.cs // Implementing a queue by inheriting from class List. using System; using LinkedListLibrary; namespace QueueInheritanceLibrary { // class QueueInheritance inherits List's capabilities public class QueueInheritance : List { // pass name "queue" to List constructor public QueueInheritance() : base( "queue" ) { } // end constructor
Fig. 24.16
| QueueInheritance
extends class List. (Part 1 of 2.)
1096 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
Chapter 24
Data Structures
// place dataValue at end of queue by inserting // dataValue at end of linked list public void Enqueue( object dataValue ) { InsertAtBack( dataValue ); } // end method Enqueue // remove item from front of queue by removing // item at front of linked list public object Dequeue() { return RemoveFromFront(); } // end method Dequeue } // end class QueueInheritance } // end namespace QueueInheritanceLibrary
Fig. 24.16
| QueueInheritance
extends class List. (Part 2 of 2.)
The implementation of each QueueInheritance method calls the appropriate List method—method Enqueue calls InsertAtBack and method Dequeue calls RemoveFromFront. Calls to IsEmpty and Print invoke the base-class versions that were inherited from class List into QueueInheritance’s public interface. Note that class QueueInheritance uses namespace LinkedListLibrary (Fig. 24.4); thus, the class library for QueueInheritance must have a reference to the LinkedListLibrary class library. Class QueueInheritanceTest’s Main method (Fig. 24.17) creates a QueueInheritance object called queue. Lines 15–18 define four values that will be enqueued and dequeued. The program enqueues (lines 21, 23, 25 and 27) a bool containing true, a char containing '$', an int containing 34567 and a string containing "hello". Note that class QueueInheritanceTest uses namespace LinkedListLibrary and namespace QueueInheritanceLibrary; thus, the solution for class StackInheritanceTest must have references to both class libraries. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
// Fig. 24.17: QueueTest.cs // Testing class QueueInheritance. using System; using QueueInheritanceLibrary; using LinkedListLibrary; // demonstrate functionality of class QueueInheritance class QueueTest { static void Main( string[] args ) { QueueInheritance queue = new QueueInheritance(); // create objects to store in the stack bool aBoolean = true; char aCharacter = '$'; int anInteger = 34567;
Fig. 24.17 | Queue created by inheritance. (Part 1 of 2.)
string aString = "hello"; // use method Enqueue to add items to queue queue.Enqueue( aBoolean ); queue.Print(); queue.Enqueue( aCharacter ); queue.Print(); queue.Enqueue( anInteger ); queue.Print(); queue.Enqueue( aString ); queue.Print(); // use method Dequeue to remove items from queue object removedObject = null; // remove items from queue try { while ( true ) { removedObject = queue.Dequeue(); Console.WriteLine( removedObject + " dequeued" ); queue.Print(); } // end while } // end try catch ( EmptyListException emptyListException ) { // if exception occurs, print stack trace Console.Error.WriteLine( emptyListException.StackTrace ); } // end catch } // end method Main } // end class QueueTest
The queue is: True The queue is: True $ The queue is: True $ 34567 The queue is: True $ 34567 hello True dequeued The queue is: $ 34567 hello $ dequeued The queue is: 34567 hello 34567 dequeued The queue is: hello hello Empty at at at
dequeued queue LinkedListLibrary.List.RemoveFromFront() QueueInheritanceLibrary.QueueInheritance.Dequeue() QueueTest.QueueTest.Main(String[] args) in C:\examples\ch25\Fig25_17\QueueTest.cs:line 38
Fig. 24.17 | Queue created by inheritance. (Part 2 of 2.)
1097
1098
Chapter 24
Data Structures
An infinite while loop (lines 36–41) dequeues the elements from the queue in FIFO order. When there are no objects left to dequeue, method Dequeue throws an EmptyListException, and the program displays the exception’s stack trace, which shows the program execution stack at the time the exception occurred. The program uses method Print (inherited from class List) to output the contents of the queue after each operation. Note that class QueueInheritanceTest uses namespace LinkedListLibrary (Fig. 24.4) and namespace QueueInheritanceLibrary (Fig. 24.16); thus, the solution for class QueueInheritanceTest must have references to both class libraries.
24.7 Trees Linked lists, stacks and queues are linear data structures (i.e., sequences). A tree is a nonlinear, two-dimensional data structure with special properties. Tree nodes contain two or more links.
Basic Terminology This section discusses binary trees (Fig. 24.18)—trees whose nodes all contain two links (none, one or both of which may be null). The root node is the first node in a tree. Each link in the root node refers to a child. The left child is the first node in the left subtree, and the right child is the first node in the right subtree. The children of a specific node are called siblings. A node with no children is called a leaf node. Computer scientists normally draw trees from the root node down—exactly the opposite of the way most trees grow in nature.
Common Programming Error 24.2 Not setting to null the links in leaf nodes of a tree is a common logic error.
24.2
Binary Search Trees In our binary tree example, we create a special binary tree called a binary search tree. A binary search tree (with no duplicate node values) has the characteristic that the values in
Root node reference
Root node
Left subtree of node containing B
B
A
D
C
Fig. 24.18 | Binary tree graphical representation.
Right subtree of node containing B
24.7 Trees
1099
any left subtree are less than the value in the subtree’s parent node, and the values in any right subtree are greater than the value in the subtree’s parent node. Figure 24.19 illustrates a binary search tree with 9 integer values. Note that the shape of the binary search tree that corresponds to a set of data can depend on the order in which the values are inserted into the tree.
24.7.1 Binary Search Tree of Integer Values The application of Figs. 24.20 and 24.21 creates a binary search tree of integers and traverses it (i.e., walks through all its nodes) in three ways—using recursive inorder, preorder and postorder traversals. The program generates 10 random numbers and inserts each into the tree. Figure 24.20 defines class Tree in namespace BinaryTreeLibrary for reuse purposes. Figure 24.21 defines class TreeTest to demonstrate class Tree’s functionality. Method Main of class TreeTest instantiates an empty Tree object, then randomly generates 10 integers and inserts each value in the binary tree by calling Tree method InsertNode. The program then performs preorder, inorder and postorder traversals of the tree. We will discuss these traversals shortly. Class TreeNode (lines 8–81 of Fig. 24.20) is a self-referential class containing three private data members—leftNode and rightNode of type TreeNode and data of type int. Initially, every TreeNode is a leaf node, so the constructor (lines 15–19) initializes references leftNode and rightNode to null. Properties LeftNode (lines 22–32), Data (lines 35–45) and RightNode (lines 48–58) provide access to a ListNode’s private data members. We discuss TreeNode method Insert (lines 62–80) shortly. Class Tree (lines 84–170 of Fig. 24.20) manipulates objects of class TreeNode. Class Tree has as private data root (line 86)—a reference to the root node of the tree. The class contains public method InsertNode (lines 97–103) to insert a new node in the tree and public methods PreorderTraversal (lines 106–109), InorderTraversal (lines 128– 131) and PostorderTraversal (lines 150–153) to begin traversals of the tree. Each of these methods calls a separate recursive utility method to perform the traversal operations on the internal representation of the tree. The Tree constructor (lines 89–92) initializes root to null to indicate that the tree initially is empty. Tree method InsertNode (lines 97–103) first determines whether the tree is empty. If so, line 100 allocates a new TreeNode, initializes the node with the integer being inserted in the tree and assigns the new node to root. If the tree is not empty, InsertNode calls TreeNode method Insert (lines 62–80), which recursively determines the location for the new node in the tree and inserts the node at that location. A node can be inserted only as a leaf node in a binary search tree. 47 25 11
77 43 31
65
44
Fig. 24.19 | Binary search tree containing 9 values.
// Fig. 24.20: BinaryTreeLibrary.cs // Declaration of class TreeNode and class Tree. using System; namespace BinaryTreeLibrary { // class TreeNode declaration class TreeNode { private TreeNode leftNode; // link to left child private int data; // data stored in node private TreeNode rightNode; // link to right child // initialize data and make this a leaf node public TreeNode( int nodeData ) { data = nodeData; leftNode = rightNode = null; // node has no children } // end constructor // LeftNode property public TreeNode LeftNode { get { return leftNode; } // end get set { leftNode = value; } // end set } // end property LeftNode // Data property public int Data { get { return data; } // end get set { data = value; } // end set } // end property Data // RightNode property public TreeNode RightNode { get {
Fig. 24.20 |
TreeNode
and Tree classes for a binary search tree. (Part 1 of 4.)
return rightNode; } // end get set { rightNode = value; } // end set } // end property RightNode // insert TreeNode into Tree that contains nodes; // ignore duplicate values public void Insert( int insertValue ) { if ( insertValue < data ) // insert in left subtree { // insert new TreeNode if ( leftNode == null ) leftNode = new TreeNode( insertValue ); else // continue traversing left subtree leftNode.Insert( insertValue ); } // end if else if ( insertValue > data ) // insert in right subtree { // insert new TreeNode if ( rightNode == null ) rightNode = new TreeNode( insertValue ); else // continue traversing right subtree rightNode.Insert( insertValue ); } // end else if } // end method Insert } // end class TreeNode // class Tree declaration public class Tree { private TreeNode root; // construct an empty Tree of integers public Tree() { root = null; } // end constructor // Insert a new node in the binary search tree. // If the root node is null, create the root node here. // Otherwise, call the insert method of class TreeNode. public void InsertNode( int insertValue ) { if ( root == null ) root = new TreeNode( insertValue ); else root.Insert( insertValue ); } // end method InsertNode
Fig. 24.20 |
TreeNode
and Tree classes for a binary search tree. (Part 2 of 4.)
and Tree classes for a binary search tree. (Part 4 of 4.)
// Fig. 24.21: TreeTest.cs // This program tests class Tree. using System; using BinaryTreeLibrary; // class TreeTest declaration public class TreeTest { // test class Tree static void Main( string[] args ) { Tree tree = new Tree(); int insertValue; Console.WriteLine( "Inserting values: " ); Random random = new Random(); // insert 10 random integers from 0-99 in tree for ( int i = 1; i 1000. Searching a (tightly packed) 1,000,000-element binary search tree requires at most 20 comparisons, because 220 > 1,000,000. Overview of the Binary Tree Exercises The chapter exercises present algorithms for other binary tree operations, such as deleting an item from a binary tree and performing a level-order traversal of a binary tree. The level-order traversal of a binary tree visits the nodes of the tree row-by-row, starting at the root-node level. On each level of the tree, a level-order traversal visits the nodes from left to right.
24.7.2 Binary Search Tree of IComparable Objects The binary tree example in Section 24.7.1 works nicely when all the data is of type int. Suppose that you want to manipulate a binary tree of double values. You could rewrite the TreeNode and Tree classes with different names and customize the classes to manipulate double values. Similarly, for each data type you could create customized versions of classes TreeNode and Tree. This results in a proliferation of code, which can become difficult to manage and maintain. Ideally, we would like to define the functionality of a binary tree once and reuse that functionality for many data types. Languages like Java™ and C# provide polymorphic capabilities that enable all objects to be manipulated in a uniform manner. Using such capabilities enables us to design a more flexible data structure. The new version of C#, C# 2.0, provides these capabilities with generics (Chapter 25). In our next example, we take advantage of C#’s polymorphic capabilities by implementing TreeNode and Tree classes that manipulate objects of any type that implements interface IComparable (namespace System). It is imperative that we be able to compare objects stored in a binary search, so we can determine the path to the insertion point of a new node. Classes that implement IComparable define method CompareTo, which compares the object that invokes the method with the object that the method receives as an argument. The method returns an int value less than zero if the calling object is less than the argument object, zero if the objects are equal and a positive value if the calling object is greater than the argument object. Also, both the calling and argument objects must be of the same data type; otherwise, the method throws an ArgumentException.
24.7 Trees
1107
The program of Figs. 24.23 and 24.24 enhances the program from Section 24.7.1 to manipulate IComparable objects. One restriction on the new versions of classes TreeNode and Tree in Fig. 24.23 is that each Tree object can contain objects of only one data type (e.g., all strings or all doubles). If a program attempts to insert multiple data types in the same Tree object, ArgumentExceptions will occur. We modified only six lines of code in class TreeNode (lines 12, 16, 36, 63, 65 and 73) and one line of code in class Tree (line 98) to enable processing of IComparable objects. With the exception of lines 65 and 73, all other changes simply replaced the type int with the type IComparable. Lines 65 and 73 previously used the < and > operators to compare the value being inserted with the value in a given node. These lines now compare IComparable objects via the interface’s CompareTo method, then test the method’s return value to determine whether it is less than zero (the calling object is less than the argument object) or greater than zero (the calling object is greater than the argument object), respectively. [Note: If this class were written using generics, the type of data, int or IComparable, could be replaced at compile time by any other type that implements the necessary operators and methods.] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
// Fig. 24.23: BinaryTreeLibrary2.cs // Declaration of class TreeNode and class Tree for IComparable // objects. using System; namespace BinaryTreeLibrary2 { // class TreeNode declaration class TreeNode { private TreeNode leftNode; // link to left child private IComparable data; // data stored in node private TreeNode rightNode; // link to right subtree // initialize data and make this a leaf node public TreeNode( IComparable nodeData ) { data = nodeData; leftNode = rightNode = null; // node has no children } // end constructor // LeftNode property public TreeNode LeftNode { get { return leftNode; } // end get set { leftNode = value; } // end set } // end property LeftNode
Fig. 24.23 |
TreeNode
and Tree classes for manipulating IComparable objects. (Part 1 of 4.)
// Data property public IComparable Data { get { return data; } // end get set { data = value; } // end set } // end property Data // RightNode property public TreeNode RightNode { get { return rightNode; } // end get set { rightNode = value; } // end set } // end property Right Node // insert TreeNode into Tree that contains nodes; // ignore duplicate values public void Insert( IComparable insertValue ) { if ( insertValue.CompareTo( data ) < 0 ) { // insert in left subtree if ( leftNode == null ) leftNode = new TreeNode( insertValue ); else // continue traversing left subtree leftNode.Insert( insertValue ); } // end if else if ( insertValue.CompareTo( data ) > 0 ) { // insert in right subtree if ( rightNode == null ) rightNode = new TreeNode( insertValue ); else // continue traversing right subtree rightNode.Insert( insertValue ); } // end else if } // end method Insert } // end class TreeNode // class Tree declaration public class Tree { private TreeNode root;
Fig. 24.23 |
TreeNode
and Tree classes for manipulating IComparable objects. (Part 2 of 4.)
// construct an empty Tree of integers public Tree() { root = null; } // end constructor // Insert a new node in the binary search tree. // If the root node is null, create the root node here. // Otherwise, call the insert method of class TreeNode. public void InsertNode( IComparable insertValue ) { if ( root == null ) root = new TreeNode( insertValue ); else root.Insert( insertValue ); } // end method InsertNode // begin preorder traversal public void PreorderTraversal() { PreorderHelper( root ); } // end method PreorderTraversal // recursive method to perform preorder traversal private void PreorderHelper( TreeNode node ) { if ( node == null ) return; // output node data Console.Write( node.Data + " " ); // traverse left subtree PreorderHelper( node.LeftNode ); // traverse right subtree PreorderHelper( node.RightNode ); } // end method PreorderHelper // begin inorder traversal public void InorderTraversal() { InorderHelper( root ); } // end method InorderTraversal // recursive method to perform inorder traversal private void InorderHelper( TreeNode node ) { if ( node == null ) return;
Fig. 24.23 |
TreeNode
and Tree classes for manipulating IComparable objects. (Part 3 of 4.)
1110
Chapter 24
Data Structures
140 // traverse left subtree 141 InorderHelper( node.LeftNode ); 142 143 // output node data 144 Console.Write( node.Data + " " ); 145 146 // traverse right subtree 147 InorderHelper( node.RightNode ); 148 } // end method InorderHelper 149 150 // begin postorder traversal 151 public void PostorderTraversal() 152 { 153 PostorderHelper( root ); 154 } // end method PostorderTraversal 155 156 // recursive method to perform postorder traversal 157 private void PostorderHelper( TreeNode node ) 158 { 159 if ( node == null ) 160 return; 161 162 // traverse left subtree 163 PostorderHelper( node.LeftNode ); 164 165 // traverse right subtree 166 PostorderHelper( node.RightNode ); 167 168 // output node data 169 Console.Write( node.Data + " " ); 170 } // end method PostorderHelper 171 172 } // end class Tree 173 } // end namespace BinaryTreeLibrary2
Fig. 24.23 |
TreeNode
and Tree classes for manipulating IComparable objects. (Part 4 of 4.)
Class TreeTest (Fig. 24.24) creates three Tree objects to store int, double and values, all of which the .NET Framework defines as IComparable types. The program populates the trees with the values in arrays intArray (line 12), doubleArray (lines 13–14) and stringArray (lines 15–16), respectively. string
1 2 3 4 5 6 7 8
// Fig. 24.24: TreeTest.cs // This program tests class Tree. using System; using BinaryTreeLibrary2; // class TreeTest declaration public class TreeTest {
Fig. 24.24 | Demonstrating class Tree with IComparable objects. (Part 1 of 3.)
// test class Tree static void Main( string[] args ) { int[] intArray = { 8, 2, 4, 3, 1, 7, 5, 6 }; double[] doubleArray = { 8.8, 2.2, 4.4, 3.3, 1.1, 7.7, 5.5, 6.6 }; string[] stringArray = { "eight", "two", "four", "three", "one", "seven", "five", "six" }; // create int Tree Tree intTree = new Tree(); populateTree( intArray, intTree, "intTree" ); traverseTree( intTree, "intTree" ); // create double Tree Tree doubleTree = new Tree(); populateTree( doubleArray, doubleTree, "doubleTree" ); traverseTree( doubleTree, "doubleTree" ); // create string Tree Tree stringTree = new Tree(); populateTree( stringArray, stringTree, "stringTree" ); traverseTree( stringTree, "stringTree" ); } // end Main // populate Tree with array elements static void populateTree( Array array, Tree tree, string name ) { Console.WriteLine( "\nInserting into " + name + ":" ); foreach ( IComparable data in array ) { Console.Write( data + " " ); tree.InsertNode( data ); } // end foreach } // end method populateTree // insert perform traversals static void traverseTree( Tree tree, string treeType ) { // perform preorder traveral of tree Console.WriteLine( "\n\nPreorder traversal of " + treeType ); tree.PreorderTraversal(); // perform inorder traveral of tree Console.WriteLine( "\n\nInorder traversal of " + treeType ); tree.InorderTraversal(); // perform postorder traveral of tree Console.WriteLine( "\n\nPostorder traversal of " + treeType ); tree.PostorderTraversal(); } // end method traverseTree } // end class TreeTest
Fig. 24.24 | Demonstrating class Tree with IComparable objects. (Part 2 of 3.)
1112
Chapter 24
Data Structures
Inserting into intTree: 8 2 4 3 1 7 5 6 Preorder traversal of intTree 8 2 1 4 3 7 5 6 Inorder traversal of intTree 1 2 3 4 5 6 7 8 Postorder traversal of intTree 1 3 6 5 7 4 2 8 Inserting into doubleTree: 8.8 2.2 4.4 3.3 1.1 7.7 5.5 6.6 Preorder traversal of doubleTree 8.8 2.2 1.1 4.4 3.3 7.7 5.5 6.6 Inorder traversal of doubleTree 1.1 2.2 3.3 4.4 5.5 6.6 7.7 8.8 Postorder traversal of doubleTree 1.1 3.3 6.6 5.5 7.7 4.4 2.2 8.8 Inserting into stringTree: eight two four three one seven five six Preorder traversal of stringTree eight two four five three one seven six Inorder traversal of stringTree eight five four one seven six three two Postorder traversal of stringTree five six seven one three four two eight
Fig. 24.24 | Demonstrating class Tree with IComparable objects. (Part 3 of 3.) Method PopulateTree (lines 35–44) receives as arguments an Array containing the initializer values for the Tree, a Tree in which the array elements will be placed and a string representing the Tree name, then inserts each Array element into the Tree. Method TraverseTree (lines 47–60) receives as arguments a Tree and a string representing the Tree name, then outputs the preorder, inorder and postorder traversals of the Tree. Note that the inorder traversal of each Tree outputs the data in sorted order regardless of the data type stored in the Tree. Our polymorphic implementation of class Tree invokes the appropriate data type’s CompareTo method to determine the path to each value’s insertion point by using the standard binary search tree insertion rules. Also, notice that the Tree of strings appears in alphabetical order.
24.8 Wrap-Up In this chapter, you learned that simple types are value-type structs, but can still be used anywhere objects are expected in a program due to boxing and unboxing conversions.
24.8 Wrap-Up
1113
You learned that linked lists are collections of data items that are “linked together in a chain.” You also learned that a program can perform insertions and deletions anywhere in a linked list (though our implementation only performed insertions and deletions at the ends of the list). We demonstrated that the stack and queue data structures are constrained versions of lists. For stacks, you saw that insertions and deletions are made only at the top—so stacks are known as last-in-first out (LIFO) data structures. For queues, which represent waiting lines, you saw that insertions are made at the tail and deletions are made from the head—so queues are known as first-in-first out (FIFO) data structures. We also presented the binary tree data structure. You saw a binary search tree that facilitated highspeed searching and sorting of data and efficient duplicate elimination. In the next chapter, we introduce generics, which allow you to declare a family of classes and methods that implement the same functionality on any type.
25 Generics …our special individuality, as distinguished from our generic humanity. —Oliver Wendell Holmes, Sr.
OBJECTIVES In this chapter you will learn:
Every man of genius sees the world at a different angle from his fellows.
I
To create generic methods that perform identical tasks on arguments of different types.
I
To create a generic Stack class that can be used to store objects of any class or interface type.
Born under one law, to another bound.
I
To understand how to overload generic methods with non-generic methods or with other generic methods.
—Lord Brooke
I
To understand the new() constraint of a type parameter.
I
To apply multiple constraints to a type parameter.
I
The relationship between generics and inheritance.
—Havelock Ellis
Outline
25.1 Introduction
25.1 25.2 25.3 25.4 25.5 25.6 25.7 25.8
1115
Introduction Motivation for Generic Methods Generic Method Implementation Type Constraints Overloading Generic Methods Generic Classes Notes on Generics and Inheritance Wrap-Up
25.1 Introduction In Chapter 24, we presented data structures that stored and manipulated object references. You could store any object in our data structures. One inconvenient aspect of storing object references occurs when retrieving them from a collection. An application usually needs to process specific types of objects. As a result, the object references obtained from a collection typically need to be downcast to an appropriate type to allow the application to process the objects correctly. In addition, data of value types (e.g., int and double) must be boxed to be manipulated with object references, which increases the overhead of processing such data. Also, processing all data as type object limits the C# compiler’s ability to perform type checking. Though we can easily create data structures that manipulate any type of data as objects (as we did in Chapter 24), it would be nice if we could detect type mismatches at compile time—this is known as compile-time type safety. For example, if a Stack should store only int values, attempting to push a string onto that Stack should cause a compile-time error. Similarly, a Sort method should be able to compare elements that are all guaranteed to have the same type. If we create type-specific versions of class Stack class and method Sort, the C# compiler would certainly be able to ensure compile-time type safety. However, this would require that we create many copies of the same basic code. This chapter discusses one of C#’s newest features—generics—which provides the means to create the general models mentioned above. Generic methods enable you to specify, with a single method declaration, a set of related methods. Generic classes enable you to specify, with a single class declaration, a set of related classes. Similarly, generic interfaces enable you to specify, with a single interface declaration, a set of related interfaces. Generics provide compile-time type safety. [Note: You can also implement generic structs and delegates. For more information, see the C# language specification.] We can write a generic method for sorting an array of objects, then invoke the generic method separately with an int array, a double array, a string array and so on, to sort each different type of array. The compiler performs type checking to ensure that the array passed to the sorting method contains only elements of the same type. We can write a single generic Stack class that manipulates a stack of objects, then instantiate Stack objects for a stack of ints, a stack of doubles, a stack of strings and so on. The compiler performs type checking to ensure that the Stack stores only elements of the same type. This chapter presents examples of generic methods and generic classes. It also considers the relationships between generics and other C# features, such as overloading and
1116
Chapter 25
Generics
inheritance. Chapter 26, Collections, discusses the .NET Framework’s generic and nongeneric collections classes. A collection is a data structure that maintains a group of related objects or values. The .NET Framework collection classes use generics to allow you to specify the exact types of object that a particular collection will store.
25.2 Motivation for Generic Methods Overloaded methods are often used to perform similar operations on different types of data. To motivate generic methods, let’s begin with an example (Fig. 25.1) that contains three overloaded PrintArray methods (lines 23–29, lines 32–38 and lines 41–47). These methods display the elements of an int array, a double array and a char array, respectively. Soon, we will reimplement this program more concisely and elegantly using a single generic method. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
// Fig. 25.1: OverloadedMethods.cs // Using overloaded methods to print arrays of different types. using System; class OverloadedMethods { static void Main( string[] args ) { // create arrays of int, double and char int[] intArray = { 1, 2, 3, 4, 5, 6 }; double[] doubleArray = { 1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7 }; char[] charArray = { 'H', 'E', 'L', 'L', 'O' }; Console.WriteLine( "Array intArray contains:" ); PrintArray( intArray ); // pass an int array argument Console.WriteLine( "Array doubleArray contains:" ); PrintArray( doubleArray ); // pass a double array argument Console.WriteLine( "Array charArray contains:" ); PrintArray( charArray ); // pass a char array argument } // end Main // output int array static void PrintArray( int[] inputArray ) { foreach ( int element in inputArray ) Console.Write( element + " " ); Console.WriteLine( "\n" ); } // end method PrintArray // output double array static void PrintArray( double[] inputArray ) { foreach ( double element in inputArray ) Console.Write( element + " " );
Fig. 25.1 | Displaying arrays of different types using overloaded methods. (Part 1 of 2.)
25.2 Motivation for Generic Methods
37 38 39 40 41 42 43 44 45 46 47 48
1117
Console.WriteLine( "\n" ); } // end method PrintArray // output char array static void PrintArray( char[] inputArray ) { foreach ( char element in inputArray ) Console.Write( element + " " ); Console.WriteLine( "\n" ); } // end method PrintArray } // end class OverloadedMethods
Array intArray contains: 1 2 3 4 5 6 Array doubleArray contains: 1.1 2.2 3.3 4.4 5.5 6.6 7.7 Array charArray contains: H E L L O
Fig. 25.1 | Displaying arrays of different types using overloaded methods. (Part 2 of 2.) The program begins by declaring and initializing three arrays—six-element int array (line 11) and five-element arrays. When the compiler encounters a method call, it attempts to locate a method declaration that has the same method name and parameters that match the argument types in the method call. In this example, each PrintArray call exactly matches one of the PrintArray method declarations. For example, line 15 calls PrintArray with intArray as its argument. At compile time, the compiler determines argument intArray’s type (i.e., int[]), attempts to locate a method named PrintArray that specifies a single int[] parameter (which it finds at lines 23–29) and sets up a call to that method. Similarly, when the compiler encounters the PrintArray call at line 17, it determines argument doubleArray’s type (i.e., double[]), then attempts to locate a method named PrintArray that specifies a single double[] parameter (which it finds at lines 32–38) and sets up a call to that method. Finally, when the compiler encounters the PrintArray call at line 19, it determines argument charArray’s type (i.e., char[]), then attempts to locate a method named PrintArray that specifies a single char[] parameter (which it finds at lines 41–47) and sets up a call to that method. Study each PrintArray method. Note that the array element type (int, double or char) appears in two locations in each method—the method header (lines 23, 32 and 41) and the foreach statement header (lines 25, 34 and 43). If we replace the element types in each method with a generic name—we chose E to represent the “element” type—then all three methods would look like the one in Fig. 25.2. It appears that if we can replace the array element type in each of the three methods with a single “generic type parameter,” then we should be able to declare one PrintArray method that can display the elements of any array. The method in Fig. 25.2 will not compile, because its syntax is not correct. We declare a generic PrintArray method with the proper syntax in Fig. 25.3. intArray (line 10), seven-element double array doubleArray char array charArray (line 12). Then, lines 14–19 output the
1118 1 2 3 4 5 6 7
Chapter 25
Generics
static void PrintArray( E[] inputArray ) { foreach ( E element in inputArray ) Console.Write( element + " " ); Console.WriteLine( "\n" ); } // end method PrintArray
Fig. 25.2 |
PrintArray
method in which actual type names are replaced by convention with
the generic name E.
25.3 Generic Method Implementation If the operations performed by several overloaded methods are identical for each argument type, the overloaded methods can be more compactly and conveniently coded using a generic method. You can write a single generic method declaration that can be called at different times with arguments of different types. Based on the types of the arguments passed to the generic method, the compiler handles each method call appropriately. Figure 25.3 reimplements the application of Fig. 25.1 using a generic PrintArray method (lines 24–30). Note that the PrintArray method calls in lines 16, 18 and 20 are identical to those of Fig. 25.1, the outputs of the two applications are identical and the code in Fig. 25.3 is 17 lines shorter than the code in Fig. 25.1. As illustrated in Fig. 25.3, generics enable us to create and test our code once, then reuse that code for many different types of data. This demonstrates the expressive power of generics. Line 24 begins method PrintArray’s declaration. All generic method declarations have a type parameter list delimited by angle brackets (< E > in this example) that follows 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
// Fig. 25.3: GenericMethod.cs // Using overloaded methods to print arrays of different types. using System; using System.Collections.Generic; class GenericMethod { static void Main( string[] args ) { // create arrays of int, double and char int[] intArray = { 1, 2, 3, 4, 5, 6 }; double[] doubleArray = { 1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7 }; char[] charArray = { 'H', 'E', 'L', 'L', 'O' }; Console.WriteLine( "Array intArray contains:" ); PrintArray( intArray ); // pass an int array argument Console.WriteLine( "Array doubleArray contains:" ); PrintArray( doubleArray ); // pass a double array argument Console.WriteLine( "Array charArray contains:" ); PrintArray( charArray ); // pass a char array argument } // end Main
Fig. 25.3 | Printing array elements using generic method PrintArray. (Part 1 of 2.)
25.3 Generic Method Implementation
23 24 25 26 27 28 29 30 31
1119
// output array of all types static void PrintArray< E >( E[] inputArray ) { foreach ( E element in inputArray ) Console.Write( element + " " ); Console.WriteLine( "\n" ); } // end method PrintArray } // end class GenericMethod
Array intArray contains: 1 2 3 4 5 6 Array doubleArray contains: 1.1 2.2 3.3 4.4 5.5 6.6 7.7 Array charArray contains: H E L L O
Fig. 25.3 | Printing array elements using generic method PrintArray. (Part 2 of 2.) the method’s name. Each type parameter list contains one or more type parameters, separated by commas. A type parameter is an identifier that is used in place of actual type names. The type parameters can be used to declare the return type, the parameter types and the local variable types in a generic method declaration; the type parameters act as placeholders for the types of the arguments passed to the generic method. A generic method’s body is declared like that of any other method. Note that the type parameter names throughout the method declaration must match those declared in the type parameter list. For example, line 26 declares element in the foreach statement as type E, which matches the type parameter (E) declared in line 24. Also, a type parameter can be declared only once in the type parameter list but can appear more than once in the method’s parameter list. Type parameter names need not be unique among different generic methods.
Common Programming Error 25.1 If you forget to include the type parameter list when declaring a generic method, the compiler will not recognize the type parameter names when they are encountered in the method. This results in compilation errors. 25.1
Method PrintArray’s type parameter list (line 24) declares type parameter E as the placeholder for the array element type that PrintArray will output. Note that E appears in the parameter list as the array element type (line 24). The foreach statement header (line 26) also uses E as the element type. These are the same two locations where the overloaded PrintArray methods of Fig. 25.1 specified int, double or char as the array element type. The remainder of PrintArray is identical to the version presented in Fig. 25.1.
Good Programming Practice 25.1 It is recommended that type parameters be specified as individual capital letters. Typically, a type parameter that represents the type of an element in an array (or other collection) is named E for “element” or T for “type.” 25.1
As in Fig. 25.1, the program of Fig. 25.3 begins by declaring and initializing six-element int array intArray (line 11), seven-element double array doubleArray (line 12) and
1120
Chapter 25
Generics
five-element char array charArray (line 13). Then each array is output by calling PrintArray (lines 16, 18 and 20)—once with argument intArray, once with argument doubleArray and once with argument charArray. When the compiler encounters a method call such as line 16, it analyzes the set of methods (both non-generic and generic) that might match the method call looking for a method that matches the call exactly. If there are no exact matches, the compiler picks the best match. If there are no matching methods, or if there is more than one best match, the compiler generates an error. The complete details of method call resolution can be found in Section 14.5.5.1 of the Ecma C# Language Specification www.ecma-international.org/publications/files/ECMA-ST/Ecma-334.pdf
or Section 20.9.7 of the Microsoft C# Language Specification 2.0 msdn.microsoft.com/vcsharp/programming/language/
In the case of line 16, the compiler determines that an exact match occurs if the type parameter E in lines 24 and 26 of method PrintArray’s declaration is replaced with the type of the elements in the method call’s argument intArray (i.e., int). Then, the compiler sets up a call to PrintArray with the int as the type argument for the type parameter E. This is known as the type inferencing process. The same process is repeated for the calls to method PrintArray in lines 18 and 20.
Common Programming Error 25.2 If the compiler cannot find a single non-generic or generic method declaration that is a best match for a method call, or if there are multiple best matches, a compilation error occurs. 25.2
You can also use explicit type arguments to indicate the exact type that should be used to call a generic function. For example, line 16 could be written as PrintArray< int >( intArray ); // pass an int array argument
The preceding method call explicitly provides the type argument (int) that should be used to replace type parameter E in lines 24 and 26 of the PrintArray method’s declaration. The compiler also determines whether the operations performed on the method’s type parameters can be applied to elements of the type stored in the array argument. The only operation performed on the array elements in this example is to output the string representation of the elements. Line 27 performs an implicit conversion for every value type array element and an implicit ToString call on every reference type array element. Since all objects have a ToString method, the compiler is satisfied that line 27 performs a valid operation for any array element. By declaring PrintArray as a generic method in Fig. 25.3, we eliminated the need for the overloaded methods of Fig. 25.1, saving 17 lines of code and creating a reusable method that can output the string representations of the elements in any array, not just arrays of int, double or char elements.
25.4 Type Constraints In this section, we present a generic Maximum method that determines and returns the largest of its three arguments (all of the same type). The generic method in this example uses
25.4 Type Constraints
1121
the type parameter to declare both the method’s return type and its parameters. Normally, when comparing values to determine which one is greater, you would use the > operator. However, this operator is not overloaded for use with every type that is built into the FCL or that might be defined by extending those types. Generic code is restricted to performing operations that are guaranteed to work for every possible type. Thus, an expression like variable1 < variable2 is not allowed unless the compiler can ensure that the operator < is provided for every type that will ever be used in the generic code. Similarly, you cannot call a method on a generic-type variable unless the compiler can ensure that all types that will ever be used in the generic code support that method. IComparable< E > Interface
It is possible to compare two objects of the same type if that type implements the generic interface IComparable< T > (of namespace System). A benefit of implementing interface IComparable< T > is that IComparable< T > objects can be used with the sorting and searching methods of classes in the System.Collections.Generic namespace—we discuss those methods in Chapter 26, Collections. The structures in the FCL that correspond to the simple types all implement this interface. For example, the structure for simple type double is Double and the structure for simple type int is Int32—both Double and Int32 implement the IComparable interface. Types that implement IComparable< T > must declare a CompareTo method for comparing objects. For example, if we have two ints, int1 and int2, they can be compared with the expression: int1.CompareTo( int2 )
Method CompareTo must return 0 if the objects are equal, a negative integer if int1 is less than int2 or a positive integer if int1 is greater than int2. It is the responsibility of the programmer who declares a type that implements IComparable< T > to declare method CompareTo such that it compares the contents of two objects of that type and returns the appropriate result.
Specifying Type Constraints Even though IComparable objects can be compared, they cannot be used with generic code by default, because not all types implement interface IComparable< T >. However, we can restrict the types that can be used with a generic method or class to ensure that they meet certain requirements. This feature—known as a type constraint—restricts the type of the argument supplied to a particular type parameter. Figure 25.4 declares method Maximum (lines 20–33) with a type constraint that requires each of the method’s arguments to be of type IComparable< T >. This restriction is important because not all objects can be compared. However, all IComparable< T > objects are guaranteed to have a CompareTo method that can be used in method Maximum to determine the largest of its three arguments. Generic method Maximum uses type parameter T as the return type of the method (line 20), as the type of method parameters x, y and z (line 20), and as the type of local variable max (line 22). Generic method Maximum’s where clause (after the parameter list in line 20) specifies the type constraint for type parameter T. In this case, the clause where T : IComparable< T > indicates that this method requires the type arguments to implement interface IComparable< T >. If no type constraint is specified, the default type constraint is object.
// Fig 25.4: MaximumTest.cs // Generic method maximum returns the largest of three objects. using System; class MaximumTest { static void Main( string[] args ) { Console.WriteLine( "Maximum of {0}, {1} and {2} is {3}\n", 3, 4, 5, Maximum( 3, 4, 5 ) ); Console.WriteLine( "Maximum of {0}, {1} and {2} is {3}\n", 6.6, 8.8, 7.7, Maximum( 6.6, 8.8, 7.7 ) ); Console.WriteLine( "Maximum of {0}, {1} and {2} is {3}\n", "pear", "apple", "orange", Maximum( "pear", "apple", "orange" ) ); } // end Main // generic function determines the // largest of the IComparable objects static T Maximum< T >( T x, T y, T z ) where T : IComparable < T > { T max = x; // assume x is initially the largest // compare y with max if ( y.CompareTo( max ) > 0 ) max = y; // y is the largest so far // compare z with max if ( z.CompareTo( max ) > 0 ) max = z; // z is the largest return max; // return largest object } // end method Maximum } // end class MaximumTest
Maximum of 3, 4 and 5 is 5 Maximum of 6.6, 8.8 and 7.7 is 8.8 Maximum of pear, apple and orange is pear
Fig. 25.4 | Generic method Maximum with a type constraint on its type parameter. C# provides several kinds of type constraints. A class constraint indicates that the type argument must be an object of a specific base class or one of its subclasses. An interface constraint indicates that the type argument’s class must implement a specific interface. The type constraint in line 20 is an interface constraint, because IComparable< T > is an interface. You can specify that the type argument must be a reference type or a value type by using the reference type constraint (class) or the value type constraint (struct), respectively. Finally, you can specify a constructor constraint—new()—to indicate that the generic code can use operator new to create new objects of the type represented by the type parameter. If a type parameter is specified with a constructor constraint, the type
25.5 Overloading Generic Methods
1123
argument’s class must provide public a parameterless or default constructor to ensure that objects of the class can be created without passing constructor arguments; otherwise, a compilation error occurs. It is possible to apply multiple constraints to a type parameter. To do so, simply provide a comma-separated list of constraints in the where clause. If you have a class constraint, reference type constraint or value type constraint, it must be listed first—only one of these types of constraints can be used for each type parameter. Interface constraints (if any) are listed next. The constructor constraint is listed last (if there is one).
Analyzing the Code Method Maximum assumes that its first argument (x) is the largest and assigns it to local variable max (line 22). Next, the if statement at lines 25–26 determines whether y is greater than max. The condition invokes y’s CompareTo method with the expression y.CompareTo( max ). If y is greater than max, then y is assigned to variable max (line 26). Similarly, the statement at lines 29–30 determines whether z is greater than max. If so, line 30 assigns z to max. Then, line 32 returns max to the caller. In Main (lines 7–16), line 10 calls Maximum with the integers 3, 4 and 5. Generic method Maximum is a match for this call, but its arguments must implement interface IComparable< T > to ensure that they can be compared. Type int is a synonym for struct Int32, which implements interface IComparable< int >. Thus, ints (and other simple types) are valid arguments to method Maximum. Line 12 passes three double arguments to Maximum. Again, this is allowed because double is a synonym for the Double struct, which implements IComparable< double >. Line 15 passes Maximum three strings, which are also IComparable< string > objects. Note that we intentionally placed the largest value in a different position in each method call (lines 10, 12 and 15) to show that the generic method always finds the maximum value, regardless of its position in the argument list and regardless of the inferred type argument.
25.5 Overloading Generic Methods A generic method may be overloaded. A class can provide two or more generic methods with the same name but different method parameters. For example, we could provide a second version of generic method PrintArray (Fig. 25.3) with the additional parameters lowIndex and highIndex that specify the portion of the array to output. A generic method can also be overloaded by another generic method with the same method name and a different number of type parameters, or by a generic method with different numbers of type parameters and method parameters. A generic method can be overloaded by non-generic methods that have the same method name and number of parameters. When the compiler encounters a method call, it searches for the method declaration that most precisely matches the method name and the argument types specified in the call. For example, generic method PrintArray of Fig. 25.3 could be overloaded with a version specific to strings that outputs the strings in neat, tabular format. If the compiler cannot match a method call to either a non-generic method or a generic method, or if there is ambiguity due to multiple possible matches, the compiler generates an error. Generic methods can also be overloaded by non-generic methods that have the same method name but a different number of method parameters.
1124
Chapter 25
Generics
25.6 Generic Classes The concept of a data structure (e.g., a stack), that contains data elements can be understood independently of the element type it manipulates. A generic class provides a means for describing a class in a type-independent manner. We can then instantiate type-specific objects of the generic class. This capability is an opportunity for software reusability. Once you have a generic class, you can use a simple, concise notation to indicate the actual type(s) that should be used in place of the class’s type parameter(s). At compilation time, the compiler ensures the type safety of your code, and the runtime system replaces type parameters with actual arguments to enable your client code to interact with the generic class. One generic Stack class, for example, could be the basis for creating many Stack classes (e.g., “Stack of double,” “Stack of int,” “Stack of char,” “Stack of Employee”). Figure 25.5 presents a generic Stack class declaration. A generic class declaration is similar to a non-generic class declaration, except that the class name is followed by a type parameter list (line 5) and, in this case, a constraint on its type parameter (which we will discuss shortly). Type parameter E represents the element type the Stack will manipulate. As with generic methods, the type parameter list of a generic class can have one or more type parameters separated by commas. Type parameter E is used throughout the Stack class declaration (Fig. 25.5) to represent the element type. Class Stack declares variable elements as an array of type E (line 8). This array (created at line 20 or 22) will store the Stack’s elements. [Note: This example implements a Stack as an array. As you have seen in Chapter 24, Data Structures, Stacks are commonly implemented also as linked lists.]
// Fig. 25.5: Stack.cs // Generic class Stack using System; class Stack< E > { private int top; // location of the top element private E[] elements; // array that stores Stack elements // parameterless constructor creates a Stack of the default size public Stack() : this( 10 ) // default stack size { // empty constructor; calls constructor at line 17 to perform init } // end Stack constructor // constructor creates a Stack of the specified number of elements public Stack( int stackSize ) { if ( stackSize > 0 ) // validate stackSize elements = new E[ stackSize ]; // create stackSize elements else elements = new E[ 10 ]; // create 10 elements
Fig. 25.5 | Generic class Stack declaration. (Part 1 of 2.)
top = -1; // Stack initially empty } // end Stack constructor // push element onto the Stack; if successful, return true // otherwise, throw FullStackException public void Push( E pushValue ) { if ( top == elements.Length - 1 ) // Stack is full throw new FullStackException( String.Format( "Stack is full, cannot push {0}", pushValue ) ); top++; // increment top elements[ top ] = pushValue; // place pushValue on Stack } // end method Push // return the top element if not empty // else throw EmptyStackException public E Pop() { if ( top == -1 ) // Stack is empty throw new EmptyStackException( "Stack is empty, cannot pop" ); top--; // decrement top return elements[ top + 1 ]; // return top value } // end method Pop } // end class Stack
Fig. 25.5 | Generic class Stack declaration. (Part 2 of 2.) Class Stack has two constructors. The parameterless constructor (lines 11–14) passes the default stack size (10) to the one-argument constructor, using the syntax this (line 11) to invoke another constructor in the same class. The one-argument constructor (lines 17– 25) validates the stackSize argument and creates an array of the specified stackSize (if it is greater than 0) or an array of 10 elements, otherwise. Method Push (lines 29–37) first determines whether an attempt is being made to push an element onto a full Stack. If so, lines 32–33 throw a FullStackException (declared in Fig. 25.6). If the Stack is not full, line 35 increments the top counter to indicate the new top position, and line 36 places the argument in that location of array elements. Method Pop (lines 41–48) first determines whether an attempt is being made to pop an element from an empty Stack. If so, line 44 throws an EmptyStackException (declared in Fig. 25.7). Otherwise, line 46 decrements the top counter to indicate the new top position, and line 47 returns the original top element of the Stack. Classes FullStackException (Fig. 25.6) and EmptyStackException (Fig. 25.7) each provide a parameterless constructor and a one-argument constructor of exception classes (as discussed in Section 12.8). The parameterless constructor sets the default error message, and the one-argument constructor sets a custom error message. As with generic methods, when a generic class is compiled, the compiler performs type checking on the class’s type parameters to ensure that they can be used with the code in the generic class. The constraints determine the operations that can be performed on the type parameters. The runtime system replaces the type parameters with the actual types at
1126 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
Generics
// Fig. 25.6: FullStackException.cs // Indicates a stack is full. using System; class FullStackException : ApplicationException { // parameterless constructor public FullStackException() : base( "Stack is full" ) { // empty constructor } // end FullStackException constructor // one-parameter constructor public FullStackException( string exception ) : base( exception ) { // empty constructor } // end FullStackException constructor } // end class FullStackException
// Fig. 25.7: EmptyStackException.cs // Indicates a stack is empty using System; class EmptyStackException : ApplicationException { // parameterless constructor public EmptyStackException() : base( "Stack is empty" ) { // empty constructor } // end EmptyStackException constructor // one-parameter constructor public EmptyStackException( string exception ) : base( exception ) { // empty constructor } // end EmptyStackException constructor } // end class EmptyStackException
Fig. 25.7 |
EmptyStackException
class declaration.
runtime. For class Stack (Fig. 25.5), no type constraint is specified, so the default type constraint, object, is used. The scope of a generic class’s type parameter is the entire class. Now, let’s consider an application (Fig. 25.8) that uses the Stack generic class. Lines 13–14 declare variables of type Stack< double > (pronounced “Stack of double”) and Stack< int > (pronounced “Stack of int”). The types double and int are the Stack’s type arguments. The compiler replaces the type parameters in the generic class so that the compiler can perform type checking. Method Main instantiates objects doubleStack of size 5 (line 18) and intStack of size 10 (line 19), then calls methods TestPushDouble (lines 28–48), TestPopDouble (lines 51–73), TestPushInt (lines 76–96) and TestPopInt (lines 99–121) to manipulate the two Stacks in this example.
// pop elements from stack try { Console.WriteLine( "\nPopping elements from doubleStack" ); double popValue; // store element removed from stack // remove all elements from Stack while ( true ) { popValue = doubleStack.Pop(); // pop from doubleStack Console.Write( "{0:F1} ", popValue ); } // end while } // end try catch ( EmptyStackException exception ) { Console.Error.WriteLine(); Console.Error.WriteLine( "Message: " + exception.Message ); Console.Error.WriteLine( exception.StackTrace ); } // end catch } // end method TestPopDouble // test Push method with intStack static void TestPushInt() { // push elements onto stack try { Console.WriteLine( "\nPushing elements onto intStack" ); // push elements onto stack foreach ( int element in intElements ) { Console.Write( "{0} ", element ); intStack.Push( element ); // push onto intStack } // end foreach } // end try catch ( FullStackException exception ) { Console.Error.WriteLine(); Console.Error.WriteLine( "Message: " + exception.Message ); Console.Error.WriteLine( exception.StackTrace ); } // end catch } // end method TestPushInt // test Pop method with intStack static void TestPopInt() { // pop elements from stack try { Console.WriteLine( "\nPopping elements from intStack" );
Fig. 25.8 | Generic class Stack test program. (Part 2 of 3.)
25.6 Generic Classes
1129
105 106 int popValue; // store element removed from stack 107 108 // remove all elements from Stack 109 while ( true ) 110 { 111 popValue = intStack.Pop(); // pop from intStack 112 Console.Write( "{0} ", popValue ); 113 } // end while 114 } // end try 115 catch ( EmptyStackException exception ) 116 { 117 Console.Error.WriteLine(); 118 Console.Error.WriteLine( "Message: " + exception.Message ); 119 Console.Error.WriteLine( exception.StackTrace ); 120 } // end catch 121 } // end method TestPopInt 122 } // end class StackTest Pushing elements onto doubleStack 1.1 2.2 3.3 4.4 5.5 6.6 Message: Stack is full, cannot push 6.6 at Stack`1.Push(E pushValue) in C:\Examples\ch25\Fig25_05\Stack\Stack.cs:line 32 at StackTest.TestPushDouble() in C:\Examples\ch25\Fig25_05\Stack\StackTest.cs:line 39 Popping elements from doubleStack 5.5 4.4 3.3 2.2 1.1 Message: Stack is empty, cannot pop at Stack`1.Pop() in C:\Examples\ch25\Fig25_05\Stack\Stack.cs:line 44 at StackTest.TestPopDouble() in C:\Examples\ch25\Fig25_05\Stack\StackTest.cs:line 63 Pushing elements onto intStack 1 2 3 4 5 6 7 8 9 10 11 Message: Stack is full, cannot push 11 at Stack`1.Push(E pushValue) in C:\Examples\ch25\Fig25_05\Stack\Stack.cs:line 32 at StackTest.TestPushInt() in C:\Examples\ch25\Fig25_05\Stack\StackTest.cs:line 87 Popping elements from intStack 10 9 8 7 6 5 4 3 2 1 Message: Stack is empty, cannot pop at Stack`1.Pop() in C:\Examples\ch25\Fig25_05\Stack\Stack.cs:line 44 at StackTest.TestPopInt() in C:\Examples\ch25\Fig25_05\Stack\StackTest.cs:line 111
Fig. 25.8 | Generic class Stack test program. (Part 3 of 3.) Method TestPushDouble (lines 28–48) invokes method Push to place the double values 1.1, 2.2, 3.3, 4.4 and 5.5 stored in array doubleElements onto doubleStack. The for statement terminates when the test program attempts to Push a sixth value onto doubleStack (which is full, because doubleStack can store only five elements). In this case, the method throws a FullStackException (Fig. 25.6) to indicate that the Stack is full.
1130
Chapter 25
Generics
Lines 42–47 catch this exception, and print the message and stack trace information. The stack trace indicates the exception that occurred and shows that Stack method Push generated the exception at lines 34–35 of the file Stack.cs (Fig. 25.5). The trace also shows that method Push was called by StackTest method TestPushDouble at line 39 of StackTest.cs. This information enables you to determine the methods that were on the method call stack at the time that the exception occurred. Because the program catches the exception, the C# runtime environment considers the exception to have been handled, and the program can continue executing. Method TestPopDouble (lines 51–73) invokes Stack method Pop in an infinite while loop to remove all the values from the stack. Note in the output that the values are popped off in last-in-first-out order—this, of course, is the defining characteristic of stacks. The while loop (lines 61–65) continues until the stack is empty. An EmptyStackException occurs when an attempt is made to pop from the empty stack. This causes the program to proceed to the catch block (lines 67–72) and handle the exception, so the program can continue executing. When the test program attempts to Pop a sixth value, the doubleStack is empty, so method Pop throws an EmptyStackException. Method TestPushInt (lines 76–96) invokes Stack method Push to place values onto intStack until it is full. Method TestPopInt (lines 99–121) invokes Stack method Pop to remove values from intStack until it is empty. Once again, note that the values pop off in last-in-first-out order.
Creating Generic Methods to Test Class Stack< E > Note that the code in methods TestPushDouble and TestPushInt is almost identical for pushing values onto a Stack< double > or a Stack< int >, respectively. Similarly the code in methods TestPopDouble and TestPopInt is almost identical for popping values from a Stack< double > or a Stack< int >, respectively. This presents another opportunity to use generic methods. Figure 25.9 declares generic method TestPush (lines 32–53) to perform the same tasks as TestPushDouble and TestPushInt in Fig. 25.8—that is, Push values onto a Stack< E > . Similarly, generic method TestPop (lines 55–77) performs the same tasks as TestPopDouble and TestPopInt in Fig. 25.8—that is, Pop values off a Stack< E >. Note that the output of Fig. 25.9 precisely matches the output of Fig. 25.8.
1 2 3 4 5 6 7 8 9 10 11 12
// Fig 25.9: StackTest.cs // Stack generic class test program. using System; using System.Collections.Generic; class StackTest { // create arrays of doubles and ints static double[] doubleElements = new double[]{ 1.1, 2.2, 3.3, 4.4, 5.5, 6.6 }; static int[] intElements = new int[]{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 };
Fig. 25.9 | Passing a generic type Stack to a generic method. (Part 1 of 3.)
static Stack< double >doubleStack; // stack stores double objects static Stack< int >intStack; // stack stores int objects static void Main( string[] args ) { doubleStack = new Stack< double >( 5 ); // Stack of doubles intStack = new Stack< int >( 10 ); // Stack of ints // push doubles onto doubleStack TestPush( "doubleStack", doubleStack, doubleElements ); // pop doubles from doubleStack TestPop( "doubleStack", doubleStack ); // push ints onto intStack TestPush( "intStack", intStack, intElements ); // pop ints from intStack TestPop( "intStack", intStack ); } // end Main static void TestPush< E >( string name, Stack< E > stack, IEnumerable< E > elements ) { // push elements onto stack try { Console.WriteLine( "\nPushing elements onto " + name ); // push elements onto stack foreach ( E element in elements ) { Console.Write( "{0} ", element ); stack.Push( element ); // push onto stack } // end foreach } // end try catch ( FullStackException exception ) { Console.Error.WriteLine(); Console.Error.WriteLine( "Message: " + exception.Message ); Console.Error.WriteLine( exception.StackTrace ); } // end catch } // end method TestPush static void TestPop< E >( string name, Stack< E > stack ) { // push elements onto stack try { Console.WriteLine( "\nPopping elements from " + name ); E popValue; // store element removed from stack
Fig. 25.9 | Passing a generic type Stack to a generic method. (Part 2 of 3.)
1132 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78
Chapter 25
Generics
// remove all elements from Stack while ( true ) { popValue = stack.Pop(); // pop from stack Console.Write( "{0} ", popValue ); } // end while } // end try catch ( EmptyStackException exception ) { Console.Error.WriteLine(); Console.Error.WriteLine( "Message: " + exception.Message ); Console.Error.WriteLine( exception.StackTrace ); } // end catch } // end TestPop } // end class StackTest
Pushing elements onto doubleStack 1.1 2.2 3.3 4.4 5.5 6.6 Message: Stack is full, cannot push 6.6 at Stack`1.Push(E pushValue) in C:\Examples\ch25\Fig25_05\Stack\Stack.cs:line 35 at StackTest.TestPush[E](String name, Stack`1 stack, IEnumerable`1 elements) in C:\Examples\ch25\Fig25_05\Stack\StackTest.cs:line 44 Popping elements from doubleStack 5.5 4.4 3.3 2.2 1.1 Message: Stack is empty, cannot pop at Stack`1.Pop() in C:\Examples\ch25\Fig25_05\Stack\Stack.cs:line 49 at StackTest.TestPop[E](String name, Stack`1 stack) in C:\Examples\ch25\Fig25_05\Stack\StackTest.cs:line 67 Pushing elements onto intStack 1 2 3 4 5 6 7 8 9 10 11 Message: Stack is full, cannot push 11 at Stack`1.Push(E pushValue) in C:\Examples\ch25\Fig25_05\Stack\Stack.cs:line 35 at StackTest.TestPush[E](String name, Stack`1 stack, IEnumerable`1 elements) in C:\Examples\ch25\Fig25_05\Stack\StackTest.cs:line 44 Popping elements from intStack 10 9 8 7 6 5 4 3 2 1 Message: Stack is empty, cannot pop at Stack`1.Pop() in C:\Examples\ch25\Fig25_05\Stack\Stack.cs:line 49 at StackTest.TestPop[E](String name, Stack`1 stack) in C:\Examples\ch25\Fig25_05\Stack\StackTest.cs:line 67
Fig. 25.9 | Passing a generic type Stack to a generic method. (Part 3 of 3.) Method Main (lines 17–30) creates the Stack< double > (line 19) and Stack< int > (line 20) objects. Lines 23–29 invoke generic methods TestPush and TestPop to test the Stack objects. Generic method TestPush (lines 32–53) uses type parameter E (specified at line 32) to represent the data type stored in the Stack. The generic method takes three arguments—a string that represents the name of the Stack object for output purposes, an
25.7 Notes on Generics and Inheritance
1133
object of type Stack< E > and an IEnumerable< E >—the type of elements that will be Pushed onto Stack< E >. Note that the compiler enforces consistency between the type of the Stack and the elements that will be pushed onto the Stack when Push is invoked, which is the type argument of the generic method call. Generic method TestPop (lines 55– 77) takes two arguments—a string that represents the name of the Stack object for output purposes and an object of type Stack< E >. Notice that the type parameter E used in both TestPush and TestPop methods has a new constraint, because both methods take an object of type Stack< E > and the type parameter in the Stack generic class declaration has a new constraint (line 5 in Fig. 25.5). This ensures that the objects that TestPush and TestPop manipulate can be used with generic Stack class, which requires objects stored in the Stack to have a default or parameterless public constructor.
25.7 Notes on Generics and Inheritance Generics can be used with inheritance in several ways: •
A generic class can be derived from a non-generic class. For example, class object (which is not a generic class) is a direct or indirect base class of every generic class.
•
A generic class can be derived from another generic class. Recall that in Chapter 24, the non-generic Stack class (Fig. 24.12) inherits from the non-generic List class (Fig. 24.5). You could also create a generic Stack class by inheriting from a generic List class.
•
A non-generic class can be derived from a generic class. For example, you can implement a non-generic AddressList class that inherits from a generic List class and stores only Address objects.
25.8 Wrap-Up This chapter introduced generics—one of C#’s newest capabilities. We discussed how generics ensure compile-time type safety by checking for type mismatches at compile time. You learned that the compiler will allow generic code to compile only if all operations performed on the type parameters in the generic code are supported for all types that could be used with the generic code. You also learned how to declare generic methods and classes using type parameters. We demonstrated how to use a type constraint to specify the requirements for a type parameter—a key component of compile-time type safety. We discussed several kinds of type constraints, including reference type constraints, value type constraints, class constraints, interface constraints and constructor constraints. You learned that a constructor constraint indicates that the type argument must provide a public parameterless or default constructor so that objects of that type can be created with new. We also discussed how to implement multiple type constraints for a type parameter. We showed how generics improve code reuse. Finally, we mentioned several ways to use generics in inheritance. In the next chapter, we demonstrate the .NET FCL’s collection classes, interfaces and algorithms. Collection classes are pre-built data structures that you can reuse in your applications, saving you time. We present both generic collections and the older, non-generic collections.
26 Collections The shapes a bright container can contain! —Theodore Roethke
OBJECTIVES
Not by age but by capacity is wisdom acquired.
In this chapter you will learn:
—Titus Maccius Plautus
I
The non-generic and generic collections that are provided by the .NET Framework.
I
To use class Array’s static methods to manipulate arrays.
I
To use enumerators to “walk through” a collection.
I
To use the foreach statement with the .NET collections.
I
To use non-generic collection classes ArrayList, Stack and Hashtable.
I
To use generic collection classes SortedDictionary and LinkedList.
I
To use synchronization wrappers to make collections safe in multithreaded applications.
It is a riddle wrapped in a mystery inside an enigma. —Winston Churchill
I think this is the most extraordinary collection of talent, of human knowledge, that has ever been gathered together at the White House—with the possible exception of when Thomas Jefferson dined alone. —John F. Kennedy
Outline
26.1 Introduction
1135
26.1 26.2 26.3 26.4
Introduction Collections Overview Class Array and Enumerators Non-Generic Collections 26.4.1 Class ArrayList 26.4.2 Class Stack 26.4.3 Class Hashtable 26.5 Generic Collections 26.5.1 Generic Class SortedDictionary 26.5.2 Generic Class LinkedList 26.6 Synchronized Collections 26.7 Wrap-Up
26.1 Introduction In Chapter 24, we discussed how to create and manipulate data structures. The discussion was “low level,” in the sense that we painstakingly created each element of each data structure dynamically with new and modified the data structures by directly manipulating their elements and references to their elements. In this chapter, we consider the prepackaged data-structure classes provided by the .NET Framework. These classes are known as collection classes—they store collections of data. Each instance of one of these classes is a collection of items. Some examples of collections are the cards you hold in a card game, the songs stored in your computer, the real-estate records in your local registry of deeds (which map book numbers and page numbers to property owners), and the players on your favorite sports team. Collection classes enable programmers to store sets of items by using existing data structures, without concern for how they are implemented. This is a nice example of code reuse. Programmers can code faster and expect excellent performance, maximizing execution speed and minimizing memory consumption. In this chapter, we discuss the collection interfaces that list the capabilities of each collection type, the implementation classes and the enumerators that “walk through” collections. The .NET Framework provides three namespaces dedicated to collections. The System.Collections namespace contains collections that store references to objects. The newer System.Collections.Generic namespace contains generic classes to store collections of specified types. The newer System.Collections.Specialized namespace contains several collections that support specific types, such as strings and bits. You can learn more about this namespace at msdn2.microsoft.com/en-us/library/ system.collections.specialized.aspx . The collections in these namespaces provide standardized, reusable components; you do not need to write your own collection classes. These collections are written for broad reuse. They are tuned for rapid execution and for efficient use of memory. As new data structures and algorithms are developed that fit this framework, a large base of programmers already will be familiar with the interfaces and algorithms implemented by those data structures.
1136
Chapter 26
Collections
26.2 Collections Overview All collection classes in the .NET Framework implement some combination of the collection interfaces. These interfaces declare the operations to be performed generically on various types of collections. Figure 26.1 lists some of the interfaces of the .NET Framework collections. All the interfaces in Fig. 26.1 are declared in namespace System.Collections and have generic analogues in namespace System.Collections.Generic. Implementations of these interfaces are provided within the framework. Programmers may also provide implementations specific to their own requirements. In earlier versions of C#, the .NET Framework primarily provided the collection classes in the System.Collections and System.Collections.Specialized namespaces. These classes stored and manipulated object references. You could store any object in a collection. One inconvenient aspect of storing object references occurs when retrieving them from a collection. An application normally needs to process specific types of objects. As a result, the object references obtained from a collection typically need to be downcast to an appropriate type to allow the application to process the objects correctly. The .NET Framework 2.0 now also includes the System.Collections.Generic namespace, which uses the generics capabilities we introduced in Chapter 25. Many of these new classes are simply generic counterparts of the classes in namespace System.Collections. This means that you can specify the exact type that will be stored in a collection. You also receive the benefits of compile-time type checking—the compiler ensures that you are using appropriate types with your collection and, if not, issues compile-time error messages. Also, once you specify the type stored in a collection, any item you retrieve from the collection will have the correct type. This eliminates the need for explicit type casts that can throw InvalidCastExceptions at execution time if the referenced object is not of the appropriate type. This also eliminates the overhead of explicit casting, improving efficiency. Interface
Description
ICollection
The root interface in the collections hierarchy from which interfaces IList and IDictionary inherit. Contains a Count property to determine the size of a collection and a CopyTo method for copying a collection’s contents into a traditional array.
IList
An ordered collection that can be manipulated like an array. Provides an indexer for accessing elements with an int index. Also has methods for searching and modifying a collection, including Add, Remove, Contains and IndexOf.
IDictionary
A collection of values, indexed by an arbitrary “key” object. Provides an indexer for accessing elements with an object index and methods for modifying the collection (e.g. Add, Remove). IDictionary property Keys contains the objects used as indices, and property Values contains all the stored objects.
IEnumerable
An object that can be enumerated. This interface contains exactly one method, GetEnumerator, which returns an IEnumerator object (discussed in Section 26.3). ICollection implements IEnumerable, so all collection classes implement IEnumerable directly or indirectly.
Fig. 26.1 | Some common collection interfaces.
26.2 Collections Overview
1137
In this chapter, we demonstrate six collection classes—Array, ArrayList, Stack, Hashtable, generic SortedDictionary and generic LinkedList—plus built-in array capabilities. Namespace System.Collections provides several other data structures, including BitArray (a collection of true/false values), Queue and SortedList (a collection of key/ value pairs that are sorted by key and can be accessed either by key or by index). Figure 26.2 summarizes many of the collection classes. We also discuss the IEnumerator interface. Collection classes can create enumerators that allow programmers to walk through the collections. Although these enumerators have different implementations, they all implement the IEnumerator interface so that they can be processed polymorphically. As we will soon see, the foreach statement is simply a convenient notation for using an enumerator. In the next section, we begin our discussion by examining enumerators and the collections capabilities for array manipulation. Class System
Implements
Description
IList
The base class of all conventional arrays. See Section 26.3.
namespace:
Array
System.Collections
namespace:
ArrayList
IList
Mimics conventional arrays, but will grow or shrink as needed to accommodate the number of elements. See Section 26.4.1.
BitArray
ICollection
A memory-efficient array of bools.
Hashtable
IDictionary
An unordered collection of key–value pairs that can be accessed by key. See Section 26.4.3.
Queue
ICollection
A first-in first-out collection. See Section 24.6.
SortedList
IDictionary
A generic Hashtable that sorts data by keys and can be accessed either by key or by index.
Stack
ICollection
A last-in, first-out collection. See Section 26.4.2.
System.Collections.Generic
namespace:
Dictionary< K, E >
IDictionary< K, E >
A generic, unordered collection of key– value pairs that can be accessed by key.
LinkedList< E >
ICollection< E >
A doubly linked list. See Section 26.5.2.
List< E >
IList< E >
A generic ArrayList.
Fig. 26.2 | Some collection classes of the .NET Framework. (Part 1 of 2.)
1138
Chapter 26
Collections
Class
Implements
Description
Queue< E >
ICollection< E >
A generic Queue.
SortedDictionary<
IDictionary< K, E >
A Dictionary that sorts the data by the keys in a binary tree. See Section 26.5.1.
SortedList< K, E >
IDictionary< K, E >
A generic SortedList.
Stack< E >
ICollection< E >
A generic Stack.
K, E >
[Note: All collection classes directly or indirectly implement ICollection and IEnumerable (or the equivalent generic interfaces ICollection< E > and IEnumerable< E > for generic collections).]
Fig. 26.2 | Some collection classes of the .NET Framework. (Part 2 of 2.)
26.3 Class Array and Enumerators Chapter 8 presented basic array-processing capabilities. All arrays implicitly inherit from abstract base class Array (namespace System), which defines property Length, which specifies the number of elements in the array. In addition, class Array provides static methods that provide algorithms for processing arrays. Typically, class Array overloads these methods—for example, Array method Reverse can reverse the order of the elements in an entire array or can reverse the elements in a specified range of elements in an array. For a complete list of class Array’s static methods visit: msdn2.microsoft.com/en-us/library/system.array.aspx
Figure 26.3 demonstrates several static methods of class Array. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
// Fig. 26.3: UsingArray.cs // Array class static methods for common array manipulations. using System; using System.Collections; // demonstrate algorithms of class Array public class UsingArray { private static int[] intValues = { 1, 2, 3, 4, 5, 6 }; private static double[] doubleValues = { 8.4, 9.3, 0.2, 7.9, 3.4 }; private static int[] intValuesCopy; // method Main demonstrates class Array's methods public static void Main( string[] args ) { intValuesCopy = new int[ intValues.Length ]; // defaults to zeroes
// sort doubleValues Array.Sort( doubleValues ); // copy intValues into intValuesCopy Array.Copy( intValues, intValuesCopy, intValues.Length ); Console.WriteLine( "\nArray values after Sort and Copy:\n" ); PrintArrays(); // output array contents Console.WriteLine(); // search for 5 in intValues int result = Array.BinarySearch( intValues, 5 ); if ( result >= 0 ) Console.WriteLine( "5 found at element {0} in intValues", result ); else Console.WriteLine( "5 not found in intValues" ); // search for 8763 in intValues result = Array.BinarySearch( intValues, 8763 ); if ( result >= 0 ) Console.WriteLine( "8763 found at element {0} in intValues", result ); else Console.WriteLine( "8763 not found in intValues" ); } // end method Main // output array content with enumerators private static void PrintArrays() { Console.Write( "doubleValues: " );
Fig. 26.3 |
// iterate through the double array with an enumerator IEnumerator enumerator = doubleValues.GetEnumerator(); while ( enumerator.MoveNext() ) Console.Write( enumerator.Current + " " ); Console.Write( "\nintValues: " ); // iterate through the int array with an enumerator enumerator = intValues.GetEnumerator(); while ( enumerator.MoveNext() ) Console.Write( enumerator.Current + " " ); Console.Write( "\nintValuesCopy: " ); // iterate through the second int array with a foreach statement foreach ( int element in intValuesCopy ) Console.Write( element + " " );
Array
class used to perform common array manipulations. (Part 2 of 3.)
1140 73 74 75
Chapter 26
Collections
Console.WriteLine(); } // end method PrintArrays } // end class UsingArray
Initial array values: doubleValues: 8.4 9.3 0.2 7.9 3.4 intValues: 1 2 3 4 5 6 intValuesCopy: 0 0 0 0 0 0 Array values after Sort and Copy: doubleValues: 0.2 3.4 7.9 8.4 9.3 intValues: 1 2 3 4 5 6 intValuesCopy: 1 2 3 4 5 6 5 found at element 4 in intValues 8763 not found in intValues
Fig. 26.3 |
Array
class used to perform common array manipulations. (Part 3 of 3.)
The using directives in lines 3–4 include the namespaces System (for classes Array and Console) and System.Collections (for interface IEnumerator, which we discuss shortly). References to the assemblies for these namespaces are implicitly included in every application, so we do not need to add any new references to the project file. Our test class declares three static array variables (lines 9–11). The first two lines initialize intValues and doubleValues to an int and double array, respectively. Static variable intValuesCopy is intended to demonstrate the Array’s Copy method, so it is left with the default value null—it does not yet refer to an array. Line 16 initializes intValuesCopy to an int array with the same length as array intValues. Line 19 calls the PrintArrays method (lines 49–74) to output the initial contents of all three arrays. We discuss the PrintArrays method shortly. We can see from the output of Fig. 26.3 that each element of array intValuesCopy is initialized to the default value 0. Line 22 uses static Array method Sort to sort array doubleValues. When this method returns, the array contains its original elements sorted in ascending order. Line 25 uses static Array method Copy to copy elements from array intValues to array intValuesCopy. The first argument is the array to copy (intValues), the second argument is the destination array (intValuesCopy) and the third argument is an int representing the number of elements to copy (in this case, intValues.Length specifies all elements). Lines 32 and 40 invoke static Array method BinarySearch to perform binary searches on array intValues. Method BinarySearch receives the sorted array in which to search and the key for which to search. The method returns the index in the array at which it finds the key (or a negative number if the key was not found). Notice that BinarySearch assumes that it receives a sorted array. Its behavior on an unsorted array is unpredictable.
Common Programming Error 26.1 Passing an unsorted array to BinarySearch is a logic error—the value returned is undefined.
26.1
26.3 Class Array and Enumerators
1141
The PrintArrays method (lines 49–74) uses class Array’s methods to loop though each array. In line 54, the GetEnumerator method obtains an enumerator for array intValues. Recall that Array implements the IEnumerable interface. All arrays inherit implicitly from Array, so both the int[] and double[] array types implement IEnumerable interface method GetEnumerator, which returns an enumerator that can iterate over the collection. Interface IEnumerator (which all enumerators implement) defines methods MoveNext and Reset and property Current. MoveNext moves the enumerator to the next element in the collection. The first call to MoveNext positions the enumerator at the first element of the collection. MoveNext returns true if there is at least one more element in the collection; otherwise, the method returns false. Method Reset positions the enumerator before the first element of the collection. Methods MoveNext and Reset throw an InvalidOperationException if the contents of the collection are modified in any way after the enumerator is created. Property Current returns the object at the current location in the collection.
Common Programming Error 26.2 If a collection is modified after an enumerator is created for that collection, the enumerator immediately becomes invalid—any methods called with the enumerator after this point throw InvalidOperationExceptions. For this reason, enumerators are said to be “fail fast.” 26.2
When an enumerator is returned by the GetEnumerator method in line 54, it is initially positioned before the first element in Array doubleValues. Then when line 56 calls MoveNext in the first iteration of the while loop, the enumerator advances to the first element in doubleValues. The while statement in lines 56–57 loops over each element until the enumerator passes the end of doubleValues and MoveNext returns false. In each iteration, we use the enumerator’s Current property to obtain and output the current array element. Lines 62–65 iterate over array intValues. Notice that PrintArrays is called twice (lines 19 and 28), so GetEnumerator is called twice on doubleValues. The GetEnumerator method (lines 54 and 62) always returns an enumerator positioned before the first element. Also notice that the IEnumerator property Current is read-only. Enumerators cannot be used to modify the contents of collections, only to obtain the contents. Lines 70–71 use a foreach statement to iterate over the collection elements like an enumerator. In fact, the foreach statement behaves exactly like an enumerator. Both loop over the elements of an array one-by-one in a well-defined order. Neither allows you to modify the elements during the iteration. This is not a coincidence. The foreach statement implicitly obtains an enumerator via the GetEnumerator method and uses the enumerator’s MoveNext method and Current property to traverse the collection, just as we did explicitly in lines 54–57. For this reason, we can use the foreach statement to iterate over any collection that implements the IEnumerable interface—not just arrays. We demonstrate this functionality in the next section when we discuss class ArrayList. Other static Array methods include Clear (to set a range of elements to 0 or null), CreateInstance (to create a new array of a specified type), IndexOf (to locate the first occurrence of an object in an array or portion of an array), LastIndexOf (to locate the last occurrence of an object in an array or portion of an array) and Reverse (to reverse the contents of an array or portion of an array).
1142
Chapter 26
Collections
26.4 Non-Generic Collections The System.Collections namespace in the .NET Framework Class Library is the primary source for non-generic collections. These classes provide standard implementations of many of the data structures discussed in Chapter 24 with collections that store references of type object. In this section, we demonstrate classes ArrayList, Stack and Hashtable.
26.4.1 Class ArrayList In most programming languages, conventional arrays have a fixed size—they cannot be changed dynamically to conform to an application’s execution-time memory requirements. In some applications, this fixed-size limitation presents a problem for programmers. They must choose between using fixed-size arrays that are large enough to store the maximum number of elements the application may require and using dynamic data structures that can grow and shrink the amount of memory required to store data in response to the changing requirements of an application at execution time. The .NET Framework’s ArrayList collection class mimics the functionality of conventional arrays and provides dynamic resizing of the collection through the class’s methods. At any time, an ArrayList contains a certain number of elements less than or equal to its capacity—the number of elements currently reserved for the ArrayList. An application can manipulate the capacity with ArrayList property Capacity. If an ArrayList needs to grow, it by default doubles its Capacity.
Performance Tip 26.1 As with linked lists, inserting additional elements into an ArrayList whose current size is less than its capacity is a fast operation. 26.1
Performance Tip 26.2 It is a slow operation to insert an element into an ArrayList that needs to grow larger to accommodate a new element. An ArrayList that is at its capacity must have its memory reallocated and the existing values copied into it. 26.2
Performance Tip 26.3 If storage is at a premium, use method TrimToSize of class ArrayList to trim an ArrayList to its exact size. This will optimize an ArrayList’s memory use. Be careful—if the application needs to insert additional elements, the process will be slower because the ArrayList must grow dynamically (trimming leaves no room for growth). 26.3
Performance Tip 26.4 The default capacity increment, doubling the size of the ArrayList, may seem to waste storage, but doubling is an efficient way for an ArrayList to grow quickly to “about the right size.” This is a much more efficient use of time than growing the ArrayList by one element at a time in response to insert operations. 26.4
ArrayLists store references to objects. ArrayList can contain objects of any type. properties of class ArrayList.
All classes derive from class object, so an Figure 26.4 lists some useful methods and
26.4 Non-Generic Collections
Method or Property
1143
Description
Add
Adds an object to the ArrayList and returns an int specifying the index at which the object was added.
Capacity
Property that gets and sets the number of elements for which space is currently reserved in the ArrayList.
Clear
Removes all the elements from the ArrayList.
Contains
Returns true if the specified object is in the ArrayList; otherwise, returns false.
Count
Read-only property that gets the number of elements stored in the ArrayList.
IndexOf
Returns the index of the first occurrence of the specified object in the ArrayList.
Insert
Inserts an object at the specified index.
Remove
Removes the first occurrence of the specified object.
RemoveAt
Removes an object at the specified index.
RemoveRange
Removes a specified number of elements starting at a specified index in the ArrayList.
Sort
Sorts the ArrayList.
TrimToSize
Sets the Capacity of the ArrayList to the number of elements the ArrayList currently contains (Count).
Fig. 26.4 | Some methods and properties of class ArrayList. Figure 26.5 demonstrates class ArrayList and several of its methods. Class Arraybelongs to the System.Collections namespace (line 4). Lines 8–11 declare two arrays of strings (colors and removeColors) that we will use to fill two ArrayList objects. Recall from Section 9.11 that constants must be initialized at compile-time, but readonly variables can be initialized at execution time. Arrays are objects created at execution time, so we declare colors and removeColors with readonly—not const—to make them unmodifiable. When the application begins execution, we create an ArrayList with an initial capacity of one element and store it in variable list (line 16). The foreach statement in lines 20–21 adds the five elements of array colors to list via ArrayList’s Add method, so list grows to accommodate these new elements. Line 25 uses ArrayList’s overloaded constructor to create a new ArrayList initialized with the contents of array removeColors, then assigns it to variable removeList. This constructor can initialize the contents of an ArrayList with the elements of any ICollection passed to it. Many of the collection classes have such a constructor. Notice that the constructor call in line 25 performs the task of lines 20–21. List
1144
Chapter 26
Collections
Line 28 calls method DisplayInformation (lines 38–55) to output the contents of the list. This method uses a foreach statement to traverse the elements of an ArrayList. As we discussed in Section 26.3, the foreach statement is a convenient shorthand for calling ArrayList’s GetEnumerator method and using an enumerator to traverse the elements of the collection. Also, we must use an iteration variable of type object because class ArrayList is non-generic and stores references to objects.
// Fig. 26.5: ArrayListTest.cs // Using class ArrayList. using System; using System.Collections; public class ArrayListTest { private static readonly string[] colors = { "MAGENTA", "RED", "WHITE", "BLUE", "CYAN" }; private static readonly string[] removeColors = { "RED", "WHITE", "BLUE" }; // create ArrayList, add colors to it and manipulate it public static void Main( string[] args ) { ArrayList list = new ArrayList( 1 ); // initial capacity of 1 // add the elements of the colors array // to the ArrayList list foreach ( string color in colors ) list.Add( color ); // add color to the ArrayList list // add elements in the removeColors array to // the ArrayList removeList with the ArrayList constructor ArrayList removeList = new ArrayList( removeColors ); Console.WriteLine( "ArrayList: " ); DisplayInformation( list ); // output the list // remove from ArrayList list the colors in removeList RemoveColors( list, removeList ); Console.WriteLine( "\nArrayList after calling RemoveColors: " ); DisplayInformation( list ); // output list contents } // end method Main // displays information on the contents of an array list private static void DisplayInformation( ArrayList arrayList ) { // iterate through array list with a foreach statement foreach ( object element in arrayList ) Console.Write( "{0} ", element ); // invokes ToString
// display the size and capacity Console.WriteLine( "\nSize = {0}; Capacity = {1}", arrayList.Count, arrayList.Capacity ); int index = arrayList.IndexOf( "BLUE" ); if ( index != -1 ) Console.WriteLine( "The array list contains BLUE at index {0}.", index ); else Console.WriteLine( "The array list does not contain BLUE." ); } // end method DisplayInformation // remove colors specified in secondList from firstList private static void RemoveColors( ArrayList firstList, ArrayList secondList ) { // iterate through second ArrayList like an array for ( int count = 0; count < secondList.Count; count++ ) firstList.Remove( secondList[ count ] ); } // end method RemoveColors } // end class ArrayListTest
ArrayList: MAGENTA RED WHITE BLUE CYAN Size = 5; Capacity = 8 The array list contains BLUE at index 3. ArrayList after calling RemoveColors: MAGENTA CYAN Size = 2; Capacity = 8 The array list does not contain BLUE.
Fig. 26.5 | Using class ArrayList. (Part 2 of 2.) We use the Count and Capacity properties in line 46 to display the current number of elements and the maximum number of elements that can be stored without allocating more memory to the ArrayList. The output of Fig. 26.5 indicates that the ArrayList has capacity 8—recall that an ArrayList doubles its capacity whenever it needs more space. In line 48, we invoke method IndexOf to determine the position of the string "BLUE" in arrayList and store the result in local variable index. IndexOf returns -1 if the element is not found. The if statement in lines 50–54 checks if index is -1 to determine whether arrayList contains "BLUE". If it does, we output its index. ArrayList also provides method Contains, which simply returns true if an object is in the ArrayList, and false otherwise. Method Contains is preferred if we do not need the index of the element.
Performance Tip 26.5 methods IndexOf and Contains each perform a linear search, which is a costly operation for large ArrayLists. If the ArrayList is sorted, use ArrayList method BinarySearch to perform a more efficient search. Method BinarySearch returns the index of the element, or a negative number if the element is not found. ArrayList
26.5
1146
Chapter 26
Collections
After method DisplayInformation returns, we call method RemoveColors (lines 58– 64) with the two ArrayLists. The for statement in lines 62–63 iterates over ArrayList secondList. Line 63 uses an indexer to access an ArrayList element—by following the ArrayList reference name with square brackets ([]) containing the desired index of the element. An ArgumentOutOfRangeException occurs if the specified index is not both greater than 0 and less than the number of elements currently stored in the ArrayList (specified by the ArrayList’s Count property). We use the indexer to obtain each of secondList’s elements, then remove each one from firstList with the Remove method. This method deletes a specified item from an ArrayList by performing a linear search and removing (only) the first occurrence of the specified object. All subsequent elements shift toward the beginning of the ArrayList to fill the emptied position. After the call to RemoveColors, line 34 again outputs the contents of list, confirming that the elements of removeList were, indeed, removed.
26.4.2 Class Stack The Stack class implements a stack data structure and provides much of the functionality that we defined in our own implementation in Section 24.3. Refer back to that section for a discussion of stack data structure concepts. We created a test application in Fig. 24.11 to demonstrate the StackInheritance data structure that we developed. We adapt Fig. 24.14 in Fig. 26.6 to demonstrate the .NET Framework collection class Stack. The using directive in line 4 allows us to use the Stack class with its unqualified name from the System.Collections namespace. Line 10 creates a Stack with the default initial capacity (10 elements). As one might expect, class Stack has methods Push and Pop to perform the basic stack operations. Method Push takes an object as an argument and inserts it at the top of the Stack. If the number of items on the Stack (the Count property) is equal to the capacity at the time of the Push operation, the Stack grows to accommodate more objects. Lines 19–26 use method Push to add four elements (a bool, a char, an int and a string) to the stack and invoke method PrintStack (lines 50–64) after each Push to output the contents of the stack. Notice that this non-generic Stack class can store only references to objects, so each of the value-type items—the bool, the char and the int—are implicitly boxed before they are added to the Stack. (Namespace System.Collections.Generic provides a generic Stack class that has many of the same methods and properties used in Fig. 26.6.) Method PrintStack (lines 50–64) uses Stack property Count (implemented to fulfill the contract of interface ICollection) to obtain the number of elements in stack. If the stack is not empty (i.e., Count is not equal to 0), we use a foreach statement to iterate over the stack and output its contents by implicitly invoking the ToString method of each element. The foreach statement implicitly invokes Stack’s GetEnumerator method, which we could have called explicitly to traverse the stack via an enumerator. Method Peek returns the value of the top stack element, but does not remove the element from the Stack. We use Peek at line 30 to obtain the top object of the Stack, then output that object, implicitly invoking the object’s ToString method. An InvalidOperationException occurs if the Stack is empty when the application calls Peek. (We do not need an exception handling block because we know the stack is not empty here.)
// Fig. 26.6: StackTest.cs // Demonstrating class Stack. using System; using System.Collections; public class StackTest { public static void Main( string[] args ) { Stack stack = new Stack(); // default Capacity of 10 // create objects to store in the stack bool aBoolean = true; char aCharacter = '$'; int anInteger = 34567; string aString = "hello"; // use method Push to add items to (the top of) the stack stack.Push( aBoolean ); PrintStack( stack ); stack.Push( aCharacter ); PrintStack( stack ); stack.Push( anInteger ); PrintStack( stack ); stack.Push( aString ); PrintStack( stack ); // check the top element of the stack Console.WriteLine( "The top element of the stack is {0}\n", stack.Peek() ); // remove items from stack try { while ( true ) { object removedObject = stack.Pop(); Console.WriteLine( removedObject + " popped" ); PrintStack( stack ); } // end while } // end try catch ( InvalidOperationException exception ) { // if exception occurs, print stack trace Console.Error.WriteLine( exception ); } // end catch } // end Main // print the contents of a stack private static void PrintStack( Stack stack ) {
Fig. 26.6 | Demonstrating class Stack. (Part 1 of 2.)
1147
1148 52 53 54 55 56 57 58 59 60 61 62 63 64 65
Chapter 26
Collections
if ( stack.Count == 0 ) Console.WriteLine( "stack is empty\n" ); // the stack is empty else { Console.Write( "The stack is: " ); // iterate through the stack with a foreach statement foreach ( object element in stack ) Console.Write( "{0} ", element ); // invokes ToString Console.WriteLine( "\n" ); } // end else } // end method PrintStack } // end class StackTest
The stack is: True The stack is: $ True The stack is: 34567 $ True The stack is: hello 34567 $ True The top element of the stack is hello hello popped The stack is: 34567 $ True 34567 popped The stack is: $ True $ popped The stack is: True True popped stack is empty System.InvalidOperationException: Stack empty. at System.Collections.Stack.Pop() at StackTest.Main(String[] args) in C:\examples\ch27\ fig27_06\StackTest\StackTest.cs:line 37
Fig. 26.6 | Demonstrating class Stack. (Part 2 of 2.) Method Pop takes no arguments—it removes and returns the object currently on top of the Stack. An infinite loop (lines 35–40) pops objects off the stack and outputs them until the stack is empty. When the application calls Pop on the empty stack, an InvalidOperationException is thrown. The catch block (lines 42–46) outputs the exception, implicitly invoking the InvalidOperationException’s ToString method to obtain its error message and stack trace.
Common Programming Error 26.3 Attempting to Peek or Pop an empty Stack (a Stack whose Count property is 0) causes an InvalidOperationException.
26.3
26.4 Non-Generic Collections
1149
Although Fig. 26.6 does not demonstrate it, class Stack also has method Contains, which returns true if the Stack contains the specified object, and returns false otherwise.
26.4.3 Class Hashtable When an application creates objects of new or existing types, it needs to manage those objects efficiently. This includes sorting and retrieving objects. Sorting and retrieving information with arrays is efficient if some aspect of your data directly matches the key value and if those keys are unique and tightly packed. If you have 100 employees with nine-digit Social Security numbers and you want to store and retrieve employee data by using the Social Security number as a key, it would nominally require an array with 999,999,999 elements, because there are 999,999,999 unique nine-digit numbers. If you have an array that large, you could get very high performance storing and retrieving employee records by simply using the Social Security number as the array index, but it would be a large waste of memory. Many applications have this problem—either the keys are of the wrong type (i.e., not non-negative integers), or they are of the right type, but they are sparsely spread over a large range. What is needed is a high-speed scheme for converting keys such as Social Security numbers and inventory part numbers to unique array indices. Then, when an application needs to store something, the scheme could convert the application key rapidly to an index and the record of information could be stored at that location in the array. Retrieval occurs the same way—once the application has a key for which it wants to retrieve the data record, the application simply applies the conversion to the key, which produces the array subscript where the data resides in the array and retrieves the data. The scheme we describe here is the basis of a technique called hashing, in which we store data in a data structure called a hash table. Why the name? Because, when we convert a key into an array subscript, we literally scramble the bits, making a “hash” of the number. The number actually has no real significance beyond its usefulness in storing and retrieving this particular data record. A glitch in the scheme occurs when collisions occur (i.e., two different keys “hash into” the same cell, or element, in the array). Since we cannot sort two different data records to the same space, we need to find an alternative home for all records beyond the first that hash to a particular array subscript. One scheme for doing this is to “hash again” (i.e., to reapply the hashing transformation to the key to provide a next candidate cell in the array). The hashing process is designed to be quite random, so the assumption is that with just a few hashes, an available cell will be found. Another scheme uses one hash to locate the first candidate cell. If the cell is occupied, successive cells are searched linearly until an available cell is found. Retrieval works the same way—the key is hashed once, the resulting cell is checked to determine whether it contains the desired data. If it does, the search is complete. If it does not, successive cells are searched linearly until the desired data is found. The most popular solution to hash-table collisions is to have each cell of the table be a hash “bucket”—typically, a linked list of all the key–value pairs that hash to that cell. This is the solution that the .NET Framework’s Hashtable class implements. The load factor affects the performance of hashing schemes. The load factor is the ratio of the number of objects stored in the hash table to the total number of cells of the hash table. As this ratio gets higher, the chance of collisions tends to increase.
1150
Chapter 26
Collections
Performance Tip 26.6 The load factor in a hash table is a classic example of a space/time trade-off: By increasing the load factor, we get better memory utilization, but the application runs slower due to increased hashing collisions. By decreasing the load factor, we get better application speed because of reduced hashing collisions, but we get poorer memory utilization because a larger portion of the hash table remains empty. 26.6
Computer science students study hashing schemes in courses called “Data Structures” and “Algorithms.” Recognizing the value of hashing, the .NET Framework provides class Hashtable to enable programmers to easily employ hashing in applications. This concept is profoundly important in our study of object-oriented programming. Classes encapsulate and hide complexity (i.e., implementation details) and offer userfriendly interfaces. Crafting classes to do this properly is one of the most valued skills in the field of object-oriented programming. A hash function performs a calculation that determines where to place data in the hash table. The hash function is applied to the key in a key–value pair of objects. Class Hashtable can accept any object as a key. For this reason, class object defines method GetHashCode, which all objects inherit. Most classes that are candidates to be used as keys in a hash table override this method to provide one that performs efficient hash code calculations for a specific type. For example, a string has a hash code calculation that is based on the contents of the string. Figure 26.7 uses a Hashtable to count the number of occurrences of each word in a string. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
// Fig. 26.7: HashtableTest.cs // Application counts the number of occurrences of each word in a string // and stores them in a hash table. using System; using System.Text.RegularExpressions; using System.Collections; public class HashtableTest { public static void Main( string[] args ) { // create hash table based on user input Hashtable table = CollectWords(); // display hash table content DisplayHashtable( table ); } // end method Main // create hash table from user input private static Hashtable CollectWords() { Hashtable table = new Hashtable(); // create a new hash table Console.WriteLine( "Enter a string: " ); // prompt for user input string input = Console.ReadLine(); // get input
Fig. 26.7 | Application counts the number of occurrences of each word in a string and stores them in a hash table. (Part 1 of 2.)
// split input text into tokens string[] words = Regex.Split( input, @"\s+" ); // processing input words foreach ( string word in words ) { string wordKey = word.ToLower(); // get word in lowercase // if the hash table contains the word if ( table.ContainsKey( wordKey ) ) { table[ wordKey ] = ( ( int ) table[ wordKey ] ) + 1; } // end if else // add new word with a count of 1 to hash table table.Add( wordKey, 1 ); } // end foreach return table; } // end method CollectWords // display hash table content private static void DisplayHashtable( Hashtable table ) { Console.WriteLine( "\nHashtable contains:\n{0,-12}{1,-12}", "Key:", "Value:" ); // generate output for each key in hash table // by iterating through the Keys property with a foreach statement foreach ( object key in table.Keys ) Console.WriteLine( "{0,-12}{1,-12}", key, table[ key ] ); Console.WriteLine( "\nsize: {0}", table.Count ); } // end method DisplayHashtable } // end class HashtableTest
Enter a string: As idle as a painted ship upon a painted ocean Hashtable contains: Key: Value: painted 2 a 2 upon 1 as 2 ship 1 idle 1 ocean 1 size: 7
Fig. 26.7 | Application counts the number of occurrences of each word in a string and stores them in a hash table. (Part 2 of 2.)
1152
Chapter 26
Collections
Lines 4–6 contain using directives for namespaces System (for class Console), System.Text.RegularExpressions (for class Regex, discussed in Chapter 16, Strings, Characters and Regular Expressions) and System.Collections (for class Hashtable). Class HashtableTest declares three static methods. Method CollectWords (lines 20– 46) inputs a string and returns a Hashtable in which each value stores the number of times that word appears in the string and the word is used for the key. Method DisplayHashtable (lines 49–60) displays the Hashtable passed to it in column format. The Main method (lines 10–17) simply invokes CollectWords (line 13), then passes the Hashtable returned by CollectWords to DisplayHashtable in line 16. Method CollectWords (lines 20–46) begins by initializing local variable table with a new Hashtable (line 22) that has a default initial capacity of 0 elements and a default maximum load factor of 1.0. When the number of items in the Hashtable becomes greater than the number of cells times the load factor, the capacity is increased automatically. (This implementation detail is invisible to clients of the class.) Lines 24–25 prompt the user and input a string. We use static method Split of class Regex in line 28 to divide the string by its whitespace characters. This creates an array of “words,” which we then store in local variable words. The foreach statement in lines 31–43 loops over every element of array words. Each word is converted to lowercase with string method ToLower, then stored in variable wordKey (line 33). Then line 36 calls Hashtable method ContainsKey to determine whether the word is in the hash table (and thus has occurred previously in the string). If the Hashtable does not contain an entry for the word, line 42 uses Hashtable method Add to create a new entry in the hash table, with the lowercase word as the key and an object containing 1 as the value. Note that autoboxing occurs when the application passes integer 1 to method Add, because the hash table stores both the key and value in references to type object.
Common Programming Error 26.4 Using the Add method to add a key that already exists in the hash table causes an ArgumentException. 26.4
If the word is already a key in the hash table, line 38 uses the Hashtable’s indexer to obtain and set the key’s associated value (the word count) in the hash table. We first downcast the value obtained by the get accessor from an object to an int. This unboxes the value so that we can increment it by 1. Then, when we use the indexer’s set accessor to assign the key’s associated value, the incremented value is implicitly reboxed so that it can be stored in the hash table. Notice that invoking the get accessor of a Hashtable indexer with a key that does not exist in the hash table obtains a null reference. Using the set accessor with a key that does not exist in the hash table creates a new entry, as if you had used the Add method. Line 45 returns the hash table to the Main method, which then passes it to method DisplayHashtable (lines 49–60), which displays all the entries. This method uses readonly property Keys (line 56) to get an ICollection that contains all the keys. Because ICollection extends IEnumerable, we can use this collection in the foreach statement in lines 56–57 to iterate over the keys of the hash table. This loop accesses and outputs each key and its value in the hash table using the iteration variable and table’s get accessor. Each key and its value is displayed in a field width of -12. The negative field width indicates that the output is left justified. Note that a hash table is not sorted, so the key–value
26.5 Generic Collections
1153
pairs are not displayed in any particular order. Line 59 uses Hashtable property Count to get the number of key–value pairs in the Hashtable. Lines 56–57 could have also used the foreach statement with the Hashtable object itself, instead of using the Keys property. If you use a foreach statement with a Hashtable object, the iteration variable will be of type DictionaryEntry. The enumerator of a Hashtable (or any other class that implements IDictionary) uses the DictionaryEntry structure to store key–value pairs. This structure provides properties Key and Value for retrieving the key and value of the current element. If you do not need the key, class Hashtable also provides a read-only Values property that gets an ICollection of all the values stored in the Hashtable. We can use this property to iterate through the values stored in the Hashtable without regard for where they are stored.
Problems with Non-Generic Collections In the word-counting application of Fig. 26.7, our Hashtable stores its keys and data as object references, even though we store only string keys and int values by convention. This results in some awkward code. For example, line 38 was forced to unbox and box the int data stored in the Hashtable every time it incremented the count for a particular key. This is inefficient. A similar problem occurs in line 56—the iteration variable of the foreach statement is an object reference. If we need to use any of its string-specific methods, we need an explicit downcast. This can cause subtle bugs. Suppose we decide to improve the readability of Fig. 26.7 by using the indexer’s set accessor instead of the Add method to add a key/value pair in line 42, but accidentally type: table[ wordKey ] = wordKey; // initialize to 1
This statement will create a new entry with a string key and string value instead of an int value of 1. Although the application will compile correctly, this is clearly incorrect. If a word appears twice, line 38 will try to downcast this string to an int, causing an InvalidCastException at execution time. The error that appears at execution time will indicate that the problem is at line 38, where the exception occurred, not at line 42. This makes the error more difficult to find and debug, especially in large software applications where the exception may occur in a different file—and even in a different assembly. In Chapter 25, we introduced generics. In the next two sections, we demonstrate how to use generic collections.
26.5 Generic Collections The System.Collections.Generic namespace in the FCL is a new addition for C# 2.0. This namespace contains generic classes that allow us to create collections of specific types. As you saw in Fig. 26.2, many of the classes are simply generic versions of non-generic collections. A couple of classes implement new data structures. In this section, we demonstrate generic collections SortedDictionary and LinkedList.
26.5.1 Generic Class SortedDictionary A dictionary is the general term for a collection of key–value pairs. A hash table is one way to implement a dictionary. The .NET Framework provides several implementations of dictionaries, both generic and non-generic (all of which implement the IDictionary in-
1154
Chapter 26
Collections
terface in Fig. 26.1). The application in Fig. 26.8 is a modification of Fig. 26.7 that uses the generic class SortedDictionary. Generic class SortedDictionary does not use a hash table, but instead stores its key–value pairs in a binary search tree. (We discuss binary trees in depth in Section 24.5.) As the class name suggests, the entries in SortedDictionary are sorted in the tree by key. When the key implements generic interface IComparable, the SortedDictionary uses the results of IComparable method CompareTo to sort the keys. Notice that despite these implementation details, we use the same public methods, properties and indexers with classes Hashtable and SortedDictionary in the same ways. In fact, except for the generic-specific syntax, Fig. 26.8 looks remarkably similar to Fig. 26.7. This is the beauty of object-oriented programming. Line 6 contains a using directive for the System.Collections.Generic namespace, which contains class SortedDictionary. The generic class SortedDictionary takes two type arguments—the first specifies the type of key (i.e., string), and the second specifies the type of value (i.e., int). We have simply replaced the word Hashtable in line 13 and lines 23–24 with SortedDictionary< string, int > to create a dictionary of int values keyed with strings. Now, the compiler can check and notify us if we attempt to store an object of the wrong type in the dictionary. Also, because the compiler now knows that the data structure contains int values, there is no longer any need for the downcast in line 40. This allows line 40 to use the much more concise prefix increment (++) notation. These are the only changes made to methods Main and CollectWords. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
// Fig. 26.8: SortedDictionaryTest.cs // Application counts the number of occurrences of each word in a string // and stores them in a generic sorted dictionary. using System; using System.Text.RegularExpressions; using System.Collections.Generic; public class SortedDictionaryTest { public static void Main( string[] args ) { // create sorted dictionary based on user input SortedDictionary< string, int > dictionary = CollectWords(); // display sorted dictionary content DisplayDictionary( dictionary ); } // end method Main // create sorted dictionary from user input private static SortedDictionary< string, int > CollectWords() { // create a new sorted dictionary SortedDictionary< string, int > dictionary = new SortedDictionary< string, int >(); Console.WriteLine( "Enter a string: " ); // prompt for user input string input = Console.ReadLine(); // get input
Fig. 26.8 | Application counts the number of occurrences of each word in a string and stores them in a generic sorted dictionary. (Part 1 of 2.)
// split input text into tokens string[] words = Regex.Split( input, @"\s+" ); // processing input words foreach ( string word in words ) { string wordKey = word.ToLower(); // get word in lowercase // if the dictionary contains the word if ( dictionary.ContainsKey( wordKey ) ) { ++dictionary[ wordKey ]; } // end if else // add new word with a count of 1 to the dictionary dictionary.Add( wordKey, 1 ); } // end foreach return dictionary; } // end method CollectWords // display dictionary content private static void DisplayDictionary< K, V >( SortedDictionary< K, V > dictionary ) { Console.WriteLine( "\nSorted dictionary contains:\n{0,-12}{1,-12}", "Key:", "Value:" ); // generate output for each key in the sorted dictionary // by iterating through the Keys property with a foreach statement foreach ( K key in dictionary.Keys ) Console.WriteLine( "{0,-12}{1,-12}", key, dictionary[ key ] ); Console.WriteLine( "\nsize: {0}", dictionary.Count ); } // end method DisplayDictionary } // end class SortedDictionaryTest
Enter a string: We few, we happy few, we band of brothers Sorted dictionary contains: Key: Value: band 1 brothers 1 few, 2 happy 1 of 1 we 3 size: 6
Fig. 26.8 | Application counts the number of occurrences of each word in a string and stores them in a generic sorted dictionary. (Part 2 of 2.)
1156
Chapter 26
Collections
Static method DisplayDictionary (lines 51–63) has been modified to be completely generic. It takes type parameters K and V. These parameters are used in line 52 to indicate that DisplayDictionary takes a SortedDictionary with keys of type K and values of type V. We use type parameter K again in line 59 as the type of the iteration key. This use of generics is a marvelous example of code reuse. If we decide to change the application to count the number of times each character appears in a string, method DisplayDictionary could receive an argument of type SortedDictionary< char, int > without modification.
Performance Tip 26.7 Because class SortedDictionary keeps its elements sorted in a binary tree, obtaining or inserting a key–value pair takes O(log n) time, which is fast compared to linear searching then inserting. 26.7
Common Programming Error 26.5 Invoking the get accessor of a SortedDictionary indexer with a key that does not exist in the collection causes a KeyNotFoundException. This behavior is different from that of the Hashtable indexer’s get accessor, which would return null. 26.5
26.5.2 Generic Class LinkedList Chapter 24 began our discussion of data structures with the concept of a linked list. We end our discussion with the .NET Framework’s generic LinkedList class. The LinkedList class is a doubly-linked list—we can navigate the list both backwards and forwards with nodes of generic class LinkedListNode. Each node contains property Value and read-only properties Previous and Next. The Value property’s type matches LinkedList’s single type parameter because it contains the data stored in the node. The Previous property gets a reference to the preceding node in the linked list (or null if the node is the first of the list). Similarly, the Next property gets a reference to the subsequent reference in the linked list (or null if the node is the last of the list). We demonstrate a few linked-list manipulations in Fig. 26.9. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
// Fig. 26.9: LinkedListTest.cs // Using LinkedLists. using System; using System.Collections.Generic; public class LinkedListTest { private static readonly string[] colors = { "black", "yellow", "green", "blue", "violet", "silver" }; private static readonly string[] colors2 = { "gold", "white", "brown", "blue", "gray" }; // set up and manipulate LinkedList objects public static void Main( string[] args ) { LinkedList< string > list1 = new LinkedList< string >();
// add elements to first linked list foreach ( string color in colors ) list1.AddLast( color ); // add elements to second linked list via constructor LinkedList< string > list2 = new LinkedList< string >( colors2 ); Concatenate( list1, list2 ); // concatenate list2 onto list1 PrintList( list1 ); // print list1 elements Console.WriteLine( "\nConverting strings in list1 to uppercase\n" ); ToUppercaseStrings( list1 ); // convert to uppercase string PrintList( list1 ); // print list1 elements Console.WriteLine( "\nDeleting strings between BLACK and BROWN\n" ); RemoveItemsBetween( list1, "BLACK", "BROWN" ); PrintList( list1 ); // print list1 elements PrintReversedList( list1 ); // print list in reverse order } // end method Main // output list contents private static void PrintList< E >( LinkedList< E > list ) { Console.WriteLine( "Linked list: " ); foreach ( E value in list ) Console.Write( "{0} ", value ); Console.WriteLine(); } // end method PrintList // concatenate the second list on the end of the first list private static void Concatenate< E >( LinkedList< E > list1, LinkedList< E > list2 ) { // concatenate lists by copying element values // in order from the second list to the first list foreach ( E value in list2 ) list1.AddLast( value ); // add new node } // end method Concatenate // locate string objects and convert to uppercase private static void ToUppercaseStrings( LinkedList< string > list ) { // iterate over the list by using the nodes LinkedListNode< string > currentNode = list.First; while ( currentNode != null ) { string color = currentNode.Value; // get value in node currentNode.Value = color.ToUpper(); // convert to uppercase
Fig. 26.9 | Using LinkedLists. (Part 2 of 3.)
1158
Chapter 26
Collections
currentNode = currentNode.Next; // get next node 71 } // end while 72 } // end method ToUppercaseStrings 73 74 75 // delete list items between two given items 76 private static void RemoveItemsBetween< E >( LinkedList< E > list, 77 E startItem, E endItem ) 78 { 79 // get the nodes corresponding to the start and end item LinkedListNode< E > currentNode = list.Find( startItem ); 80 LinkedListNode< E > endNode = list.Find( endItem ); 81 82 83 // remove items after the start item 84 // until we find the last item or the end of the linked list 85 while ( ( currentNode.Next != null ) && 86 ( currentNode.Next != endNode ) ) 87 { list.Remove( currentNode.Next ); // remove next node 88 89 } // end while 90 } // end method RemoveItemsBetween 91 92 // print reversed list 93 private static void PrintReversedList< E >( LinkedList< E > list ) 94 { 95 Console.WriteLine( "Reversed List:" ); 96 97 // iterate over the list by using the nodes LinkedListNode< E > currentNode = list.Last; 98 99 while ( currentNode != null ) 100 { 101 Console.Write( "{0} ", currentNode.Value ); 102 currentNode = currentNode.Previous; // get previous node 103 } // end while 104 105 106 Console.WriteLine(); 107 } // end method PrintReversedList 108 } // end class LinkedListTest Linked list: black yellow green blue violet silver gold white brown blue gray Converting strings in list1 to uppercase Linked list: BLACK YELLOW GREEN BLUE VIOLET SILVER GOLD WHITE BROWN BLUE GRAY Deleting strings between BLACK and BROWN Linked list: BLACK BROWN BLUE GRAY Reversed List: GRAY BLUE BROWN BLACK
Fig. 26.9 | Using LinkedLists. (Part 3 of 3.)
26.5 Generic Collections
1159
The using directive in line 4 allows us to use the LinkedList class by its unqualified name. Lines 16–23 create LinkedLists list1 and list2 of strings and fill them with the contents of arrays colors and colors2, respectively. Note that LinkedList is a generic class that has one type parameter for which we specify the type argument string in this example (lines 16 and 23). We demonstrate two ways to fill the lists. In lines 19–20, we use the foreach statement and method AddLast to fill list1. The AddLast method creates a new LinkedListNode (with the given value available via the Value property), and appends this node to the end of the list. There is also an AddFirst method that inserts a node at the beginning of the list. Line 23 invokes the constructor that takes an IEnumerable< string > parameter. All arrays implicitly inherit from the generic interfaces IList and IEnumerable with the type of the array as the type argument, so the string array colors2 implements IEnumerable< string >. The type parameter of this generic IEnumerable matches the type parameter of the generic LinkedList object. This constructor call copies the contents of the array colors2 to list2. Line 25 calls generic method Concatenate (lines 51–58) to append all elements of list2 to the end of list1. Line 26 calls method PrintList (lines 40–48) to output list1’s contents. Line 29 calls method ToUppercaseStrings (lines 61–73) to convert each string element to uppercase, then line 30 calls PrintList again to display the modified strings. Line 33 calls method RemoveItemsBetween (lines 76–90) to remove the elements between "BLACK" and "BROWN", but not including either. Line 35 outputs the list again, then line 36 invokes method PrintReversedList (lines 93–107) to print the list in reverse order. Generic method Concatenate (lines 51–58) iterates over list2 with a foreach statement and calls method AddLast to append each value to the end of list1. The LinkedList class’s enumerator loops over the values of the nodes, not the nodes themselves, so the iteration variable has type E. Notice that this creates a new node in list1 for each node in list2. One LinkedListNode cannot be a member of more than one LinkedList. Any attempt to add a node from one LinkedList to another generates an InvalidOperationException. If you want the same data to belong to more than one LinkedList, you must make a copy of the node for each list. Generic method PrintList (lines 40–48) similarly uses a foreach statement to iterate over the values in a LinkedList, and outputs them. Method ToUppercaseStrings (lines 61–73) takes a linked list of strings and converts each string value to uppercase. This method replaces the strings stored in the list, so we cannot use an enumerator (via a foreach statement) as in the previous two methods. Instead, we obtain the first LinkedListNode via the First property (line 64), and use a while statement to loop through the list (lines 66–72). Each iteration of the while statement obtains and updates the contents of currentNode via property Value, using string method ToUpper to create an uppercase version of string color. At the end of each iteration, we move the current node to the next node in the list by assigning currentNode to the node obtained by its own Next property (line 71). The Next property of the last node of the list gets null, so when the while statement iterates past the end of the list, the loop exits. Notice that it does not make sense to declare ToUppercaseStrings as a generic method, because it uses the string-specific methods of the values in the nodes. Methods PrintList (lines 40–48) and Concatenate (lines 51–58) do not need to use any stringspecific methods, so they can be declared with generic type parameters to promote maximal code reuse.
1160
Chapter 26
Collections
Generic method RemoveItemsBetween (lines 76–90) removes a range of items between two nodes. Lines 80–81 obtain the two “boundary” nodes of the range by using method Find. This method performs a linear search on the list, and returns the first node that contains a value equal to the passed argument. Method Find returns null if the value is not found. We store the node preceding the range in local variable currentNode and the node following the range in endNode. The while statement in lines 85–89 removes all the elements between currentNode and endNode. On each iteration of the loop, we remove the node following currentNode by invoking method Remove (line 88). Method Remove takes a LinkedListNode, splices that node out of the LinkedList, and fixes the references of the surrounding nodes. After the Remove call, currentNode’s Next property now gets the node following the node just removed, and that node’s Previous property now gets currentNode. The while statement continues to loop until there are no nodes left between currentNode and endNode, or until currentNode is the last node in the list. (Note that there is also an overloaded version of method Remove that performs a linear search for the specified value and removes the first node in the list that contains it.) Method PrintReversedList (lines 93–107) prints the list backward by navigating the nodes manually. Line 98 obtains the last element of the list via the Last property and stores it in currentNode. The while statement in lines 100–104 iterates through the list backwards by moving the currentNode reference to the previous node at the end of each iteration, then exiting when we move past the beginning of the list. Note how similar this code is to lines 64–72, which iterated through the list from the beginning to the end.
26.6 Synchronized Collections In Chapter 15, we discussed multithreading. Most of the non-generic collections are unsynchronized by default, so they can operate efficiently when multithreading is not required. Because they are unsynchronized, however, concurrent access to a collection by multiple threads could cause errors. To prevent potential threading problems, synchronization wrappers are used for many of the collections that might be accessed by multiple threads. A wrapper object receives method calls, adds thread synchronization (to prevent concurrent access to the collection) and passes the calls to the wrapped collection object. Most of the non-generic collection classes in the .NET Framework provide static method Synchronized, which returns a synchronized wrapping object for the specified object. For example, the following code creates a synchronized ArrayList: ArrayList notSafeList = new ArrayList(); ArrayList threadSafeList = ArrayList.Synchronized( notSafeList );
The collections in the .NET Framework do not all provide wrappers for safe performance under multiple threads. Some guarantee no thread-safety at all. Many of the generic collections are inherently thread-safe for reading, but not for writing. To determine if a particular class is thread-safe, check that class’s documentation in the .NET Framework class library reference. Also recall that when a collection is modified, any enumerator returned previously by the GetEnumerator method becomes invalid and will throw an exception if its methods are invoked. Because other threads may change the collection, using an enumerator is not thread-safe—thus, the foreach statement is not thread-safe either. If you use an enumer-
26.7 Wrap-Up
1161
ator or foreach statement in a multithreaded application, you should use the lock keyword to prevent other threads from using the collection or use a try statement to catch the InvalidOperationException.
26.7 Wrap-Up This chapter introduced the .NET Framework collection classes. You learned about the hierarchy of interfaces that many of the collection classes implement. You saw how to use class Array to perform array manipulations. You learned that the System.Collections and System.Collections.Generic namespaces contain many non-generic and generic collection classes, respectively. We presented the non-generic classes ArrayList, Stack and Hashtable as well as generic classes SortedDictionary and LinkedList. In doing so, we discussed data structures in greater depth. We discussed dynamically expanding collections, hashing schemes, and two implementations of a dictionary. You saw the advantages of generic collections over their non-generic counterparts. You also learned how to use enumerators to traverse these data structures and obtain their contents. We demonstrated the foreach statement with many of the classes of the FCL, and explained that this works by using enumerators “behind-the-scenes” to traverse the collections. Finally, we discussed some of the issues that you should consider when using collections in multithreaded applications.
A Operator Precedence Chart A.1 Operator Precedence Operators are shown in decreasing order of precedence from top to bottom with each level of precedence separated by a horizontal black line (Fig. A.1). The associativity of the operators is shown in the right column. Operator
Type
Associativity
.
member access method call element access postfix increment postfix decrement object creation get System.Type object for a type get size in bytes of a type checked evaluation unchecked evaluation unary plus unary minus logical negation bitwise complement
Fig. A.1 | Operator precedence chart. (Part 1 of 2.)
right-to-left
A.1 Operator Precedence
Operator
Type
++
prefix increment prefix decrement cast multiplication division remainder addition subtraction right shift left shift less than greater than less than or equal to greater than or equal to type comparison type conversion is not equal to is equal to logical AND logical XOR logical OR conditional AND conditional OR null coalescing conditional assignment multiplication assignment division assignment remainder assignment addition assignment subtraction assignment left shift assignment right shift assignment logical AND assignment logical XOR assignment logical OR assignment
B Number Systems Here are only numbers ratified. —William Shakespeare
OBJECTIVES In this appendix you will learn: I
To understand basic number systems concepts, such as base, positional value and symbol value.
I
To understand how to work with numbers represented in the binary, octal and hexadecimal number systems.
I
To abbreviate binary numbers as octal numbers or hexadecimal numbers.
I
To convert octal numbers and hexadecimal numbers to binary numbers.
I
To convert back and forth between decimal numbers and their binary, octal and hexadecimal equivalents.
I
To understand binary arithmetic and how negative binary numbers are represented using two’s complement notation.
Nature has some sort of arithmetic-geometrical coordinate system, because nature has all kinds of models. What we experience of nature is in models, and all of nature’s models are so beautiful. It struck me that nature’s system must be a real beauty, because in chemistry we find that the associations are always in beautiful whole numbers—there are no fractions. —Richard Buckminster Fuller
Outline
B.1 Introduction
B.1 B.2 B.3 B.4 B.5 B.6
1165
Introduction Abbreviating Binary Numbers as Octal and Hexadecimal Numbers Converting Octal and Hexadecimal Numbers to Binary Numbers Converting from Binary, Octal or Hexadecimal to Decimal Converting from Decimal to Binary, Octal or Hexadecimal Negative Binary Numbers: Two’s Complement Notation
B.1 Introduction In this appendix, we introduce the key number systems that programmers use, especially when they are working on software projects that require close interaction with machinelevel hardware. Projects like this include operating systems, computer networking software, compilers, database systems and applications requiring high performance. When we write an integer such as 227 or –63 in a program, the number is assumed to be in the decimal (base 10) number system. The digits in the decimal number system are 0, 1, 2, 3, 4, 5, 6, 7, 8 and 9. The lowest digit is 0 and the highest digit is 9—one less than the base of 10. Internally, computers use the binary (base 2) number system. The binary number system has only two digits, namely 0 and 1. Its lowest digit is 0 and its highest digit is 1—one less than the base of 2. As we will see, binary numbers tend to be much longer than their decimal equivalents. Programmers who work in assembly languages and in high-level languages like C# that enable programmers to reach down to the machine level, find it cumbersome to work with binary numbers. So two other number systems—the octal number system (base 8) and the hexadecimal number system (base 16)—are popular primarily because they make it convenient to abbreviate binary numbers. In the octal number system, the digits range from 0 to 7. Because both the binary number system and the octal number system have fewer digits than the decimal number system, their digits are the same as the corresponding digits in decimal. The hexadecimal number system poses a problem because it requires 16 digits—a lowest digit of 0 and a highest digit with a value equivalent to decimal 15 (one less than the base of 16). By convention, we use the letters A through F to represent the hexadecimal digits corresponding to decimal values 10 through 15. Thus in hexadecimal we can have numbers like 876 consisting solely of decimal-like digits, numbers like 8A55F consisting of digits and letters and numbers like FFE consisting solely of letters. Occasionally, a hexadecimal number spells a common word such as FACE or FEED—this can appear strange to programmers accustomed to working with numbers. The digits of the binary, octal, decimal and hexadecimal number systems are summarized in Figs. B.1 and Fig. B.2. Each of these number systems uses positional notation—each position in which a digit is written has a different positional value. For example, in the decimal number 937 (the 9, the 3 and the 7 are referred to as symbol values), we say that the 7 is written in the ones position, the 3 is written in the tens position and the 9 is written in the hundreds position. Note that each of these positions is a power of the base (base 10) and that these powers begin at 0 and increase by 1 as we move left in the number (Fig. B.3).
1166
Appendix B Number Systems
Binary digit
Octal digit
Decimal digit
Hexadecimal digit
0
0
0
0
1
1
1
1
2
2
2
3
3
3
4
4
4
5
5
5
6
6
6
7
7
7
8
8
9
9 A B C D E F
(decimal value of 10) (decimal value of 11) (decimal value of 12) (decimal value of 13) (decimal value of 14) (decimal value of 15)
Fig. B.1 | Digits of the binary, octal, decimal and hexadecimal number systems. Attribute
Binary
Octal
Decimal
Hexadecimal
Base Lowest digit Highest digit
2
8
10
16
0
0
0
0
1
7
9
F
Fig. B.2 | Comparing the binary, octal, decimal and hexadecimal number systems. Positional values in the decimal number system Decimal digit Position name Positional value Positional value as a power of the base (10)
9
3
7
Hundreds
Tens
Ones
100
10
1
102
101
100
Fig. B.3 | Positional values in the decimal number system. For longer decimal numbers, the next positions to the left would be the thousands position (10 to the 3rd power), the ten-thousands position (10 to the 4th power), the hun-
B.1 Introduction
1167
dred-thousands position (10 to the 5th power), the millions position (10 to the 6th power), the ten-millions position (10 to the 7th power) and so on. In the binary number 101, the rightmost 1 is written in the ones position, the 0 is written in the twos position and the leftmost 1 is written in the fours position. Each position is a power of the base (base 2) and that these powers begin at 0 and increase by 1 as we move left in the number (Fig. B.4). So, 101 = 1 * 22 + 0 * 21 + 1 * 20 = 4 + 0 + 1 = 5. For longer binary numbers, the next positions to the left would be the eights position (2 to the 3rd power), the sixteens position (2 to the 4th power), the thirty-twos position (2 to the 5th power), the sixty-fours position (2 to the 6th power) and so on. In the octal number 425, we say that the 5 is written in the ones position, the 2 is written in the eights position and the 4 is written in the sixty-fours position. Note that each of these positions is a power of the base (base 8) and that these powers begin at 0 and increase by 1 as we move left in the number (Fig. B.5). For longer octal numbers, the next positions to the left would be the five-hundredand-twelves position (8 to the 3rd power), the four-thousand-and-ninety-sixes position (8 to the 4th power), the thirty-two-thousand-seven-hundred-and-sixty-eights position (8 to the 5th power) and so on. In the hexadecimal number 3DA, we say that the A is written in the ones position, the D is written in the sixteens position and the 3 is written in the two-hundred-and-fiftysixes position. Note that each of these positions is a power of the base (base 16) and that these powers begin at 0 and increase by 1 as we move left in the number (Fig. B.6). For longer hexadecimal numbers, the next positions to the left would be the fourthousand-and-ninety-sixes position (16 to the 3rd power), the sixty-five-thousand-fivehundred-and-thirty-sixes position (16 to the 4th power) and so on. Positional values in the binary number system Binary digit
1
0
1
Position name
Fours
Twos
Ones
Positional value
4
2
1
Positional value as a power of the base (2)
22
21
20
Fig. B.4 | Positional values in the binary number system. Positional values in the octal number system Decimal digit
4
2
5
Position name
Sixty-fours
Eights
Ones
Positional value
64
8
1
Positional value as a power of the base (8)
82
81
80
Fig. B.5 | Positional values in the octal number system.
1168
Appendix B Number Systems
Positional values in the hexadecimal number system Decimal digit
3
D
A
Position name
Two-hundred-andfifty-sixes
Sixteens
Ones
Positional value
256
16
1
Positional value as a power of the base (16)
162
161
160
Fig. B.6 | Positional values in the hexadecimal number system.
B.2 Abbreviating Binary Numbers as Octal and Hexadecimal Numbers The main use for octal and hexadecimal numbers in computing is for abbreviating lengthy binary representations. Figure B.7 highlights the fact that lengthy binary numbers can be expressed concisely in number systems with higher bases than the binary number system.
Decimal number
Binary representation
Octal representation
Hexadecimal representation
0
0
0
0
1
1
1
1
2
10
2
2
3
11
3
3
4
100
4
4
5
101
5
5
6
110
6
6
7
111
7
7
8
1000
10
8
9
1001
11
9
10
1010
12
A
11
1011
13
B
12
1100
14
C
13
1101
15
D
14
1110
16
E
15
1111
17
F
16
10000
20
10
Fig. B.7 | Decimal, binary, octal and hexadecimal equivalents.
B.3 Converting Octal and Hexadecimal Numbers to Binary Numbers
1169
A particularly important relationship that both the octal number system and the hexadecimal number system have to the binary system is that the bases of octal and hexadecimal (8 and 16 respectively) are powers of the base of the binary number system (base 2). Consider the following 12-digit binary number and its octal and hexadecimal equivalents. See if you can determine how this relationship makes it convenient to abbreviate binary numbers in octal or hexadecimal. The answer follows the numbers. Binary number
100011010001
Octal equivalent
4321
Hexadecimal equivalent
8D1
To see how the binary number converts easily to octal, simply break the 12-digit binary number into groups of three consecutive bits each and write those groups over the corresponding digits of the octal number as follows: 100 4
011 3
010 2
001 1
Note that the octal digit you have written under each group of three bits corresponds precisely to the octal equivalent of that 3-digit binary number, as shown in Fig. B.7. The same kind of relationship can be observed in converting from binary to hexadecimal. Break the 12-digit binary number into groups of four consecutive bits each and write those groups over the corresponding digits of the hexadecimal number as follows: 1000 8
1101 D
0001 1
Notice that the hexadecimal digit you wrote under each group of four bits corresponds precisely to the hexadecimal equivalent of that 4-digit binary number as shown in Fig. B.7.
B.3 Converting Octal and Hexadecimal Numbers to Binary Numbers In the previous section, we saw how to convert binary numbers to their octal and hexadecimal equivalents by forming groups of binary digits and simply rewriting them as their equivalent octal digit values or hexadecimal digit values. This process may be used in reverse to produce the binary equivalent of a given octal or hexadecimal number. For example, the octal number 653 is converted to binary simply by writing the 6 as its 3-digit binary equivalent 110, the 5 as its 3-digit binary equivalent 101 and the 3 as its 3-digit binary equivalent 011 to form the 9-digit binary number 110101011. The hexadecimal number FAD5 is converted to binary simply by writing the F as its 4-digit binary equivalent 1111, the A as its 4-digit binary equivalent 1010, the D as its 4digit binary equivalent 1101 and the 5 as its 4-digit binary equivalent 0101 to form the 16-digit 1111101011010101.
B.4 Converting from Binary, Octal or Hexadecimal to Decimal We are accustomed to working in decimal, and therefore it is often convenient to convert a binary, octal, or hexadecimal number to decimal to get a sense of what the number is “really” worth. Our diagrams in Section B.1 express the positional values in decimal. To
1170
Appendix B Number Systems
convert a number to decimal from another base, multiply the decimal equivalent of each digit by its positional value and sum these products. For example, the binary number 110101 is converted to decimal 53, as shown in Fig. B.8. To convert octal 7614 to decimal 3980, we use the same technique, this time using appropriate octal positional values, as shown in Fig. B.9. To convert hexadecimal AD3B to decimal 44347, we use the same technique, this time using appropriate hexadecimal positional values, as shown in Fig. B.10.
B.5 Converting from Decimal to Binary, Octal or Hexadecimal The conversions in Section B.4 follow naturally from the positional notation conventions. Converting from decimal to binary, octal, or hexadecimal also follows these conventions.
Converting a binary number to decimal Postional values:
32
16
8
4
2
1
Symbol values:
1
1
0
1
0
1
Products:
1*32=32
1*16=16
0*8=0
1*4=4
0*2=0
1*1=1
Sum:
= 32 + 16 + 0 + 4 + 0s + 1 = 53
Fig. B.8 | Converting a binary number to decimal.
Converting an octal number to decimal Positional values:
512
64
8
1
Symbol values:
7
6
1
4
Products
7*512=3584
6*64=384
1*8=8
4*1=4
Sum:
= 3584 + 384 + 8 + 4 = 3980
Fig. B.9 | Converting an octal number to decimal.
Converting a hexadecimal number to decimal Postional values:
4096
256
16
1
Symbol values:
A
D
3
B
Products
A*4096=40960
D*256=3328
3*16=48
B*1=11
Sum:
= 40960 + 3328 + 48 + 11 = 44347
Fig. B.10 | Converting a hexadecimal number to decimal.
1171
B.5 Converting from Decimal to Binary, Octal or Hexadecimal
Suppose we wish to convert decimal 57 to binary. We begin by writing the positional values of the columns right to left until we reach a column whose positional value is greater than the decimal number. We do not need that column, so we discard it. Thus, we first write: Positional values: 64
32
16
8
4
2
1
2
1
Then we discard the column with positional value 64, leaving: 32
Positional values:
16
8
4
Next we work from the leftmost column to the right. We divide 32 into 57 and observe that there is one 32 in 57 with a remainder of 25, so we write 1 in the 32 column. We divide 16 into 25 and observe that there is one 16 in 25 with a remainder of 9 and write 1 in the 16 column. We divide 8 into 9 and observe that there is one 8 in 9 with a remainder of 1. The next two columns each produce quotients of 0 when their positional values are divided into 1, so we write 0s in the 4 and 2 columns. Finally, 1 into 1 is 1, so we write 1 in the 1 column. This yields: Positional values: 32 Symbol values: 1
16 1
8 1
4 0
2 0
1 1
and thus decimal 57 is equivalent to binary 111001. To convert decimal 103 to octal, we begin by writing the positional values of the columns until we reach a column whose positional value is greater than the decimal number. We do not need that column, so we discard it. Thus, we first write: Positional values:
512
64
8
1
Then we discard the column with positional value 512, yielding: 64
Positional values:
8
1
Next we work from the leftmost column to the right. We divide 64 into 103 and observe that there is one 64 in 103 with a remainder of 39, so we write 1 in the 64 column. We divide 8 into 39 and observe that there are four 8s in 39 with a remainder of 7 and write 4 in the 8 column. Finally, we divide 1 into 7 and observe that there are seven 1s in 7 with no remainder, so we write 7 in the 1 column. This yields: Positional values: 64 Symbol values: 1
8 4
1 7
and thus decimal 103 is equivalent to octal 147. To convert decimal 375 to hexadecimal, we begin by writing the positional values of the columns until we reach a column whose positional value is greater than the decimal number. We do not need that column, so we discard it. Thus, we first write: Positional values: 4096
256
16
1
Then we discard the column with positional value 4096, yielding: Positional values:
256
16
1
Next we work from the leftmost column to the right. We divide 256 into 375 and observe that there is one 256 in 375 with a remainder of 119, so we write 1 in the 256 column. We divide 16 into 119 and observe that there are seven 16s in 119 with a
1172
Appendix B Number Systems
remainder of 7 and write 7 in the 16 column. Finally, we divide 1 into 7 and observe that there are seven 1s in 7 with no remainder, so we write 7 in the 1 column. This yields: Positional values: 256 Symbol values: 1
16 7
1 7
and thus decimal 375 is equivalent to hexadecimal 177.
B.6 Negative Binary Numbers: Two’s Complement Notation The discussion so far in this appendix has focused on positive numbers. In this section, we explain how computers represent negative numbers using two’s complement notation. First we explain how the two’s complement of a binary number is formed, then we show why it represents the negative value of the given binary number. Consider a machine with 32-bit integers. Suppose int value = 13;
The 32-bit representation of value is 00000000 00000000 00000000 00001101
To form the negative of value we first form its one’s complement by applying C#’s bitwise complement operator (~): onesComplementOfValue = ~value;
Internally, ~value is now value with each of its bits reversed—ones become zeros and zeros become ones, as follows: value: 00000000 00000000 00000000 00001101 ~value (i.e., value’s ones complement): 11111111 11111111 11111111 11110010
To form the two’s complement of value, we simply add 1 to value’s one’s complement. Thus Two’s complement of value:
11111111 11111111 11111111 11110011
Now if this is in fact equal to –13, we should be able to add it to binary 13 and obtain a result of 0. Let us try this: 00000000 00000000 00000000 00001101 +11111111 11111111 11111111 11110011 -----------------------------------00000000 00000000 00000000 00000000
The carry bit coming out of the leftmost column is discarded and we indeed get 0 as a result. If we add the one’s complement of a number to the number, the result would be all 1s. The key to getting a result of all zeros is that the twos complement is one more than the one’s complement. The addition of 1 causes each column to add to 0 with a carry of 1. The carry keeps moving leftward until it is discarded from the leftmost bit, and thus the resulting number is all zeros.
Computers actually perform a subtraction, such as x = a - value;
by adding the two’s complement of value to a, as follows: x = a + (~value + 1);
Suppose a is 27 and value is 13 as before. If the two’s complement of value is actually the negative of value, then adding the two’s complement of value to a should produce the result 14. Let us try this: a (i.e., 27) +(~value + 1)
C Using the Visual Studio® 2005 Debugger We are built to make mistakes, coded for error. —Lewis Thomas
OBJECTIVES In this appendix you will learn: I
To use the debugger to locate and correct logic errors in a program.
What we anticipate seldom occurs; what we least expect generally happens. —Benjamin Disraeli
It is one thing to show a man that he is in error, and another to put him in possession of truth.
I
To use breakpoints to pause program execution and allow you to examine the values of variables.
I
To set, disable and remove breakpoints.
—John Locke
I
To use the Continue command to continue execution from a breakpoint.
He can run but he can’t hide.
I
To use the Locals window to view and modify variable values.
I
To use the Watch window to evaluate expressions.
I
To use the Step Into, Step Out and Step Over commands to execute a program line-by-line.
I
To use the new Visual Studio 2005 debugging features Edit and Continue and Just My Code™ debugging.
—Joe Louis
And so shall I catch the fly. —William Shakespeare
Outline
C.1 Introduction
1175
C.1 C.2 C.3 C.4
Introduction Breakpoints and the Continue Command The Locals and Watch Windows Controlling Execution Using the Step Into, Step Over, Step Out and Continue Commands C.5 Other Features C.5.1 Edit and Continue C.5.2 Exception Assistant C.5.3 Just My Code™ Debugging C.5.4 Other New Debugger Features C.6 Wrap-Up
C.1 Introduction In Chapter 3, you learned that there are two types of errors—compilation errors and logic errors—and you learned how to eliminate compilation errors from your code. Logic errors, also called bugs, do not prevent a program from compiling successfully, but can cause a program to produce erroneous results, or terminate prematurely, when it runs. Most compiler vendors, like Microsoft, provide a tool called a debugger, which allows you to monitor the execution of your programs to locate and remove logic errors. A program must successfully compile before it can be used in the debugger—the debugger helps you analyze a program while it is running. The debugger allows you to suspend program execution, examine and set variable values and much more. In this appendix, we introduce the Visual Studio debugger, several of its debugging tools and new features added for Visual Studio 2005.
C.2 Breakpoints and the Continue Command We begin by investigating breakpoints, which are markers that can be set at any executable line of code. When a running program reaches a breakpoint, execution pauses, allowing you to examine the values of variables to help determine whether logic errors exist. For example, you can examine the value of a variable that stores the result of a calculation to determine whether the calculation was performed correctly. You can also examine the value of an expression. To illustrate the features of the debugger, we use the program in Figs. C.1 and C.2, which creates and manipulates an object of class Account (Fig. C.1). This example is similar to an example you saw in Chapter 4 (Figs. 4.15 and 4.16). Therefore, it does not use features we present in later chapters like += and if…else. Execution begins in Main (lines 8– 29 of Fig. C.2). Line 10 creates an Account object with an initial balance of $50.00. Account’s constructor (lines 10–13 of Fig. C.1) accepts one argument, which specifies the Account’s initial balance. Lines 13–14 of Fig. C.2 output the initial account balance using Account property Balance. Line 16 declares and initializes local variable depositAmount. Lines 19–20 prompt the user for and input the depositAmount. Line 23 adds the deposit to the Account’s balance using its Credit method. Finally, lines 26–27 display the new balance.
// Fig. C.01: Account.cs // Account class with a constructor to // initialize instance variable balance. public class Account { private decimal balance; // instance variable that stores the balance // constructor public Account( decimal initialBalance ) { Balance = initialBalance; // set balance using property Balance } // end Account constructor // credit (add) an amount to the account public void Credit( decimal amount ) { Balance = Balance + amount; // add amount to Balance } // end method Credit // a property to get and set the account balance public decimal Balance { get { return balance; } // end get set { // validate that value is greater than 0; // if it is not, balance is set to the default value 0 if ( value > 0 ) balance = value; if ( value Insert Breakpoint. You can set as many breakpoints as you like. Set breakpoints at lines 19, 23 and 28 of your code. A solid circle appears in the margin indicator bar where you clicked and the entire code statement is highlighted, indicating that breakpoints have been set (Fig. C.3). When the program runs, the debugger suspends execution at any line that contains a breakpoint. The program then enters break mode. Breakpoints can be set before running a program, in break mode and during execution. 2. Beginning the debugging process. After setting breakpoints in the code editor, select Build > Build Solution to compile the program, then select Debug > Start Debugging (or press the F5 key) to begin the debugging process. While debugging a console application, the Command Prompt window appears (Fig. C.4), allowing program interaction (input and output).
1178
Appendix C
Margin indicator bar
Using the Visual Studio® 2005 Debugger
Breakpoints
Fig. C.3 | Setting breakpoints.
Fig. C.4
| Account program running.
3. Examining program execution. Program execution pauses at the first breakpoint (line 19), and the IDE becomes the active window (Fig. C.5). The yellow arrow to the left of line 19 indicates that this line contains the next statement to execute. The IDE also highlights the line as well.
Yellow arrow
Next executable statement
Fig. C.5 | Program execution suspended at the first breakpoint.
C.2 Breakpoints and the Continue Command 4. Using the
Continue
1179
command to resume execution. To resume execution, select
Debug > Continue (or press the F5 key). The Continue command will execute the
5. 6.
7.
8.
statements from the current point in the program to the next breakpoint or the end of Main, whichever comes first. The program continues executing and pauses for input at line 20. Enter 49.99 in the Command Prompt window as the deposit amount. When you press Enter, the program executes until it stops at the next breakpoint (line 23). Notice that when you place the mouse pointer over the variable name depositAmount, its value is displayed in a Quick Info box (Fig. C.6). As you’ll see, this can help you spot logic errors in your programs. Continuing program execution. Use the Debug > Continue command to execute line 23. The program displays the result of its calculation (Fig. C.7). Disabling a breakpoint. To disable a breakpoint, right click a line of code in which the breakpoint has been set and select Breakpoint > Disable Breakpoint. You can also right click the breakpoint itself and select Disable Breakpoint. The disabled breakpoint is indicated by a hollow circle (Fig. C.8)—the breakpoint can be re-enabled by clicking inside the hollow circle, or by right clicking the line marked by the hollow circle (or the circle itself) and selecting Breakpoint > Enable Breakpoint. Removing a breakpoint. To remove a breakpoint that you no longer need, right click the line of code on which the breakpoint has been set and select Breakpoint > Delete Breakpoint. You also can remove a breakpoint by clicking the circle in the margin indicator bar. Finishing program execution. Select Debug > Continue to execute the program to completion.
Fig. C.6 | QuickInfo box displays value of variable depositAmount.
Fig. C.7 | Program output.
1180
Appendix C
Using the Visual Studio® 2005 Debugger
Disabled breakpoint
Fig. C.8 | Disabled breakpoint.
C.3 The Locals and Watch Windows In the preceding section, you learned that the Quick Info feature allows you to examine the value of a variable. In this section, you will learn how to use the Locals window to assign new values to variables while your program is running. You will also use the Watch window to examine the values of expressions. 1. Inserting breakpoints. Set a breakpoint at line 23 (Fig. C.9) in the source code by left clicking in the margin indicator bar to the left of line 23. Use the same technique to set breakpoints at lines 26 and 28 as well. 2. Starting debugging. Select Debug > Start Debugging. Type 49.99 at the Enter deposit amount for account1: prompt (Fig. C.10) and press Enter so that the program reads the value you just entered. The program executes until the breakpoint at line 23.
Fig. C.9 | Setting breakpoints at lines 23 and 26.
C.3 The Locals and Watch Windows
1181
Fig. C.10 | Entering the deposit amount before the breakpoint is reached. 3. Suspending program execution. When the program reaches line 23, Visual Studio suspends program execution and switches the program into break mode (Fig. C.11). At this point, the statement in line 20 (Fig. C.2) has input the depositAmount that you entered (49.99), the statement in lines 21–22 has output that the program is adding that amount to the account1 balance and the statement in line 23 is the next statement that will execute. 4. Examining data. Once the program has entered break mode, you can explore the values of your local variables using the debugger’s Locals window. To view the Locals window, select Debug > Windows > Locals. Click the plus box to the left of account1 in the Name column of the Locals window (Fig. C.12). This allows you to view each of account1’s instance variable values individually, including the value for balance (50). Note that the Locals window displays properties of a class as data, which is why you see both the Balance property and the balance instance variable in the Locals window. In addition, the current value of local variable depositAmount (49.99) and the args parameter of Main are also displayed. 5. Evaluating arithmetic and boolean expressions. You can evaluate arithmetic and boolean expressions using the Watch window. Select Debug > Windows > Watch to display the window (Fig. C.13). In the first row of the Name column (which should be blank initially), type (depositAmount + 10) * 5, then press Enter. The value 299.95 is displayed (Fig. C.13). In the next row of the Name column in the Watch window, type depositAmount == 200, then press Enter. This expression determines whether the value contained in depositAmount is 200. Expressions containing the == symbol are bool expressions. The value returned is false (Fig. C.13), because depositAmount does not currently contain the value 200. 6. Resuming execution. Select Debug > Continue to resume execution. Line 23 executes, crediting the account with the deposit amount, and the program enters break mode again at line 26. Select Debug > Windows > Locals. The updated balance instance variable and Balance property value are now displayed (Fig. C.14). 7. Modifying values. Based on the value input by the user (49.99), the account balance output by the program should be $99.99. However, you can use the Locals window to change variable values during program execution. This can be valuable for experimenting with different values and for locating logic errors in programs. In the Locals window, click the Value field in the balance row to select the value 99.99. Type 66.99, then press Enter. The debugger changes the value of balance (and the Balance property as well), then displays its new value in red (Fig. C.15). Now select Debug > Continue to execute lines 26–27. Notice that the new value of balance is displayed in the Command Prompt window. 8. Stopping the debugging session. Select Debug > Stop Debugging. Delete all breakpoints.
1182
Appendix C
Using the Visual Studio® 2005 Debugger
Fig. C.11 | Program execution pauses when the debugger reaches the breakpoint at line 23.
Fig. C.12 | Examining variable depositAmount.
Evaluating an arithmetic expression
Evaluating a bool
expression
Fig. C.13 | Examining the values of expressions.
Updated value of the balance variable
Fig. C.14 | Displaying the value of local variables.
C.4 Step Into, Step Over, Step Out and Continue Commands
1183
Value modified in the debugger
Fig. C.15 | Modifying the value of a variable.
C.4 Controlling Execution Using the Step Into, Step Over, Step Out and Continue Commands Sometimes you will need to execute a program line-by-line to find and fix logic errors. Stepping through a portion of your program this way can help you verify that a method’s code executes correctly. The commands you learn in this section allow you to execute a method line-by-line, execute all the statements of a method or execute only the remaining statements of a method (if you have already executed some statements within the method). 1. Setting a breakpoint. Set a breakpoint at line 23 by left clicking in the margin indicator bar (Fig. C.16). 2. Starting the debugger. Select Debug > Start Debugging. Enter the value 49.99 at the Enter deposit amount for account1: prompt. Program execution halts when the program reaches the breakpoint at line 23. 3. Using the Step Into command. The Step Into command executes the next statement in the program (the highlighted line of Fig. C.17) and immediately halts. If the statement to execute is a method call, control transfers to the called method. The Step Into command allows you to follow execution into a method and confirm its execution by individually executing each statement inside the method. Select Debug > Step Into (or press F11) to enter the Credit method (Fig. C.18).
Fig. C.16 | Setting a breakpoint in the program.
1184
Appendix C
Using the Visual Studio® 2005 Debugger
Next statement to execute is a method call
Fig. C.17 | Using the Step Into command to execute a statement.
Next statement to execute
Fig. C.18 | Stepping into the Credit method. 4. Using the Step Over command. Select Debug > Step Over to enter the Credit method’s body (line 17 in Fig. C.18) and transfer control to line 18 (Fig. C.19). The Step Over command behaves like the Step Into command when the next statement to execute does not contain a method call or access a property. You will see how the Step Over command differs from the Step Into command in Step 10. 5. Using the Step Out command. Select Debug > Step Out to execute the remaining statements in the method and return control to the calling method. Often, in lengthy methods, you will want to look at a few key lines of code, then continue debugging the caller’s code. The Step Out command is useful for executing the remainder of a method and returning to the caller. 6. Setting a breakpoint. Set a breakpoint (Fig. C.20) at line 28 of Fig. C.2. You will make use of this breakpoint in the next step. 7. Using the Continue command. Select Debug > Continue to execute until the next breakpoint is reached at line 28. This feature saves time when you do not want to step line-by-line through many lines of code to reach the next breakpoint.
C.4 Step Into, Step Over, Step Out and Continue Commands
1185
Control is transferred to the next statement
Fig. C.19 | Stepping over a statement in the Credit method.
Fig. C.20 | Setting a second breakpoint in the program. 8. Stopping the debugger. Select Debug > Stop Debugging to end the debugging session. 9. Starting the debugger. Before we can demonstrate the next debugger feature, you must restart the debugger. Start it, as you did in Step 2, and enter the same value (49.99). The debugger pauses execution at line 23. 10. Using the Step Over command. Select Debug > Step Over (Fig. C.21). Recall that this command behaves like the Step Into command when the next statement to execute does not contain a method call. If the next statement to execute contains a method call, the called method executes in its entirety (without pausing execution at any statement inside the method—unless there is a breakpoint in the method), and the arrow advances to the next executable line (after the method call) in the current method. In this case, the debugger executes line 23, located in Main (Fig. C.2). Line 23 calls the Credit method. Then, the debugger pauses execution at line 26, the next executable statement. 11. Stopping the debugger. Select Debug > Stop Debugging. Remove all remaining breakpoints.
1186
Appendix C
Using the Visual Studio® 2005 Debugger
The Credit method executes without stepping into it when you select the Step Over command
Fig. C.21 | Using the debugger’s Step Over command.
C.5 Other Features Visual Studio 2005 provides many new debugging features that simplify the testing and debugging process. We discuss some of these features in this section.
C.5.1 Edit and Continue The Edit and Continue feature allows you to make modifications or changes to your code in debug mode, then continue executing the program without having to recompile your code. 1. Setting a breakpoint. Set a breakpoint at line 19 in your example (Fig. C.22). 2. Starting the debugger. Select Debug > Start Debugging. When execution begins, the account1 balance is displayed. The debugger enters break mode when it reaches the breakpoint at line 19.
Fig. C.22 | Setting a breakpoint at line 19.
C.5 Other Features
1187
3. Changing the input prompt text. Suppose you wish to modify the input prompt text to provide the user with a range of values for variable depositAmount. Rather than stopping the debugging process, add the text "(from $1–500):" to the end of "Enter deposit amount for account1" at line 19 in the code view window (Fig. C.23). Select Debug > Continue. The application prompts you for input using the updated text (Fig. C.24). In this example, we wanted to make a change in the text for our input prompt before line 19 executes. However, if you want to make a change to a line that already executed, you must select a prior statement in your code from which to continue execution. 1. Setting a breakpoint. Set a breakpoint at line 21 (Fig. C.25).
Fig. C.23 | Changing the text of the input prompt while the application is in Debug mode.
Fig. C.24 | Application prompt displaying the updated text.
Fig. C.25 | Setting a breakpoint at line 21.
1188
Appendix C
Using the Visual Studio® 2005 Debugger
2. Starting the debugger. Delete the "(from $1-500)" text you just added in the previous steps. Select Debug > Start Debugging. When execution begins, the prompt Enter deposit amount for account1: appears. Enter the value 650 at the prompt (Fig. C.26). The debugger enters break mode at line 21 (Fig. C.26). 3. Changing the input prompt text. Let’s say that you once again wish to modify the input prompt text to provide the user with a range of values for variable depositAmount. Add the text "(from $1–500):" to the end of "Enter deposit amount for account1" in line 19 inside the code view window. 4. Setting the next statement. For the program to update the input prompt text correctly, you must set the execution point to a previous line of code. Right click in line 16 and select Set Next Statement from the menu that appears (Fig. C.27).
Fig. C.26 | Stopping execution at the breakpoint in line 21.
Fig. C.27 | Setting the next statement to execute.
C.5 Other Features
1189
5. Select Debug > Continue. The application prompts you again for input using the updated text (Fig. C.28). 6. Stopping the debugger. Select Debug > Stop Debugging. Certain types of change are not allowed with the Edit and Continue feature once the program begins execution. These include changing class names, adding or removing method parameters, adding public fields to a class and adding or removing methods. If a particular change that you make to your program is not allowed during the debugging process, Visual Studio displays a dialog box as shown in Fig. C.29.
C.5.2 Exception Assistant Another new feature in Visual Studio 2005 is the Exception Assistant. You can run a program by selecting either Debug > Start Debugging or Debug > Start Without Debugging. If you select the option Debug > Start Debugging and the runtime environment detects uncaught exceptions, the application pauses, and a window called the Exception Assistant appears indicating where the exception occurred, the type of the exception and links to helpful information on handling the exception. We discuss the Exception Assistant in detail in Section 12.4.3.
C.5.3 Just My Code™ Debugging Throughout this book, we produce increasingly substantial programs that often include a combination of code written by the programmer and code generated by Visual Studio. The IDE-generated code can be difficult for novices (and even experienced programmers) to understand—fortunately, you rarely need to look at this code. Visual Studio 2005 provides a new debugging feature called Just My Code™, that allows programmers to test and debug only the portion of the code they have written. When this option is enabled, the debugger will always step over method calls to methods of classes that you did not write.
Fig. C.28 | Program execution continues with updated prompt text.
Fig. C.29 | Dialog box stating that certain program edits are not allowed during program execution.
1190
Appendix C
Using the Visual Studio® 2005 Debugger
You can change this setting in the debugger options. Select Tools > Options. In the dialog, select the Debugging category to view the available debugging tools and options. Then click the checkbox that appears next to the Enable Just My Code (Managed only) option (Fig. C.30) to enable or disable this feature. Options
C.5.4 Other New Debugger Features All of the features discussed thus far in this section are available in all versions of Visual Studio, including Visual C# 2005 Express Edition. The Visual Studio 2005 debugger offers additional new features, such as visualizers, tracepoints and more, which you can learn about at msdn.microsoft.com/vcsharp/2005/overview/debugger.
C.6 Wrap-Up In this appendix, you learned how to enable the debugger and set breakpoints so that you can examine your code and results while a program executes. This capability enables you to locate and fix logic errors in your programs. You also learned how to continue execution after a program suspends execution at a breakpoint and how to disable and remove breakpoints. We showed how to use the debugger’s Watch and Locals windows to evaluate arithmetic and boolean expressions. We also demonstrated how to modify a variable’s value during program execution so that you can see how changes in values affect your results. You learned how to use the debugger’s Step Into command to debug methods called during your program’s execution. You saw how the Step Over command can be used to execute a method call without stopping the called method. You used the Step Out command to continue execution until the end of the current method. You also learned that the Continue command continues execution until another breakpoint is found or the program terminates. Finally, we discussed new features of the Visual Studio 2005 debugger, including Edit and Continue, the Exception Assistant and Just My Code debugging.
Fig. C.30 | Enabling the Just My Code debugging feature in Visual Studio.
D ASCII Character Set 0
1
2
3
4
5
6
7
8
9
0
nul
soh
stx
etx
eot
enq
ack
bel
bs
ht
1
nl
vt
ff
cr
so
si
dle
dc1
dc2
dc3
2
dc4
nak
syn
etb
can
em
sub
esc
fs
gs
3
rs
us
sp
!
"
#
$
%
&
‘
4
(
)
*
+
,
-
.
/
0
1
5
2
3
4
5
6
7
8
9
:
;
6
<
=
>
?
@
A
B
C
D
E
7
F
G
H
I
J
K
L
M
N
O
8
P
Q
R
S
T
U
V
W
X
Y
9
Z
[
\
]
^
_
’
a
b
c
10
d
e
f
g
h
i
j
k
l
m
11
n
o
p
q
r
s
t
u
v
w
12
x
y
z
{
|
}
~
del
Fig. D.1 | ASCII Character Set. The digits at the left of the table are the left digits of the decimal equivalents (0–127) of the character code, and the digits at the top of the table are the right digits of the character code. For example, the character code for “F” is 70, and the character code for “&” is 38. Most users of this book are interested in the ASCII character set used to represent English characters on many computers. The ASCII character set is a subset of the Unicode character set used by C# to represent characters from most of the world’s languages. For more information on the Unicode character set, see Appendix E.
E Unicode®
OBJECTIVES In this appendix you will learn: I
Unicode fundamentals.
I
The mission of the Unicode Consortium.
I
The design basis of Unicode.
I
The three Unicode encoding forms: UTF-8, UTF-16 and UTF-32.
I
Characters and glyphs.
I
The advantages and disadvantages of using Unicode.
Outline
E.1 Introduction
E.1 E.2 E.3 E.4 E.5 E.6
1193
Introduction Unicode Transformation Formats Characters and Glyphs Advantages/Disadvantages of Unicode Using Unicode Character Ranges
E.1 Introduction The use of inconsistent character encodings (i.e., numeric values associated with characters) in the developing of global software products causes serious problems, because computers process information as numbers. For instance, the character “a” is converted to a numeric value so that a computer can manipulate that piece of data. Many countries and corporations have developed their own encoding systems that are incompatible with the encoding systems of other countries and corporations. For example, the Microsoft Windows operating system assigns the value 0xC0 to the character “A with a grave accent”; the Apple Macintosh operating system assigns that same value to an upside-down question mark. This results in the misrepresentation and possible corruption of data when data is not processed as intended. In the absence of a widely-implemented universal character-encoding standard, global software developers had to localize their products extensively before distribution. Localization includes the language translation and cultural adaptation of content. The process of localization usually includes significant modifications to the source code (such as the conversion of numeric values and the underlying assumptions made by programmers), which results in increased costs and delays releasing the software. For example, some English-speaking programmers might design global software products assuming that a single character can be represented by one byte. However, when those products are localized for Asian markets, the programmer’s assumptions are no longer valid; thus, the majority, if not the entirety, of the code needs to be rewritten. Localization is necessary with each release of a version. By the time a software product is localized for a particular market, a newer version, which needs to be localized as well, may be ready for distribution. As a result, it is cumbersome and costly to produce and distribute global software products in a market where there is no universal character-encoding standard. In response to this situation, the Unicode Standard, an encoding standard that facilitates the production and distribution of software, was created. The Unicode Standard outlines a specification to produce consistent encoding of the world’s characters and symbols. Software products that handle text encoded in the Unicode Standard need to be localized, but the localization process is simpler and more efficient because the numeric values need not be converted and the assumptions made by programmers about the character encoding are universal. The Unicode Standard is maintained by a nonprofit organization called the Unicode Consortium, whose members include Apple, IBM, Microsoft, Oracle, Sun Microsystems, Sybase and many others. When the Consortium envisioned and developed the Unicode Standard, they wanted an encoding system that was universal, efficient, uniform and unambiguous. A universal encoding system encompasses all commonly used characters. An efficient encoding system
1194
Appendix E Unicode®
allows text files to be parsed easily. A uniform encoding system assigns fixed values to all characters. An unambiguous encoding system represents a given character in a consistent manner. These four terms are referred to as the Unicode Standard design basis.
E.2 Unicode Transformation Formats Although Unicode incorporates the limited ASCII character set (i.e., a collection of characters), it encompasses a more comprehensive character set. In ASCII each character is represented by a byte containing 0s and 1s. One byte is capable of storing the binary numbers from 0 to 255. Each character is assigned a number between 0 and 255; thus, ASCII-based systems can support only 256 characters, a tiny fraction of world’s characters. Unicode extends the ASCII character set by encoding the vast majority of the world’s characters. The Unicode Standard encodes all of those characters in a uniform numerical space from 0 to 10FFFF hexadecimal. An implementation will express these numbers in one of several transformation formats, choosing the one that best fits the particular application at hand. Three such formats are in use, called UTF-8, UTF-16 and UTF-32, depending on the size of the units—in bits—being used. UTF-8, a variable-width encoding form, requires one to four bytes to express each Unicode character. UTF-8 data consists of 8-bit bytes (sequences of one, two, three or four bytes depending on the character being encoded) and is well suited for ASCII-based systems, where there is a predominance of one-byte characters (ASCII represents characters as one byte). Currently, UTF-8 is widely implemented in UNIX systems and in databases. The variable-width UTF-16 encoding form expresses Unicode characters in units of 16 bits (i.e., as two adjacent bytes, or a short integer in many machines). Most Unicode characters are expressed in a single 16-bit unit. However, characters with values above FFFF hexadecimal are expressed with an ordered pair of 16-bit units called surrogates. Surrogates are 16-bit integers in the range D800 through DFFF, which are used solely for the purpose of “escaping” into higher numbered characters. Approximately one million characters can be expressed in this manner. Although a surrogate pair requires 32 bits to represent characters, it is space-efficient to use these 16-bit units. Surrogates are rare characters in current implementations. Many string-handling implementations are written in terms of UTF-16. [Note: Details and sample code for UTF-16 handling are available on the Unicode Consortium Web site at www.unicode.org.] Implementations that require significant use of rare characters or entire scripts encoded above FFFF hexadecimal should use UTF-32, a 32-bit, fixed-width encoding form that usually requires twice as much memory as UTF-16 encoded characters. The major advantage of the fixed-width UTF-32 encoding form is that it expresses all characters uniformly, so it is easy to handle in arrays. Figure E.1 shows the different ways in which the three encoding forms handle character encoding. There are few guidelines that state when to use a particular encoding form. The best encoding form to use depends on computer systems and business protocols, not on the data itself. Typically, the UTF-8 encoding form should be used where computer systems and business protocols require data to be handled in 8-bit units, particularly in legacy systems being upgraded, because it often simplifies changes to existing programs. For this reason, UTF-8 has become the encoding form of choice on the Internet. Likewise, UTF-16 is the encoding form of choice on Microsoft Windows applications. UTF-32 is
E.3 Characters and Glyphs
1195
Character
UTF-8
UTF-16
UTF-32
Latin capital letter A
0x41
0x0041
0x00000041
Greek capital letter ALPHA
0xCD 0x91
0x0391
0x00000391
CJK unified ideograph-4E95
0xE4 0xBA 0x95
0x4E95
0x00004E95
Old italic letter A
0xF0 0x80 0x83 0x80
0xDC00 0xDF00
0x00010300
Fig. E.1 | Correlation between the three encoding forms. likely to become more widely used in the future as more characters are encoded with values above FFFF hexadecimal. Also, UTF-32 requires less sophisticated handling than UTF16 in the presence of surrogate pairs.
E.3 Characters and Glyphs The Unicode Standard consists of characters, written components (i.e., alphabetic letters, numerals, punctuation marks, accent marks, etc.) that can be represented by numeric values. Examples of characters include: U+0041 Latin capital letter A. In the first character representation, U+yyyy is a code value, in which U+ refers to Unicode code values, as opposed to other hexadecimal values. The yyyy represents a four-digit hexadecimal number of an encoded character. Code values are bit combinations that represent encoded characters. Characters are represented with glyphs, various shapes, fonts and sizes for displaying characters. There are no code values for glyphs in the Unicode Standard. Examples of glyphs are shown in Fig. E.2. The Unicode Standard encompasses the alphabets, ideographs, syllabaries, punctuation marks, diacritics, mathematical operators, etc., that compose the written languages and scripts of the world. A diacritic is a special mark added to a character to distinguish it from another letter or to indicate an accent (e.g., in Spanish, the tilde “~” above the character “n”). Currently, Unicode provides code values for 94,140 character representations, with more than 880,000 code values reserved for future expansion.
E.4 Advantages/Disadvantages of Unicode The Unicode Standard has several significant advantages that promote its use. One is the impact it has on the performance of the international economy. Unicode standardizes the characters for the world’s writing systems to a uniform model that promotes transferring and sharing data. Programs developed using such a schema maintain their accuracy because each character has a single definition (i.e., a is always U+0061, % is always U+0025).
Fig. E.2 | Various glyphs of the character A.
1196
Appendix E Unicode®
This enables corporations to manage the high demands of international markets by processing different writing systems at the same time. Also, all characters can be managed in an identical manner, thus avoiding any confusion caused by different character-code architectures. Moreover, managing data in a consistent manner eliminates data corruption, because data can be sorted, searched and manipulated via a consistent process. Another advantage of the Unicode Standard is portability (i.e., the ability to execute software on disparate computers or with disparate operating systems). Most operating systems, databases, programming languages and Web browsers currently support Unicode. Additionally, Unicode includes more characters than any other character set. A disadvantage of the Unicode Standard is the amount of memory required by UTF16 and UTF-32. ASCII character sets are 8 bits in length, so they require less storage than the default 16-bit Unicode character set. However, the double-byte character set (DBCS) and the multi-byte character set (MBCS) that encode Asian characters (ideographs) require two to four bytes, respectively. In such instances, the UTF-16 or the UTF-32 encoding forms may be used with little hindrance on memory and performance.
E.5 Using Unicode Visual Studio uses Unicode UTF-16 encoding to represent all characters. Figure E.3 uses C# to display the text “Welcome to Unicode” in eight languages—English, French, German, Japanese, Portuguese, Russian, Spanish and Traditional Chinese. [Note: The Unicode Consortium’s Web site contains a link to code charts that lists the 16-bit Unicode code values.] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
// Fig. E.3: UnicodeForm.cs // Unicode encoding demonstration. using System; using System.Windows.Forms; namespace UnicodeDemo { public partial class UnicodeForm : Form { public UnicodeForm() { InitializeComponent(); } // assign Unicode strings to each Label private void UnicodeForm_Load( object sender, EventArgs e ) { // English char[] english = { '\u0057', '\u0065', '\u006C', '\u0063', '\u006F', '\u006D', '\u0065', '\u0020', '\u0074', '\u006F', '\u0020' }; englishLabel.Text = new string( english ) + "Unicode" + '\u0021';
Fig. E.3 | Windows application demonstrating Unicode encoding. (Part 1 of 3.)
// French char[] french = { '\u0042', '\u0069', '\u0065', '\u006E', '\u0076', '\u0065', '\u006E', '\u0075', '\u0065', '\u0020', '\u0061', '\u0075', '\u0020' }; frenchLabel.Text = new string( french ) + "Unicode" + '\u0021'; // German char[] german = { '\u0057', '\u0069', '\u006C', '\u006B', '\u006F', '\u006D', '\u006D', '\u0065', '\u006E', '\u0020', '\u007A', '\u0075', '\u0020' }; germanLabel.Text = new string( german ) + "Unicode" + '\u0021'; // Japanese char[] japanese = { '\u3078', '\u3087', '\u3045', '\u3053', '\u305D', '\u0021' }; japaneseLabel.Text = "Unicode" + new string( japanese ); // Portuguese char[] portuguese = { '\u0053', '\u0065', '\u006A', '\u0061', '\u0020', '\u0062', '\u0065', '\u006D', '\u0020', '\u0076', '\u0069', '\u006E', '\u0064', '\u006F', '\u0020', '\u0061', '\u0020' }; portugueseLabel.Text = new string( portuguese ) + "Unicode" + '\u0021'; // Russian char[] russian = { '\u0414', '\u043E', '\u0431', '\u0440', '\u043E', '\u0020', '\u043F', '\u043E', '\u0436', '\u0430', '\u043B', '\u043E', '\u0432', '\u0430', '\u0442', '\u044A', '\u0020', '\u0432', '\u0020' }; russianLabel.Text = new string( russian ) + "Unicode" + '\u0021'; // Spanish char[] spanish = { '\u0042', '\u0069', '\u0065', '\u006E', '\u0076', '\u0065', '\u006E', '\u0069', '\u0064', '\u006F', '\u0020', '\u0061', '\u0020' }; spanishLabel.Text = new string( spanish ) + "Unicode" + '\u0021'; // Simplified Chinese char[] chinese = { '\u6B22', '\u8FCE', '\u4F7F', '\u7528', '\u0020' }; chineseLabel.Text = new string( chinese ) + "Unicode" + '\u0021'; } // end method UnicodeForm_Load } // end class UnicodeForm } // end namespace UnicodeDemo
Fig. E.3 | Windows application demonstrating Unicode encoding. (Part 2 of 3.)
1198
Appendix E Unicode®
Fig. E.3 | Windows application demonstrating Unicode encoding. (Part 3 of 3.) The first welcome message (lines 19–23) contains the hexadecimal codes for the English text. The Code Charts page on the Unicode Consortium Web site contains a document that lists the code values for the Basic Latin block (or category), which includes the English alphabet. The hexadecimal codes in lines 19–20 equate to “Welcome” and a space character (\u0020). Unicode characters in C# use the format \uyyyy, where yyyy represents the hexadecimal Unicode encoding. For example, the letter “W” (in “Welcome”) is denoted by \u0057. The hexadecimal values for the word “to” and a space character appear on line 21 and the word “Unicode” is on line 23. “Unicode” is not encoded because it is a registered trademark and has no equivalent translation in most languages. The remaining welcome messages (lines 26–71) contain the hexadecimal codes for the other seven languages. The code values used for the French, German, Portuguese and Spanish text are located in the Basic Latin block, the code values used for the Traditional Chinese text are located in the CJK Unified Ideographs block, the code values used for the Russian text are located in the Cyrillic block and the code values used for the Japanese text are located in the Hiragana block. [Note: To render the Asian characters in a Windows application, you would need to install the proper language files on your computer. To do this, open the Regional Options dialog from the Control Panel (Start > Settings > Control Panel). At the bottom of the General tab is a list of languages. Check the Japanese and the Traditional Chinese checkboxes and press Apply. Follow the directions of the install wizard to install the languages. For additional assistance, visit www.unicode.org/help/display_problems.html.]
E.6 Character Ranges The Unicode Standard assigns code values, which range from 0000 (Basic Latin) to E007F (Tags), to the written characters of the world. Currently, there are code values for 94,140 characters. To simplify the search for a character and its associated code value, the Unicode Standard generally groups code values by script and function (i.e., Latin characters are grouped in a block, mathematical operators are grouped in another block, etc.). As a rule, a script is a single writing system that is used for multiple languages (e.g., the Latin script is used for English, French, Spanish, etc.). The Code Charts page on the Unicode Consortium Web site lists all the defined blocks and their respective code values. Figure E.4 lists some blocks (scripts) from the Web site and their range of code values.
E.6 Character Ranges
Script
Range of Code Values
Arabic
U+0600–U+06FF
Basic Latin
U+0000–U+007F
Bengali (India)
U+0980–U+09FF
Cherokee (Native America)
U+13A0–U+13FF
CJK Unified Ideographs (East Asia)
U+4E00–U+9FAF
Cyrillic (Russia and Eastern Europe)
U+0400–U+04FF
Ethiopic
U+1200–U+137F
Greek
U+0370–U+03FF
Hangul Jamo (Korea)
U+1100–U+11FF
Hebrew
U+0590–U+05FF
Hiragana (Japan)
U+3040–U+309F
Khmer (Cambodia)
U+1780–U+17FF
Lao (Laos)
U+0E80–U+0EFF
Mongolian
U+1800–U+18AF
Myanmar
U+1000–U+109F
Ogham (Ireland)
U+1680–U+169F
Runic (Germany and Scandinavia)
U+16A0–U+16FF
Sinhala (Sri Lanka)
U+0D80–U+0DFF
Telugu (India)
U+0C00–U+0C7F
Thai
U+0E00–U+0E7F
Fig. E.4 | Some character ranges.
1199
F Introduction to XHTML: Part 1 To read between the lines was easier than to follow the text. —Henry James
OBJECTIVES In this appendix, you will learn: I
To understand important components of XHTML documents.
I
To use XHTML to create Web pages.
I
To be able to add images to Web pages.
I
To understand how to create and use hyperlinks to navigate Web pages.
I
To be able to mark up lists of information.
High thoughts must have high language. —Aristophanes
Outline
F.1 Introduction
F.1 F.2 F.3 F.4 F.5 F.6 F.7 F.8 F.9 F.10 F.11
1201
Introduction Editing XHTML First XHTML Example W3C XHTML Validation Service Headers Linking Images Special Characters and More Line Breaks Unordered Lists Nested and Ordered Lists Web Resources
F.1 Introduction Welcome to the world of opportunity created by the World Wide Web. The Internet is now three decades old, but it was not until the Web became popular in the 1990s that the explosion of opportunity that we are still experiencing began. Exciting new developments occur almost daily—the pace of innovation is unprecedented by any other technology. In this appendix, you will develop your own Web pages. As the book proceeds, you will create increasingly appealing and powerful Web pages. In the later portion of the book, you will learn how to create complete Web-based applications. This appendix begins unlocking the power of Web-based application development with XHTML—the Extensible HyperText Markup Language. In the next appendix, we introduce more sophisticated XHTML techniques, such as tables, which are particularly useful for structuring information from databases (i.e., software that stores structured sets of data), and Cascading Style Sheets (CSS), which make Web pages more visually appealing. Unlike procedural programming languages such as C, Fortran, Cobol and Pascal, XHTML is a markup language that specifies the format of the text that is displayed in a Web browser such as Microsoft’s Internet Explorer or Netscape. One key issue when using XHTML is the separation of the presentation of a document (i.e., the document’s appearance when rendered by a browser) from the structure of the document’s information. XHTML is based on HTML (HyperText Markup Language)—a legacy technology of the World Wide Web Consortium (W3C). In HTML, it was common to specify the document’s content, structure and formatting. Formatting might specify where the browser placed an element in a Web page or the fonts and colors used to display an element. XHTML 1.1 (W3C’s latest version of W3C XHTML Recommendation at the time of publication) allows only a document’s content and structure to appear in a valid XHTML document, and not its formatting. Normally, such formatting is specified with Cascading Style Sheets. All our examples in this appendix are based upon the XHTML 1.1 Recommendation.
F.2 Editing XHTML In this appendix, we write XHTML in its source-code form. We create XHTML documents by typing them in a text editor (e.g., Notepad, Wordpad, vi, emacs) and saving them with either an.html or an .htm file-name extension.
1202
Appendix F Introduction to XHTML: Part 1
Good Programming Practice F.1 Assign documents file names that describe their functionality. This practice can help you identify documents faster. It also helps people who want to link to a page, by giving them an easy-to-remember name. For example, if you are writing an XHTML document that contains product information, you might want to call it products.html. F.1
Machines running specialized software called Web servers store XHTML documents. Clients (e.g., Web browsers) request specific resources such as the XHTML documents from the Web server. For example, typing www.deitel.com/books/downloads.html into a Web browser’s address field requests downloads.html from the Web server running at www.deitel.com. This document is located on the server in a directory named books. For now, we simply place the XHTML documents on our machine and open them using Internet Explorer.
F.3 First XHTML Example In this appendix and the next, we present XHTML markup and provide screen captures that show how Internet Explorer renders (i.e., displays) the XHTML.1 Every XHTML document we show has line numbers for the reader’s convenience. These line numbers are not part of the XHTML documents. Our first example (Fig. F.1) is an XHTML document named main.html that displays the message "Welcome to XHTML!" in the browser. The key line in the program is line 14, which tells the browser to display "Welcome to XHTML!" Now let us consider each line of the program. Lines 1–3 are required in XHTML documents to conform with proper XHTML syntax. For now, copy and paste these lines into each XHTML document you create. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
Internet and WWW How to Program - Welcome
Welcome to XHTML!
Fig. F.1 | First XHTML example. (Part 1 of 2.)
1.
All the examples presented in this book are available at www.deitel.com and on the CD-ROM that accompanies the book.
F.3 First XHTML Example
1203
Fig. F.1 | First XHTML example. (Part 2 of 2.) Lines 5–6 are XHTML comments. XHTML document creators insert comments to improve markup readability and describe the content of a document. Comments also help other people read and understand an XHTML document’s markup and content. Comments do not cause the browser to perform any action when the user loads the XHTML document into the Web browser to view the document. XHTML comments always start with . Each of our XHTML examples includes comments that specify the figure number and file name, and provide a brief description of the example’s purpose. Subsequent examples include comments in the markup, especially to highlight new features.
Good Programming Practice F.2 Place comments throughout your markup. Comments help other programmers understand the markup, assist in debugging and list useful information that you do not want the browser to render. Comments also help you understand your own markup when you revisit a document to modify or update it in the future. F.2
XHTML markup contains text that represents the content of a document and elements that specify a document’s structure. Some important elements of an XHTML document are the html element, the head element and the body element. The html element encloses the head section (represented by the head element) and the body section (represented by the body element). The head section contains information about the XHTML document, such as its title. The head section also can contain special document formatting instructions called style sheets and client-side programs called scripts for creating dynamic Web pages. The body section contains the page’s content that the browser displays when the user visits the Web page. XHTML documents delimit an element with start and end tags. A start tag consists of the element name in angle brackets (e.g., ). An end tag consists of the element name preceded by a / in angle brackets (e.g., ). In this example, lines 8 and 16 define the start and end of the html element. Note that the end tag in line 16 has the same name as the start tag, but is preceded by a / inside the angle brackets. Many start tags have attributes that provide additional information about an element. Browsers can use this additional information to determine how to process the element. Each attribute has a name and a value separated by an equals sign (=). Line 8 specifies a required attribute (xmlns) and value (http://www.w3.org/1999/xhtml) for the html element in an XHTML
1204
Appendix F Introduction to XHTML: Part 1
document. For now, simply copy and paste the html element start tag in line 8 into your XHTML documents.
Common Programming Error F.1 Not enclosing attribute values in either single or double quotes is a syntax error. However, some Web browsers may still render the element correctly. F.1
Common Programming Error F.2 Using uppercase letters in an XHTML element or attribute name is a syntax error. However, some Web browsers may still render the element correctly. F.2
An XHTML document divides the html element into two sections—head and body. Lines 9–11 define the Web page’s head section with a head element. Line 10 specifies a title element. This is called a nested element because it is enclosed in the head element’s start and end tags. The head element is also a nested element because it is enclosed in the html element’s start and end tags. The title element describes the Web page. Titles usually appear in the title bar at the top of the browser window and also as the text identifying a page when users add the page to their list of Favorites or Bookmarks that enables them to return to their favorite sites. Search engines (i.e., sites that allow users to search the Web) also use the title for cataloging purposes.
Good Programming Practice F.3 Indenting nested elements emphasizes a document’s structure and promotes readability.
F.3
Common Programming Error F.3 XHTML does not permit tags to overlap—a nested element’s end tag must appear in the document before the enclosing element’s end tag. For example, the nested XHTML tags hello cause a syntax error, because the enclosing head element’s ending tag appears before the nested title element’s ending tag. F.3
Good Programming Practice F.4 Use a consistent title-naming convention for all pages on a site. For example, if a site is named “Bailey’s Web Site,” then the title of the links page might be “Bailey’s Web Site—Links.” This practice can help users better understand the Web site’s structure. F.4
Line 13 opens the document’s body element. The body section of an XHTML document specifies the document’s content, which may include text and tags. Some tags, such as the paragraph tags (
and
) in line 14, mark up text for display in a browser. All the text placed between the
and
tags forms one paragraph. When the browser renders a paragraph, a blank line usually precedes and follows paragraph text. This document ends with two end tags (lines 15–16). These tags close the body and html elements, respectively. The tag in an XHTML document informs the browser that the XHTML markup is complete. To view this example in Internet Explorer, perform the following steps: 1. Copy the Appendix F examples onto your machine from the CD that accompanies this book (or download the examples from www.deitel.com).
F.4 W3C XHTML Validation Service
1205
2. Launch Internet Explorer and select Open... from the File Menu. This displays the Open dialog. 3. Click the Open dialog’s Browse... button to display the Microsoft Internet Explorer file dialog. 4. Navigate to the directory containing the Appendix F examples and select the file main.html, then click Open. 5. Click OK to have Internet Explorer render the document. Other examples are opened in a similar manner. At this point your browser window should appear similar to the sample screen capture shown in Fig. F.1. (Note that we resized the browser window to save space in the book.)
F.4 W3C XHTML Validation Service Programming Web-based applications can be complex, and XHTML documents must be written correctly to ensure that browsers process them properly. To promote correctly written documents, the World Wide Web Consortium (W3C) provides a validation service (validator.w3.org) for checking a document’s syntax. Documents can be validated either from a URL that specifies the location of the file or by uploading a file to the site validator.w3.org/file-upload.html. Uploading a file copies the file from the user’s computer to another computer on the Internet. Figure F.2 shows main.html (Fig. F.1) being uploaded for validation. The W3C’s Web page indicates that the service name is MarkUp Validation Service, and the validation service is able to validate the syntax of XHTML documents. All the XHTML examples in this book have been validated successfully using validator.w3.org.
Fig. F.2 | Validating an XHTML document. (Courtesy of World Wide Web Consortium (W3C).)
1206
Appendix F Introduction to XHTML: Part 1
By clicking Browse…, users can select files on their own computers for upload. After selecting a file, clicking the Validate this file button uploads and validates the file. Figure F.3 shows the results of validating main.html. This document does not contain any syntax errors. If a document does contain syntax errors, the validation service displays error messages describing the errors.
Error-Prevention Tip F.1 Most current browsers attempt to render XHTML documents even if they are invalid. This often leads to unexpected and possibly undesirable results. Use a validation service, such as the W3C MarkUp Validation Service, to confirm that an XHTML document is syntactically correct. F.1
F.5 Headers Some text in an XHTML document may be more important than other text. For example, the text in this section is considered more important than a footnote. XHTML provides six headers, called header elements, for specifying the relative importance of information. Figure F.4 demonstrates these elements (h1 through h6). Header element h1 (line 15) is considered the most significant header and is typically rendered in a larger font than the other five headers (lines 16–20). Each successive header element (i.e., h2, h3, etc.) is typically rendered in a progressively smaller font.
Portability Tip F.1 The text size used to display each header element can vary significantly between browsers.
Fig. F.3 | XHTML validation results. (Courtesy of World Wide Web Consortium (W3C).)
Internet and WWW How to Program - Headers Level Level Level Level Level Level
1 2 3 4 5 6
Header header header header header header
Fig. F.4 | Header elements h1 through h6. Look-and-Feel Observation F.1 Placing a header at the top of every XHTML page helps viewers understand the purpose of each page. F.1
Look-and-Feel Observation F.2 Use larger headers to emphasize more important sections of a Web page.
F.2
1208
Appendix F Introduction to XHTML: Part 1
F.6 Linking One of the most important XHTML features is the hyperlink, which references (or links to) other resources, such as XHTML documents and images. In XHTML, both text and images can act as hyperlinks. Web browsers typically underline text hyperlinks and color their text blue by default, so that users can distinguish hyperlinks from plain text. In Fig. F.5, we create text hyperlinks to four different Web sites. Line 17 introduces the strong element. Browsers typically display such text in a bold font. Links are created using the a (anchor) element. Line 20 defines a hyperlink that links the text Deitel to the URL assigned to attribute href, which specifies the location of a linked resource, such as a Web page, a file or an e-mail address. This particular anchor element links to a Web page located at http://www.deitel.com. When a URL does not indicate a specific document on the Web site, the Web server returns a default Web page. This page is often called index.html; however, most Web servers can be configured to use any file as the default Web page for the site. (Open http://www.deitel.com in one browser window and http://www.deitel.com/index.html in a second browser window to confirm that they are identical.) If the Web server cannot locate a requested document, it returns an error indication to the Web browser, and the browser displays a Web page containing an error message to the user. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
Internet and WWW How to Program - Links Here are my favorite sites
<strong>Click a name to go to that page.
Deitel
Prentice Hall
Yahoo!
USA Today
Fig. F.5 | Linking to other Web pages. (Part 1 of 2.)
F.6 Linking
1209
Fig. F.5 | Linking to other Web pages. (Part 2 of 2.) Anchors can link to e-mail addresses using a mailto: URL. When someone clicks this type of anchored link, most browsers launch the default e-mail program (e.g., Outlook Express) to enable the user to write an e-mail message to the linked address. Figure F.6 demonstrates this type of anchor. Lines 17–19 contain an e-mail link. The form of an email anchor is …. In this case, we link to the e-mail address [email protected]. 1 2 3 4 5 6 7 8 9
Fig. F.6 | Linking to an e-mail address. (Part 1 of 2.)
1210 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
Appendix F Introduction to XHTML: Part 1
Internet and WWW How to Program - Contact Page
My e-mail address is [email protected] . Click the address and your browser will open an e-mail message and address it to me.
Fig. F.6 | Linking to an e-mail address. (Part 2 of 2.)
F.7 Images The examples discussed so far demonstrate how to mark up documents that contain only text. However, most Web pages contain both text and images. In fact, images are an equal, if not essential, part of Web-page design. The three most popular image formats used by Web developers are Graphics Interchange Format (GIF), Joint Photographic Experts Group (JPEG) and Portable Network Graphics (PNG) images. Users can create images using specialized pieces of software, such as Adobe Photoshop Elements 2.0 (www.adobe.com), Macromedia Fireworks (www.macromedia.com) and Jasc Paint Shop Pro (www.jasc.com). Images may also be acquired from various Web sites, such as the Yahoo!
F.7 Images
1211
Picture Gallery (gallery.yahoo.com). Figure F.7 demonstrates how to incorporate images into Web pages. Lines 16–17 use an img element to insert an image in the document. The image file’s location is specified with the img element’s src attribute. In this case, the image is located in the same directory as this XHTML document, so only the image’s file name is required. Optional attributes width and height specify the image’s width and height, respectively. The document author can scale an image by increasing or decreasing the values of the image width and height attributes. If these attributes are omitted, the browser uses the 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
Internet and WWW How to Program - Welcome
Fig. F.7 | Images in XHTML files.
1212
Appendix F Introduction to XHTML: Part 1
image’s actual width and height. Images are measured in pixels (“picture elements”), which represent dots of color on the screen. The image in Fig. F.7 is 183 pixels wide and 238 pixels high.
Good Programming Practice F.5 Always include the width and the height of an image inside the tag. When the browser loads the XHTML file, it will know immediately from these attributes how much screen space to provide for the image and will lay out the page properly, even before it downloads the image. F.5
Performance Tip F.1 Including the width and height attributes in an tag can result in the browser loading and rendering pages faster. F.1
Common Programming Error F.4 Entering new dimensions for an image that change its inherent width-to-height ratio distorts the appearance of the image. For example, if your image is 200 pixels wide and 100 pixels high, you should ensure that any new dimensions have a 2:1 width-to-height ratio. F.4
Every img element in an XHTML document has an alt attribute. If a browser cannot render an image, the browser displays the alt attribute’s value. A browser may not be able to render an image for several reasons. It may not support images—as is the case with a text-based browser (i.e., a browser that can display only text)—or the client may have disabled image viewing to reduce download time. Figure F.7 shows Internet Explorer 6 rendering the alt attribute’s value when a document references a nonexistent image file (jhtp.jpg). The alt attribute is important for creating accessible Web pages for users with disabilities, especially those with vision impairments who use text-based browsers. Specialized software called a speech synthesizer often is used by people with disabilities. This software application “speaks” the alt attribute’s value so that the user knows what the browser is displaying. Some XHTML elements (called empty elements) contain only attributes and do not mark up text (i.e., text is not placed between the start and end tags). Empty elements (e.g., img) must be terminated, either by using the forward slash character (/) inside the closing right angle bracket (>) of the start tag or by explicitly including the end tag. When using the forward slash character, we add a space before the forward slash to improve readability (as shown at the ends of lines 17 and 19). Rather than using the forward slash character, lines 18–19 could be written with a closing tag as follows:
By using images as hyperlinks, Web developers can create graphical Web pages that link to other resources. In Fig. F.8, we create six different image hyperlinks. Lines 17–20 create an image hyperlink by nesting an img element in an anchor (a) element. The value of the img element’s src attribute value specifies that this image (links.jpg) resides in a directory named buttons. The buttons directory and the XHTML document are in the same directory. Images from other Web documents also can be referenced (after obtaining permission from the document’s owner) by setting the src
F.7 Images
1213
attribute to the name and location of the image. Clicking an image hyperlink takes a user to the Web page specified by the surrounding anchor element’s href attribute. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
Internet and WWW How to Program - Navigation Bar
Fig. F.8 | Images as link anchors. (Part 1 of 2.)
1214
Appendix F Introduction to XHTML: Part 1
Fig. F.8 | Images as link anchors. (Part 2 of 2.) In line 20, we introduce the br element, which most browsers render as a line break. Any markup or text following a br element is rendered on the next line. Like the img element, br is an example of an empty element terminated with a forward slash. We add a space before the forward slash to enhance readability. [Note: The last two image hyperlinks in Fig. F.8 link to XHTML documents (i.e., table1.html and form.html) presented as examples in Appendix G and included in the Appendix G examples directory. Clicking these links now will result in errors.]
F.8 Special Characters and More Line Breaks When marking up text, certain characters or symbols (e.g.,
Click here to open an e-mail message addressed to [email protected].
You may download 3.14 x 102 characters worth of information from this site. Only one download per hour is permitted.
Note: <strong>< ¼ of the information presented here is updated daily.
Fig. F.9 | Special characters in XHTML. (Part 1 of 2.)
1216
Appendix F Introduction to XHTML: Part 1
Fig. F.9 | Special characters in XHTML. (Part 2 of 2.) Lines 27–28 contain other special characters, which can be expressed as either character entity references (i.e., word abbreviations such as amp for ampersand and copy for copyright) or numeric character references—decimal or hexadecimal (hex) values representing special characters. For example, the & character is represented in decimal and hexadecimal notation as & and &, respectively. Hexadecimal numbers are base 16 numbers—digits in a hexadecimal number have values from 0 to 15 (a total of 16 different values). The letters A–F represent the hexadecimal digits corresponding to decimal values 10–15. Thus in hexadecimal notation we can have numbers like 876 consisting solely of decimal-like digits, numbers like DA19F consisting of digits and letters and numbers like DCB consisting solely of letters. We discuss hexadecimal numbers in detail in Appendix B, Number Systems. In lines 34–36, we introduce three new elements. Most browsers render the del element as strike-through text. With this format users can easily indicate document revisions. To superscript text (i.e., raise text on a line with a decreased font size) or subscript text (i.e., lower text on a line with a decreased font size), use the sup or sub element, respectively. We also use character entity reference < for a less-than sign and ¼ for the fraction 1/4 (line 38). In addition to special characters, this document introduces a horizontal rule, indicated by the tag in line 25. Most browsers render a horizontal rule as a horizontal line. The tag also inserts a line break above and below the horizontal line.
F.9 Unordered Lists Up to this point, we have presented basic XHTML elements and attributes for linking to resources, creating headers, using special characters and incorporating images. In this section, we discuss how to organize information on a Web page using lists. Figure F.10 displays text in an unordered list (i.e., a list that does not order its items by letter or number). The unordered list element ul creates a list in which each item begins with a bullet symbol
Internet and WWW How to Program - Links Here are my favorite sites
<strong>Click on a name to go to that page.
Deitel
W3C
Yahoo!
CNN
Fig. F.10 | Unordered lists in XHTML.
1217
1218
Appendix F Introduction to XHTML: Part 1
(called a disc). Each entry in an unordered list (element ul in line 20) is an li (list item) element (lines 23, 25, 27 and 29). Most Web browsers render these elements with a line break and a bullet symbol indented from the beginning of the new line.
F.10 Nested and Ordered Lists Lists may be nested to represent hierarchical relationships, as in an outline format. Figure F.11 demonstrates nested lists and ordered lists. The ordered list element ol creates a list in which each item begins with a number. A Web browser indents each nested list to indicate a hierarchical relationship. The first ordered list begins at line 33. Items in an ordered list are enumerated one, two, three and so on. Nested ordered lists are enumerated in the same manner. The items in the outermost unordered list (line 18) are preceded by discs. List items nested inside the unordered list of line 18 are preceded by circles. Although not demonstrated in this example, subsequent nested list items are preceded by squares. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
Internet and WWW How to Program - Lists The Best Features of the Internet
You can meet new people from countries around the world.
You have access to new media as it becomes public:
New games
New applications
For business
Fig. F.11 | Nested and ordered lists in XHTML. (Part 1 of 2.)
Fig. F.11 | Nested and ordered lists in XHTML. (Part 2 of 2.)
1219
1220
Appendix F Introduction to XHTML: Part 1
F.11 Web Resources www.w3.org/TR/xhtml11
The XHTML 1.1 Recommendation contains XHTML 1.1 general information, compatibility issues, document type definition information, definitions, terminology and much more. www.xhtml.org
XHTML.org provides XHTML development news and links to other XHTML resources, including books and articles. www.w3schools.com/xhtml/default.asp
The XHTML School provides XHTML quizzes and references. This page also contains links to XHTML syntax, validation and document type definitions. validator.w3.org
This is the W3C XHTML validation service site. hotwired.lycos.com/webmonkey/00/50/index2a.html
This site provides an article about XHTML. Key sections of the article overview XHTML and discuss tags, attributes and anchors. wdvl.com/Authoring/Languages/XML/XHTML
The Web Developers Virtual Library provides an introduction to XHTML. This site also contains articles, examples and links to other technologies. www.w3.org/TR/2001/REC-xhtml11-20010531
The XHTML 1.1 DTD documentation site provides technical specifications of XHTML 1.1 syntax.
G Introduction to XHTML: Part 2 Yea, from the table of my memory I’ll wipe away all trivial fond records. —William Shakespeare
OBJECTIVES In this appendix, you will learn: I
To be able to create tables with rows and columns of data.
I
To be able to control table formatting.
I
To be able to create and use forms.
I
To be able to create and use image maps to aid in Webpage navigation.
I
To be able to make Web pages accessible to search engines using tags.
I
To be able to use the frameset element to display multiple Web pages in a single browser window.
Introduction Basic XHTML Tables Intermediate XHTML Tables and Formatting Basic XHTML Forms More Complex XHTML Forms Internal Linking Creating and Using Image Maps meta Elements frameset Element Nested framesets Web Resources
G.1 Introduction In the preceding appendix, we introduced XHTML. We built several complete Web pages featuring text, hyperlinks, images, horizontal rules and line breaks. In this appendix, we discuss more substantial XHTML features, including presentation of information in tables and incorporating forms for collecting information from a Web-page visitor. We also introduce internal linking and image maps for enhancing Web-page navigation, and frames for displaying multiple documents in the browser. By the end of this appendix, you will be familiar with the most commonly used XHTML features and will be able to create more complex Web documents.
G.2 Basic XHTML Tables Tables are used to organize data in rows and columns. Our first example (Fig. G.1) creates a table with six rows and two columns to display price information for fruit. Tables are defined with the table element (lines 16–66). Lines 16–18 specify the start tag for a table element that has several attributes. The border attribute specifies the table’s border width in pixels. To create a table without a border, set border to "0". This example assigns attribute width the value "40%" to set the table’s width to 40 percent of the browser’s width. A developer can also set attribute width to a specified number of pixels. Try resizing the browser window to see how the width of the window affects the width of the table. 1 2 3 4 5 6 7 8
Table caption Table header Table body Table footer Table border
Fig. G.1 | XHTML table. (Part 3 of 3.) As its name implies, attribute summary (lines 17–18) describes the table’s contents. Speech devices use this attribute to make the table more accessible to users with visual impairments. The caption element (line 22) describes the table’s content and helps textbased browsers interpret the table data. Text inside the tag is rendered above the table by most browsers. Attribute summary and element caption are two of the many XHTML features that make Web pages more accessible to users with disabilities. A table has three distinct sections—head, body and foot. The head section (or header cell) is defined with a thead element (lines 26–31), which contains header information such as column names. Each tr element (lines 27–30) defines an individual table row. The columns in the head section are defined with th elements. Most browsers center text formatted by th (table header column) elements and display them in bold. Table header elements are nested inside table row elements. The foot section (lines 35–40) is defined with a tfoot (table foot) element. The text placed in the footer commonly includes calculation results and footnotes. Like other sections, the foot section can contain table rows, and each row can contain columns. The body section, or table body, contains the table’s primary data. The table body (lines 44–64) is defined in a tbody element. In the body, each tr element specifies one row. Data cells contain individual pieces of data and are defined with td (table data) elements within each row.
G.3 Intermediate XHTML Tables and Formatting
1225
G.3 Intermediate XHTML Tables and Formatting In the preceding section, we explored the structure of a basic table. In Fig. G.2, we enhance our discussion of tables by introducing elements and attributes that allow the document author to build more complex tables. The table begins in line 17. Element colgroup (lines 22–27) groups and formats columns. The col element (line 26) specifies two attributes in this example. The align attribute determines the alignment of text in the column. The span attribute determines how many columns the col element formats. In this case, we set align’s value to "right" and span’s value to "1" to right align text in the first column (the column containing the picture of the camel in the sample screen capture). Table cells are sized to fit the data they contain. Document authors can create larger data cells by using the attributes rowspan and colspan. The values assigned to these attributes specify the number of rows or columns occupied by a cell. The th element at lines 36–39 uses the attribute rowspan = "2" to allow the cell containing the picture of the camel to use two vertically adjacent cells (thus the cell spans two rows). The th element in lines 42–45 uses the attribute colspan = "4" to widen the header cell (containing Camelid comparison and Approximate as of 9/2002) to span four cells.
Common Programming Error G.1 When using colspan and rowspan to adjust the size of table data cells, keep in mind that the modified cells will occupy more than one column or row. Other rows or columns of the table must compensate for the extra rows or columns spanned by individual cells. If they do not, the formatting of your table will be distorted and you may inadvertently create more columns and rows than you originally intended. G.1
Fig. G.2 | Complex XHTML table. (Part 3 of 3.) Line 42 introduces the attribute valign, which aligns data vertically and may be assigned one of four values—"top" aligns data with the top of the cell, "middle" vertically centers data (the default for all data and header cells), "bottom" aligns data with the bottom of the cell and "baseline" ignores the fonts used for the row data and sets the bottom of all text in the row on a common baseline (i.e., the horizontal line at which each character in a word is aligned).
G.4 Basic XHTML Forms When browsing Web sites, users often need to provide such information as search keywords, e-mail addresses and zip codes. XHTML provides a mechanism, called a form, for collecting such data from a user. Data that users enter on a Web page normally is sent to a Web server that provides access to a site’s resources (e.g., XHTML documents, images). These resources are located either on the same machine as the Web server or on a machine that the Web server can access through the network. When a browser requests a Web page or file that is located on a server, the server processes the request and returns the requested resource. A request contains the name and path of the desired resource and the method of communication (called a protocol). XHTML documents use the Hypertext Transfer Protocol (HTTP).
1228
Appendix G Introduction to XHTML: Part 2
Figure G.3 sends the form data to the Web server, which passes the form data to a CGI (Common Gateway Interface) script (i.e., a program) written in Perl, C or some other language. The script processes the data received from the Web server and typically returns information to the Web server. The Web server then sends the information as an XHTML document to the Web browser. [Note: This example demonstrates client-side functionality. If the form is submitted (by clicking Submit Your Entries) an error occurs because we have not yet configured the required server-side functionality.] Forms can contain visual and nonvisual components. Visual components include clickable buttons and other graphical user interface components with which users interact. Nonvisual components, called hidden inputs, store any data that the document author specifies, such as e-mail addresses and XHTML document file names that act as links. The form is defined in lines 23–52 by a form element. Attribute method (line 23) specifies how the form’s data is sent to the Web server.
Fig. G.3 | Form with hidden fields and a text box. (Part 2 of 2.) Using method = "post" appends form data to the browser request, which contains the protocol (i.e., HTTP) and the requested resource’s URL. Scripts located on the Web server’s computer (or on a computer accessible through the network) can access the form data sent as part of the request. For example, a script may take the form information and update an electronic mailing list. The other possible value, method = "get", appends the form data directly to the end of the URL. For example, the URL /cgi-bin/formmail might have the form information name = bob appended to it. The action attribute in the tag specifies the URL of a script on the Web server; in this case, it specifies a script that e-mails form data to an address. Most Internet Service Providers (ISPs) have a script like this on their site; ask the Web site system administrator how to set up an XHTML document to use the script correctly.
1230
Appendix G Introduction to XHTML: Part 2
Lines 28–33 define three input elements that specify data to provide to the script that processes the form (also called the form handler). These three input elements have the type attribute "hidden", which allows the document author to send form data that is not input by a user. The three hidden inputs are: an e-mail address to which the data will be sent, the email’s subject line and a URL where the browser will be redirected after submitting the form. Two other input attributes are name, which identifies the input element, and value, which provides the value that will be sent (or posted) to the Web server.
Good Programming Practice G.1 Place hidden input elements at the beginning of a form, immediately after the opening tag. This placement allows document authors to locate hidden input elements quickly. G.1
We introduce another type of input in lines 38–39. The "text" input inserts a text box into the form. Users can type data in text boxes. The label element (lines 37–40) provides users with information about the input element’s purpose.
Look-and-Feel Observation G.1 Include a label element for each form element to help users determine the purpose of each form element. G.1
The input element’s size attribute specifies the number of characters visible in the text box. Optional attribute maxlength limits the number of characters input into the text box. In this case, the user is not permitted to type more than 30 characters into the text box. There are two other types of input elements in lines 46–49. The "submit" input element is a button. When the user presses a "submit" button, the browser sends the data in the form to the Web server for processing. The value attribute sets the text displayed on the button (the default value is Submit Query). The "reset" input element allows a user to reset all form elements to their default values. The value attribute of the "reset" input element sets the text displayed on the button (the default value is Reset).
G.5 More Complex XHTML Forms In the preceding section, we introduced basic forms. In this section, we introduce elements and attributes for creating more complex forms. Figure G.4 contains a form that solicits user feedback about a Web site. 1 2 3 4 5 6 7 8
Fig. G.4 | Form with text areas, a password box and checkboxes. (Part 1 of 4.)
Fig. G.4 | Form with text areas, a password box and checkboxes. (Part 3 of 4.)
G.5 More Complex XHTML Forms
1233
Fig. G.4 | Form with text areas, a password box and checkboxes. (Part 4 of 4.) The textarea element (lines 37–39) inserts a multiline text box, called a text area, into the form. The number of rows is specified with the rows attribute, and the number of columns (i.e., characters) is specified with the cols attribute. In this example, the textarea is four rows high and 36 characters wide. To display default text in the text area, place the text between the and tags. Default text can be specified in other input types, such as text boxes, by using the value attribute The "password" input in lines 46–47 inserts a password box with the specified size. A password box allows users to enter sensitive information, such as credit card numbers and passwords, by “masking” the information input with asterisks (*). The actual value input is sent to the Web server, not the characters that mask the input. Lines 54–71 introduce the checkbox form element. Checkboxes enable users to select from a set of options. When a user selects a checkbox, a check mark appears in the check box. Otherwise, the checkbox remains empty. Each "checkbox" input creates a new checkbox. Checkboxes can be used individually or in groups. Checkboxes that belong to a group are assigned the same name (in this case, "thingsliked").
Common Programming Error G.2 When your form has several checkboxes with the same name, you must make sure that they have different values, or the scripts running on the Web server will not be able to distinguish them. G.2
We continue our discussion of forms by presenting a third example that introduces several additional form elements from which users can make selections (Fig. G.5). In this
Fig. G.5 | Form including radio buttons and a drop-down list. (Part 3 of 4.)
G.6 Internal Linking
1237
Fig. G.5 | Form including radio buttons and a drop-down list. (Part 4 of 4.) example, we introduce two new input types. The first type is the radio button (lines 76– 94) specified with type "radio". Radio buttons are similar to checkboxes, except that only one radio button in a group of radio buttons may be selected at any time. The radio buttons in a group all have the same name attributes and are distinguished by their different value attributes. The attribute-value pair checked = "checked" (line 77) indicates which radio button, if any, is selected initially. The checked attribute also applies to checkboxes.
Common Programming Error G.3 Not setting the name attributes of the radio buttons in a form to the same name is a logic error because it lets the user select all of them at the same time. G.3
The select element (lines 104–117) provides a drop-down list of items from which the user can select an item. The name attribute identifies the drop-down list. The option element (lines 105–116) adds items to the drop-down list. The option element’s selected attribute specifies which item initially is displayed as the selected item.
G.6 Internal Linking In Appendix F, we discussed how to hyperlink one Web page to another. Figure G.6 introduces internal linking—a mechanism that enables the user to jump between locations in the same document. Internal linking is useful for long documents that contain many
1238
Appendix G Introduction to XHTML: Part 2
sections. Clicking an internal link enables users to find a section without scrolling through the entire document. Line 16 contains a tag with the id attribute (called "features") for an internal hyperlink. To link to a tag with this attribute inside the same Web page, the href attribute of an anchor element includes the id attribute value preceded by a pound sign (as in #features). Lines 61–62 contain a hyperlink with the id features as its target. Selecting this hyperlink in a Web browser scrolls the browser window to the h1 tag in line 16. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
Internet and WWW How to Program - List The Best Features of the Internet
Go to Favorite Bugs
You can meet people from countries around the world.
You have access to new media as it becomes public:
New games
New applications
For Business
For Pleasure
Around the clock news
Search Engines
Shopping
Programming
XHTML
Java
Dynamic HTML
Scripts
Fig. G.6 | Internal hyperlinks to make pages more navigable. (Part 1 of 3.)
Fig. G.6 | Internal hyperlinks to make pages more navigable. (Part 2 of 3.)
1240
Appendix G Introduction to XHTML: Part 2
Fig. G.6 | Internal hyperlinks to make pages more navigable. (Part 3 of 3.) Look-and-Feel Observation G.2 Internal hyperlinks are useful in XHTML documents that contain large amounts of information. Internal links to different parts of the page makes it easier for users to navigate the page. They do not have to scroll to find the section they want. G.2
Although not demonstrated in this example, a hyperlink can specify an internal link in another document by specifying the document name followed by a pound sign and the id value, as in: href = "filename.html#id"
For example, to link to a tag with the id attribute called booklist in books.html, href is assigned "books.html#booklist".
G.7 Creating and Using Image Maps In Appendix F, we demonstrated how images can be used as hyperlinks to link to other resources on the Internet. In this section, we introduce another technique for image linking called image maps, which designates certain areas of an image (called hotspots) as links.1 Figure G.7 introduces image maps and hotspots.
1.
Current Web browsers do not support XHTML 1.1 image maps. For this reason we are using XHTML 1.0 Transitional, an earlier W3C version of XHTML. In order to validate the code in Figure G.7 as XHTML 1.1, remove the # from the usemap attribute of the img tag (line 53).
Fig. G.7 | Image with links anchored to an image map. (Part 1 of 2.)
1241
1242 54 55 56
Appendix G Introduction to XHTML: Part 2
Fig. G.7 | Image with links anchored to an image map. (Part 2 of 2.) Lines 20–48 define an image map by using a map element. Attribute id (line 20) identifies the image map. If id is omitted, the map cannot be referenced by an image (which we will see momentarily). Hotspots are defined with area elements (as shown in lines 25– 27). Attribute href (line 25) specifies the link’s target (i.e., the resource to which to link). Attributes shape (line 25) and coords (line 26) specify the hotspot’s shape and coordinates, respectively. Attribute alt (line 27) provides alternative text for the link.
Common Programming Error G.4 Not specifying an id attribute for a map element prevents an img element from using the map’s area elements to define hotspots.
G.1
The markup in lines 25–27 creates a rectangular hotspot (shape = "rect") for the coordinates specified in the coords attribute. A coordinate pair consists of two numbers representing the locations of a point on the x-axis and the y-axis, respectively. The x-axis extends horizontally and the y-axis extends vertically from the upper-left corner of the image. Every point on an image has a unique x-y coordinate. For rectangular hotspots, the required coordinates are those of the upper-left and lower-right corners of the rectangle. In this case, the upper-left corner of the rectangle is located at 2 on the x-axis and 123 on the y-axis, annotated as (2, 123). The lower-right corner of the rectangle is at (54, 143). Coordinates are measured in pixels.
G.8 meta Elements
1243
Common Programming Error G.5 Overlapping coordinates of an image map cause the browser to render the first hotspot it encounters for the area. G.1
The map area at lines 39–41 assigns the shape attribute "poly" to create a hotspot in the shape of a polygon using the coordinates in attribute coords. These coordinates represent each vertex, or corner, of the polygon. The browser connects these points with lines to form the hotspot’s area. The map area at lines 45–47 assigns the shape attribute "circle" to create a circular hotspot. In this case, the coords attribute specifies the circle’s center coordinates and the circle’s radius, in pixels. To use an image map with an img element, you must assign the img element’s usemap attribute to the id of a map. Lines 52–53 reference the image map "#picture". The image map is located within the same document, so internal linking is used.
G.8 meta Elements Search engines are used to find Web sites. They usually catalog sites by following links from page to page (known as spidering or crawling) and saving identification and classification information for each page. One way that search engines catalog pages is by reading the content in each page’s meta elements, which specify information about a document. Two important attributes of the meta element are name, which identifies the type of meta element, and content, which provides the information search engines use to catalog pages. Figure G.8 introduces the meta element. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
Internet and WWW How to Program - Welcome
Fig. G.8 |
meta
tags provide keywords and a description of a page. (Part 1 of 2.)
We have designed this site to teach about the wonders of <strong>XHTML. XHTML is better equipped than HTML to represent complex data on the Internet. XHTML takes advantage of XML’s strict syntax to ensure well-formedness. Soon you will know about many of the great new features of XHTML.
Have Fun With the Site!
Fig. G.8 |
meta
tags provide keywords and a description of a page. (Part 2 of 2.)
Lines 14–16 demonstrate a "keywords" meta element. The content attribute of such a meta element provides search engines with a list of words that describe a page. These words are compared with words in search requests. Thus, including meta elements and their content information can draw more viewers to your site. Lines 18–21 demonstrate a "description" meta element. The content attribute of such a meta element provides a three- to four-line description of a site, written in sentence form. Search engines also use this description to catalog your site and sometimes display this information as part of the search results.
Software Engineering Observation G.1 meta elements are not visible to users and must be placed inside the head section of your XHTML
document. If meta elements are not placed in this section, they will not be read by search engines.
G.1
G.9 frameset Element
1245
G.9 frameset Element All of the Web pages we present in this book have the ability to link to other pages, but can display only one page at a time. Frames allow a Web developer to display more than one XHTML document in the browser simultaneously. Figure G.9 uses frames to display the documents in Fig. G.8 and Fig. G.10. Most of our earlier examples adhere to the XHTML 1.1 document type, whereas these use the XHTML 1.0 document types.1 These document types are specified in lines 2–3 and are required for documents that define framesets or use the target attribute to work with framesets. A document that defines a frameset normally consists of an html element that contains a head element and a frameset element (lines 23–40). In Fig. G.9, the tag (line 23) informs the browser that the page contains frames. Attribute cols specifies the frameset’s column layout. The value of cols gives the width of each frame, either in pixels or as a percentage of the browser width. In this case, the attribute cols = "110,*" informs the browser that there are two vertical frames. The first frame extends 110 pixels from the left edge of the browser window, and the second frame fills the remainder of the browser width (as indicated by the asterisk). Similarly, frameset attribute rows can be used to specify the number of rows and the size of each row in a frameset. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
Internet and WWW How to Program - Main
Fig. G.9 | XHTML frames document with navigation and content. (Part 1 of 3.) 1.
XHTML 1.1 no longer supports the use of frames. The W3C recommends using Cascading Style Sheets to achieve the same effect. Frames are still widely used on the Internet and supported by most browsers, however. The frameset element and the target attribute are still supported in the XHTML 1.0 Frameset and the XHTML 1.0 Transitional document type definitions, respectively. Please refer to www.w3.org/TR/xhtml1/#dtds for more information.
This page uses frames, but your browser does not support them.
Please, follow this link to browse our site without frames.
Left frame leftframe
Right frame main
Fig. G.9 | XHTML frames document with navigation and content. (Part 2 of 3.)
G.9 frameset Element
1247
Fig. G.9 | XHTML frames document with navigation and content. (Part 3 of 3.) The documents that will be loaded into the frameset are specified with frame elements (lines 27–28 in this example). Attribute src specifies the URL of the page to display in the frame. Each frame has name and src attributes. The first frame (which covers 110 pixels on the left side of the frameset), named leftframe, displays the page nav.html (Fig. G.10). The second frame, named main, displays the page main.html (Fig. G.8). Attribute name identifies a frame, enabling hyperlinks in a frameset to specify the target frame in which a linked document should display when the user clicks the link. For example
loads links.html in the frame whose name is "main". Not all browsers support frames. XHTML provides the noframes element (lines 30– 38) to enable XHTML document designers to specify alternative content for browsers that do not support frames.
Portability Tip G.1 Some browsers do not support frames. Use the noframes element inside a frameset to direct users to a nonframed version of your site. G.1
1248
Appendix G Introduction to XHTML: Part 2
Figure G.10 is the Web page displayed in the left frame of Fig. G.9. This XHTML document provides the navigation buttons that, when clicked, determine which document is displayed in the right frame. Line 27 (Fig. G.9) displays the XHTML page in Fig. G.10. Anchor attribute target (line 18 in Fig. G.10) specifies that the linked documents are loaded in frame main (line 28 in Fig. G.9). A target can be set to a number of preset values: "_blank" loads the page into a new browser window, "_self" loads the page into the frame in which the anchor element appears and "_top" loads the page into the full browser window (i.e., removes the frameset). 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
Internet and WWW How to Program - Navigation Bar
Fig. G.10 | XHTML document displayed in the left frame of Fig. G.9. (Part 1 of 2.)
G.10 Nested framesets
42 43 44 45 46 47 48 49 50
1249
Fig. G.10 | XHTML document displayed in the left frame of Fig. G.9. (Part 2 of 2.)
G.10 Nested framesets You can use the frameset element to create more complex layouts in a Web page by nesting framesets, as in Fig. G.11. The nested frameset in this example displays the XHTML documents in Fig. G.7, Fig. G.8 and Fig. G.10. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
Internet and WWW How to Program - Main
Fig. G.11 | Framed Web site with a nested frameset. (Part 1 of 2.)
1250 33 34 35 36 37 38 39 40 41 42 43 44
Appendix G Introduction to XHTML: Part 2
This page uses frames, but your browser does not support them.
Please, follow this link to browse our site without frames.
Right frame contains these two nested frames
Fig. G.11 | Framed Web site with a nested frameset. (Part 2 of 2.) The outer frameset element (lines 23–43) defines two columns. The left frame extends over the first 110 pixels from the left edge of the browser, and the right frame occupies the rest of the window’s width. The frame element on line 24 specifies that the document nav.html (Fig. G.10) will be displayed in the left column. Lines 28–31 define a nested frameset element for the second column of the outer frameset. This frameset defines two rows. The first row extends 175 pixels from the top of the browser window, as indicated by rows = "175,*". The second row occupies the
G.11 Web Resources
1251
remainder of the browser window’s height. The frame element at line 29 specifies that the first row of the nested frameset will display picture.html (Fig. G.7). The frame element in line 30 specifies that the second row of the nested frameset will display main.html (Fig. G.8).
Error-Prevention Tip G.1 When using nested frameset elements, indent every level of tag. This practice makes the page clearer and easier to debug. G.1
G.11 Web Resources www.vbxml.com/xhtml/articles/xhtml_tables
The VBXML.com Web site contains a tutorial on creating XHTML tables. www.webreference.com/xml/reference/xhtml.html
This Web page contains a list of frequently used XHTML tags, such as header tags, table tags, frame tags and form tags. It also provides a description of each tag.
H HTML/XHTML Special Characters The table of Fig. H.1 shows many commonly used HTML/XHTML special characters— called character entity references by the World Wide Web Consortium. For a complete list of character entity references, see the site www.w3.org/TR/REC-html40/sgml/entities.html
I HTML/XHTML Colors I.1 Colors Colors may be specified by using a standard name (such as aqua) or a hexadecimal RGB value (such as #00FFFF for aqua). Of the six hexadecimal digits in an RGB value, the first two represent the amount of red in the color, the middle two represent the amount of green in the color, and the last two represent the amount of blue in the color. For example, black is the absence of color and is defined by #000000, whereas white is the maximum amount of red, green and blue and is defined by #FFFFFF. Pure red is #FF0000, pure green (which is called lime) is #00FF00 and pure blue is #0000FF. Note that green in the standard is defined as #008000. Figure I.1 contains the HTML/XHTML standard color set. Figure I.2 contains the HTML/XHTML extended color set. Color name
Value
Color name
Value
aqua
#00FFFF
navy
#000080
black
#000000
olive
#808000
blue
#0000FF
purple
#800080
fuchsia
#FF00FF
red
#FF0000
gray
#808080
silver
#C0C0C0
green
#008000
teal
#008080
lime
#00FF00
yellow
#FFFF00
maroon
#800000
white
#FFFFFF
Fig. I.1 | HTML/XHTML standard colors and hexadecimal RGB values.
1254
Chapter I HTML/XHTML Colors
Color name
Value
Color name
Value
aliceblue
#F0F8FF
dodgerblue
#1E90FF
antiquewhite
#FAEBD7
firebrick
#B22222
aquamarine
#7FFFD4
floralwhite
#FFFAF0
azure
#F0FFFF
forestgreen
#228B22
beige
#F5F5DC
gainsboro
#DCDCDC
bisque
#FFE4C4
ghostwhite
#F8F8FF
blanchedalmond
#FFEBCD
gold
#FFD700
blueviolet
#8A2BE2
goldenrod
#DAA520
brown
#A52A2A
greenyellow
#ADFF2F
burlywood
#DEB887
honeydew
#F0FFF0
cadetblue
#5F9EA0
hotpink
#FF69B4
chartreuse
#7FFF00
indianred
#CD5C5C
chocolate
#D2691E
indigo
#4B0082
coral
#FF7F50
ivory
#FFFFF0
cornflowerblue
#6495ED
khaki
#F0E68C
cornsilk
#FFF8DC
lavender
#E6E6FA
crimson
#DC1436
lavenderblush
#FFF0F5
cyan
#00FFFF
lawngreen
#7CFC00
darkblue
#00008B
lemonchiffon
#FFFACD
darkcyan
#008B8B
lightblue
#ADD8E6
darkgoldenrod
#B8860B
lightcoral
#F08080
darkgray
#A9A9A9
lightcyan
#E0FFFF
darkgreen
#006400
lightgoldenrodyellow
#FAFAD2
darkkhaki
#BDB76B
lightgreen
#90EE90
darkmagenta
#8B008B
lightgrey
#D3D3D3
darkolivegreen
#556B2F
lightpink
#FFB6C1
darkorange
#FF8C00
lightsalmon
#FFA07A
darkorchid
#9932CC
lightseagreen
#20B2AA
darkred
#8B0000
lightskyblue
#87CEFA
darksalmon
#E9967A
lightslategray
#778899
darkseagreen
#8FBC8F
lightsteelblue
#B0C4DE
darkslateblue
#483D8B
lightyellow
#FFFFE0
darkslategray
#2F4F4F
limegreen
#32CD32
darkturquoise
#00CED1
mediumaquamarine
#66CDAA
darkviolet
#9400D3
mediumblue
#0000CD
deeppink
#FF1493
mediumorchid
#BA55D3
deepskyblue
#00BFFF
mediumpurple
#9370DB
Fig. I.2 | XHTML extended colors and hexadecimal RGB values (Part 1 of 2.).
I.1 Colors
Color name
Value
Color name
Value
dimgray
#696969
mediumseagreen
#3CB371
mediumslateblue
#7B68EE
powderblue
#B0E0E6
mediumspringgreen
#00FA9A
rosybrown
#BC8F8F
mediumturquoise
#48D1CC
royalblue
#4169E1
mediumvioletred
#C71585
saddlebrown
#8B4513
midnightblue
#191970
salmon
#FA8072
mintcream
#F5FFFA
sandybrown
#F4A460
mistyrose
#FFE4E1
seagreen
#2E8B57
moccasin
#FFE4B5
seashell
#FFF5EE
navajowhite
#FFDEAD
sienna
#A0522D
oldlace
#FDF5E6
skyblue
#87CEEB
olivedrab
#6B8E23
slateblue
#6A5ACD
orange
#FFA500
slategray
#708090
orangered
#FF4500
snow
#FFFAFA
orchid
#DA70D6
springgreen
#00FF7F
palegoldenrod
#EEE8AA
steelblue
#4682B4
palegreen
#98FB98
tan
#D2B48C
paleturquoise
#AFEEEE
thistle
#D8BFD8
palevioletred
#DB7093
tomato
#FF6347
papayawhip
#FFEFD5
turquoise
#40E0D0
peachpuff
#FFDAB9
violet
#EE82EE
peru
#CD853F
wheat
#F5DEB3
pink
#FFC0CB
whitesmoke
#F5F5F5
plum
#DDA0DD
yellowgreen
#9ACD32
Fig. I.2 | XHTML extended colors and hexadecimal RGB values (Part 2 of 2.).
1255
J ATM Case Study Code J.1 ATM Case Study Implementation This appendix contains the complete working implementation of the ATM system that we designed in the nine “Software Engineering Case Study” sections in Chapters 1, 3–9 and 11. The implementation comprises 655 lines of C# code. We consider the 11 classes in the order in which we identified them in Section 4.11 (with the exception of Transaction, which was introduced in Chapter 11 as the base class of classes BalanceInquiry, Withdrawal and Deposit): •
ATM
•
Screen
•
Keypad
•
CashDispenser
•
DepositSlot
•
Account
•
BankDatabase
•
Transaction
•
BalanceInquiry
•
Withdrawal
•
Deposit
We apply the guidelines discussed in Section 9.17 and Section 11.9 to code these classes based on how we modeled them in the UML class diagrams of Fig. 11.21 and Fig. 11.22. To develop the bodies of class methods, we refer to the activity diagrams presented in Section 6.9 and the communication and sequence diagrams presented in Section 8.14. Note that our ATM design does not specify all the program logic and may not specify all the attributes and operations required to complete the ATM implementa-
J.2 Class ATM
1257
tion. This is a normal part of the object-oriented design process. As we implement the system, we complete the program logic and add attributes and behaviors as necessary to construct the ATM system specified by the requirements document in Section 3.10. We conclude the discussion by presenting a test harness (ATMCaseStudy in Section J.13) that creates an object of class ATM and starts it by calling its Run method. Recall that we are developing a first version of the ATM system that runs on a personal computer and uses the keyboard and monitor to approximate the ATM’s keypad and screen. Also, we simulate the actions of the ATM’s cash dispenser and deposit slot. We attempt to implement the system so that real hardware versions of these devices could be integrated without significant code changes. [Note: For the purpose of this simulation, we have provided two predefined accounts in class BankDatabase. The first account has the account number 12345 and the PIN 54321. The second account has the account number 98765 and the PIN 56789. You should use these accounts when testing the ATM.]
J.2 Class ATM Class ATM (Fig. J.1) represents the ATM as a whole. Lines 5–11 implement the class’s attributes. We determine all but one of these attributes from the UML class diagrams of Figs. 11.21 and 11.22. Line 5 declares the bool attribute userAuthenticated from Fig. 11.22. Line 6 declares an attribute not found in our UML design—int attribute currentAccountNumber, which keeps track of the account number of the current authenticated user. Lines 7–11 declare reference-type instance variables corresponding to the ATM class’s associations modeled in the class diagram of Fig. 11.21. These attributes allow the ATM to access its parts (i.e., its Screen, Keypad, CashDispenser and DepositSlot) and interact with the bank’s account information database (i.e., a BankDatabase object). 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
// ATM.cs // Represents an automated teller machine. public class ATM { private bool userAuthenticated; // true if user is authenticated private int currentAccountNumber; // user's account number private Screen screen; // reference to ATM's screen private Keypad keypad; // reference to ATM's keypad private CashDispenser cashDispenser; // ref to ATM's cash dispenser private DepositSlot depositSlot; // reference to ATM's deposit slot private BankDatabase bankDatabase; // ref to account info database // enumeration that represents main menu options private enum MenuOption { BALANCE_INQUIRY = 1, WITHDRAWAL = 2, DEPOSIT = 3, EXIT_ATM = 4 } // end enum MenuOption
Fig. J.1 | Class ATM represents the ATM. (Part 1 of 4.)
// parameterless constructor initializes instance variables public ATM() { userAuthenticated = false; // user is not authenticated to start currentAccountNumber = 0; // no current account number to start screen = new Screen(); // create screen keypad = new Keypad(); // create keypad cashDispenser = new CashDispenser(); // create cash dispenser depositSlot = new DepositSlot(); // create deposit slot bankDatabase = new BankDatabase(); // create account info database } // end constructor // start ATM public void Run() { // welcome and authenticate users; perform transactions while ( true ) // infinite loop { // loop while user is not yet authenticated while ( !userAuthenticated ) { screen.DisplayMessageLine( "\nWelcome!" ); AuthenticateUser(); // authenticate user } // end while PerformTransactions(); // for authenticated user userAuthenticated = false; // reset before next ATM session currentAccountNumber = 0; // reset before next ATM session screen.DisplayMessageLine( "\nThank you! Goodbye!" ); } // end while } // end method Run // attempt to authenticate user against database private void AuthenticateUser() { // prompt for account number and input it from user screen.DisplayMessage( "\nPlease enter your account number: " ); int accountNumber = keypad.GetInput(); // prompt for PIN and input it from user screen.DisplayMessage( "\nEnter your PIN: " ); int pin = keypad.GetInput(); // set userAuthenticated to boolean value returned by database userAuthenticated = bankDatabase.AuthenticateUser( accountNumber, pin ); // check whether authentication succeeded if ( userAuthenticated ) currentAccountNumber = accountNumber; // save user's account #
Fig. J.1 | Class ATM represents the ATM. (Part 2 of 4.)
else screen.DisplayMessageLine( "Invalid account number or PIN. Please try again." ); } // end method AuthenticateUser // display the main menu and perform transactions private void PerformTransactions() { Transaction currentTransaction; // transaction being processed bool userExited = false; // user has not chosen to exit // loop while user has not chosen exit option while ( !userExited ) { // show main menu and get user selection int mainMenuSelection = DisplayMainMenu(); // decide how to proceed based on user's menu selection switch ( ( MenuOption ) mainMenuSelection ) { // user chooses to perform one of three transaction types case MenuOption.BALANCE_INQUIRY: case MenuOption.WITHDRAWAL: case MenuOption.DEPOSIT: // initialize as new object of chosen type currentTransaction = CreateTransaction( mainMenuSelection ); currentTransaction.Execute(); // execute transaction break; case MenuOption.EXIT_ATM: // user chose to terminate session screen.DisplayMessageLine( "\nExiting the system..." ); userExited = true; // this ATM session should end break; default: // user did not enter an integer from 1-4 screen.DisplayMessageLine( "\nYou did not enter a valid selection. Try again." ); break; } // end switch } // end while } // end method PerformTransactions // display the main menu and return an input selection private int DisplayMainMenu() { screen.DisplayMessageLine( "\nMain menu:" ); screen.DisplayMessageLine( "1 - View my balance" ); screen.DisplayMessageLine( "2 - Withdraw cash" ); screen.DisplayMessageLine( "3 - Deposit funds" ); screen.DisplayMessageLine( "4 - Exit\n" ); screen.DisplayMessage( "Enter a choice: " ); return keypad.GetInput(); // return user's selection } // end method DisplayMainMenu
Fig. J.1 | Class ATM represents the ATM. (Part 3 of 4.)
1260
Appendix J ATM Case Study Code
124 125 // return object of specified Transaction derived class 126 private Transaction CreateTransaction( int type ) 127 { 128 Transaction temp = null; // null Transaction reference 129 130 // determine which type of Transaction to create 131 switch ( ( MenuOption ) type ) 132 { 133 // create new BalanceInquiry transaction 134 case MenuOption.BALANCE_INQUIRY: 135 temp = new BalanceInquiry( currentAccountNumber, 136 screen, bankDatabase); 137 break; 138 case MenuOption.WITHDRAWAL: // create new Withdrawal transaction 139 temp = new Withdrawal( currentAccountNumber, screen, 140 bankDatabase, keypad, cashDispenser); 141 break; 142 case MenuOption.DEPOSIT: // create new Deposit transaction 143 temp = new Deposit( currentAccountNumber, screen, 144 bankDatabase, keypad, depositSlot); 145 break; 146 } // end switch 147 148 return temp; 149 } // end method CreateTransaction 150 } // end class ATM
Fig. J.1 | Class ATM represents the ATM. (Part 4 of 4.) Lines 14–20 declare an enumeration that corresponds to the four options in the ATM’s main menu (i.e., balance inquiry, withdrawal, deposit and exit). Lines 23–32 declare class ATM’s constructor, which initializes the class’s attributes. When an ATM object is first created, no user is authenticated, so line 25 initializes userAuthenticated to false. Line 26 initializes currentAccountNumber to 0 because there is no current user yet. Lines 27–30 instantiate new objects to represent the parts of the ATM. Recall that class ATM has composition relationships with classes Screen, Keypad, CashDispenser and DepositSlot, so class ATM is responsible for their creation. Line 31 creates a new BankDatabase. As you will soon see, the BankDatabase creates two Account objects that can be used to test the ATM. [Note: If this were a real ATM system, the ATM class would receive a reference to an existing database object created by the bank. However, in this implementation, we are only simulating the bank’s database, so class ATM creates the BankDatabase object with which it interacts.]
Implementing the Operation The class diagram of Fig. 11.22 does not list any operations for class ATM. We now implement one operation (i.e., public method) in class ATM that allows an external client of the class (i.e., class ATMCaseStudy; Section J.13) to tell the ATM to run. ATM method Run (lines 35–52) uses an infinite loop (lines 38–51) to repeatedly welcome a user, attempt to authenticate the user and, if authentication succeeds, allow the user to perform transactions.
J.2 Class ATM
1261
After an authenticated user performs the desired transactions and exits, the ATM resets itself, displays a goodbye message and restarts the process for the next user. We use an infinite loop here to simulate the fact that an ATM appears to run continuously until the bank turns it off (an action beyond the user’s control). An ATM user can exit the system, but cannot turn off the ATM completely. Inside method Run’s infinite loop, lines 41–45 cause the ATM to repeatedly welcome and attempt to authenticate the user as long as the user has not been authenticated (i.e., the condition !userAuthenticated is true). Line 43 invokes method DisplayMessageLine of the ATM’s screen to display a welcome message. Like Screen method DisplayMessage designed in the case study, method DisplayMessageLine (declared in lines 14–17 of Fig. J.2) displays a message to the user, but this method also outputs a newline after displaying the message. We add this method during implementation to give class Screen’s clients more control over the placement of displayed messages. Line 44 invokes class ATM’s private utility method AuthenticateUser (declared in lines 55–75) to attempt to authenticate the user.
Authenticating the User We refer to the requirements document to determine the steps necessary to authenticate the user before allowing transactions to occur. Line 58 of method AuthenticateUser invokes method DisplayMessage of the ATM’s screen to prompt the user to enter an account number. Line 59 invokes method GetInput of the ATM’s keypad to obtain the user’s input, then stores this integer in local variable accountNumber. Method AuthenticateUser next prompts the user to enter a PIN (line 62), and stores the PIN in local variable pin (line 63). Next, lines 66–67 attempt to authenticate the user by passing the accountNumber and pin entered by the user to the bankDatabase’s AuthenticateUser method. Class ATM sets its userAuthenticated attribute to the bool value returned by this method—userAuthenticated becomes true if authentication succeeds (i.e., the accountNumber and pin match those of an existing Account in bankDatabase) and remains false otherwise. If userAuthenticated is true, line 71 saves the account number entered by the user (i.e., accountNumber) in the ATM attribute currentAccountNumber. The other methods of class ATM use this variable whenever an ATM session requires access to the user’s account number. If userAuthenticated is false, lines 73–74 call the screen’s DisplayMessageLine method to indicate that an invalid account number and/or PIN was entered, so the user must try again. Note that we set currentAccountNumber only after authenticating the user’s account number and the associated PIN—if the database cannot authenticate the user, currentAccountNumber remains 0. After method Run attempts to authenticate the user (line 44), if userAuthenticated is still false (line 41), the while loop body (lines 41–45) executes again. If userAuthenticated is now true, the loop terminates, and control continues with line 47, which calls class ATM’s private utility method PerformTransactions. Performing Transactions Method PerformTransactions (lines 78–111) carries out an ATM session for an authenticated user. Line 80 declares local variable Transaction, to which we assign a BalanceInquiry, Withdrawal or Deposit object representing the ATM transaction currently being processed. Note that we use a Transaction variable here to allow us to take advan-
1262
Appendix J ATM Case Study Code
tage of polymorphism. Also, note that we name this variable after the role name included in the class diagram of Fig. 4.21—currentTransaction. Line 81 declares another local variable—a bool called userExited that keeps track of whether the user has chosen to exit. This variable controls a while loop (lines 84–110) that allows the user to execute an unlimited number of transactions before choosing to exit. Within this loop, line 87 displays the main menu and obtains the user’s menu selection by calling ATM utility method DisplayMainMenu (declared in lines 114–123). This method displays the main menu by invoking methods of the ATM’s screen and returns a menu selection obtained from the user through the ATM’s keypad. Line 87 stores the user’s selection, returned by DisplayMainMenu, in local variable mainMenuSelection. After obtaining a main menu selection, method PerformTransactions uses a switch statement (lines 90–109) to respond to the selection appropriately. If mainMenuSelection is equal to the underlying value of any of the three enum members representing transaction types (i.e., if the user chose to perform a transaction), lines 97–98 call utility method CreateTransaction (declared in lines 126–149) to return a newly instantiated object of the type that corresponds to the selected transaction. Variable currentTransaction is assigned the reference returned by method CreateTransaction, then line 99 invokes method Execute of this transaction to execute it. We discuss Transaction method Execute and the three Transaction derived classes shortly. Note that we assign to the Transaction variable currentTransaction an object of one of the three Transaction derived classes so that we can execute transactions. For example, if the user chooses to perform a balance inquiry, ( MenuOption ) mainMenuSelection (line 90) matches the case label MenuOption.BALANCE_INQUIRY, and CreateTransaction returns a BalanceInquiry object (lines 97–98). Thus, currentTransaction refers to a BalanceInquiry and invoking currentTransaction.Execute() (line 99) results in BalanceInquiry’s version of Execute being called polymorphically.
Creating Transactions Method CreateTransaction (lines 126–149) uses a switch statement (lines 131–146) to instantiate a new Transaction derived class object of the type indicated by the parameter type. Recall that method PerformTransactions passes mainMenuSelection to method CreateTransaction only when mainMenuSelection contains a value corresponding to one of the three transaction types. So parameter type (line 126) receives one of the values MenuOption.BALANCE_INQUIRY, MenuOption.WITHDRAWAL or MenuOption.DEPOSIT. Each case in the switch statement instantiates a new object by calling the appropriate Transaction derived class constructor. Note that each constructor has a unique parameter list, based on the specific data required to initialize the derived class object. A BalanceInquiry (lines 135–136) requires only the account number of the current user and references to the ATM’s screen and the bankDatabase. In addition to these parameters, a Withdrawal (lines 139–140) requires references to the ATM’s keypad and cashDispenser, and a Deposit (lines 143–144) requires references to the ATM’s keypad and depositSlot. We discuss the transaction classes in detail in Sections J.9–J.12. After executing a transaction (line 99 in method PerformTransactions), userExited remains false, and the while loop in lines 84–110 repeats, returning the user to the main menu. However, if a user does not perform a transaction and instead selects the main menu option to exit, line 103 sets userExited to true, causing the condition in line 84 of
J.3 Class Screen
1263
the while loop (!userExited) to become false. This while is the final statement of method PerformTransactions, so control returns to line 47 of the calling method Run. If the user enters an invalid main menu selection (i.e., not an integer in the range 1–4), lines 106–107 display an appropriate error message, userExited remains false (as set in line 81) and the user returns to the main menu to try again. When method PerformTransactions returns control to method Run, the user has chosen to exit the system, so lines 48–49 reset the ATM’s attributes userAuthenticated and currentAccountNumber to false and 0, respectively, to prepare for the next ATM user. Line 50 displays a goodbye message to the current user before the ATM welcomes the next user.
J.3 Class Screen Class Screen (Fig. J.2) represents the screen of the ATM and encapsulates all aspects of displaying output to the user. Class Screen simulates a real ATM’s screen with the computer monitor and outputs text messages using standard console output methods Console.Write and Console.WriteLine. In the design portion of this case study, we endowed class Screen with one operation—DisplayMessage. For greater flexibility in displaying messages to the Screen, we now declare three Screen methods—DisplayMessage, DisplayMessageLine and DisplayDollarAmount. Method DisplayMessage (lines 8–11) takes a string as an argument and prints it to the screen using Console.Write. The cursor stays on the same line, making this method appropriate for displaying prompts to the user. Method DisplayMessageLine (lines 14– 17) does the same using Console.WriteLine, which outputs a newline to move the cursor to the next line. Finally, method DisplayDollarAmount (lines 20–23) outputs a properly formatted dollar amount (e.g., $1,234.56). Line 22 uses method Console.Write to output a decimal value formatted as currency with a dollar sign, two decimal places and commas to increase the readability of large dollar amounts. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
// Screen.cs // Represents the screen of the ATM using System; public class Screen { // displays a message without a terminating carriage return public void DisplayMessage( string message ) { Console.Write( message ); } // end method DisplayMessage // display a message with a terminating carriage return public void DisplayMessageLine( string message ) { Console.WriteLine( message ); } // end method DisplayMessageLine
Fig. J.2 | Class Screen represents the screen of the ATM. (Part 1 of 2.)
1264 19 20 21 22 23 24
Appendix J ATM Case Study Code
// display a dollar amount public void DisplayDollarAmount( decimal amount ) { Console.Write( "{0:C}", amount ); } // end method DisplayDollarAmount } // end class Screen
Fig. J.2 | Class Screen represents the screen of the ATM. (Part 2 of 2.)
J.4 Class Keypad Class Keypad (Fig. J.3) represents the keypad of the ATM and is responsible for receiving all user input. Recall that we are simulating this hardware, so we use the computer’s keyboard to approximate the keypad. We use method Console.ReadLine to obtain keyboard input from the user. A computer keyboard contains many keys not found on the ATM’s keypad. We assume that the user presses only the keys on the computer keyboard that also appear on the keypad—the keys numbered 0–9 and the Enter key. Method GetInput (lines 8–11) invokes Convert method ToInt32 to convert the input returned by Console.ReadLine (line 10) to an int value. [Note: Method ToInt32 can throw a FormatException if the user enters non-integer input. Because the real ATM’s keypad permits only integer input, we simply assume that no exceptions will occur. See Chapter 12, Exception Handling, for information on catching and processing exceptions.] Recall that ReadLine obtains all the input used by the ATM. Class Keypad’s GetInput method simply returns the integer input by the user. If a client of class Keypad requires input that satisfies some particular criteria (i.e., a number corresponding to a valid menu option), the client must perform the appropriate error checking.
J.5 Class CashDispenser Class CashDispenser (Fig. J.4) represents the cash dispenser of the ATM. Line 6 declares constant INITIAL_COUNT, which indicates the number of $20 bills in the cash dispenser when the ATM starts (i.e., 500). Line 7 implements attribute billCount (modeled in Fig. 11.22), which keeps track of the number of bills remaining in the CashDispenser at any time. The constructor (lines 10–13) sets billCount to the initial count. [Note: We as1 2 3 4 5 6 7 8 9 10 11 12
// Keypad.cs // Represents the keypad of the ATM. using System; public class Keypad { // return an integer value entered by user public int GetInput() { return Convert.ToInt32( Console.ReadLine() ); } // end method GetInput } // end class Keypad
Fig. J.3 | Class Keypad represents the ATM’s keypad.
J.5 Class CashDispenser
1265
sume that the process of adding more bills to the CashDispenser and updating the billCount occur outside the ATM system.] Class CashDispenser has two public methods—DispenseCash (lines 16–21) and IsSufficientCashAvailable (lines 24–31). The class trusts that a client (i.e., Withdrawal) calls method DispenseCash only after establishing that sufficient cash is available by calling method IsSufficientCashAvailable. Thus, DispenseCash simulates dispensing the requested amount of cash without checking whether sufficient cash is available. Method IsSufficientCashAvailable (lines 24–31) has a parameter amount that specifies the amount of cash in question. Line 27 calculates the number of $20 bills required to dispense the specified amount. The ATM allows the user to choose only withdrawal amounts that are multiples of $20, so we convert amount to an integer value and divide it by 20 to obtain the number of billsRequired. Line 30 returns true if the CashDispenser’s billCount is greater than or equal to billsRequired (i.e., enough bills are available) and false otherwise (i.e., not enough bills). For example, if a user wishes to withdraw $80 (i.e., billsRequired is 4), but only three bills remain (i.e., billCount is 3), the method returns false. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
// CashDispenser.cs // Represents the cash dispenser of the ATM public class CashDispenser { // the default initial number of bills in the cash dispenser private const int INITIAL_COUNT = 500; private int billCount; // number of $20 bills remaining // parameterless constructor initializes billCount to INITIAL_COUNT public CashDispenser() { billCount = INITIAL_COUNT; // set billCount to INITIAL_COUNT } // end constructor // simulates dispensing the specified amount of cash public void DispenseCash( decimal amount ) { // number of $20 bills required int billsRequired = ( ( int ) amount ) / 20; billCount -= billsRequired; } // end method DispenseCash // indicates whether cash dispenser can dispense desired amount public bool IsSufficientCashAvailable( decimal amount ) { // number of $20 bills required int billsRequired = ( ( int ) amount ) / 20; // return whether there are enough bills available return ( billCount >= billsRequired ); } // end method IsSufficientCashAvailable } // end class CashDispenser
Fig. J.4 | Class CashDispenser represents the ATM’s cash dispenser.
1266
Appendix J ATM Case Study Code
Method DispenseCash (lines 16–21) simulates cash dispensing. If our system were hooked up to a real hardware cash dispenser, this method would interact with the hardware device to physically dispense the cash. Our simulated version of the method simply decreases the billCount of bills remaining by the number required to dispense the specified amount (line 20). Note that it is the responsibility of the client of the class (i.e., Withdrawal) to inform the user that cash has been dispensed—CashDispenser does not interact directly with Screen.
J.6 Class DepositSlot Class DepositSlot (Fig. J.5) represents the deposit slot of the ATM. This class simulates the functionality of a real hardware deposit slot. DepositSlot has no attributes and only one method—IsDepositEnvelopeReceived (lines 7–10)—which indicates whether a deposit envelope was received. Recall from the requirements document that the ATM allows the user up to two minutes to insert an envelope. The current version of method IsDepositEnvelopeReceived simply returns true immediately (line 9), because this is only a software simulation, so we assume that the user inserts an envelope within the required time frame. If an actual hardware deposit slot were connected to our system, method IsDepositEnvelopeReceived would be implemented to wait for a maximum of two minutes to receive a signal from the hardware deposit slot indicating that the user has indeed inserted a deposit envelope. If IsDepositEnvelopeReceived were to receive such a signal within two minutes, the method would return true. If two minutes were to elapse and the method still had not received a signal, then the method would return false.
J.7 Class Account Class Account (Fig. J.6) represents a bank account. Each Account has four attributes (modeled in Fig. 11.22)—accountNumber, pin, availableBalance and totalBalance. Lines 5–8 implement these attributes as private instance variables. For each of the instance variables accountNumber, availableBalance and totalBalance, we provide a property with the same name as the attribute, but starting with a capital letter. For example, property AccountNumber corresponds to the accountNumber attribute modeled in Fig. 11.22. Clients of this class do not need to modify the accountNumber instance variable, so AccountNumber is declared as a read-only property (i.e., it provides only a get accessor). 1 2 3 4 5 6 7 8 9 10 11
// DepositSlot.cs // Represents the deposit slot of the ATM public class DepositSlot { // indicates whether envelope was received (always returns true, // because this is only a software simulation of a real deposit slot) public bool IsDepositEnvelopeReceived() { return true; // deposit envelope was received } // end method IsDepositEnvelopeReceived } // end class DepositSlot
Fig. J.5 | Class DepositSlot represents the ATM’s deposit slot.
// Account.cs // Class Account represents a bank account. public class Account { private int accountNumber; // account number private int pin; // PIN for authentication private decimal availableBalance; // available withdrawal amount private decimal totalBalance; // funds available + pending deposit // four-parameter constructor initializes attributes public Account( int theAccountNumber, int thePIN, decimal theAvailableBalance, decimal theTotalBalance ) { accountNumber = theAccountNumber; pin = thePIN; availableBalance = theAvailableBalance; totalBalance = theTotalBalance; } // end constructor // read-only property that gets the account number public int AccountNumber { get { return accountNumber; } // end get } // end property AccountNumber // read-only property that gets the available balance public decimal AvailableBalance { get { return availableBalance; } // end get } // end property AvailableBalance // read-only property that gets the total balance public decimal TotalBalance { get { return totalBalance; } // end get } // end property TotalBalance // determines whether a user-specified PIN matches PIN in Account public bool ValidatePIN( int userPIN ) { return ( userPIN == pin ); } // end method ValidatePIN
Fig. J.6 | Class Account represents a bank account. (Part 1 of 2.)
1268 53 54 55 56 57 58 59 60 61 62 63 64 65
Appendix J ATM Case Study Code
// credits the account (funds have not yet cleared) public void Credit( decimal amount ) { totalBalance += amount; // add to total balance } // end method Credit // debits the account public void Debit( decimal amount ) { availableBalance -= amount; // subtract from available balance totalBalance -= amount; // subtract from total balance } // end method Debit } // end class Account
Fig. J.6 | Class Account represents a bank account. (Part 2 of 2.) Class Account has a constructor (lines 11–18) that takes an account number, the PIN established for the account, the initial available balance and the initial total balance as arguments. Lines 14–17 assign these values to the class’s attributes (i.e., instance variables). Note that Account objects would normally be created externally to the ATM system. However, in this simulation, the Account objects are created in the BankDatabase class (Fig. J.7).
Read-Only Properties of Class Account Read-only property AccountNumber (lines 21–27) provides access to an Account’s accountNumber instance variable. We include this property in our implementation so that a client of the class (e.g., BankDatabase) can identify a particular Account. For example, BankDatabase contains many Account objects, and it can access this property on each of its Account objects to locate the one with a specific account number. Read-only properties AvailableBalance (lines 30–36) and TotalBalance (lines 39– 45) allow clients to retrieve the values of private decimal instance variables availableBalance and totalBalance, respectively. Property AvailableBalance represents the amount of funds available for withdrawal. Property TotalBalance represents the amount of funds available, plus the amount of deposited funds pending confirmation of cash in deposit envelopes or clearance of checks in deposit envelopes. public
Methods of Class Account Method ValidatePIN (lines 48–51) determines whether a user-specified PIN (i.e., parameter userPIN) matches the PIN associated with the account (i.e., attribute pin). Recall that we modeled this method’s parameter userPIN in the UML class diagram of Fig. 7.23. If the two PINs match, the method returns true; otherwise, it returns false. Method Credit (lines 54–57) adds an amount of money (i.e., parameter amount) to an Account as part of a deposit transaction. Note that this method adds the amount only to instance variable totalBalance (line 56). The money credited to an account during a deposit does not become available immediately, so we modify only the total balance. We assume that the bank updates the available balance appropriately at a later time, when the amount of cash in the deposit envelope has be verified and the checks in the deposit envelope have cleared. Our implementation of class Account includes only methods required for carrying out ATM transactions. Therefore, we omit the methods that some other bank public
J.8 Class BankDatabase
1269
system would invoke to add to instance variable availableBalance to confirm a deposit or to subtract from attribute totalBalance to reject a deposit. Method Debit (lines 60–64) subtracts an amount of money (i.e., parameter amount) from an Account as part of a withdrawal transaction. This method subtracts the amount from both instance variable availableBalance (line 62) and instance variable totalBalance (line 63), because a withdrawal affects both balances.
J.8 Class BankDatabase Class BankDatabase (Fig. J.7) models the bank database with which the ATM interacts to access and modify a user’s account information. We determine one reference-type attribute for class BankDatabase based on its composition relationship with class Account. Recall from Fig. 11.21 that a BankDatabase is composed of zero or more objects of class Account. Line 5 declares attribute accounts—an array that will store Account objects— to implement this composition relationship. Class BankDatabase has a parameterless constructor (lines 8–15) that initializes accounts with new Account objects (lines 13–14). Note that the Account constructor (Fig. J.6, lines 11–18) has four parameters—the account number, the PIN assigned to the account, the initial available balance and the initial total balance. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
// BankDatabase.cs // Represents the bank account information database public class BankDatabase { private Account[] accounts; // array of the bank's Accounts // parameterless constructor initializes accounts public BankDatabase() { // create two Account objects for testing and // place them in the accounts array accounts = new Account[ 2 ]; // create accounts array accounts[ 0 ] = new Account( 12345, 54321, 1000.00M, 1200.00M ); accounts[ 1 ] = new Account( 98765, 56789, 200.00M, 200.00M ); } // end constructor // retrieve Account object containing specified account number private Account GetAccount( int accountNumber ) { // loop through accounts searching for matching account number foreach ( Account currentAccount in accounts ) { if ( currentAccount.AccountNumber == accountNumber ) return currentAccount; } // end foreach // account not found return null; } // end method GetAccount
Fig. J.7 | Class BankDatabase represents the bank’s account information database. (Part 1 of 2.)
// determine whether user-specified account number and PIN match // those of an account in the database public bool AuthenticateUser( int userAccountNumber, int userPIN) { // attempt to retrieve the account with the account number Account userAccount = GetAccount( userAccountNumber ); // if account exists, return result of Account function ValidatePIN if ( userAccount != null ) return userAccount.ValidatePIN( userPIN ); // true if match else return false; // account number not found, so return false } // end method AuthenticateUser // return available balance of Account with specified account number public decimal GetAvailableBalance( int userAccountNumber ) { Account userAccount = GetAccount( userAccountNumber ); return userAccount.AvailableBalance; } // end method GetAvailableBalance // return total balance of Account with specified account number public decimal GetTotalBalance( int userAccountNumber ) { Account userAccount = GetAccount(userAccountNumber); return userAccount.TotalBalance; } // end method GetTotalBalance // credit the Account with specified account number public void Credit( int userAccountNumber, decimal amount ) { Account userAccount = GetAccount( userAccountNumber ); userAccount.Credit( amount ); } // end method Credit // debit the Account with specified account number public void Debit( int userAccountNumber, decimal amount ) { Account userAccount = GetAccount( userAccountNumber ); userAccount.Debit( amount ); } // end method Debit } // end class BankDatabase
Fig. J.7 | Class BankDatabase represents the bank’s account information database. (Part 2 of 2.) Recall that class BankDatabase serves as an intermediary between class ATM and the actual Account objects that contain users’ account information. Thus, methods of class BankDatabase invoke the corresponding methods and properties of the Account object belonging to the current ATM user.
Utility Method GetAccount We include private utility method GetAccount (lines 18–29) to allow the BankDatabase to obtain a reference to a particular Account within the accounts array. To locate the usprivate
J.9 Class Transaction
1271
er’s Account, the BankDatabase compares the value returned by property AccountNumber for each element of accounts to a specified account number until it finds a match. Lines 21–25 traverse the accounts array. If currentAccount’s account number equals the value of parameter accountNumber, the method returns currentAccount. If no account has the given account number, then line 28 returns null.
Methods Method AuthenticateUser (lines 33–43) proves or disproves the identity of an ATM user. This method takes a user-specified account number and a user-specified PIN as arguments and indicates whether they match the account number and PIN of an Account in the database. Line 36 calls method GetAccount, which returns either an Account with userAccountNumber as its account number or null to indicate that userAccountNumber is invalid. If GetAccount returns an Account object, line 40 returns the bool value returned by that object’s ValidatePIN method. Note that BankDatabase’s AuthenticateUser method does not perform the PIN comparison itself—rather, it forwards userPIN to the Account object’s ValidatePIN method to do so. The value returned by Account method ValidatePIN (line 40) indicates whether the user-specified PIN matches the PIN of the user’s Account, so method AuthenticateUser simply returns this value (line 40) to the client of the class (i.e., ATM). The BankDatabase trusts the ATM to invoke method AuthenticateUser and receive a return value of true before allowing the user to perform transactions. BankDatabase also trusts that each Transaction object created by the ATM contains the valid account number of the current authenticated user and that this account number is passed to the remaining BankDatabase methods as argument userAccountNumber. Methods GetAvailableBalance (lines 46–50), GetTotalBalance (lines 53–57), Credit (lines 60–64) and Debit (lines 67–71) therefore simply retrieve the user’s Account object with utility method GetAccount, then invoke the appropriate Account method on that object. We know that the calls to GetAccount within these methods will never return null, because userAccountNumber must refer to an existing Account. Note that GetAvailableBalance and GetTotalBalance return the values returned by the corresponding Account properties. Also, note that methods Credit and Debit simply redirect parameter amount to the Account methods they invoke. public
J.9 Class Transaction Class Transaction (Fig. J.8) is an abstract base class that represents the notion of an ATM transaction. It contains the common features of derived classes BalanceInquiry, Withdrawal and Deposit. This class expands on the “skeleton” code first developed in Section 11.9. Line 3 declares this class to be abstract. Lines 5–7 declare the class’s private instance variables. Recall from the class diagram of Fig. 11.22 that class Transaction contains the property AccountNumber that indicates the account involved in the Transaction. Line 5 implements the instance variable accountNumber to maintain the AccountNumber property’s data. We derive attributes screen (implemented as instance variable userScreen in line 6) and bankDatabase (implemented as instance variable database in line 7) from class Transaction’s associations, modeled in Fig. 11.21. All transactions require access to the ATM’s screen and the bank’s database.
// Transaction.cs // Abstract base class Transaction represents an ATM transaction. public abstract class Transaction { private int accountNumber; // account involved in the transaction private Screen userScreen; // reference to ATM's screen private BankDatabase database; // reference to account info database // three-parameter constructor invoked by derived classes public Transaction( int userAccount, Screen theScreen, BankDatabase theDatabase ) { accountNumber = userAccount; userScreen = theScreen; database = theDatabase; } // end constructor // read-only property that gets the account number public int AccountNumber { get { return accountNumber; } // end get } // end property AccountNumber // read-only property that gets the screen reference public Screen UserScreen { get { return userScreen; } // end get } // end property UserScreen // read-only property that gets the bank database reference public BankDatabase Database { get { return database; } // end get } // end property Database // perform the transaction (overridden by each derived class) public abstract void Execute(); // no implementation here } // end class Transaction
Fig. J.8 |
abstract
base class Transaction represents an ATM transaction.
Class Transaction has a constructor (lines 10–16) that takes the current user’s account number and references to the ATM’s screen and the bank’s database as arguments. Because Transaction is an abstract class (line 3), this constructor is never called directly
J.10 Class BalanceInquiry
1273
to instantiate Transaction objects. Instead, this constructor is invoked by the constructors of the Transaction derived classes via constructor initializers. Class Transaction has three public read-only properties—AccountNumber (lines 19– 25), UserScreen (lines 28–34) and Database (lines 37–43). Derived classes of Transaction inherit these properties and use them to gain access to class Transaction’s private instance variables. Note that we chose the names of the UserScreen and Database properties for clarity—we wanted to avoid property names that are the same as the class names Screen and BankDatabase, which can be confusing. Class Transaction also declares abstract method Execute (line 46). It does not make sense to provide an implementation for this method in class Transaction, because a generic transaction cannot be executed. Thus, we declare this method to be abstract, forcing each Transaction concrete derived class to provide its own implementation that executes the particular type of transaction.
J.10 Class BalanceInquiry Class BalanceInquiry (Fig. J.9) inherits from Transaction and represents an ATM balance inquiry transaction (line 3). BalanceInquiry does not have any attributes of its own, but it inherits Transaction attributes accountNumber, screen and bankDatabase, which are accessible through Transaction’s public read-only properties. The BalanceInquiry constructor (lines 6–8) takes arguments corresponding to these attributes and forwards them to Transaction’s constructor by invoking the constructor initializer with keyword base (line 8). The body of the constructor is empty. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
// BalanceInquiry.cs // Represents a balance inquiry ATM transaction public class BalanceInquiry : Transaction { // five-parameter constructor initializes base class variables public BalanceInquiry( int userAccountNumber, Screen atmScreen, BankDatabase atmBankDatabase ) : base( userAccountNumber, atmScreen, atmBankDatabase ) {} // performs transaction; overrides Transaction's abstract method public override void Execute() { // get the available balance for the current user's Account decimal availableBalance = Database.GetAvailableBalance( AccountNumber ); // get the total balance for the current user's Account decimal totalBalance = Database.GetTotalBalance( AccountNumber ); // display the balance information on the screen UserScreen.DisplayMessageLine( "\nBalance Information:" ); UserScreen.DisplayMessage( " - Available balance: " ); UserScreen.DisplayDollarAmount( availableBalance ); UserScreen.DisplayMessage( "\n - Total balance: " );
Fig. J.9 | Class BalanceInquiry represents a balance inquiry ATM transaction. (Part 1 of 2.)
1274 25 26 27 28
Appendix J ATM Case Study Code
UserScreen.DisplayDollarAmount( totalBalance ); UserScreen.DisplayMessageLine( "" ); } // end method Execute } // end class BalanceInquiry
Fig. J.9 | Class BalanceInquiry represents a balance inquiry ATM transaction. (Part 2 of 2.) Class BalanceInquiry overrides Transaction’s abstract method Execute to provide a concrete implementation (lines 11–27) that performs the steps involved in a balance inquiry. Lines 14–15 obtain the specified Account’s available balance by invoking the GetAvailableBalance method of the inherited property Database. Note that line 15 uses the inherited property AccountNumber to get the account number of the current user. Line 18 retrieves the specified Account’s total balance. Lines 21–26 display the balance information on the ATM’s screen using the inherited property UserScreen. Recall that DisplayDollarAmount takes a decimal argument and outputs it to the screen formatted as a dollar amount with a dollar sign. For example, if a user’s available balance is 1000.50M, line 23 outputs $1,000.50. Note that line 26 inserts a blank line of output to separate the balance information from subsequent output (i.e., the main menu repeated by class ATM after executing the BalanceInquiry).
J.11 Class Withdrawal Class Withdrawal (Fig. J.10) extends Transaction and represents an ATM withdrawal transaction. This class expands on the “skeleton” code for this class developed in Fig. 11.24. Recall from the class diagram of Fig. 11.22 that class Withdrawal has one attribute, amount, which line 5 declares as a decimal instance variable. Fig. 11.21 models associations between class Withdrawal and classes Keypad and CashDispenser, for which lines 6–7 implement reference attributes keypad and cashDispenser, respectively. Line 10 declares a constant corresponding to the cancel menu option. Class Withdrawal’s constructor (lines 13–21) has five parameters. It uses the constructor initializer to pass parameters userAccountNumber, atmScreen and atmBankDatabase to base class Transaction’s constructor to set the attributes that Withdrawal inherits from Transaction. The constructor also takes references atmKeypad and atmCashDispenser as parameters and assigns them to reference-type attributes keypad and cashDispenser, respectively. 1 2 3 4 5 6 7 8 9 10
// Withdrawal.cs // Class Withdrawal represents an ATM withdrawal transaction. public class Withdrawal : Transaction { private decimal amount; // amount to withdraw private Keypad keypad; // reference to Keypad private CashDispenser cashDispenser; // reference to cash dispenser // constant that corresponds to menu option to cancel private const int CANCELED = 6;
Fig. J.10 | Class Withdrawal represents an ATM withdrawal transaction. (Part 1 of 4.)
// five-parameter constructor public Withdrawal( int userAccountNumber, Screen atmScreen, BankDatabase atmBankDatabase, Keypad atmKeypad, CashDispenser atmCashDispenser ) : base( userAccountNumber, atmScreen, atmBankDatabase ) { // initialize references to keypad and cash dispenser keypad = atmKeypad; cashDispenser = atmCashDispenser; } // end constructor // perform transaction, overrides Transaction's abstract method public override void Execute() { bool cashDispensed = false; // cash was not dispensed yet // transaction was not canceled yet bool transactionCanceled = false; // loop until cash is dispensed or the user cancels do { // obtain the chosen withdrawal amount from the user int selection = DisplayMenuOfAmounts(); // check whether user chose a withdrawal amount or canceled if ( selection != CANCELED ) { // set amount to the selected dollar amount amount = selection; // get available balance of account involved decimal availableBalance = Database.GetAvailableBalance( AccountNumber ); // check whether the user has enough money in the account if ( amount