main

Book Cover
Core C# and .NET
By Stephen C. Perry
...............................................
Publisher: Prentice Hall PTR
Pub Date: September 06, 2005
ISBN: 0-13-147227-5
Pages: 1008

Table of Contents | Index
Overview

  • Written for C# 2.0 and .NET 2.0: contains coverage of generics, Master Pages, the DataGridView, and other new features

  • Covers Web development, Windows development, data management, security, threading, remoting, and much more

  • Presents hundreds of non-trivial code examples that help you solve real-world problems

The Complete and Comprehensive Developer's Guide to C# 2.0 and .NET 2.0

Core C# and .NET is the no-nonsense, example-rich guide to achieving exceptional results with C# 2.0 and .NET 2.0. Writing for experienced programmers, Stephen Perry presents today's best practices for leveraging both C# 2.0 language features and Microsoft's .NET 2.0 infrastructure.

Like all books in the Core Series, Core C# and .NET focuses on solving real-world problems with serious, non-trivial code. Perry's broad, deep coverage ranges from new C# generics to Web services, from reflection to security. He systematically introduces the development of Windows Forms applications and the effective use of GDI+ graphics classes. He offers detailed guidance on data management with XML and ADO.NET, plus advanced coverage of threading, remoting, and code security. Finally, Perry presents an extensive section on Web development, covering ASP.NET, state management, HTTP requests, and much more.

With practical insights into everything from scalability to localization, this is the C# book you've been searching for: your definitive guide to building production-quality C# applications.

Core C# and .NET delivers

  • Best practices for building C#.NET Windows applications, Web applications, and Web services

  • Expert insight into security, scalability, and other crucial issues

  • Hundreds of professional-quality code examples

  • In-depth coverage of the latest C# 2.0 features, including generics

EVERY CORE SERIES BOOK:

DEMONSTRATES practical techniques used by professional developers

FEATURES robust, thoroughly tested sample code and realistic examples

FOCUSES on the cutting-edge technologies you need to master today

PROVIDES expert advice that will help you build superior software


Table of Contents

Book Cover
Core C# and .NET
By Stephen C. Perry
...............................................
Publisher: Prentice Hall PTR
Pub Date: September 06, 2005
ISBN: 0-13-147227-5
Pages: 1008

Table of Contents | Index

Copyright
PRENTICE HALL CORE SERIES
About the Author
Foreword
Preface
Acknowledgments
Part I. Fundamentals of C# Programming and Introduction to .NET
 | Chapter 1. Introduction to .NET and C#
 |  | Section 1.1. Overview of the .NET Framework
 |  | Section 1.2. Common Language Runtime
 |  | Section 1.3. Framework Class Library
 |  | Section 1.4. Working with the .NET Framework and SDK
 |  | Section 1.5. Understanding the C# Compiler
 |  | Section 1.6. Summary
 |  | Section 1.7. Test Your Understanding
 | Chapter 2. C# Language Fundamentals
 |  | Section 2.1. The Layout of a C# Program
 |  | Section 2.2. Primitives
 |  | Section 2.3. Operators: Arithmetic, Logical, and Conditional
 |  | Section 2.4. Loops
 |  | Section 2.5. C# Preprocessing Directives
 |  | Section 2.6. Strings
 |  | Section 2.7. Enumerated Types
 |  | Section 2.8. Arrays
 |  | Section 2.9. Reference and Value Types
 |  | Section 2.10. Summary
 |  | Section 2.11. Test Your Understanding
 | Chapter 3. Class Design in C#
 |  | Section 3.1. Introduction to a C# Class
 |  | Section 3.2. Defining a Class
 |  | Section 3.3. Overview of Class Members
 |  | Section 3.4. Constants, Fields, and Properties
 |  | Section 3.5. Methods
 |  | Section 3.6. Constructors
 |  | Section 3.7. Delegates and Events
 |  | Section 3.8. Operator Overloading
 |  | Section 3.9. Interfaces
 |  | Section 3.10. Generics
 |  | Section 3.11. Structures
 |  | Section 3.12. Structure Versus Class
 |  | Section 3.13. Summary
 |  | Section 3.14. Test Your Understanding
 | Chapter 4. Working with Objects in C#
 |  | Section 4.1. Object Creation
 |  | Section 4.2. Exception Handling
 |  | Section 4.3. Implementing System.Object Methods in a Custom Class
 |  | Section 4.4. Working with .NET Collection Classes and Interfaces
 |  | Section 4.5. Object Serialization
 |  | Section 4.6. Object Life Cycle Management
 |  | Section 4.7. Summary
 |  | Section 4.8. Test Your Understanding
Part II. Creating Applications Using the .NET Framework Class Library
 | Chapter 5. C# Text Manipulation and File I/O
 |  | Section 5.1. Characters and Unicode
 |  | Section 5.2. The String Class
 |  | Section 5.3. Comparing Strings
 |  | Section 5.4. Searching, Modifying, and Encoding a String's Content
 |  | Section 5.5. StringBuilder
 |  | Section 5.6. Formatting Numeric and DateTime Values
 |  | Section 5.7. Regular Expressions
 |  | Section 5.8. System.IO: Classes to Read and Write Streams of Data
 |  | Section 5.9. System.IO: Directories and Files
 |  | Section 5.10. Summary
 |  | Section 5.11. Test Your Understanding
 | Chapter 6. Building Windows Forms Applications
 |  | Section 6.1. Programming a Windows Form
 |  | Section 6.2. Windows.Forms Control Classes
 |  | Section 6.3. The Form Class
 |  | Section 6.4. Working with Menus
 |  | Section 6.5. Adding Help to a Form
 |  | Section 6.6. Forms Inheritance
 |  | Section 6.7. Summary
 |  | Section 6.8. Test Your Understanding
 | Chapter 7. Windows Forms Controls
 |  | Section 7.1. A Survey of .NET Windows Forms Controls
 |  | Section 7.2. Button Classes, Group Box, Panel, and Label
 |  | Section 7.3. PictureBox and TextBox Controls
 |  | Section 7.4. ListBox, CheckedListBox, and ComboBox Classes
 |  | Section 7.5. The ListView and TreeView Classes
 |  | Section 7.6. The ProgressBar, Timer, and StatusStrip Classes
 |  | Section 7.7. Building Custom Controls
 |  | Section 7.8. Using Drag and Drop with Controls
 |  | Section 7.9. Using Resources
 |  | Section 7.10. Summary
 |  | Section 7.11. Test Your Understanding
 | Chapter 8. .NET Graphics Using GDI+
 |  | Section 8.1. GDI+ Overview
 |  | Section 8.2. Using the Graphics Object
 |  | Section 8.3. Images
 |  | Section 8.4. Summary
 |  | Section 8.4. Test Your Understanding
 | Chapter 9. Fonts, Text, and Printing
 |  | Section 9.1. Fonts
 |  | Section 9.2. Drawing Text Strings
 |  | Section 9.3. Printing
 |  | Section 9.4. Summary
 |  | Section 9.5. Test Your Understanding
 | Chapter 10. Working with XML in .NET
 |  | Section 10.1. Working with XML
 |  | Section 10.2. Techniques for Reading XML Data
 |  | Section 10.3. Techniques for Writing XML Data
 |  | Section 10.4. Using XPath to Search XML
 |  | Section 10.5. Summary
 |  | Section 10.6. Test Your Understanding
 | Chapter 11. ADO.NET
 |  | Section 11.1. Overview of the ADO.NET Architecture
 |  | Section 11.2. Data Access Models: Connected and Disconnected
 |  | Section 11.3. ADO.NET Connected Model
 |  | Section 11.4. DataSets, DataTables, and the Disconnected Model
 |  | Section 11.5. XML and ADO.NET
 |  | Section 11.6. Summary
 |  | Section 11.7. Test Your Understanding
 | Chapter 12. Data Binding with Windows Forms Controls
 |  | Section 12.1. Overview of Data Binding
 |  | Section 12.2. Using Simple and Complex Data Binding in an Application
 |  | Section 12.3. The DataGridView Class
 |  | Section 12.4. Summary
 |  | Section 12.4. Test Your Understanding
Part III. Advanced Use of C# and the .NET Framework
 | Chapter 13. Asynchronous Programming and Multithreading
 |  | Section 13.1. What Is a Thread?
 |  | Section 13.2. Asynchronous Programming
 |  | Section 13.3. Working Directly with Threads
 |  | Section 13.4. Thread Synchronization
 |  | Section 13.5. Summary
 |  | Section 13.5. Test Your Understanding
 | Chapter 14. Creating Distributed Applications with Remoting
 |  | Section 14.1. Application Domains
 |  | Section 14.2. Remoting
 |  | Section 14.3. Leasing and Sponsorship
 |  | Section 14.4. Summary
 |  | Section 14.5. Test Your Understanding
 | Chapter 15. Code Refinement, Security, and Deployment
 |  | Section 15.1. Following .NET Code Design Guidelines
 |  | Section 15.2. Strongly Named Assemblies
 |  | Section 15.3. Security
 |  | Section 15.4. Application Deployment Considerations
 |  | Section 15.5. Summary
 |  | Section 15.6. Test Your Understanding
Part Programming for the Internet
 | Chapter 16. ASP.NET Web Forms and Controls
 |  | Section 16.1. Client-Server Interaction over the Internet
 |  | Section 16.2. Web Forms Controls
 |  | Section 16.3. Data Binding and Data Source Controls
 |  | Section 16.4. Validation Controls
 |  | Section 16.5. Master and Content Pages
 |  | Section 16.6. Building and Using Custom Web Controls
 |  | Section 16.7. Selecting a Web Control to Display Data
 |  | Section 16.8. Summary
 |  | Section 16.9. Test Your Understanding
 | Chapter 17. The Asp.Net Application Environment
 |  | Section 17.1. HTTP Request and Response Classes
 |  | Section 17.2. ASP.NET and Configuration Files
 |  | Section 17.3. ASP.NET Application Security
 |  | Section 17.4. Maintaining State
 |  | Section 17.5. Caching
 |  | Section 17.6. Creating a Web Client with WebRequest and WebResponse
 |  | Section 17.7. HTTP Pipeline
 |  | Section 17.8. Summary
 |  | Section 17.9. Test Your Understanding
 | Chapter 18. XML Web Services
 |  | Section 18.1. Introduction to Web Services
 |  | Section 18.2. Building an XML Web Service
 |  | Section 18.3. Building an XML Web Service Client
 |  | Section 18.4. Understanding WSDL and SOAP
 |  | Section 18.5. Using Web Services with Complex Data Types
 |  | Section 18.6. Web Services Performance
 |  | Section 18.7. Summary
 |  | Section 18.8. Test Your Understanding
Appendix A. Features Specific to .NET 2.0 and C# 2.0
Appendix B. DataGridView Events and Delegates
Answers to Chapter Exercises
 | Chapter 1
 | Chapter 2
 | Chapter 3
 | Chapter 4
 | Chapter 5
 | Chapter 6
 | Chapter 7
 | Chapter 8
 | Chapter 9
 | Chapter 10
 | Chapter 11
 | Chapter 12
 | Chapter 13
 | Chapter 14
 | Chapter 15
 | Chapter 16
 | Chapter 17
 | Chapter 18
Index


Copyright

Many of the designations used by manufacturers and sellers to distinguish their products are claimed as trademarks. Where those designations appear in this book, and the publisher was aware of a trademark claim, the designations have been printed with initial capital letters or in all capitals.

The author and publisher have taken care in the preparation of this book, but make no expressed or implied warranty of any kind and assume no responsibility for errors or omissions. No liability is assumed for incidental or consequential damages in connection with or arising out of the use of the information or programs contained herein.

The publisher offers excellent discounts on this book when ordered in quantity for bulk purchases or special sales, which may include electronic versions and/or custom covers and content particular to your business, training goals, marketing focus, and branding interests. For more information, please contact:

U. S. Corporate and Government Sales
(800) 382-3419
corpsales@pearsontechgroup.com

For sales outside the U. S., please contact:

International Sales
international@pearsoned.com

Visit us on the Web: www.phptr.com

Library of Congress Cataloging-in-Publication Data

Perry, Stephen (Stephen C.)
Core C# and .NET / Stephen Perry.
p. cm.
ISBN 0131472275
1. C++ (Computer program language) 2. Microsoft .NET. I. Title.
QA76.73.C153P468 2005
005.13'3--dc22
2005021301

Copyright © 2006 Pearson Education, Inc.

All rights reserved. Printed in the United States of America. This publication is protected by copyright, and permission must be obtained from the publisher prior to any prohibited reproduction, storage in a retrieval system, or transmission in any form or by any means, electronic, mechanical, photocopying, recording, or likewise. For information regarding permissions, write to:

Pearson Education, Inc.
Rights and Contracts Department
One Lake Street
Upper Saddle River, NJ 07458

Text printed in the United States on recycled paper at R. R. Donnelley in Crawfordsville, Indiana.

First printing, September 2005


PRENTICE HALL CORE SERIES

Core J2EE Patterns, Second Edition, Alur/Malks/Crupi

Core PHP Programming, Third Edition, Atkinson/Suraski

Core Lego Mindstorms, Bagnall

Core JSTL, Geary

Core JavaServer Faces, Geary/Horstmann

Core Web Programming, Second Edition, Hall/Brown

Core Servlets and JavaServer Pages, Second Edition, Hall/Brown

Core Java™ 2, Volume I—Fundamentals, Horstmann/Cornell

Core Java™ 2, Volume II—Advanced Features, Horstmann/Cornell

Core C# and .NET, Perry

Core CSS, Second Edition, Schengili-Roberts

Core Security Patterns, Steel/Nagappan/Lai

Core Java Data Objects, Tyagi/Vorburger/McCammon/Bobzin

Core Web Application Development with PHP and MySQL, Wandschneider


About the Author

Stephen Perry is a software architect specializing in the design and implementation of .NET applications. For the past three years he has designed and developed significant .NET-based solutions for clients in the textile, furniture, legal, and medical professions. Prior to that, he worked for more than 20 years in all phases of software development. With the extra 25 hours a week now available from the completion of the book, he'll have more time for triathlon training and watching "Seinfeld" reruns.


Foreword

Learning a new programming language, or even a new version of one, can be a lot like traveling to a foreign country. Some things will look familiar. Some things will look very odd. You can usually get by if you stick to the familiar; but, who wants to just "get by"? Often times, it's the odd things in a country, culture, or language where you can realize many interesting and valuable benefits.

To do it right, however, you'll need a proper guide or guide book. This is essential. Just as a good guide book will tell you what to see, when to see it, what to eat, what to avoid, and a few tips, so will a good programming book tell you what frameworks and classes to use, how to call their methods, what bad practices to avoid, and a few productivity tips and best practices.

This is exactly what Steve has provided you.

If you were to approach C# 2.0 from a 1.0 perspective, or from some other language background, you'd be missing out on all its new offerings. For example, I'm a seasoned developer and set in my ways. If you're like me, then you probably still write methods, loops, and other design patterns the same way you have for many years. You know it's not producing the most efficient code, but it's quick. It's easy to read and understand; and it works.

Steve has literally written the "Core" C# book here. He begins by introducing you to the important C# concepts and their interaction with the .NET Framework; but, then he deviates from the other C# reference books on the market and jumps right into the application of C#. These include most of the common, daily tasks that you could be asked to perform, such as working with text, files, databases, XML, Windows forms and controls, printing, ASP.NET Web applications, Web services, and remoting. Steve even provides you with asynchronous, multithreaded, security, and deployment topics as a bonus. You won't need another book on your shelf.

So, what are you waiting for? I think it's time to break a few of your familiar habits and experience some new culture!

Richard Hundhausen
Author, Introducing Microsoft Visual Studio 2005 Team System


Preface

"The process of preparing programs for a digital computer is especially attractive because it not only can be economically and scientifically rewarding, it can also be an aesthetic experience much like composing poetry or music."

— Donald Knuth, Preface to Fundamental Algorithms (1968)

Thirty-seven years later, programmers still experience the same creative satisfaction from developing a well-crafted program. It can be 10 lines of recursive code that pops into one's head at midnight, or it can be an entire production management system whose design requires a year of midnights. Then, as now, good programs still convey an impression of logic and naturalness—particularly to their users.

But the challenges have evolved. Software is required to be more malleable—it may be run from a LAN, the Internet, or a cellular phone. Security is also a much bigger issue, because the code may be accessible all over the world. This, in turn, raises issues of scalability and how to synchronize code for hundreds of concurrent users. More users bring more cultures, and the concomitant need to customize programs to meet the language and culture characteristics of a worldwide client base.

.NET—and the languages written for it—addresses these challenges as well as any unified development environment. This book is written for developers, software architects, and students who choose to work with the .NET Framework. All code in the book is written in C#, although only one chapter is specifically devoted to the syntactical structure of the C# language.

This book is not an introduction to programming—it assumes you are experienced in a computer language. This book is not an introduction to object-oriented programming (OOP)—although it will re-enforce the principles of encapsulation, polymorphism, and inheritance through numerous examples. Finally, this book is not an introduction to using Visual Studio.NET to develop C# programs. VS.NET is mentioned, but the emphasis is on developing and understanding C# and the .NET classes—independent of any IDE.

This book is intended for the experienced programmer who is moving to .NET and wants to get an overall feel for its capabilities. You may be a VB6 or C++ programmer seeking exposure to .NET; a VB.NET programmer expanding your repertoire into C#; or—and yes it does happen occasionally—a Java programmer investigating life on the far side. Here's what you'll find if you choose to journey through this book.

  • 18 Chapters. The first four chapters should be read in order. They provide an introduction to C# and a familiarity with using the .NET class libraries. The remaining chapters can be read selectively based on your interests. Chapters 6 and 7 describe how to develop Windows Forms applications. Chapters 8 and 9 deal with GDI+—the .NET graphics classes. Chapters 10 through 12 are about working with data. Both XML and ADO.NET are discussed. Chapters 13, 14, and 15 tackle the more advanced topics of threading, remoting, and code security, respectively. The final chapters form a Web trilogy: Chapter 16 discusses ASP.NET Web page development; Chapter 17 looks behind the scenes at how to manage state information and manage HTTP requests; the book closes with a look at Web Services in Chapter 18.

  • .NET 2.0. The manuscript went to publication after the release of Beta 2.0. As such, it contains information based on that release. The 2.0 topics are integrated within the chapters, rather than placing them in a special 2.0 section. However, as a convenience, Appendix A contains a summary and separate index to the .NET 2.0 topics.

  • Coding examples. Most of the code examples are short segments that emphasize a single construct or technique. The objective is to avoid filler code that does nothing but waste paper. Only when it is essential does a code example flow beyond a page in length. Note that all significant code examples are available as a download from www.corecsharp.net or indirectly at www.phptr.com/title/0131472275. To access the download area, enter the keyword parsifal.

  • Questions and answers. Each chapter ends with a section of questions to test your knowledge. The answers are available in a single section at the end of the book.

  • Fact rather than opinion. This book is not based on my opinion; it is based on the features inherent in .NET and C#. Core recommendations and notes are included with the intent of providing insight rather than opinion.

Although some will disagree, if you really want to learn C# and .NET, shut down your IDE, pull out your favorite text editor, and learn how to use the C# compiler from the command line. After you have mastered the fundamentals, you can switch to VS.NET and any other IDE for production programming.

Finally, a word about .NET and Microsoft: This book was developed using Microsoft .NET 1.x and Whidbey betas. It includes topics such as ADO.NET and ASP.NET that are very much Microsoft proprietary implementations. In fact, Microsoft has applied to patent these methodologies. However, all of C# and many of the .NET basic class libraries are based on a standard that enables them to be ported to other platforms. Now, and increasingly in the future, many of the techniques described in this book will be applicable to .NET like implementations (such as the Mono project, http://www.mono-project.com/Main_Page) on non-Windows platforms.


Acknowledgments

I have received assistance from a great number of people over the 21 months that went into the research and development of this book. I wish to thank first my wife, Rebecca, who tirelessly read through pages of unedited manuscripts, and used her systems programming background to add valuable recommendations. Next, I wish to thank the reviewers whose recommendations led to better chapter organization, fewer content and code errors, and a perspective on which topics to emphasize. Reviewers included Greg Beamer, James Edelen, Doug Holland, Curtiss Howard, Anand Narayanaswamy, and Gordon Weakliem. Special thanks go to Richard Hundhausen whose recommendations went well beyond the call of duty; and Cay Horstmann, who read every preliminary chapter and whose Java allegiance made him a wonderful and influential "Devil's Advocate." I also wish to thank Dr. Alan Tharp who encouraged the idea of writing a book on .NET and remains my most respected advisor in the computer profession.

Finally, it has been a pleasure working with the editors and staff at Prentice Hall PTR. I'm particularly grateful for the efforts of Stephane Nakib, Joan Murray, Ebony Haight, Jessica D'Amico, Kelli Brooks, and Vanessa Moore. This book would not exist without the efforts of my original editor Stephane Nakib. The idea for the book was hers, and her support for the project kept it moving in the early stages. My other editor, Joan Murray, took over midway through the project and provided the oversight, advice, and encouragement to complete the project. Production editor Vanessa Moore and copy editor Kelli Brooks performed the "dirty work" of turning the final manuscript—with its myriad inconsistencies and word misuse—into a presentable book. To them, I am especially grateful. Working with professionals such as these was of inestimable value on those days when writing was more Sisyphean than satisfying.


    Part I: Fundamentals of C# Programming and Introduction to .NET

    Chapter 1 Introduction to .NET and C#

    Chapter 2 C# Language Fundamentals

    Chapter 3 Class Design in C#

    Chapter 4 Working with Objects in C#


Chapter 1. Introduction to .NET and C#

Topics in This Chapter

  • Overview of the .NET Framework: Architecture and features.

  • Common Language Runtime: An overview of the tasks performed by the runtime portion of the .NET Framework: Just-in-Time (JIT) compiler, loading assemblies, and code verification.

  • Common Type System and Common Language Specifications: Rules that govern Common Language Runtime (CLR) compatibility and language interoperability.

  • Assemblies: A look at the structure of an assembly, the philosophy behind it, and the difference between private and shared assemblies.

  • Framework Class Library: The Framework Library supplies hundreds of base classes grouped into logical namespaces.

  • Development Tools: Several tools are provided with .NET to aid code development. These include Ildasm for disassembling code, WinCV to view the properties of a class, and the Framework Configuration tool.

  • Compiling and Running C# Programs: Using the C# compiler from the command line and options for structuring an application.

The effective use of a language requires a good deal more than learning the syntax and features of the language. In fact, the greater part of the learning curve for new technology is now concentrated in the programming environment. It is not enough to be proficient with the C# language; the successful developer and software architect must also be cognizant of the underlying class libraries and the tools available to probe these libraries, debug code, and check the efficiency of underlying code.

The purpose of this chapter is to provide an awareness of the .NET environment before you proceed to the syntax and semantics of the C# language. The emphasis is on how the environment, not the language, will affect the way you develop software. If you are new to .NET, it is necessary to embrace some new ideas. .NET changes the way you think of dealing with legacy code and version control; it changes the way program resources are disposed of; it permits code developed in one language to be used by another; it simplifies code deployment by eliminating a reliance on the system registry; and it creates a self-describing metalanguage that can be used to determine program logic at runtime. You will bump into all of these at some stage of the software development process, and they will influence how you design and deploy your applications.

To the programmer's eye, the .NET platform consists of a runtime environment coupled with a base class library. The layout of this chapter reflects that viewpoint. It contains separate sections on the Common Language Runtime (CLR) and the Framework Class Library (FCL). It then presents the basic tools that a developer may use to gain insight into the inner workings of the .NET Framework, as well as manage and distribute applications. As a prelude to Chapter 2, the final section introduces the C# compiler with examples of its use.


1.1. Overview of the .NET Framework

The .NET Framework is designed as an integrated environment for seamlessly developing and running applications on the Internet, on the desktop as Windows Forms, and even on mobile devices (with the Compact Framework). Its primary objectives are as follows:

  • To provide a consistent object-oriented environment across the range of applications.

  • To provide an environment that minimizes the versioning conflicts ("DLL Hell") that has bedeviled Windows (COM) programmers, and to simplify the code distribution/installation process.

  • To provide a portable environment, based on certified standards, that can be hosted by any operating system. Already, C# and a major part of the .NET runtime, the Common Language Infrastructure (CLI), have been standardized by the ECMA.[1]

    [1] ECMA International was formerly known as the European Computer Manufacturers Association and is referred to herein simply as the ECMA.

  • To provide a managed environment in which code is easily verified for safe execution.

To achieve these broad objectives, the .NET Framework designers settled on an architecture that separates the framework into two parts: the Common Language Runtime (CLR) and the Framework Class Library (FCL). Figure 1-1 provides a stylized representation of this.

Figure 1-1. .NET Framework

The CLR—which is Microsoft's implementation of the CLI standard—handles code execution and all of the tasks associated with it: compilation, memory management, security, thread management, and enforcement of type safety and use. Code that runs under the CLR is referred to as managed code. This is to distinguish it from unmanaged code that does not implement the requirements to run in the CLR—such as COM or Windows API based components.

The other major component, the Framework Class Library, is a reusable code library of types (classes, structures, and so on) available to applications running under .NET. As the figure shows, these include classes for database access, graphics, interoperating with unmanaged code, security, and both Web and Windows forms. All languages that target the .NET Framework use this common class library. Thus, after you gain experience using these types, you can apply that knowledge to any .NET language you may choose to program in.

Microsoft .NET and the CLI Standards

A natural concern for a developer that chooses to invest the time in learning C# and .NET is whether the acquired skill set can be transferred to other platforms. Specifically, is .NET a Microsoft product tethered only to the Windows operating system? Or is it a portable runtime and development platform that will be implemented on multiple operating systems? To answer the question, it is necessary to understand the relationship among Microsoft .NET, C#, and the Common Language Infrastructure (CLI) standards.

The CLI defines a platform-independent virtual code execution environment. It specifies no operating system, so it could just as easily be Linux as Windows. The centerpiece of the standard is the definition for a Common Intermediate Language (CIL) that must be produced by CLI compliant compilers and a type system that defines the data types supported by any compliant language. As described in the next section, this intermediate code is compiled into the native language of its host operating system.

The CLI also includes the standards for the C# language, which was developed and promoted by Microsoft. As such, it is the de facto standard language for .NET. However, other vendors have quickly adopted the CIL standard and produced—just to name a few—Python, Pascal, Fortran, Cobol, and Eiffel .NET compilers.

The .NET Framework, as depicted in Figure 1-1, is Microsoft's implementation of the CLI standards. The most important thing to note about this implementation is that it contains a great deal more features than are specified by the CLI architecture. To illustrate this, compare it to the CLI standards architecture shown in Figure 1-2.

Figure 1-2. Architecture defined by CLI specifications

Briefly, the CLI defines two implementations: a minimal implementation known as a Kernel Profile and a more feature rich Compact Profile. The kernel contains the types and classes required by a compiler that is CLI compliant. The Base Class Library holds the basic data type classes, as well as classes that provide simple file access, define security attributes, and implement one-dimensional arrays. The Compact Profile adds three class libraries: an XML library that defines simple XML parsing, a Network library that provides HTTP support and access to ports, and a Reflection library that supports reflection (a way for a program to examine itself through metacode).

This book, which describes the Microsoft implementation, would be considerably shorter if it described only the CLI recommendations. There would be no chapters on ADO.NET (database classes), ASP.NET (Web classes), or Windows Forms—and the XML chapters would be greatly reduced. As you may guess, these libraries depend on the underlying Windows API for functionality. In addition, .NET permits a program to invoke the Win32 API using an Interop feature. This means that a .NET developer has access not only to the Win32 API but also legacy applications and components (COM).

By keeping this rather wide bridge to Windows, Microsoft's .NET implementation becomes more of a transparent than virtual environment—not that there's anything wrong with that. It gives developers making the transition to .NET the ability to create hybrid applications that combine .NET components with preexisting code. It also means that the .NET implementation code is not going to be ported to another operating system.

The good news for developers—and readers of this book—is that the additional Microsoft features are being adopted by CLI open source initiatives. Mono[2] , one of the leading CLI projects, already includes major features such as ADO.NET, Windows Forms, full XML classes, and a rich set of Collections classes. This is particularly significant because it means the knowledge and skills obtained working with Microsoft .NET can be applied to implementations on Linux, BSD, and Solaris platforms. With that in mind, let's take an overview of the Microsoft CLI implementation.


1.2. Common Language Runtime

The Common Language Runtime manages the entire life cycle of an application: it locates code, compiles it, loads associated classes, manages its execution, and ensures automatic memory management. Moreover, it supports cross-language integration to permit code generated by different languages to interact seamlessly. This section peers into the inner workings of the Common Language Runtime to see how it accomplishes this. It is not an in-depth discussion, but is intended to make you comfortable with the terminology, appreciate the language-neutral architecture, and understand what's actually happening when you create and execute a program.

Compiling .NET Code

Compilers that are compliant with the CLR generate code that is targeted for the runtime, as opposed to a specific CPU. This code, known variously as Common Intermediate Language (CIL), Intermediate Language (IL), or Microsoft Intermediate Language (MSIL), is an assembler-type language that is packaged in an EXE or DLL file. Note that these are not standard executable files and require that the runtime's Just-in-Time (JIT) compiler convert the IL in them to a machine-specific code when an application actually runs. Because the Common Language Runtime is responsible for managing this IL, the code is known as managed code.

This intermediate code is one of the keys to meeting the .NET Framework's formal objective of language compatibility. As Figure 1-3 illustrates, the Common Language Runtime neither knows—nor needs to know—which language an application is created in. Its interaction is with the language-independent IL. Because applications communicate through their IL, output from one compiler can be integrated with code produced by a different compiler.

Figure 1-3. Common Language Runtime functions

Another .NET goal, platform portability, is addressed by localizing the creation of machine code in the JIT compiler. This means that IL produced on one platform can be run on any other platform that has its own framework and a JIT compiler that emits its own machine code.

In addition to producing IL, compilers that target the CLR must emit metadata into every code module. The metadata is a set of tables that allows each code module to be self-descriptive. The tables contain information about the assembly containing the code, as well as a full description of the code itself. This information includes what types are available, the name of each type, type members, the scope or visibility of the type, and any other type features. Metadata has many uses:

  • The most important use is by the JIT compiler, which gathers all the type information it needs for compiling directly from the metacode. It also uses this information for code verification to ensure the program performs correct operations. For example, the JIT ensures that a method is called correctly by comparing the calling parameters with those defined in the method's metadata.

  • Metadata is used in the Garbage Collection process (memory management). The garbage collector (GC) uses metadata to know when fields within an object refer to other objects so that the GC can determine what objects can and can't have their memory reclaimed.

  • .NET provides a set of classes that provide the functionality to read metadata from within a program. This functionality is known collectively as reflection. It is a powerful feature that permits a program to query the code at runtime and make decisions based on its discovery. As we will see later in the book, it is the key to working with custom attributes, which are a C#-supported construct for adding custom metadata to a program.

IL and metadata are crucial to providing language interoperability, but its real-world success hinges on all .NET compilers supporting a common set of data types and language specifications. For example, two languages cannot be compatible at the IL level if one language supports a 32-bit signed integer and the other does not. They may differ syntactically (for example, C# int versus a Visual Basic Integer), but there must be agreement of what base types each will support.

As discussed earlier, the CLI defines a formal specification, called the Common Type System (CTS), which is an integral part of the Common Language Runtime. It describes how types are defined and how they must behave in order to be supported by the Common Language Runtime.

Common Type System

The CTS provides a base set of data types for each language that runs on the .NET platform. In addition, it specifies how to declare and create custom types, and how to manage the lifetime of instances of these types. Figure 1-4 shows how .NET organizes the Common Type System.

Figure 1-4. Base types defined by Common Type System

Two things stand out in this figure. The most obvious is that types are categorized as reference or value types. This taxonomy is based on how the types are stored and accessed in memory: reference types are accessed in a special memory area (called a heap) via pointers, whereas value types are referenced directly in a program stack. The other thing to note is that all types, both custom and .NET defined, must inherit from the predefined System.Object type. This ensures that all types support a basic set of inherited methods and properties.

Core Note

In .NET, "type" is a generic term that refers to a class, structure, enumeration, delegate, or interface.

A compiler that is compliant with the CTS specifications is guaranteed that its types can be hosted by the Common Language Runtime. This alone does not guarantee that the language can communicate with other languages. There is a more restrictive set of specifications, appropriately called the Common Language Specification (CLS), that provides the ultimate rules for language interoperability. These specifications define the minimal features that a compiler must include in order to target the CLR.

Table 1-1 contains some of the CLS rules to give you a flavor of the types of features that must be considered when creating CLS-compliant types (a complete list is included with the .NET SDK documentation).

Table 1-1. Selected Common Language Specification Features and Rules

Feature

Rule

Visibility (Scope)

The rules apply only to those members of a type that are available outside the defining assembly.

Characters and casing

For two variables to be considered distinct, they must differ by more than just their case.

Primitive types

The following primitive data types are CLS compliant: Byte, Int16, Int32, Int64, Single, Double, Boolean, Char, Decimal, IntPtr, and String.

Constructor invocation

A constructor must call the base class's constructor before it can access any of its instance data.

Array bounds

All dimensions of arrays must have a lower bound of zero (0).

Enumerations

The underlying type of an enumeration (enum) must be of the type Byte, Int16, Int32, or Int64.

Method signature

All return and parameter types used in a type or member signature must be CLS compliant.

These rules are both straightforward and specific. Let's look at a segment of C# code to see how they are applied:

public class Conversion

{

   public double Metric( double inches)

   { return (2.54 * inches); }

   public double metric( double miles)

   { return (miles / 0.62); }

}

Even if you are unfamiliar with C# code, you should still be able to detect where the code fails to comply with the CLS rules. The second rule in the table dictates that different names must differ by more than case. Obviously, MeTRic fails to meet this rule. This code runs fine in C#, but a program written in Visual Basic.NET—which ignores case sensitivity—would be unable to distinguish between the upper and lowercase references.

Assemblies

All of the managed code that runs in .NET must be contained in an assembly. Logically, the assembly is referenced as one EXE or DLL file. Physically, it may consist of a collection of one or more files that contain code or resources such as images or XML data.

An assembly is created when a .NET compatible compiler converts a file containing source code into a DLL or EXE file. As shown in Figure 1-5, an assembly contains a manifest, metadata, and the compiler-generated Intermediate Language (IL). Let's take a closer look at these:

Manifest. Each assembly must have one file that contains a manifest. The manifest is a set of tables containing metadata that lists the names of all files in the assembly, references to external assemblies, and information such as name and version that identify the assembly. Strongly named assemblies (discussed later) also include a unique digital signature. When an assembly is loaded, the CLR's first order of business is to open the file containing the manifest so it can identify the members of the assembly.

Metadata. In addition to the manifest tables just described, the C# compiler produces definition and reference tables. The definition tables provide a complete description of the types contained in the IL. For instance, there are tables defining types, methods, fields, parameters, and properties. The reference tables contain information on all references to types and other assemblies. The JIT compiler relies on these tables to convert the IL to native machine code.

IL. The role of Intermediate Language has already been discussed. Before the CLR can use IL, it must be packaged in an EXE or DLL assembly. The two are not identical: an EXE assembly must have an entry point that makes it executable; a DLL, on the other hand, is designed to function as a code library holding type definitions.

Figure 1-5. Single file assembly

The assembly is more than just a logical way to package executable code. It forms the very heart of the .NET model for code deployment, version control, and security:

  • All managed code, whether it is a stand-alone program, a control, or a DLL library containing reusable types, is packaged in an assembly. It is the most atomic unit that can be deployed on a system. When an application begins, only those assemblies required for initialization must be present. Other assemblies are loaded on demand. A judicious developer can take advantage of this to partition an application into assemblies based on their frequency of use.

  • In .NET jargon, an assembly forms a version boundary. The version field in the manifest applies to all types and resources in the assembly. Thus, all the files comprising the assembly are treated as a single unit with the same version. By decoupling the physical package from the logical, .NET can share a logical attribute among several physical files. This is the fundamental characteristic that separates an assembly from a system based on the traditional DLLs.

  • An assembly also forms a security boundary on which access permissions are based. C# uses access modifiers to control how types and type members in an assembly can be accessed. Two of these use the assembly as a boundary: public permits unrestricted access from any assembly; internal restricts access to types and members within the assembly.

As mentioned, an assembly may contain multiple files. These files are not restricted to code modules, but may be resource files such as graphic images and text files. A common use of these files is to permit resources that enable an application to provide a screen interface tailored to the country or language of the user. There is no limit to the number of files in the assembly. Figure 1-6 illustrates the layout of a multi-file assembly.

Figure 1-6. Multi-file assembly

In the multi-file assembly diagram, notice that the assembly's manifest contains the information that identifies all files in the assembly.

Although most assemblies consist of a single file, there are several cases where multi-file assemblies are advantageous:

  • They allow you to combine modules created in different programming languages. A programming shop may rely on Visual Basic.NET for its Rapid Application Development (RAD) and C# for component or enterprise development. Code from both can coexist and interact in the .NET assembly.

  • Code modules can be partitioned to optimize how code is loaded into the CLR. Related and frequently used code should be placed in one module; infrequently used code in another. The CLR does not load the modules until they are needed. If creating a class library, go a step further and group components with common life cycle, version, and security needs into separate assemblies.

  • Resource files can be placed in their own module separate from IL modules. This makes it easier for multiple applications to share common resources.

Multi-file assemblies can be created by executing the C# compiler from the command line or using the Assembly Linker utility, Al.exe. An example using the C# compiler is provided in the last section of this chapter. Notably, Visual Studio.NET 2005 does not support the creation of multi-file assemblies.

Private and Shared Assemblies

Assemblies may be deployed in two ways: privately or globally. Assemblies that are located in an application's base directory or a subdirectory are called privately deployed assemblies. The installation and updating of a private assembly could not be simpler. It only requires copying the assembly into the directory, called the AppBase, where the application is located. No registry settings are needed. In addition, an application configuration file can be added to override settings in an application's manifest and permit an assembly's files to be moved within the AppBase.

A shared assembly is one installed in a global location, called the Global Assembly Cache (GAC), where it is accessible by multiple applications. The most significant feature of the GAC is that it permits multiple versions of an assembly to execute side-by-side. To support this, .NET overcomes the name conflict problem that plagues DLLs by using four attributes to identify an assembly: the file name, a culture identity, a version number, and a public key token.

Public assemblies are usually located in the assembly directory located beneath the system directory of the operating system (WINNT\ on a Microsoft Windows 2000 operating system). As shown in Figure 1-7, the assemblies are listed in a special format that displays their four attributes (.NET Framework includes a DLL file that extends Windows Explorer to enable it to display the GAC contents). Let's take a quick look at these four attributes:

Assembly Name. Also referred to as the friendly name, this is the file name of the assembly minus the extension.

Version. Every assembly has a version number that applies to all files in the assembly. It consists of four numbers in the format

<major number>.<minor number>.<build>.<revision>

Typically, the major and minor version numbers are updated for changes that break backward compatibility. A version number can be assigned to an assembly by including an AssemblyVersion attribute in the assembly's source code.

Culture Setting. The contents of an assembly may be associated with a particular culture or language. This is designated by a two-letter code such as "en" for English or "fr" for French, and can be assigned with an AssemblyCulture attribute placed in source code:

[assembly: AssemblyCulture ("fr-CA")]

Public Key Token. To ensure that a shared assembly is unique and authentic, .NET requires that the creator mark the assembly with a strong name. This process, known as signing, requires the use of a public/private key pair. When the compiler builds the assembly, it uses the private key to generate a strong name. The public key is so large that a token is created by hashing the public key and taking its last eight bytes. This token is placed in the manifest of any client assembly that references a shared assembly and is used to identify the assembly during execution.

Figure 1-7. Partial listing of Global Assembly Directory

Core Note

An assembly that is signed with a public/private key is referred to as a strongly named assembly. All shared assemblies must have a strong name.

Precompiling an Assembly

After an assembly is loaded, the IL must be compiled to the machine's native code. If you are used to working with executables already in a machine code format, this should raise questions about performance and whether it's possible to create equivalent "executables" in .NET. The answer to the second part of the statement is yes; .NET does provide a way to precompile an assembly.

The .NET Framework includes a Native Image Generator (Ngen) tool that is used to compile an assembly into a "native image" that is stored in a native image cache—a reserved area of the GAC. Any time the CLR loads an assembly, it checks the cache to see if it has an associated native image available; if it does, it loads the precompiled code. On the surface, this seems a good idea to improve performance. However, in reality, there are several drawbacks.

Ngen creates an image for a hypothetical machine architecture, so that it will run, for example, on any machine with an x86 processor. In contrast, when the JIT in .NET runs, it is aware of the specific machine it is compiling for and can accordingly make optimizations. The result is that its output often outperforms that of the precompiled assembly. Another drawback to using a native image is that changes to a system's hardware configuration or operating system—such as a service pack update—often invalidate the precompiled assembly.

Core Recommendation

As a rule, a dynamically compiled assembly provides performance equal to, or better than, that of a precompiled executable created using Ngen.

Code Verification

As part of the JIT compile process, the Common Language Runtime performs two types of verification: IL verification and metadata validation. The purpose is to ensure that the code is verifiably type-safe. In practical terms, this means that parameters in a calling and called method are checked to ensure they are the same type, or that a method returns only the type specified in its return type declaration. In short, the CLR searches through the IL and metadata to make sure that any value assigned to a variable is of a compatible type; if not, an exception occurs.

Core Note

By default, code produced by the C# compiler is verifiably type-safe. However, there is an unsafe keyword that can be used to relax memory access restrictions within a C# program (such as referencing beyond an array boundary).

A benefit of verified code is that the CLR can be certain that the code cannot affect another application by accessing memory outside of its allowable range. Consequently, the CLR is free to safely run multiple applications in a single process or address space, improving performance and reducing the use of OS resources.


1.3. Framework Class Library

The Framework Class Library (FCL) is a collection of classes and other types (enumerations, structures, and interfaces) that are available to managed code written in any language that targets the CLR. This is significant, because it means that libraries are no longer tied to specific compilers. As a developer, you can familiarize yourself with the types in a library and be assured that you can use this knowledge with whatever .NET language you choose.

The resources within the FCL are organized into logical groupings called namespaces. For the most part, these groupings are by broad functionality. For example, types used for graphical operations are grouped into the System.Drawing and System.Drawing.Drawing2D namespaces; types required for file I/O are members of the System.IO namespace. Namespaces represent a logical concept, not a physical one.

The FCL comprises hundreds of assemblies (DLLs), and each assembly may contain multiple namespaces. In addition, a namespace may span multiple assemblies. To demonstrate, let's look inside an FCL assembly.

Figure 1-8 displays a portion of the output generated by using Ildasm.exe to examine the contents of the mscorlib assembly. Although this only a partial listing, you can see that mscorlib contains System, the preeminent namespace in .NET, which serves as a repository for the types that give .NET its basic functionality. The assembly is also home to the System.Collections namespace, which includes classes and interfaces used for manipulating collections of data.

Figure 1-8. Output from Ildasm shows the namespaces and types that comprise an assembly

Table 1-2 lists some of the most important namespaces in .NET. For reference, the last column in each row includes a chapter number in this book where you'll find the namespace(s) used.

Table 1-2. Selected FCL Namespaces

Namespace

Use

Chapter

System

Contains the basic data types used by all applications. It also contains exception classes, predefined attributes, a Math library, and classes for managing the application environment.

3, 18

System.Collections

System.Collections.Specialized

System.Collections.Generic

Interfaces and classes used to manage collections of objects. These collections include the ArrayList, Hashtable, and Stack.

4

System.Data

System.Data.OracleClient

System.Data.SqlClient

System.Data.OleDb

System.Data.Odbc

Classes used for database operations (ADO.NET). The client namespaces support Oracle and SQL Server, respectively; OledDb and Odbc define the data connection used.

11, 12

System.Diagnostics

Contains classes that can be used to trace program execution, debug, and work with system logs and performance counters.

13

System.Drawing

System.Drawing.Drawing2D

System.Drawing.Printing

System.Drawing.Text

Provides graphics functionality for GDI+. These namespaces contain a class used for drawing as well as pens, brushes, geometric shapes, and fonts.

8, 9

System.Globalization

Contains classes that define culture-related information that affects the way dates, currency, and symbols are represented.

5

System.IO

Provides file and data stream I/O. These classes provide a way to access the underlying file systems of the host operating system.

5

System.Net

Classes that support network protocols and operations. Examples include WebRequest and WebResponse that request and fetch a Web page.

17

System.Reflection

System.Reflection.Emit

Contains types that permit the runtime inspection of metadata. The Emit namespace allows a compiler or tool to generate metadata and IL dynamically.

7, 15, App. B

System.Runtime.InterOpServices

Provides interoperability between managed and unmanaged code such as legacy DLLs or COM.

8

System.Security

System.Security.Permissions

System.Security.Cryptography

Classes used to manage .NET security. Defines classes that control access to operations and resources.

5, 15

System.Text.RegularExpressions

Classes that support .NET's regular expression engine.

5

System.Threading

System.Threading.Thread

Manages threading activites: thread creation, synchronization, and thread pool access.

13

System.Web

System.Web.Services

System.Web.UI

System.Web.UI.WebControls

System.Web.Security

The Internet-related classes referred to as ASP.NET. They manage browser-server communication requirements, manipulate cookies, and contain the controls that adorn a Web page.

Web.Services includes those classes required for SOAP-based XML messaging.

Web.UI includes classes and interfaces used for creating controls and pages that comprise Web forms.

16, 17, 18

System.Windows.Forms

Classes used to build Windows desktop GUI applications. Controls including the ListBox, TextBox, DataGrid, and buttons are found here.

6, 7

System.Xml

Types for processing XML.

10

Namespaces provide a roadmap for navigating the FCL. For example, if your applications are Web based, you'll spend most of your time exploring the types in the System.Web.* namespaces. After you have learned the basics of .NET and gained proficiency with C#, you'll find that much of your time is spent familiarizing yourself with the built-in types contained in the Framework Class Library.


1.4. Working with the .NET Framework and SDK

The .NET Framework Software Development Kit (SDK) contains the tools, compilers, and documentation required to create software that will run on any machine that has the .NET Framework installed. It is available as a free download (100 megabytes) from Microsoft that can be installed on Windows XP, Windows 2000, Windows Server 2003, and subsequent Windows operating systems. If you have Visual Studio.NET installed, there is no need to download it because VS.NET automatically does it for you.

Clients using software developed with the SDK do not require the SDK on their machine; however, they do require a compatible version of the .NET Framework. This .NET Framework Redistributable is available as a free download[3] (20+ megabytes) and should be distributed to clients along with applications that require it. This redistributable can be installed on Windows 98 and ME, in addition to the ones listed for the SDK. With minor exceptions, .NET applications will run identically on all operating system platforms, because they are targeted for the Common Language Runtime and not the operating system. There are some system requirements such as a minimum Internet Explorer version of 5.01. These are listed at the download site.

[3] http://msdn.microsoft.com/netframework/downloads/updates/default.aspx

Updating the .NET Framework

Unlike many development environments, installing a new version of the framework is almost effortless. The installation process places the updated version in a new directory having the name of the version. Most importantly, there is no file dependency between the new and older versions. Thus, all versions are functional on your system. Although it varies by operating system, the versions are usually in the path

\winnt\Microsoft.NET\Framework\v1.0.3705

\winnt\Microsoft.NET\Framework\v1.1.4322

\winnt\Microsoft.NET\Framework\v2.0.40607

The installation of any new software version raises the question of compatibility with applications developed using an older version. .NET makes it easy to run existing applications against any framework version. The key to this is the application configuration file (discussed in much greater detail in Chapter 15). This text file contains XML tags and elements that give the CLR instructions for executing an application. It can specify where external assemblies are located, which version to use, and, in this case, which versions of the .NET Framework an application or component supports. The configuration file can be created with any text editor, although it's preferable to rely on tools (such as the Framework Configuration tool) designed for the task. Your main use of the configuration file will be to test current applications against new framework releases. Although it can be done, it usually makes no sense to run an application against an earlier version than it was originally compiled against.

.NET Framework Tools

The .NET Framework automates as many tasks as possible and usually hides the details from the developer. However, there are times when manual intervention is required. These may be a need to better understand the details of an assembly or perform the housekeeping required to prepare an application for deployment. We have encountered several examples of such tasks throughout the chapter. These include the need to

  • Add a file to an assembly

  • View the contents of an assembly

  • View the details of a specific class

  • Generate a public/private key pair in order to create a strongly named assembly

  • Edit configuration files

Many of these are better discussed within the context of later chapters. However, it is useful to be aware of which tools are available for performing these tasks; and a few, such as those for exploring classes and assemblies, should be mastered early in the .NET learning curve.

Table 1-3 lists some of the useful tools available to develop and distribute your applications. Three of these, Ildasm.exe, wincv.exe, and the .NET Framework Configuration tool, are the subject of further discussion.

Table 1-3. Selected .NET Framework Tools

Tool

Description

Al.exe

Assembly Linker

Can be used for creating an assembly composed of modules from different compilers. It is also used to build resource-only (satellite) assemblies.

Fuslogvw.exe

Assembly Binding Log Viewer

Used to troubleshoot the assembly loading process. It traces the steps followed while attempting to load an assembly.

Gacutil.exe

Global Assembly Cache tool

Is used to install or delete an assembly in the Global Assembly Cache. It can also be used for listing the GAC's contents.

Ildasm.exe

MSIL Disassembler

A tool for exploring an assembly, its IL, and metadata.

Mscorcfg.msc

NET Framework Configuration tool

A Microsoft Management Console (MMC) snap-in used to configure an assembly while avoiding direct manual changes to an application's configuration file. Designed primarily for administrators, a subset, Framework Wizards. Available for individual programmers.

Ngen.exe

Native Image Generator

Compiles an assembly's IL into native machine code. This image is then placed in the native image cache.

Sn.exe

Strong Name tool

Generates the keys that are used to create a strong—or signed—assembly.

wincv.exe

Windows Forms Class Viewer

A visual interface to display searchable information about a class.

Wsdl.exe

Web Services Description Language tool

Generates descriptive information about a Web Service that is used by a client to access the service.

Many of these tools are located in an SDK subdirectory:

c:\Program Files\Microsoft.NET\SDK\v2.0\Bin

To execute the tools at the command line (on a Windows operating system) while in any directory, it is first necessary to place the path to the utilities in the system Path variable. To do this, follow these steps:

1. | Right click on the My Computer icon and select Properties.
2. | Select Advanced – Environment Variables.
3. | Choose the Path variable and add the SDK subdirectory path to it.

If you have Visual Studio installed, a simpler approach is to use the preconfigured Visual Studio command prompt. It automatically initializes the path information that enables you to access the command-line tools.

Ildasm.exe

The Intermediate Language Disassembler utility is supplied with the .NET Framework SDK and is usually located in the Bin subdirectory along the path where the SDK is installed. It is invaluable for investigating the .NET assembly environment and is one of the first tools you should become familiar with as you begin to work with .NET assemblies and code.

The easiest way to use the utility is to type in

C:\>Ildasm /adv

at a command-line prompt (the optional /adv switch makes advanced viewing options available). This invokes the GUI that provides a File menu you use to select the assembly to view. Note that it does not open files in the Global Assembly Cache.

Figure 1-9 shows an example of the output created when an assembly is opened in Ildasm. The contents are displayed in a readable, hierarchical format that contains the assembly name, corecsharp1, and all of its members.

Figure 1-9. View assembly contents with Ildasm.exe

This hierarchy can then be used to drill down to the underlying IL (or CIL) instructions for a specific member. As an example, let's consider the Conversion class. The figure shows that it consists of three methods: MeTRic, conversion, and metric. The original source code confirms this:

public class Conversion

{

   public double Metric( double inches)

   { return (2.54 * inches); }

   [CLSCompliantAttribute(false)]

   public double metric( double miles)

   { return (miles / 0.62); }

   public double conversion( double pounds)

   { return (pounds * 454);}

}

Double clicking on the MeTRic method brings up a screen that displays its IL (Figure 1-10).

Figure 1-10. View of the IL

Ildasm can be used as a learning tool to solidify the concepts of IL and assemblies. It also has some practical uses. Suppose you have a third-party component (assembly) to work with for which there is no documentation. Ildasm provides a useful starting point in trying to uncover the interface details of the assembly.

Core Suggestion

Ildasm has a File – Dump menu option that makes it useful for saving program documentation in a text file. Select Dump Metainfo to create a lengthy human-readable form of the assembly's metadata; select Dump Statistics to view a profile of the assembly that details how many bytes each part uses.

Ildasm and Obfuscation

One of the natural concerns facing .NET developers is how to protect their code when a tool such as Ildasm—and other commercially available disassemblers—can be used to expose it. One solution is to use obfuscation—a technique that uses renaming and code manipulation sleight of hand to make the contents of an assembly unreadable by humans.

It is important to understand that obfuscation is not encryption. Encryption requires a decryption step so that the JIT compiler can process the code. Obfuscation transforms the IL code into a form that can be compiled using the tools of your development environment (see Figure 1-11).

Figure 1-11. Obfuscation conceals the original Intermediate Language

The obfuscated code is functionally equivalent to the assembly's IL code and produces identical results when run by the CLR. How does it do this? The most common trick is to rename meaningful types and members with names that have no intrinsic meaning. If you look at obfuscated code, you'll see a lot of types named "a" or "b," for example. Of course, the obfuscation algorithm must be smart enough not to rename types that are used by outside assemblies that depend on the original name. Another common trick is to alter the control flow of the code without changing the logic. For example, a while statement may be replaced with a combination of goto and if statements.

An obfuscator is not included in the .NET SDK. Dotfuscator Community Edition, a limited-feature version of a commercial product, is available with Visual Studio.NET. Despite being a relatively unsophisticated product—and only available for the Microsoft environment—it is a good way to become familiar with the process. Several vendors now offer more advanced obfuscator products.

wincv.exe

WinCV is a class viewer analogous to the Visual Studio Object Viewer, for those not using Visual Studio. It is located in the Program Files\Microsoft.Net\SDK\V1.x\Bin directory and can be run from the command prompt. When the window appears, type the name of the class you want to view into the Searching For box (see Figure 1-12).

Figure 1-12. Using WinCV to view type definition of the Array class

[View full size image]

WinCV provides a wealth of information about any type in the base class libraries. The four highlighted areas provide a sampling of what is available:

  1. System.Array is the class that is being explored.

  2. This class is located in the mscorlib.dll assembly. We have already mentioned that this assembly contains the .NET managed types.

  3. This list contains the class, object, and interfaces that the Array class inherits from.

  4. The definition of each method in the class is included. This definition, which includes accessibility, type, and parameters, is called the method's signature.

Framework Configuration Tool

This tool provides an easy way to manage and configure assemblies as well as set security policies for accessing code. This tool is packaged as a Microsoft Management Console (MMC) snap-in. To access it, select Administrative Tools from the Control Panel; then select the Microsoft .NET Framework Configuration tool. This tool is designed for administrators who need to do the following:

  • Manage assemblies. Assemblies can be added to the GAC or deleted.

  • Configure assemblies. When an assembly is updated, the publisher of the assembly is responsible for updating the binding policy of the assembly. This policy tells the CLR which version of an assembly to load when an application references an assembly. For example, if assembly version 1.1 replaces 1.0, the policy redirects version 1.0 to 1.1 so that it is loaded. This redirection information is contained in a configuration file.

  • View .NET Framework security and modify an assembly's security. .NET security allows an assembly to be assigned certain permissions or rights. In addition, an assembly can require that other assemblies accessing it have certain permissions.

  • Manage how individual applications interact with an assembly or set of assemblies. You can view a list of all assemblies an application uses and set the version that your application uses.

To illustrate a practical use of the configuration tool, let's look at how it can be used to address one of the most common problems that plagues the software development process: the need to drop back to a previous working version when a current application breaks. This can be a difficult task when server DLLs or assemblies are involved. .NET offers a rather clever solution to this problem: Each time an application runs, it logs the set of assemblies that are used by the program. If they are unchanged from the previous run, the CLR ignores them; if there are changes, however, a snapshot of the new set of assemblies is stored.

When an application fails, one option for the programmer is to revert to a previous version that ran successfully. The configuration tool can be used to redirect the application to an earlier assembly. However, there may be multiple assemblies involved. This is where the configuration tool comes in handy. It allows you to view previous assembly configurations and select the assemblies en masse to be used with the application.

To view and select previous configurations, select Applications – Fix an Application from the Configuration tool menu. Figure 1-13 combines the two dialog boxes that subsequently appear. The main window lists applications that have run and been recorded. The smaller window (a portion of a larger dialog) is displayed when you click on an application. This window lists the most recent (up to five) configurations associated with the application. You simply select the assembly configuration that you want the application to use.

Figure 1-13. Using application configuration tool to select assembly version

This configuration tool is clearly targeted for administrators. Individual developers should rely on a subset of this tool that is packaged as three wizards: Adjust .NET Security, Trust An Assembly, and Fix An Application. Access these by selecting Framework Wizards from Administrative Tools.


1.5. Understanding the C# Compiler

Many developers writing nontrivial .NET applications rely on Visual Studio or some other Integrated Development Environment (IDE) to enter source code, link external assemblies, perform debugging, and create the final compiled output. If you fall into this category, it is not essential that you understand how to use the .NET SDK and raw C# compiler; however, it will increase your understanding of the .NET compilation process and give you a better feel for working with assemblies. As a byproduct, it will also acquaint you with the command line as a way to work with SDK programs. Many of the utilities presented in the previous section are invoked from the command line, and you will occasionally find it useful to perform compilation in that environment rather than firing up your IDE.

Figure 1-14 shows the basic steps that occur in converting source code to the final compiled output. The purpose of this section is to demonstrate how a text editor and the C# compiler can be used to build an application. Along the way, it will provide a detailed look at the many compiler options that are hidden by the IDE.

Figure 1-14. Compilation process

Locating the Compiler

The C# compiler, csc.exe, is located in the path where the .NET Framework is installed:

C:\winnt\Microsoft.NET\Framework\v2.0.40607

Of course, this may vary depending on your operating system and the version of Framework installed. To make the compiler available from the command line in any current directory, you must add this path to the system Path variable. Follow the steps described in the previous section for setting the path for the SDK utilities.

Type in the following statement at the command line to verify that the compiler can be accessed:

C:\>csc /help

Compiling from the Command Line

To compile the C# console application client.cs into the executable client.exe, enter either of the following statements at the command prompt:

C:\> csc client.cs

C:\> csc /t:exe client.cs

Both statements compile the source into an executable (.exe) file—the default output from the compiler. As shown in Table 1-4, the output type is specified using the /t: flag. To create a DLL file, set the target value to library. For a WinForms application, specify /t:winexe. Note that you can use /t:exe to create a WinForms application, but the console will be visible as background window.

Table 1-4. Selected Options for the C# Command-Line Compiler

Option

Description

/addmodule

Specifies a module that is to be included in the assembly created. This is an easy way to create a multi-file assembly.

/debug

Causes debug information to be produced.

/define

Preprocessor directive can be passed to compiler: /define:DEBUG.

/delaysign

Builds an assembly using delayed signing of the strong name. This is discussed in Chapter 15.

/doc

Used to specify that an output file containing XML documentation is to be produced.

/keyfile

Specifies the path to the .snk file containing the key pair used for strong signing (see Chapter 15).

/lib

Specifies where assemblies included in the /reference option are located.

/out

Name of the file containing compiled output. The default is the name of the input file with .exe suffix.

/reference (/r)

References an external assembly.

/resource

Used to embed resource files into the assembly that is created.

/target (/t)

Specifies the type of output file created:

/t:exe builds a *.exe console application. This is the default output.

/t:library builds a *.dll assembly.

/t:module builds a module (Portable Executable file) that does not contain a manifest.

/t:winexe builds a *.exe Windows Forms assembly.

The real value of working with the raw compiler is the ability to work with multiple files and assemblies. For demonstration purposes, create two simple C# source files: client.cs and clientlib.cs.

client.cs

   using System;

   public class MyApp

   {

      static void Main(string[] args)

      {

         ShowName.ShowMe("Core C#");

      }

   }

clientlib.cs

   using System;

   public class ShowName

   {

      public static void ShowMe(string MyName)

      {

         Console.WriteLine(MyName);

      }

   }

It's not important to understand the code details, only that the client routine calls a function in clientlib that writes a message to the console. Using the C# compiler, we can implement this relationship in a number of ways that not only demonstrate compiler options but also shed light on the use of assemblies.

Example 1: Compiling Multiple Files

The C# compiler accepts any number of input source files. It combines their output into a single file assembly:

csc /out:client.exe client.cs clientlib.cs

Example 2: Creating and Using a Code Library

The code in clientlib can be placed in a separate library that can be accessed by any client:

csc /t:library clientlib.cs

The output is an assembly named clientlib.dll. Now, compile the client code and reference this external assembly:

csc /r:clientlib.dll  client.cs

The output is an assembly named client.exe. If you examine this with Ildasm, you see that the manifest contains a reference to the clientlib assembly.

Example 3: Creating an Assembly with Multiple Files

Rather than existing as a separate assembly, clientlib can also be packaged as a separate file inside the client.exe assembly. Because only one file in an assembly may contain a manifest, it is first necessary to complile clientlib.cs into a Portable Executable[4] (PE) module. This is done by selecting module as the target output:

[4] The PE format defines the layout for executable files that run on 32- or 64-bit Windows systems.

csc /t:module clientlib.cs

The output file is clientfile.netmodule. Now, it can be placed in the client.exe assembly by using the compiler's addmodule switch:

csc /addmodule:clientlib.netmodule client.cs

The resultant assembly consists of two files: client.exe and clientlib.netmodule.

These examples, shown in Figure 1-15, illustrate the fact that even a simple application presents the developer with multiple architectural choices for implementing an application.

Figure 1-15. Options for deploying an application

    Previous Section < Day Day Up > Next Section


1.6. Summary

The .NET Framework consists of the Common Language Runtime (CLR) and the Framework Class Library (FCL). The CLR manages all the tasks associated with code execution. It first ensures that code is CLR compliant based on the Common Language Specification (CLS) standard. It then loads an application and locates all dependent assemblies. Its Just-in-Time (JIT) compiler converts the IL contained in an application's assembly, the smallest deployable code unit in .NET, into native machine code. During the actual program execution, the CLR handles security, manages threads, allocates memory, and performs garbage collection for releasing unused memory.

All code must be packaged in an assembly in order for the CLR to use it. An assembly is either a single file or grouping of multiple physical files treated as a single unit. It may contain code modules as well as resource files.

The FCL provides a reusable set of classes and other types that are available to all CLR-compliant code. This eliminates the need for compiler-specific libraries. Although the FCL consists of several physical DLLs containing over a thousand types, it's made manageable by the use of namespaces that impose a logical hierarchy over all the types.

To assist the developer in debugging and deploying software, .NET includes a set of utilities that enables an administrator to perform such tasks as managing assemblies, precompiling assemblies, adding files to an assembly, and viewing class details. In addition, a wealth of open source .NET tools is becoming available to aid the development process.


1.7. Test Your Understanding

1: | What portable environment must be installed on a client's machine to enable it to run a .NET application?
2: | What is managed code? What is unmanaged code?
3: | What is the difference between the Common Type System and the Common Language Specification?
4: | How does the CLR allow code from different compilers to interact?
5: | What is the role of the Global Assembly Cache?
6: | What four components make up the identity of a strongly named assembly?
7: | What is the relationship between a namespace and an assembly?
8: | Describe what these commonly used acronyms stand for: CLR, GAC, FCL, IL.


Chapter 2. C# Language Fundamentals

Topics in This Chapter

  • Overview of a C# Program: In addition to the basic elements that comprise a C# program, a developer needs to be aware of other .NET features such as commenting options and recommended naming conventions.

  • Primitives: Primitives are the basic data types defined by the FCL to represent numbers, characters, and dates.

  • Operators: C# uses traditional operator syntax to perform arithmetic and conditional operations.

  • Program Flow Statements: Program flow can be controlled using if and switch statements for selection; and while, do, for, and foreach clauses for iteration.

  • String: The string class supports the expected string operations: concatenation, extracting substrings, searching for instances of a character pattern, and both case sensitive and insensitive comparisons.

  • Enums: An enumeration is a convenient way to assign descriptions that can be used to reference an underlying set of values.

  • Using Arrays: Single- or multi-dimensional arrays of any type can be created in C#. After an array is created, the System.Array class can be used to sort and copy the array.

  • Reference and Value Types: All types in .NET are either a value or reference type. It is important to understand the differences and how they can affect a program's performance.

In September 2000, an ECMA[1] (international standardization group for information and communication systems) task group was established to define a Microsoft proposed standard for the C# programming language. Its stated design goal was to produce "a simple, modern, general-purpose, object-oriented programming language." The result, defined in a standard known as ECMA-334, is a satisfyingly clean language with a syntax that resembles Java, and clearly borrows from C++ and C. It's a language designed to promote software robustness with array bounds checking, strong type checking, and the prohibition of uninitialized variables.

[1] ECMA International was formerly known as European Computer Manufacturers Association and is referred to herein simply as ECMA.

This chapter introduces you to the fundamentals of the language: It illustrates the basic parts of a C# program; compares value and reference types; and describes the syntax for operators and statements used for looping and controlling program flow. As an experienced programmer, this should be familiar terrain through which you can move quickly. However, the section on value and reference types may demand a bit more attention. Understanding the differences in how .NET handles value and reference types can influence program design choices.


2.1. The Layout of a C# Program

Figure 2-1 illustrates some of the basic features of a C# program.

Figure 2-1. Basic elements of a C# program

The code in Figure 2-1 consists of a class MyApp that contains the program logic and a class Apparel that contains the data. The program creates an instance of Apparel and assigns it to myApparel. This object is then used to print the values of the class members FabType and Price to the console. The important features to note include the following:

  1. The using statement specifies the namespace System. Recall from Chapter 1, "Introduction to .NET and C#," that the .NET class libraries are organized into namespaces and that the System namespace contains all of the simple data types. The using statement tells the compiler to search this namespace when resolving references, making it unnecessary to use fully qualified names. For example, you can refer to label rather than System.Web.UI.WebControls.Label.

  2. All programming logic and data must be contained within a type definition. All program logic and data must be embedded in a class, structure, enum, interface, or delegate. Unlike Visual Basic, for instance, C# has no global variable that exists outside the scope of a type. Access to types and type members is strictly controlled by access modifiers. In this example, the access modifier public permits external classes—such as MyApp—to access the two members of the Apparel class.

  3. A Main() method is required for every executable C# application. This method serves as the entry point to the application; it must always have the static modifier and the M must be capitalized. Overloaded forms of Main()define a return type and accept a parameter list as input.

    Return an integer value:

static int Main()

{

   return 0; // must return an integer value

}

    Receive a list of command-line arguments as a parameter and return an integer value:

static int Main(string[] args)

{

   // loop through arguments

   foreach(string myArg in args)

      Console.WriteLine(myArg);

   return 0;

}

    The parameter is a string array containing the contents of the command line used to invoke the program. For example, this command line executes the program MyApparel and passes it two parameter values:

C:\> MyApparel 5 6

Core Note

The contents of the command line are passed as an argument to the Main() method. The System.Environment.CommandLine property also exposes the command line's contents.

General C# Programming Notes

Case Sensitivity

All variable and keywords are distinguished by case sensitivity. Replace class with Class in Figure 2-1 and the code will not compile.

Naming Conventions

The ECMA standard provides naming convention guidelines to be followed in your C# code. In addition to promoting consistency, following a strict naming policy can minimize errors related to case sensitivity that often result from undisciplined naming schemes. Table 2-1 summarizes some of the more important recommendations.

Table 2-1. C# Naming Conventions

Type

Case

Notes and Examples

Class

Pascal

  • Use noun or noun phrases.

  • Try to avoid starting with I because this is reserved for interfaces.

  • Do not use underscores.

Constant

Pascal

public const double GramToPound = 454.0 ;

Enum Type

Pascal

  • Use Pascal case for the enum value names.

  • Use singular name for enums.

public enum WarmColor { Orange, Yellow, Brown}

Event

Pascal

  • The method that handles events should have the suffix EventHandler.

  • Event argument classes should have the suffix EventArgs.

Exception

Pascal

  • Has suffix Exception.

Interface

Pascal

  • Has prefix of I.

IDisposable

Local Variable

Camel

  • Variables with public access modifier use Pascal

int myIndex.

Method

Pascal

  • Use verb or verb phrases for name.

Namespace

Pascal

  • Do not have a namespace and class with the same name.

  • Use prefixes to avoid namespaces having the same name. For example, use a company name to categorize namespaces developed by that company.

Acme.GraphicsLib

Property

Pascal

  • Use noun or noun phrase.

Parameter

Camel

  • Use meaningful names that describe the parameter's purpose.

Note that the case of a name may be based on two capitalization schemes:

  1. Pascal. The first character of each word is capitalized (for example, MyClassAdder).

  2. Camel. The first character of each word, except the first, is capitalized (for example, myClassAdder).

The rule of thumb is to use Pascal capitalization everywhere except with parameters and local variables.

Commenting a C# Program

The C# compiler supports three types of embedded comments: an XML version and the two single-line (//) and multi-line (/* */) comments familiar to most programmers:

//   for a single line

/*   for one or more lines

                      */

/// <remarks> XML comment describing a class </remarks>

An XML comment begins with three slashes (///) and usually contains XML tags that document a particular aspect of the code such as a structure, a class, or class member. The C# parser can expand the XML tags to provide additional information and export them to an external file for further processing.

The <remarks> tag—shown in Figure 2-1—is used to describe a type (class). The C# compiler recognizes eight other primary tags that are associated with a particular program element (see Table 2-2). These tags are placed directly above the lines of code they refer to.

Table 2-2. XML Documentation Tags

Tag

Description

<example>

Text illustrating an example of using a particular program feature goes between the beginning and ending tags.

<exception cref="Excep">

cref attribute contains name of exception.

///<exceptioncref="NoParmException">

</exception>

<include file="myXML">

file attribute is set to name of another XML file that is to be included in the XML documentation produced by this source code.

<param name="parm1">

name attribute contains the name of the parameter.

<permission cref= "">

Most of the time this is set to the following:

///<permissioncref="System.Security.Permis-

sionSet"> </permission>

<remarks>

Provides additional information about a type not found in the <summary> section.

<returns>

Place a textual description of what is returned from a method or property between the beginning and ending tags.

<seealso cref="price">

The cref attribute is set to the name of an associated type, field, method, or other type member.

<summary>

Contains a class description; is used by IntelliSense in VisualStudio.NET.

The value of the XML comments lies in the fact that they can be exported to a separate XML file and then processed using standard XML parsing techniques. You must instruct the compiler to generate this file because it is not done by default.

The following line compiles the source code consoleapp.cs and creates an XML file consoleXML:

C:\> csc consoleapp.cs /doc:consoleXML.xml

If you compile the code in Figure 2-1, you'll find that the compiler generates warnings for all public members in your code:

Warning CS1591: Missing XML comment for publicly visible type ...

To suppress this, add the /nowarn:1591 option to the compile-line command. The option accepts multiple warning codes separated with a comma.

Core Note

Many documentation tools are available to transform and extend the C# XML documentation output. One of the most advanced is NDoc (ndoc.sourceforge.net), an open source tool that not only formats the XML but uses reflection to glean further information about an assembly.


2.2. Primitives

The next three sections of this chapter describe features that you'll find in most programming languages: variables and data types, operators, expressions, and statements that control the flow of operations. The discussion begins with primitives. As the name implies, these are the core C# data types used as building blocks for more complex class and structure types. Variables of this type contain a single value and always have the same predefined size. Table 2-3 provides a formal list of primitives, their corresponding core data types, and their sizes.

Table 2-3. C# Primitive Data Types

C# Primitive Type

FCL Data Type

Description

object

System.Object

Ultimate base type of all other types.

string

System.String

A sequence of Unicode characters.

decimal

System.Decimal

Precise decimal with 28 significant digits.

bool

System.Boolean

A value represented as true or false.

char

System.Char

A 16-bit Unicode character.

byte

System.Byte

8-bit unsigned integral type.

sbyte

System.SByte

8-bit signed integral type.

short

System.Int16

16-bit signed integral type.

int

System.Int32

32-bit signed integral type.

long

System.Int64

64-bit signed integral type.

ushort

System.UInt16

16-bit unsigned integral type.

uint

System.UInt32

32-bit unsigned integral type.

ulong

System.UIint64

64-bit unsigned integral type.

single (float)

System.Single

Single-precision floating-point type.

double

System.Double

Double-precision floating-point type.

As the table shows, primitives map directly to types in the base class library and can be used interchangeably. Consider these statements:

System.Int32 age = new System.Int32(17);

int age = 17;

System.Int32 age = 17;

They all generate exactly the same Intermediate Language (IL) code. The shorter version relies on C# providing the keyword int as an alias for the System.Int32 type. C# performs aliasing for all primitives.

Here are a few points to keep in mind when working with primitives:

  • The keywords that identify the value type primitives (such as int) are actually aliases for an underlying structure (struct type in C#). Special members of these structures can be used to manipulate the primitives. For example, the Int32 structure has a field that returns the largest 32-bit integer and a method that converts a numeric string to an integer value:

int iMax = int.MaxValue;     // Return largest integer

int pVal = int.Parse("100"); // converts string to int

    The C# compiler supports implicit conversions if the conversion is a "safe" conversion that results in no loss of data. This occurs when the target of the conversion has a greater precision than the object being converted, and is called a widening conversion. In the case of a narrowing conversion, where the target has less precision, the conversion must have explicit casting. Casting is used to coerce, or convert, a value of one type into that of another. This is done syntactically by placing the target data type in parentheses in front of the value being converted: int i = (int)y;.

short i16 = 50;    // 16-bit integer

int i32 = i16;     // Okay: int has greater precision

i16 = i32;         // Fails: short is 16 bit, int is 32

i16 = (short) i32; // Okay since casting used

  • Literal values assigned to the types float, double, and decimal require that their value include a trailing letter: float requires F or f; double has an optional D or d; and decimal requires M or m.

decimal pct = .15M; // M is required for literal value

The remainder of this section offers an overview of the most useful primitives with the exception of string, which is discussed later in the chapter.

decimal

The decimal type is a 128-bit high-precision floating-point number. It provides 28 decimal digits of precision and is used in financial calculations where rounding cannot be tolerated. This example illustrates three of the many methods available to decimal type. Also observe that when assigning a literal value to a decimal type, the M suffix must be used.

decimal iRate = 3.9834M;         // decimal requires M

iRate = decimal.Round(iRate,2);  // Returns 3.98

decimal dividend = 512.0M;

decimal divisor = 51.0M;

decimal p = decimal.Parse("100.05");

// Next statement returns remainder = 2

decimal rem = decimal.Remainder(dividend,divisor);

bool

The only possible values of a bool type are true and false. It is not possible to cast a bool value to an integer—for example, convert true to a 1, or to cast a 1 or 0 to a bool.

bool bt = true;

string bStr = bt.ToString(); // returns "true"

bt = (bool) 1;               // fails

char

The char type represents a 16-bit Unicode character and is implemented as an unsigned integer. A char type accepts a variety of assignments: a character value placed between individual quote marks (' '); a casted numeric value; or an escape sequence. As the example illustrates, char also has a number of useful methods provided by the System.Char structure:

myChar =  'B';       // 'B' has an ASCII value of 66

myChar = (char) 66;  // Equivalent to 'B'

myChar = '\u0042';   // Unicode escape sequence

myChar = '\x0042';   // Hex escape sequence

myChar = '\t';       // Simple esc sequence:horizontal tab

bool bt;

string pattern = "123abcd?";

myChar = pattern[0];                  // '1'

bt = char.IsLetter(pattern,3);        // true  ('a')

bt = char.IsNumber(pattern,3);        // false

bt = char.IsLower(pattern,0);         // false ('1')

bt = char.IsPunctuation(pattern,7);   // true  ('?')

bt = char.IsLetterOrDigit(pattern,1); // true

bt = char.IsNumber(pattern,2);        // true  ('3')

string kstr="K";

char k = char.Parse(kstr);

byte, sbyte

A byte is an 8-bit unsigned integer with a value from 0 to 255. An sbyte is an 8-bit signed integer with a value from –128 to 127.

byte[] b = {0x00, 0x12, 0x34, 0x56, 0xAA, 0x55, 0xFF};

string s = b[4].ToString(); // returns 170

char myChar = (char) b[3];

short, int, long

These represent 16-, 32-, and 64-bit signed integer values, respectively. The unsigned versions are also available (ushort, uint, ulong).

short i16 = 200;

i16 = 0xC8 ;     // hex value for 200

int i32 = i16;   // no casting required

single, double

These are represented in 32-bit single-precision and 64-bit double-precision formats. In .NET 1.x, single is referred to as float.

  • The single type has a value range of 1.5 x 10 –45 to 3.4 x 1038 with 7-decimal digit precision.

  • The double type has a value range of 5 x 10–324 to 1.7 x 10308 with 15- to 16-decimal digit precision.

  • Floating-point operations return NaN (Not a Number) to signal that the result of the operation is undefined. For example, dividing 0.0 by 0.0 results in NaN.

  • Use the System.Convert method when converting floating-point numbers to another type.

float xFloat = 24567.66F;

int xInt = Convert.ToInt32(xFloat);  // returns 24567

int xInt2 = (int) xFloat;

if(xInt == xInt2) {  }               // False

string xStr = Convert.ToString(xFloat);

single zero = 0;

if (Single.IsNaN(0 / zero)) {  }     // True

double xDouble = 124.56D;

Note that the F suffix is used when assigning a literal value to a single type, and D is optional for a double type.

Using Parse and TryParse to Convert a Numeric String

The primitive numeric types include Parse and tryParse methods that are used to convert a string of numbers to the specified numeric type. This code illustrates:

short shParse  = Int16.Parse("100");

int iParse     = Int32.Parse("100");

long lparse    = Int64.Parse("100");

decimal dParse = decimal.Parse("99.99");

float sParse   = float.Parse("99.99");

double dbParse = double.Parse("99.99");

TRyParse, introduced in .NET 2.0, provides conditional parsing. It returns a boolean value indicating whether the parse is successful, which provides a way to avoid formal exception handling code. The following example uses an Int32 type to demonstrate the two forms of tryParse:

int result;

// parse string and place result in result parameter

bool ok = Int32.TryParse("100", out result);

bool ok = Int32.TryParse("100", NumberStyles.Integer, null,

                         out result);

In the second form of this method, the first parameter is the text string being parsed, and the second parameter is a NumberStyles enumeration that describes what the input string may contain. The value is returned in the fourth parameter.


2.3. Operators: Arithmetic, Logical, and Conditional

The C# operators used for arithmetic operations, bit manipulation, and conditional program flow should be familiar to all programmers. This section presents an overview of these operators that is meant to serve as a syntactical reference.

Arithmetic Operators

Table 2-4 summarizes the basic numerical operators. The precedence in which these operators are applied during the evaluation of an expression is shown in parentheses, with 1 being the highest precedence.

Table 2-4. Numerical Operators

Operator

Description

Example

+

-

(3)

Addition

Subtraction

int x = y + 10;

*

/

%

(2)

Multiplication

Division,

Modulo

int x = 60;

int y = 15;

int z = x * y / 2;  // 450

y = x % 29 ;  // remainder is 2

++

--

(1)

Prefix/postfix

Increment/decrement

x = 5;

Console.WriteLine(x++) // x = 5

Console.WriteLine(++x) // x = 6

~

(1)

Bitwise complement

int x = ~127; // returns -128

>>

<<

(4)

Shift right

Shift left

byte x = 10; // binary 10 is 01010

int result = x << 1; // 20 = 10100

result = x >> 2;     //  5 = 00101

Works with byte, char, short, int, and long

&

|

^

(5-6-7)

Bitwise AND

Bitwise OR

Bitwise XOR

byte x = 12;        //    001100

byte y = 11;        //    001011

int result = x & y; //8 = 001000

result = x ^ y;     //7 = 000111

Core Note

C# does not provide an exponentiation operator. Instead, use the Math.Pow() method to raise a number to a power, and Math.Exp() to raise e to a power.

Conditional and Relational Operators

Relational operators are used to compare two values and determine their relationship. They are generally used in conjunction with conditional operators to form more complex decision constructs. Table 2-5 provides a summary of C# relational and conditional operators.

Table 2-5. Relational and Conditional Boolean Operators

Statement

Description

Example

==

!=

Equality

Inequality

if (x == y) {...}

<

<=

>

>=

Numeric less than

Less than or equal to

Greater than

Greater than or equal to

if (x <= y) {...}

&&

||

Logical AND

Logical OR

if (x == y && y < 30) {...}

If first expression is false, second is not evaluated

&

|

Logical AND

Logical OR

if (x== y | y < 30) {...}

Always evaluates second expression

!

Logical negation

if !(x ==y && y < 30) {...}

Note the two forms of the logical AND/OR operations. The && and || operators do not evaluate the second expression if the first is false—a technique known as short circuit evaluation. The & and | operators always evaluate both expressions. They are used primarily when the expression values are returned from a method and you want to ensure that the methods are called.

In addition to the operators in Table 2-5, C# supports a ?: operator for conditionally assigning a value to a variable. As this example shows, it is basically shorthand for using an if-else statement:

string pass;

int grade=74;

If(grade >= 70) pass="pass"; else pass="fail";

//     expression   ? op1  : op2

pass = (grade >= 70)  ? "pass" : "fail";

If the expression is TRue, the ?: operator returns the first value; if it's false, the second is returned.

Control Flow Statements

The C# language provides if and switch conditional constructs that should be quite familiar to C++ and Java programmers. Table 2-6 provides a summary of these statements.

Table 2-6. Control Flow Statements

Conditional Statement

Example

if (boolean expression) {

   // statements

} else {

   // statements

}

if (bmi < 24.9) {

   weight = "normal";

   riskFactor = 2;

} else {

   weight = "over";

   riskFactor=6;

}

switch (expression)

{

   case constant expression:

      // statements;

      // break/goto/return()

   case constant expression:

      // statements;

      // break/goto/return()

   default:

      // statements;

      // break/goto/return()

}

  • Constant expression may be an integer, enum value, or string.

  • No "fall through" is permitted. Each case block must end with a statement that transfers control.

switch (ndx)

{

   case 1:

      fabric = "cotton";

      blend = "100%";

      break;

   case 2:  // combine 2 & 3

   case 3:

      fabric = "cotton";

      blend = "60%";

      break;

   default:  // optional

      fabric = "cotton";

      blend = "50%";

      break;

}

if-else

Syntax:

if ( boolean expression ) statement

if ( boolean expression ) statement1 else statement2

C# if statements behave as they do in other languages. The only issue you may encounter is how to format the statements when nesting multiple if-else clauses.

// Nested if statements

if (age > 16)                         if (age > 16)

{                                        if (sex == "M")

   if (sex == "M")                          type = "Man";

   {                                     else

      type = "Man";                         type = "Woman" ;

   } else {                           else

      type = "Woman" ;                   type = "child";

   }

} else {

   type = "child";

}

Both code segments are equivalent. The right-hand form takes advantage of the fact that curly braces are not required to surround single statements; and the subordinate if clause is regarded as a single statement, despite the fact that it takes several lines. The actual coding style selected is not as important as agreeing on a single style to be used.

switch

Syntax:

switch( expression ) {switch block}

The expression is one of the int types, a character, or a string. The switch block consists of case labels—and an optional default label—associated with a constant expression that must implicitly convert to the same type as the expression. Here is an example using a string expression:

// switch with string expression

using System;

public class MyApp

{

   static void Main(String[] args)

   {

      switch (args[0])

      {

         case "COTTON":   // is case sensitive

         case "cotton":

              Console.WriteLine("A good natural fiber.");

              goto case "natural";

         case "polyester":

              Console.WriteLine("A no-iron synthetic fiber.");

              break;

         case "natural":

              Console.WriteLine("A Natural Fiber. ");

              break;

         default:

              Console.WriteLine("Fiber is unknown.");

              break;

      }

   }

}

The most important things to observe in this example are as follows:

  • C# does not permit execution to fall through one case block to the next. Each case block must end with a statement that transfers control. This will be a break, goto. or return statement.

  • Multiple case labels may be associated with a single block of code.

  • The switch statement is case sensitive; in the example, "Cotton" and "COTTON" represent two different values.


2.4. Loops

C# provides four iteration statements: while, do, for, and foreach. The first three are the same constructs you find in C, C++, and Java; the foreach statement is designed to loop through collections of data such as arrays.

while loop

Syntax:

while ( boolean expression ) { body }

The statement(s) in the loop body are executed until the boolean expression is false. The loop does not execute if the expression is initially false.

Example:

byte[] r = {0x00, 0x12, 0x34, 0x56, 0xAA, 0x55, 0xFF};

int ndx=0;

int totVal = 0;

while (ndx <=6)

{

   totVal +=    r[ndx];

   ndx += 1;

}

do loop

Syntax:

do { do-body } while ( boolean expression );

This is similar to the while statement except that the evaluation is performed at the end of the iteration. Consequently, this loop executes at least once.

Example:

byte[] r = {0x00, 0x12, 0x34, 0x56, 0xAA, 0x55, 0xFF};

int ndx=0;

int totVal = 0;

do

{

   totVal += r[ndx];

   ndx += 1;

}

while (ndx <= 6);

for loop

Syntax:

for ( [initialization]; [termination condition]; [iteration] )

     { for-body }

The for construct contains initialization, a termination condition, and the iteration statement to be used in the loop. All are optional. The initialization is executed once, and then the condition is checked; as long as it is TRue, the iteration update occurs after the body is executed. The iteration statement is usually a simple increment to the control variable, but may be any operation.

Example:

int[] r = {80, 88, 90, 72, 68, 94, 83};

int totVal = 0;

for (int ndx = 0; ndx <= 6; ndx++) {

   totVal += r[ndx];

}

If any of the clauses in the for statement are left out, they must be accounted for elsewhere in the code. This example illustrates how omission of the for-iteration clause is handled:

for   (ndx = 0; ndx < 6; )

{

   totVal += r[ndx];

   ndx++;          // increment here

}

You can also leave out all of the for clauses:

for (;;) { body   }   // equivalent to while(true) { body }

A return, goto, or break statement is required to exit this loop.

foreach loop

Syntax:

foreach ( type identifier in collection )  { body }

The type and identifier declare the iteration variable. This construct loops once for each element in the collection and sets the iteration variable to the value of the current collection element. The iteration variable is read-only, and a compile error occurs if the program attempts to set its value.

For demonstration purposes, we will use an array as the collection. Keep in mind, however, that it is not restricted to an array. There is a useful set of collection classes defined in .NET that work equally well with foreach. We look at those in Chapter 4, "Working with Objects in C#."

Example:

int totVal = 0;

foreach (int arrayVal in r)

{

   totVal += arrayVal;

}

In a one-dimensional array, iteration begins with index 0 and moves in ascending order. In a multi-dimensional array, iteration occurs through the rightmost index first. For example, in a two-dimensional array, iteration begins in the first column and moves across the row. When it reaches the end, it moves to the next row of the first column and iterates that row.

Transferring Control Within a Loop

It is often necessary to terminate a loop, or redirect the flow of statements within the loop body, based on conditions that arise during an iteration. For example, a while (true) loop obviously requires that the loop termination logic exists in the body. Table 2-7 summarizes the principal statements used to redirect the program flow.

Table 2-7. Statements to Exit a Loop or Redirect the Iteration

Statement

Description

Example

break

Redirects program control to the end point of a containing loop construct.

while (true) {

   ndx+=1;

   if (ndx >10) break;

}

continue

Starts a new iteration of enclosing loop without executing remaining statements in loop.

while (ndx <10) {

   ndx +=1;

   if(ndx %2 =1) continue;

   totVal += ndx;

}

goto

identifier;

goto case exp;

goto default;

Directs program control to a label, a case statement within a switch block, or the default statement within a switch block.

The goto may not transfer control into a nested scope—for example, a loop.

public int FindMatch(string myColor)

{

   string[] colorsAvail("blueaqua",

      "red", "green","navyblue");

   int loc;

   int matches=0;

   foreach (colorType in colorsAvail)

   {

      loc = colortype.IndexOf(myColor);

      if (loc >=0) goto Found;

      continue;

Found:

      matches += 1;

   }

      return(matches);

}

return

[expression] ;

Returns program control to the method that called the current method. Returns no argument if the enclosing method has a void return type.

public double Area(double w, double l)

{

   return w * l;

}

There are few occasions where the use of a goto statement improves program logic. The goto default version may be useful in eliminating redundant code inside a switch block, but aside from that, avoid its use.


2.5. C# Preprocessing Directives

Preprocessing directives are statements read by the C# compiler during its lexical analysis phase. They can instruct the compiler to include/exclude code or even abort compilation based on the value of preprocessing directives.

A preprocessor directive is identified by the # character that must be the first nonblank character in the line. Blank spaces are permitted before and after the # symbol. Table 2-8 lists the directives that C# recognizes.

Table 2-8. Preprocessing Directives

C# Preprocessing Symbol

Description

#define

#undef

Used to define and undefine a symbol. Defining a symbol makes it evaluate to true when used in a #if directive.

#if

#elif

#else

#endif

Analogues to the C# if, else if, and else statements.

#line

Changes the line number sequence and can identify which file is the source for the line.

#region

#endregion

Used to specify a block of code that you can expand or collapse when using the outlining feature of Visual Studio.NET.

#error

#warning

#error causes the compiler to report a fatal error.

#warning causes the compiler to report a warning and continue processing.

The three most common uses for preprocessing directives are to perform conditional compilation, add diagnostics to report errors and warnings, and define code regions.

Conditional Compilation

The #if related directives are used to selectively determine which code is included during compilation. Any code placed between the #if statement and #endif statement is included or excluded based on whether the #if condition is true or false. This is a powerful feature that is used most often for debug purposes. Here is an example that illustrates the concept:

#define DEBUG

using System;

public class MyApp

{

   public static void Main()

   {

      #if (DEBUG)

         Console.WriteLine("Debug Mode");

      #else

         Console.WriteLine("Release Mode");

      #endif

   }

}

Any #define directives must be placed at the beginning of the .cs file. A conditional compilation symbol has two states: defined or undefined. In this example, the DEBUG symbol is defined and the subsequent #if (DEBUG) statement evaluates to true. The explicit use of the #define directive permits you to control the debug state of each source file. Note that if you are using Visual Studio, you can specify a Debug build that results in the DEBUG symbol being automatically defined for each file in the project. No explicit #define directive is required.

You can also define a symbol on the C# compile command line using the /Define switch:

csc /Define:DEBUG myproject.cs

Compiling code with this statement is equivalent to including a #Define DEBUG statement in the source code.

Diagnostic Directives

Diagnostic directives issue warning and error messages that are treated just like any other compile-time errors and warnings. The #warning directive allows compilation to continue, whereas the #error terminates it.

#define CLIENT

#define DEBUG

using System;

public class MyApp

{

   public static void Main()

   {

      #if DEBUG && INHOUSE

         #warning Debug is on.

      #elif DEBUG && CLIENT

         #error Debug not allowed in Client Code.

      #endif

   // Rest of program follows here

In this example, compilation will terminate with an error message since DEBUG and CLIENT are defined.

Code Regions

The region directives are used to mark sections of code as regions. The region directive has no semantic meaning to the C# compiler, but is recognized by Visual Studio.NET, which uses it to hide or collapse code regions. Expect other third-party source management tools to take advantage of these directives.

#region

   // any C# statements

#endregion


2.6. Strings

The System.String, or string class, is a reference type that is represented internally by a sequence of 16-bit Unicode characters. Unlike other reference types, C# treats a string as a primitive type: It can be declared as a constant, and it can be assigned a literal string value.

String Literals

Literal values assigned to string variables take two forms: literals enclosed in quotation marks, and verbatim strings that begin with @" and end with a closing double quote ("). The difference between the two is how they handle escape characters. Regular literals respond to the meaning of escape characters, whereas verbatim strings treat them as regular text. Table 2-9 provides a summary of the escape characters that can be placed in strings.

Table 2-9. String Escape Characters

Escape Character

Description

\'

Inserts a single quote into a string

\"

Inserts a double quote

\\

Inserts a backslash; useful for file paths

\a

System alert

\b

Backspace

\f

Form feed

\n

Inserts a new line

\r

Carriage return

\t

Horizontal tab

\u

Unicode character

\v

Vertical tab

\0

Null character

A verbatim string serves the purpose its name implies: to include any character placed between the beginning and ending double quote. The following segment provides several examples of using literals:

string myQuote, path;

myQuote = @"The solution is in the problem.";

myQuote = "The solution\nis in the problem.";

myQuote = "The Unicode representation of f is \u0066";

// The next two statements assign the same value to myQuote.

myQuote = @"""The solution is in the problem. """;

myQuote = "\"The solution is in the problem. "";

// The next two statements assign the same value to path.

path    = @"c:\my documents\notes.txt";

path    = "c:\\my documents\\notes.txt";

path    = "c:\my documents\notes.txt";      // Fails

The regular literal string is normally your best choice because it supports the escape sequences. The verbatim is to be favored when the text contains backslashes. Its most common use is with file path values and Regular Expression matching patterns (discussed in Chapter 5, "C# Text Manipulation and File I/O").

String Manipulation

The System.String class contains a variety of string manipulation members. These include ways to determine a string's length, extract a substring, compare strings, and convert a string to upper- or lowercase. The following examples illustrate some of the more common operations.

Indexing Individual Characters in a String

The foreach and while loops offer the easiest way to iterate through the characters in a string. In both cases, the operations are read-only.

// Example 1 - using foreach statement

string myQuote = "The solution is in the problem.";

foreach (char cc in myQuote)

{

   Console.Write(cc.ToString());

}

// Example 2 - using while loop

int ndx = 0;

while (ndx < myQuote.Length)

{

   Console.Write(myQuote[ndx].ToString());

   ndx += 1;

}

Note that before an individual character can be displayed or assigned to a string, it must be converted to a string type.

String Concatenation

The + operator is used for concatenating two strings: s1 + s2 . Only one of these has to be a string type; the other can be any type, and its ToString method is called automatically to convert it.

string s1 = "My age = ";

int myAge = 28;

string cat = s1 + myAge;       // My age = 28

MyClass clStr = new MyClass;

Cat = "Class Name = " + clStr; // Class Name = MyClass

The concatenation operation is simple to use, but it is important to understand what is going on behind the scenes: During concatenation, the strings being joined are copied and a new combined string is allocated space. Each concatenation results in the allocation of more memory equal to the length of the new string. This is an acceptable use of resources as long as the number of concatenations is minimal. However, if concatenation occurs inside a long loop, an application's performance can suffer.

Consider an example where an HTML document is constructed by inserting the <br> tag between names in a list.

// assume names is an array containing 1000 names

string nameList = "";

foreach (string custName in names)

{

   // This is inefficient and should be avoided.

   nameList = nameList + custName+"<br>";

}

Each loop results in the creation of a new string consisting of the previous string plus the new appended name and tag. A better approach is to use the StringBuilder class as a replacement for the concatenation operator. This class sets aside memory to operate on strings and thus avoids the copying and memory allocation drawbacks of the concatenation (+) operator. It includes methods to append, insert, delete, remove, and replace characters. StringBuilder is discussed in Chapter 5.

Extracting and Locating Substrings

The Substring method extracts selected portions of a string. Its two overloads are illustrated here:

string poem = "In Xanadu did Kubla Khan";

string poemSeg;

poemSeg = poem.Substring(10);     // did Kubla Khan

// second argument specifies length

poemSeg = poem.Substring(0,9);    // In Xanadu

The IndexOf method locates the next occurrence of a character pattern within a string. It searches for the occurrence from the beginning of the string or a specified location. Listing 2-1 illustrates this.

IndexOf() performs a case-sensitive search. To ensure both upper- and lowercase instances are counted, you could convert the original string to lowercase (ToLower()) before searching it. Note that there is also a LastIndexOf method that locates the last instance of a character pattern within a string.

Listing 2-1. Locating Text Occurrences in a String

// Method to count the occurrences of text in a given string

public static int CharCount(String strSource,String strToFind)

{

   int iCount=0;  // string type has index of 0

   int iPos=strSource.IndexOf(strToFind);

   while(iPos!=-1)

   {

      iCount++;

      iPos=strSource.IndexOf(strToFind, iPos+1);

   }

   return iCount;

}

public class MyApp

{

   static void Main()

   {

      string txt = "In Xanadu did Kubla Khan";

      int ct = CharCount(txt, "a"); // ct = 4

   }

}

Comparing Strings

This topic is more complex than one would expect. The first hint of this is when you look at the System.String members and discover that there are four comparison methods: Compare, CompareOrdinal, CompareTo, and Equals. The choice of a comparison method is based on factors such as whether the comparison should be case sensitive and whether it should take culture into account.

The .NET environment is designed to handle international character sets, currencies, and dates. To support this, the handling and representation of strings can be tailored to different countries and cultures. Consider, for example, how to compare the same date in U.S. and European format. The dates "12/19/04" and "19/12/04" are logically equal, but do not have the same code value. Only a comparison method that takes culture into consideration would consider them equal. Chapter 5 explains how the various comparison methods work and the factors to be considered in selecting one.

For the majority of applications, nothing more than the standard equality (==) operator is required. This code segment illustrates its use:

bool isMatch;

string title = "Ancient Mariner";

isMatch = (title           == "ANCIENT MARINER");    // false

isMatch = (title.ToUpper() == "ANCIENT MARINER");    // true

isMatch = (title           == "Ancient"+" Mariner"); // true

isMatch =  title.Equals("Ancient Mariner");          // true

Note that the == operator is just a syntactical shortcut for calling the Equals method; it is actually faster to call Equals()directly.


2.7. Enumerated Types

An enumerated type, or enum as it's called in C#, offers a convenient way to create a structured set of symbols to represent constant values.

Syntax:

[access modifiers]enum <identifier> [:enum-base]{enum body}

Example:

enum Fabric :short {

   Cotton = 1,

   Silk   = 2,

   Wool   = 4,

   Rayon  = 8,

   Other  = 128

}

Note: If the enum symbols are not set to a value, they are set automatically to the sequence 0, 1, 2, 3, and so on.

The access modifiers define the scope of the enum. The default is internal, which permits it to be accessed by any class in its assembly. Use public to make it available to any class in any assembly.

The optional enum-base defines the underlying type of the constants that correspond to the symbolic names. This must be an integral value of the type byte, sbyte, short, ushort, int, uint, long, or ulong. The default is int.

Working with Enumerations

Enumerated types not only improve program readability, but also minimize code changes when the underlying value changes. In such cases, all references to the value remain valid. Another advantage is that enumerated types are strongly typed. This means, for example, that when an enum type is passed as a parameter, the receiving method must have a matching parameter of the same type; otherwise, a compiler error occurs.

The code segment in Listing 2-2 illustrates these ideas using the Fabric enum from the preceding example.

Listing 2-2. Using an Enumerated Type

static double GetPrice(Fabric fab)

{

   switch(fab)

   {

      case Fabric.Cotton: return(3.55);

      case Fabric.Silk:   return(5.65);

      case Fabric.Wool:   return(4.05);

      case Fabric.Rayon:  return(3.20);

      case Fabric.Other:  return(2.50);

      default: return(0.0);

   }

}

static void Main()

{

   Fabric fab = Fabric.Cotton;

   int fabNum   = (int) fab;             // 1

   string fabType =  fab.ToString();     // "Cotton"

   string fabVal  =  fab.ToString("D");  // "1"

   double cost = GetPrice(fab);          // 3.55

}

Things to note:

  • Casting is required to set the value of an enum to an integer variable: fabNum = (int) fab;

  • The character value of the underlying constant value can be obtained using the ToString() method with the parameter "D". "D" is a format character that converts a value to its decimal form.

  • Passing an instance of the Fabric enum to GetPrice requires that the corresponding parameter in the GetPrice method is declared as the same type.

This example shows how easy it is to obtain the symbol name or constant value when the instance of an enum is known—that is, Cotton. But suppose there is a need to determine whether an enum contains a member with a specific symbol or constant value. You could use foreach to loop through the enum members, but there is a better solution. Enumerations implicitly inherit from System.Enum, and this class contains a set of methods that can be used to query an enumeration about its contents.

System.Enum Methods

Three of the more useful System.Enum methods are Enum.IsDefined, Enum.Parse, and Enum.GetName. The first two methods are often used together to determine if a value or symbol is a member of an enum, and then to create an instance of it. The easiest way to understand them is to see them in use. In this example, the enum Fabric is queried to determine if it contains a symbol matching a given string value. If so, an instance of the enum is created and the GetName method is used to print one of its values.

string fabStr = "Cotton";

// Determine if symbol Cotton exists in Fabric enum

if (Enum.IsDefined(typeof(Fabric),fabStr))

{

   // Create enum instance

   Fabric fab = (Fabric)Enum.Parse(

                 typeof(Fabric) , fabStr);

   // Output from the following statement is: "Silk"

   Console.WriteLine("Second value of Fabric Enum is: " +

                     Enum.GetName(typeof(Fabric), 2));

}

The IsDefined method takes two parameters: an enumeration type that the typeof operator returns and a string representing the symbol to be tested for. Another form of this method tests for a specified constant value if a numeric value is passed as the second parameter.

The Parse method takes the same arguments and creates an instance of an enumerated type. The variable fab created here is equivalent to the one created in Listing 2-2. It is important to ensure that the enum member exists before using the Parse method. If it does not, an exception is thrown.

The GetName method returns a string value of the enum whose value is passed as the second argument to the method. In this example, "Silk" is returned because its constant value is 2.

Enums and Bit Flags

It was not by accident that the values of the Fabric enum were set to powers of 2. Enum members are often used in logical operations where such values have obvious advantages in mapping to unique bit values. You may have code to identify a combination of values:

Fabric cotWool = Fabric.Cotton | Fabric.Wool;

Console.WriteLine(cotWool.ToString());  // Output: 5

It would be more meaningful if the output identified the variable as a combination of wool and cotton. This can be accomplished by adding the [Flags] attribute to the enum declaration:

[Flags]

enum Fabric :short {

The ToString() method checks an enum declaration to see if this attribute is present. If so, it treats the enum as a set of bitmapped flag elements. In this example, it cannot find a symbolic value equal to 5, so it uses the bit pattern "101" and prints the symbols having the bit patterns "001" and "100". The new output is a comma- delimited list: "Cotton, Wool".


2.8. Arrays

C#, like most programming languages, provides the array data structure as a way to collect and manipulate values of the same type. Unlike other languages, C# provides three types of arrays. One is implemented as an ArrayList object; another as a generic List object; and a third is derived from the System.Array class. The latter, which is discussed here, has the traditional characteristics most programmers associate with an array. The ArrayList is a more flexible object that includes special methods to insert and delete elements as well as dynamically resize itself. The List is a type-safe version of the ArrayList that was introduced with .NET 2.0 and may eventually replace the ArrayList. The List and ArrayList are discussed in Chapter 4, along with other Collection classes.

Before looking at the details of creating and using an array, you should be familiar with its general features:

  • Each element in the array is of the type specified in the array declaration. Besides the usual primitive types, an array may contain structures, objects, enums, and even other arrays.

  • Common Language Specification (CLS) compliance requires that all arrays be zero-based to ensure language interoperability (for example, an array reference in C# can be passed to code written in VB.NET). Although it is possible to create a non-zero–based array in C#, it is discouraged because array operations are optimized for zero-based dimensions.

  • When an array is created, it is allocated space for a fixed number of elements. Its capacity cannot be dynamically increased. The ArrayList/List is usually a better choice when the number of elements the array must hold is unknown.

Declaring and Creating an Array

Syntax:

<type> identifier [ ] = new <type> [n]  [{ initializer list}]

Example:

int[] myRating;         // declares an array

myRating = new int[5];  // creates array and allocates memory

int[] myRating = new int[5] {3,4,7,2,8};

int[] myRating = {3,4,7,2,8};  // shorthand version

// Create array containing instances of an Apparel class.

Apparel myApparel = {new Apparel(), new Apparel(),

                     new Apparel());

// Set to an enum

Fabric[] enumArray = new Fabric[2];

enumArray[0] = Fabric.Cotton;

// Create a 2-dimensional array with 3 rows and 2 columns

int[ , ] myRatings = {{3 , 7}, {4 , 9}, {2, 6}};

The size of the array is determined from the explicit dimensions or the number of elements in the optional initializer list. If a dimension and an initializer list are both included, the number of elements in the list must match the dimension(s). If no initialization values are specified, the array elements are initialized to 0 for numeric types or null for all others. The CLR enforces bounds checking—any attempt to reference an array index outside its dimensions results in an exception.

Using System.Array Methods and Properties

The System.Array class defines a number of properties and methods that are used to query an array and manipulate its contents. Array operations include sorting, copying, searching for specific values, and clearing the contents. Table 2-10 summarizes some of the more useful members of the System.Array class.

Table 2-10. Selected Members of System.Array

Member

Type

Description

Length

Instance property

Total number of elements in the array.

Rank

Instance property

Number of dimensions of the array.

CreateInstance

Static method

Creates an Array object.

To create a single-dimensional array:

int[] rank1 =

   (int[]) Array.CreateInstance(typeof(int),4);

GetUpperBound(n)

Instance method

The upper bound of a specified dimension n. Returned value is Length – 1.

d0 = myArray.GetUpperBound(0);

d1=  myArray.GetUpperBound(1);

Sort

Static method

Sorts the elements in an array or a section of an array.

Reverse

Static method

Reverses the elements in a one-dimensional array.

IndexOf,

LastIndexOf

Static method

Returns the index of the first or last occurrence of a value in a one-dimensional array.

Clone

Instance method

Copies the contents of an array into a new array. The new array is a shallow copy of the original—that is, reference pointers, not values, are copied.

Copy

Static method

Copies a selected portion of one array to another.

Clear

Static method

Sets a specified range of elements in an array to zero or null.

The members are classified as static or instance. A static member is not associated with any particular array. It operates as a built-in function that takes any array as a parameter. The instance members, on the other hand, are associated with a specific instance of an array. The example shown in Listing 2-3 demonstrates how to use many of these class members.

Listing 2-3. Working with Arrays Using System.Array Members

class MyApp

{

   static void Main()

   {

      string[] artists = {"Rembrandt", "Velazquez",

            "Botticelli", "Goya", "Manet","El Greco"};

      // ..Sort array in ascending order

      Array.Sort(artists);

      // ..Invert the array

      Array.Reverse(artists);

      PrintArray(artists);  // call method to list array

      int ndx = Array.IndexOf(artists,"Goya");   // ndx = 3

      // ..Clone the array

      string[] artClone = (string[]) artists.Clone();

      // Do arrays point to same address?

      bool eq = Object.ReferenceEquals(

            artClone[0],artists[0]);  // true

      Array.Clear(artClone,0,artClone.Length);

      // ..Copy selected members of artists to artClone

      Array.Copy(artists,1,artClone,0,4);

      eq = Object.ReferenceEquals(

            artClone[0],artists[1]);  // true

   }

   // List contents of Array

   public static void PrintArray(string[] strArray)

   {

      for ( int i = 0; i<= strArray.GetUpperBound(0); i++ )

      {

         Console.WriteLine(strArray[i]);

      }

   }

}

Things to note:

  • The Sort method has many overloaded forms. The simplest takes a single-dimensional array as a parameter and sorts it in place. Other forms permit arrays to be sorted using an interface defined by the programmer. This topic is examined in Chapter 4.

  • The Clone method creates a copy of the artists array and assigns it to artClone. The cast (string[]) is required, because Clone returns an Object type. The Object.ReferenceEquals method is used to determine if the cloned array points to the same address as the original. Because string is a reference type, the clone merely copies pointers to the original array contents, rather than copying the contents. If the arrays had been value types, the actual contents would have been copied, and ReferenceEquals would have returned false.

  • The Copy method copies a range of elements from one array to another and performs any casting as required. In this example, it takes the following parameters

(source, source index, target, target index, # to copy)


2.9. Reference and Value Types

The Common Language Runtime (CLR) supports two kinds of types: reference types and value types (see Figure 2-2 on the following page). Reference types include classes, arrays, interfaces, and delegates. Value types include the primitive data types such as int, char, and byte as well as struct and enum types. Value and reference types are distinguished by their location in the .NET class hierarchy and the way in which .NET allocates memory for each. We'll look at both, beginning with the class inheritance hierarchy.

Figure 2-2. Hierarchy of common reference and value types

System.Object and System.ValueType

Both reference and value types inherit from the System.Object class. The difference is that almost all reference types inherit directly from it, whereas value types inherit further down the hierarchy—directly from the System.ValueType class.

As the base for all types, System.Object provides a set of methods that you can expect to find on all types. This set includes the ToString method used throughout this chapter, as well as methods to clone a type, create a unique hash code for a type, and compare type instances for equality. Chapter 4 discusses these methods in detail and describes how to implement them on custom classes.

System.ValueType inherits from System.Object. It does not add any members, but does override some of the inherited methods to make them more suitable for value types. For example, Equals() is overridden to return true if the value of two objects' fields match. By definition, all value types implicitly inherit from the ValueType class.

Memory Allocation for Reference and Value Types

The primary difference between value and reference types is the way the CLR handles their memory requirements. Value types are allocated on a runtime stack, and reference types are placed on a managed heap that is referenced from the stack.

Figure 2-3 illustrates how the value and reference types from our example (refer to Figure 2-1) are represented in memory. Let's step through what happens when an instance of a reference type is created and is then assigned to a second variable:

Apparel myApparel  = new Apparel();

Apparel myApparel2 = myApparel;

Figure 2-3. Memory layout for value and reference types

  1. The CLR allocates memory for the object on the top of the managed heap.

  2. Overhead information for the object is added to the heap. This information consists of a pointer to the object's method table and a SyncBlockIndex that is used to synchronize access to the object among multiple threads.

  3. The myApparel object is created as an instance of the Apparel class, and its Price and FabType fields are placed on the heap.

  4. The reference to myApparel is placed on the stack.

  5. When a new reference variable myApparel2 is created, it is placed on the stack and given a pointer to the existing object. Both reference variables—myApparel and myApparel2—now point to the same object.

Creating a reference object can be expensive in time and resources because of the multiple steps and required overhead. However, setting additional references to an existing object is quite efficient, because there is no need to make a physical copy of the object. The reverse is true for value types.

Boxing

.NET contains a special object type that accepts values of any data type. It provides a generic way to pass parameters and assign values when the type of the value being passed or assigned is not tied to a specific data type. Anything assigned to object must be treated as a reference type and stored on the heap. Consider the following statements:

int age = 17;

object refAge = age;

The first statement creates the variable age and places its value on the stack; the second assigns the value of age to a reference type. It places the value 17 on the heap, adds the overhead pointers described earlier, and adds a stack reference to it. This process of wrapping a value type so that it is treated as a reference type is known as boxing. Conversely, converting a reference type to a value type is known as unboxing and is performed by casting an object to its original type. Here, we unbox the object created in the preceding example:

int newAge = (int) refAge;

string newAge = (string) refAge;   // Fails. InvalidCastException

Note that the value being unboxed must be of the same type as the variable to which it is being cast.

In general, boxing can be ignored because the CLR handles the details transparently. However, it should be considered when designing code that stores large amounts of numeric data in memory. To illustrate, consider the System.Array and ArrayList classes mentioned earlier. Both are reference types, but they perform quite differently when used to store simple data values.

The ArrayList methods are designed to work on the generic object type. Consequently, the ArrayList stores all its items as reference types. If the data to be stored is a value type, it must be boxed before it can be stored. The array, on the other hand, can hold both value and reference types. It treats the reference types as the ArrayList does, but does not box value types.

The following code creates an array and an ArrayList of integer values. As shown in Figure 2-4, the values are stored quite differently in memory.

// Create array with four values

Int[] ages = {1,2,3,4};

// Place four values in ArrayList

ArrayList ages = new ArrayList();

For (int i=0; i<4; i++) {

   ages.add(i); // expects object parameter

}

Figure 2-4. Memory layout comparison of Array and ArrayList

The array stores the values as unboxed int values; the ArrayList boxes each value. It then adds overhead required by reference types. If your application stores large amounts of data in memory and does not require the special features of the ArrayList, the array is a more efficient implementation. If using .NET 2.0 or later, the List class is the best choice because it eliminates boxing and includes the more flexible ArrayList features.

Summary of Value and Reference Type Differences

Memory Allocation

We have seen that memory allocation is the most significant difference between value and reference types. Reference types are allocated on the heap and value types on the thread or call stack. When a reference type is created, it is initialized to null, indicating it does not point to anything. A value type is initialized to zero (0).

Releasing Memory

Memory on the stack is freed when a variable goes out of scope. A garbage collection process that occurs when a system memory threshold is reached releases memory on the heap. Garbage collection is controlled by .NET and occurs automatically at unpredictable intervals. Chapter 4 discusses it in detail.

Variable Assignments

When a variable is set to a reference type, it receives a pointer to the original object—rather than the object value itself. When a variable is set to a value type, a field-by-field copy of the original variable is made and assigned to the new variable.


2.10. Summary

This chapter offers an overview of the C# language, providing the syntax and examples for using the list of features that form the core of this and just about any programming language. These features include basic data types, numerical and relational operators, loop constructs, strings, enums, and arrays. The final section stresses how all .NET types can be classified as a value or reference type. It explains the different memory allocation schemes used for the two. In addition, it looks at the concepts of boxing and unboxing: converting a value type to a reference type and converting a reference type back to a value type.


2.11. Test Your Understanding

1:

Which method is required in each C# program, and what parameters does it take?

2:

What are the three types of inline comments available in C#? Which can be exported?

3:

What is a primitive?

4:

What is printed by the following statements?

int grade=78;

bool pass = (grade >=70) ? true : false;

Console.WriteLine(pass);

5:

Which loop construct is executed at least once?

6:

How do a break and a continue statement differ?

7:

Given the following variables

char c= 'c';

double d= 120.0D;

int i=10;

string s="Rodin";

which of the following fails to compile?

  1.
c = c+ i;

  2.
s += i;

  3.
c += s;

  4.
d += i;

8:

Describe a potential drawback to using the + operator for concatenation. What is the recommended alternative?

9:

Name the two base classes from which all value types inherit.

10:

What prime value is printed by the final statement in this code?

int[] primes = new int[6] {1,3,5,7,11,13};

int[] primesClone = new int[6];

Array.Copy(primes,1,primesClone,1,5);

Console.WriteLine("Prime: "+primesClone[3]);


Chapter 3. Class Design in C#

Topics in This Chapter

  • Defining a Class: The attributes and modifiers included in a class definition influence the behavior and accessibility of the class.

  • Constants: A const type defines fixed values at compilation time.

  • Fields: A field is typically used to maintain data inside a class.

  • Properties: A property is the recommended way to expose a class's data.

  • Methods: The functionality of a class is defined by its methods.

  • Inheritance: By using inheritance, a class can take advantage of preexisting types.

  • Constructors: This special purpose method is used to initialize a class.

  • Events and Delegates: A program's actions are often triggered by events; delegates have the role of invoking methods to handle an event.

  • Operator Overloading: Operators can be used to manipulate classes.

  • Interfaces: An inherited interface defines the methods and properties that a struct or class must implement.

  • Generics: By creating a generic class to store data, it is possible to eliminate casting, boxing, and ensure type safety.

  • Structures: In some situations, a struct is a better choice than a class.

This chapter provides an advanced introduction to using classes within the .NET environment. It is not a primer on object-oriented programming (OOP) and assumes you have some familiarity with the principles of encapsulation, inheritance, and polymorphism. C# is rich in object-oriented features, and the first challenge in working with classes is to understand the variety of syntactical contstructs. At the same time, it is necessary to appreciate the interaction between C# and .NET. Not only does the Framework Class Library (FCL) provide thousands of predefined classes, but it also provides a hierarchy of base classes from which all C# classes are derived.

The chapter presents topics in a progressive fashion—each section building on the previous. If you are new to C#, you should read from beginning to end; if you're familiar with the concepts and syntax, read sections selectively. The chapter begins by presenting the syntactical construct of a class and then breaks it down in detail. Attributes, modifiers, and members of the class body (constructors, properties, fields, and methods) are all explained. Sprinkled throughout are recommended .NET guidelines and best practices for designing and using custom classes. The objective is to show not only how to use classes, but also encourage good design practices that result in efficient code.


3.1. Introduction to a C# Class

Figure 3-1 displays a class declaration followed by a body containing typical class members: a constant, fields, a constructor containing initialization code, a property, and a method. Its purpose is to familiarize you with the syntax common to most C# classes and serve as a reference for later examples.

Figure 3-1. Class declaration and body

    Previous Section < Day Day Up > Next Section


3.2. Defining a Class

A class definition consists of an optional attributes list, optional modifiers, the word class followed by the class identifier (name), and an optional list containing a base class or interfaces to be used for inheritance. Following this class declaration is the class body, consisting of the code and class members such as methods and properties.

Syntax for class definition:

[attributes] [modifiers] class  identifier [:baselist]

{class body} [;]

Classes—as do all .NET types—inherit from the System.Object class. This inheritance is implicit and is thus not specified as part of the class definition. As we'll see in the discussion of inheritance, this is important because a class can explicitly inherit from only one class.

Attributes

The optional attribute section consists of a pair of square brackets surrounding a comma-separated list of one or more attributes. An attribute consists of the attribute name followed by an optional list of positional or named arguments. The attribute may also contain an attribute target—that is, the entity to which the attribute applies.

Examples

The attribute section contains an attribute name only:

[ClassDesc]

Single attribute with named argument and positional argument (0):

[ClassDesc(Author="Knuth", 0)]

Multiple attributes can be defined within brackets.:

[ClassDesc(Author="Knuth"), ClassDesc(Author="James")]

Description

Attributes provide a way to associate additional information with a target entity. In our discussion, the target is a newly created class; but attributes may also be associated with methods, fields, properties, parameters, structures, assemblies, and modules. Their simple definition belies a truly innovative and powerful programming tool. Consider the following:

  • An attribute is an instance of a public class. As such, it has fields and properties that can be used to provide rich descriptive information about a target or targets.

  • All compilers that target the Common Language Runtime (CLR) recognize attributes and store information about them in the module's metadata. This is an elegant way to attach information to a program entity that can affect its behavior without modifying its implementation code. An application can then use reflection (a set of types for reading metadata) to read the metadata at runtime and make decisions based on its value.

  • Hundreds of predefined attributes are included in the .NET Framework Class Library (FCL). They are used heavily in dealing with interoperability issues such as accessing the Win32API or allowing .NET applications and COM objects to communicate. They also are used to control compiler operations. The[assembly:CLSComplianttrue)] attribute in Figure 3-1 tells the C# compiler to check the code for CLS compliance.

Core Note

Attributes provide a way to extend the metadata generated by the C# compiler with custom descriptive information about a class or class member.

.NET supports two types of attributes: custom attributes and standard attributes. Custom attributes are defined by the programmer. The compiler adds them to the metadata, but it's up to the programmer to write the reflection code that incorporates this metadata into the program. Standard attributes are part of the .NET Framework and recognized by the runtime and .NET compilers. The Flags attribute that was discussed in conjunction with enums in Chapter 2, "C# Language Fundamentals," is an example of this; another is the conditional attribute, described next.

Conditional Attribute

The conditional attribute is attached to methods only. Its purpose is to indicate whether the compiler should generate Intermediate Language (IL) code to call the method. The compiler makes this determination by evaluating the symbol that is part of the attribute. If the symbol is defined (using the define preprocessor directive), code that contains calls to the method is included in the IL. Here is an example to demonstrate this:

File: attribs.cs (attribs.dll)

#define DEBUG

using System;

using System.Diagnostics;   // Required for conditional attrib.

public class AttributeTest

{

   [Conditional("TRACE")]

   public static void ListTrace()

   { Console.WriteLine("Trace is On"); }

   [Conditional("DEBUG")]

   public static void ListDebug()

   { Console.WriteLine("Debug is On"); }

}

File: attribclient.cs (attribclient.exe)

#define TRACE

using System;

public class MyApp {

   static void Main()

   {

      Console.WriteLine("Testing Method Calls");

      AttributeTest.ListTrace();

      AttributeTest.ListDebug();

   }

}

Executing attribclient yields the following output:

Testing Method Calls

Trace is On

When attribclient is compiled, the compiler detects the existence of the trACE symbol, so the call to ListTrace is included. Because DEBUG is not defined, the call to ListDebug is excluded. The compiler ignores the fact that DEBUG is defined in attribs; its action is based on the symbols defined in the file containing the method calls. Note that a conditional attribute can be used only with methods having a return type of void.

Access Modifiers

The primary role of modifiers is to designate the accessibility (also called scope or visibility) of types and type members. Specifically, a class access modifier indicates whether a class is accessible from other assemblies, the same assembly, a containing class, or classes derived from a containing class.

public | A class can be accessed from any assembly.
protected | Applies only to a nested class (class defined within another class). Access is limited to the container class or classes derived from the container class.
internal | Access is limited to classes in the same assembly. This is the default access.
private | Applies only to a nested class. Access is limited to the container class.
protected internal | The only case where multiple modifiers may be used.
internal | Access is limited to the current assembly or types derived from the containing class.

Core Note

A base class must be at least as accessible as its derived class. The following raises an error:

class  Furniture { }               // default access is internal

public class Sofa : Furniture { }  // error

The error occurs because the Furniture class (internal by default) is less accessible than the derived Sofa class. Errors such as this occur most frequently when a developer relies on a default modifer. This is one reason that modifiers should be included in a declaration.

Abstract, Sealed, and Static Modifiers

In addition to the access modifiers, C# provides a dozen or so other modifiers for use with types and type members. Of these, three can be used with classes: abstract, sealed, and static.

abstract | Indicates that a class is to be used only as a base class for other classes. This means that you cannot create an instance of the class directly. Any class derived from it must implement all of its abstract methods and accessors. Despite its name, an abstract class can possess nonabstract methods and properties.
sealed | Specifies that a class cannot be inherited (used as a base class). Note that .NET does not permit a class to be both abstract and sealed.
static | Specifies that a class contains only static members (.NET 2.0).

Class Identifier

This is the name assigned to the class. The ECMA standard recommends the following guidelines for naming the identifier:

  • Use a noun or noun phrase.

  • Use the Pascal case capitalization style: The first letter in the name and the first letter of each subsequent concatenated word are capitalized—for example, BinaryTree.

  • Use abbreviations sparingly.

  • Do not use a type prefix, such as C, to designate all classes—for example, BinaryTree, not CBinaryTree.

  • Do not use the underscore character.

  • By convention, interface names always begin with I; therefore, do not use I as the first character of a class name unless I is the first letter in an entire word—for example, IntegralCalculator.

Base Classes, Interfaces, and Inheritance

This optional list contains a previously defined class or interface(s) from which a class may derive its behavior and capabilities. The new class is referred to as the derived class, and the class or interface from which it inherits is the base class or interface. A base class must be listed before any interface(s).

Example

// .. FCL Interface and user-defined base class

public interface System.Icomparable

   {Int32 CompareTo(Object object); }

class Furniture {  }

// .. Derived Classes

class Sofa: Furniture { ... }  // Inherits from one base class

// Following inherits from one base class and one interface.

class Recliner: Furniture, IComparable {...}

The C# language does not permit multiple class inheritance, thus the base list can contain only one class. Because there is no limit on the number of inherited interfaces, this serves to increase the role of interfaces in the .NET world.

Core Note

  • Inheritance from a base class is referred to as implementation inheritance. The derived class inherits all of the members of the base class. However, the base class can prevent access to a member by defining it with the private modifier.

  • Inheritance from an interface is referred to as interface inheritance because the interface does not provide implementation code. The derived class must provide the logic to implement any functions defined in the base interface(s).


3.3. Overview of Class Members

Table 3-1 provides a summary of the types that comprise a .NET class. They can be classified broadly as members that hold data—constants, fields, and properties—and members that provide functionality—the constructor, method, and event. We'll look at each individually.

Table 3-1. Class Members

Member Type

Valid In

Description

Constant

Class, Structure

A symbol that represents an unchanging value. The compiler associates it with the class—not an instance of the class.

Field

Class, Structure

A variable that holds a data value. It may be read-only or read/write.

Property

Class, Structure

Provides access to a value in a class. It uses an accessor that specifies the code to be executed in order to read or write the value. The code to read or write to a property is implemented implicitly by .NET as two separate methods.

Constructor

Class, Structure

C# has three types of constructors:

Instance. Initializes fields when an instance of a class is created.

Private. Commonly used to prevent instances of a class from being created.

Static. Initializes class before any instance is created.

Method

Class, Structure, Interface

A function associated with the class that defines an action or computation.

Events

Class, Structure, Interface

A way for a class or object to notify other classes or objects that its state has changed.

Types

Class, Structure, Interface

Classes, interfaces, structs, delegates.

Member Access Modifiers

The access modifiers used for a class declaration can also be applied to class members. They determine the classes and assemblies that have access to the class. Table 3-2 summarizes the scope of accessibility.

Table 3-2. Summary of Accessibility Provided by Access Modifiers

Access Modifiers

Class can be accessed by classes in:

Public

protected

Internal

private

Another assembly

Yes

[*]

No

[*]

Same assembly

Yes

[*]

Yes

[*]

Containing class

Yes

Yes

Yes

Yes

Class derived from containing class

Yes

Yes

Yes

No

[*] Not applicable


3.4. Constants, Fields, and Properties

Constants, fields, and properties are the members of a class that maintain the content or state of the class. As a rule of thumb, use constants for values that will never change; use fields to maintain private data within a class; and use properties to control access to data in a class. Let's now look at the details of these three class members.

Constants

C# uses the const keyword to declare variables that have a fixed, unalterable value. Listing 3-1 provides an example of using constants. Although simple, the code illustrates several basic rules for defining and accessing constants.

Listing 3-1. Constants

using System;

class Conversions

{

   public const double Cm = 2.54;

   public const double Grams = 454.0 , km = .62 ;

   public const string ProjectName = "Metrics";

}

class ShowConversions

{

   static void Main()

   {

      double pounds, gramWeight;

      gramWeight = 1362;

      pounds = gramWeight / Conversions.Grams;

      Console.WriteLine(

         "{0} Grams= {1} Pounds", gramWeight,pounds);

      Console.WriteLine("Cm per inch {0}", Conversions.Cm);

      Conversions c= new Conversions();  // Create class

                                         // instance

      // This fails to compile. Cannot access const from object

      Console.WriteLine("Cm per inch {0}", c.Cm);

   }

}

  • The const keyword can be used to declare multiple constants in one statement.

  • A constant must be defined as a primitive type, such as string or double shown here (see Chapter 2 for a list of all C# primitives).

  • Constants cannot be accessed from an instance of the class. Note that the ShowConversion class accesses the constants without instantiating the class.

The most important thing to recognize about a constant is that its value is determined at compile time. This can have important ramifications. For example, suppose the Furniture class in Figure 3-1 is contained in a DLL that is used by other assemblies as a source for the sales tax rate. If the rate changes, it would seem logical that assigning the new value to SalesTax and recompiling the DLL would then make the new value to external assemblies. However, the way .NET handles constants requires that all assemblies accessing the DLL must also be recompiled. The problem is that const types are evaluated at compile time.

When any calling routine is compiled against the DLL, the compiler locates all constant values in the DLL's metadata and hardcodes them in the executable code of the calling routine. This value is not changed until the calling routine is recompiled and reloads the value from the DLL. In cases such as tax, where a value is subject to change, it's preferable to define the value as a readonly field—sometimes referred to as a runtime constant.

Core Approach

It is often desirable to store a group of constants in a special utility or helper class. Declare them public to make them available to all classes. Because there is no need to create instances of the class, declare the class as abstract.

Fields

A field is also used to store data within a class. It differs from a const in two significant ways: Its value is determined at runtime, and its type is not restricted to primitives.

Field Modifiers

In addition to the access modifiers, fields have two additional modifiers: static and readonly (see Table 3-3).

Table 3-3. Field Modifiers

Modifier

Definition

static

The field is part of the class's state rather than any instances of the class. This means that it can be referenced directly (like a constant) by specifying classname.fieldname without creating an instance of the class.

readonly

The field can only be assigned a value in the declaration statement or class constructor. The net effect is to turn the field into a constant. An error results if code later attempts to change the value of the field.

As a rule of thumb, fields should be defined with the private attribute to ensure that the state of an object is safe from outside manipulation. Methods and properties should then be used to retrieve and set the private data if outside access is required.

Core Note

If a field is not initialized, it is set to the default value for its type: 0 for numbers, null for a reference type, single quotation marks ('') for a string, and false for boolean.

There is one case where setting a field to public makes sense: when your program requires a global constant value. By declaring a field to be public static readonly, you can create a runtime constant. For example, this declaration in Figure 3-1:

const double salesTax = .065;

can be replaced with a field

public static readonly double SalesTax = .065;

A method then references this field as Furniture.SalesTax, and the readonly modifier ensures the value cannot be changed. Note that if you create an instance of this class, you cannot access salesTax as a member of that instance. An attempt to do so results in the compile error "static member cannot be accessed with an instance reference".

Furniture chair = new Furniture("Broyhill","12422",225.00);

double itemTax = chair.SalesTax;    // Raises an error

Using Static Read-Only Fields to Reference Class Instances

Static readonly fields can be used to represent groups of related constant data by declaring them a reference type such as a class instance or array. This is of use when the data can be represented by a limited number of objects with unchanging values.

This example presents the interesting concept of fields defined as instances of their containing class. The static modifier makes this possible, because it designates the field as part of the class and not its instances. Note that the private constructor prevents clients outside the scope of the class from creating new class instances. Thus, only those objects exposed by the fields are available outside the class.

The class also contains two instance fields, yardPrice and deliveryWeeks, that are declared as private. Access to them is controlled though a public method and property:

Upholstery.silk.FabCost(10);   // Value from method

Upholstery.silk.DeliveryTime;  // Value from property

Listing 3-2. Using Static Read-Only Fields as Reference Types

public class Upholstery

{

   // fields to contain price and delivery time for fabrics

   public static readonly Upholstery silk =

      new Upholstery(15.00, 8);

   public static readonly Upholstery wool =

      new Upholstery(12.00, 6);

   public static readonly Upholstery cotton =

      new Upholstery(9.00, 6);

   private double yardPrice;

   private int deliveryWeeks;

   // constructor - set price per yard and delivery time

   // private modifier prevents external class instantiation

   private Upholstery ( double yrPrice, int delWeeks)

   {

      yardPrice = yrPrice;

      deliveryWeeks = delWeeks;

   }

   // method to return total cost of fabric

   public double FabCost(double yards)

   {

      return yards * this.yardPrice;

   }

   // property to return delivery time

   public int DeliveryTime

      {get { return deliveryWeeks;}}

   // property to return price per yard

   public double PricePerYard

      {get {return yardPrice;}}

}

Properties

A property is used to control read and write access to values within a class. Java and C++ programmers create properties by writing an accessor method to retrieve field data and a mutator method to set it. Unlike these languages, the C# compiler actually recognizes a special property construct and provides a simplified syntax for creating and accessing data. In truth, the syntax is not a whole lot different than a comparable C++ implementation, but it does allow the compiler to generate more efficient code.

Syntax:

[attributes] <modifier>   <data type>  <property name>

{

   [access modifier] get

   {  ...

      return(propertyvalue)

   }

   [access modifier] set

   {  ...  Code to set a field to the keyword value  }

}

Note:

  1. In addition to the four access modifiers, the property modifier may be static, abstract, new, virtual, or override. Abstract is used only in an abstract class; virtual is used in a base class and permits a subclass to override the property.

  2. value is an implicit parameter containing the value passed when a property is called.

  3. The get and set accessors may have different access modifiers.

Listing 3-3. Creating and Accessing a Property

public class  Upholstery

{

   private double yardPrice;

   // Property to return or set the price

   public double PricePerYard

   {

      get {return yardPrice;}   // Returns a property value

      set {                     // Sets a property value

         if ( value <= 0 )

            throw new ArgumentOutOfRangeException(

            "Price must be greater than 0.");

         yardPrice = value

      }

   }

   ...

}

The syntax for accessing the property of a class instance is the same as for a field:

// fabObj is instance of Upholstery class

double fabricPrice = fabObj.PricePerYard;

fabObj.PricePerYard = 12.50D;

The get block of code serves as a traditional accessor method and the set block as a mutator method. Only one is required. Leave out the get block to make the property write-only or the set block to make it read-only.

All return statements in the body of a get block must specify an expression that is implicitly convertible to the property type.

In this example, the code in the set block checks to ensure that the property is set to a value greater than 0. This capability to check for invalid data is a major argument in favor of encapsulating data in a property.

If you were to examine the underlying code generated for this example, you would find that C# actually creates a method for each get or set block. These names are created by adding the prefix get or set to the property name—for example, get_PricePerYard. In the unlikely case you attempt to create a method with the same name as the internal one, you will receive a compile-time error.

The use of properties is not necessarily any less efficient than exposing fields directly. For a non-virtual property that contains only a small amount of code, the JIT (Just-in-Time) compiler may replace calls to the accessor methods with the actual code contained in the get or set block. This process, known as inlining, reduces the overhead of making calls at runtime. The result is code that is as efficient as that for fields, but much more flexible.

Indexers

An indexer is often referred to as a parameterized property. Like a property, it is declared within a class, and its body may contain get and set accessors that share the same syntax as property accessors. However, an indexer differs from a property in two significant ways: It accepts one or more parameters, and the keyword this is used as its name. Here is the formal syntax:

Syntax:

[attributes] <modifier><return type> this[parameter(s)]  {

Example:

public int this [int ndx] {

Note: The static modifier is not supported because indexers work only with instances.

In a nutshell, the indexer provides a way to access a collection of values maintained within a single class instance. The parameters passed to the indexer are used as a single- or multi-dimensional index to the collection. The example in Listing 3-4 should clarify the concept.

Listing 3-4. Using Indexer to Expose an Array of Objects

using System;

using System.Collections;  // Namespace containing ArrayList

public class  Upholstery

{

   // Class to represent upholstery fabric

   private double yardPrice;

   private int deliveryWeeks;

   private string fabName;

   // Constructor

   public Upholstery (double price, int delivery, string fabric)

   {

      this.yardPrice = price;

      this.deliveryWeeks = delivery;

      this.fabName = fabric;

   }

   // Three readonly properties to return Fabric information

   public int DeliveryTime

      {get {return deliveryWeeks;}}

   public double PricePerYard

      {get {return yardPrice;}}

   public string FabricName

      {get {return fabName;}}

}

public class Fabrics

{

   // Array to hold list of objects

   private ArrayList fabricArray = new ArrayList();

   // Indexer to add or return an object from the array

   public Upholstery this[int ndx]

   {

      get {

           if(!(ndx<0 || ndx > fabricArray.Count-1))

              return (Upholstery)fabricArray[ndx];

           // Return empty object

           else return(new Upholstery(0,0,""));

          }

           set { fabricArray.Insert(ndx, value);}

   }

}

public class IndexerApp

{

   public static void Main()

   {

      Fabrics sofaFabric = new Fabrics();

      // Use Indexer to create array of Objects

      sofaFabric[0] = new Upholstery(15.00, 8, "Silk");

      sofaFabric[1] = new Upholstery(12.00, 6, "Wool");

      sofaFabric[2] = new Upholstery(9.00, 6, "Cotton");

      // Next statement prints "Fabric: Silk"

      Console.WriteLine("Fabric: {0} ",

                        sofaFabric[0].FabricName);

   }

}

The Fabrics class contains an indexer that uses the get and set accessors to control access to an internal array of Upholstery objects. A single instance of the Fabrics class is created and assigned to sofaFabric. The indexer allows the internal array to be directly accessed by an index parameter passed to the object:

Fabrics sofaFabric = new Fabrics();

// Use Indexer to create array of Objects

sofaFabric[0] = new Upholstery(15.00, 8, "Silk");

sofaFabric[1] = new Upholstery(12.00, 6, "Wool");

The advantage of using an indexer is that it hides the array handling details from the client, and it can also perform any validation checking or data modification before returning a value. Indexers are best used with objects whose properties can be represented as a collection, rather than a scalar value. In this example, the various fabrics available for sofas form a collection. We could also use the indexer to create a collection of fabrics used for curtains:

Fabrics curtainFabric = new Fabrics();

curtainFabric[0] = new Upholstery(11.00, 4, "Cotton");

curtainFabric[1] = new Upholstery(7.00, 5, "Rayon");


3.5. Methods

Methods are to classes as verbs are to sentences. They perform the actions that define the behavior of the class. A method is identified by its signature, which consists of the method name and the number and data type of each parameter. A signature is considered unique as long as no other method has the same name and matching parameter list. In addition to parameters, a method has a return type—void if nothing is returned—and a modifier list that determines its accessibility and polymorphic behavior.

For those who haven't recently boned up on Greek or object-oriented principles, polymorphism comes from the Greek poly (many) and morphos (shape). In programming terms, it refers to classes that share the same methods but implement them differently. Consider the ToString method implemented by all types. When used with an int type, it displays a numeric value as text; yet on a class instance, it displays the name of the underlying class—although this default should be overridden by a more meaningful implementation.

One of the challenges in using .NET methods is to understand the role that method modifiers play in defining the polymorphic behavior of an application. A base class uses them to signal that a method may be overridden or that an inheriting class must implement it. The inheriting class, in turn, uses modifiers to indicate whether it is overriding or hiding an inherited method. Let's look at how all this fits together.

Method Modifiers

In addition to the access modifiers, methods have seven additional modifiers shown in Table 3-4. Five of these—new, virtual, override, sealed, and abstract—provide a means for supporting polymorphism.

Table 3-4. Method Modifiers

Modifier

Description

static

The method is part of the class's state rather than any instances of the class. This means that it can be referenced directly by specifying classname.method (parameters) without creating an instance of the class.

virtual

Designates that the method can be overridden in a subclass. This cannot be used with static or private access modifiers.

override

Specifies that the method overrides a method of the same name in a base class. This enables the method to define behavior unique to the subclass. The overriden method in the base class must be virtual.

new

Permits a method in an inherited class to "hide" a non-virtual method with a same name in the base class. It replaces the original method rather than overriding it.

sealed

Prevents a derived class from overriding this method.

  • Is used in a derived class that will serve as the base for its own subclasses.

  • Must be used with the override modifier.

abstract

The method contains no implementation details and must be implemented by any subclass. Can only be used as a member of an abstract class.

extern

Indicates that the method is implemented externally. It is generally used with the DLLImport attribute that specifies a DLL to provide the implementation.

Static Modifier

As with other class members, the static modifier defines a member whose behavior is global to the class and not specific to an instance of a class. The modifier is most commonly used with constructors (described in the next section) and methods in helper classes that can be used without instantiation.

Listing 3-5. Static Method

using System;

class Conversions

{

   // class contains functions to provide metric conversions

   private static double cmPerInch = 2.54;

   private static double gmPerPound = 455;

   public static double inchesToMetric(double inches) {

      return(inches * cmPerInch);

   }

   public static double poundsToGrams(double pounds) {

      return(pounds * gmPerPound);

   }

}

class Test

{

   static void Main() {

      double cm, grams;

      cm = Conversions.inchesToMetric(28.5);

      grams = Conversions.poundsToGrams(984.4);

   }

}

In this example, the Conversions class contains methods that convert units from the English to metric system. There is no real reason to create an instance of the class, because the methods are invariant (the formulas never change) and can be conveniently accessed using the syntax classname.method(parameter).

Method Inheritance with Virtual and Override Modifiers

Inheritance enables a program to create a new class that takes the form and functionality of an existing (base) class. The new class then adds code to distinguish its behavior from that of its base class. The capability of the subclass and base class to respond differently to the same message is classical polymorphism. In practical terms, this most often means that a base and derived class(es) provide different code for methods having the same signature.

By default, methods in the base class cannot be changed in the derived class. To overcome this, .NET provides the virtual modifier as a cue to the compiler that a method can be redefined in any class that inherits it. Similarly, the compiler requires that any derived class that alters a virtual method preface the method with the override modifier. Figure 3-2 and Listing 3-6 provide a simple illustration of this.

Listing 3-6. Virtual Methods

using System;

class Fiber

{

   public  virtual string ShowMe() { return("Base");}

}

class Natural:Fiber

{

   public override string ShowMe() { return("Natural");}

}

class Cotton:Natural

{

   public override string ShowMe() { return("Cotton");}

}

class Test

{

   static void Main ()

   {

      Fiber fib1 = new Natural();  // Instance of Natural

      Fiber fib2 = new Cotton();   // Instance of Cotton

      string fibVal;

      fibVal = fib1.ShowMe();      // Returns "Natural"

      fibVal = fib2.ShowMe();      // Returns "Cotton"

   }

}

Figure 3-2. Relationship between base class and subclasses for Listing 3-6

In this example, Cotton is a subclass of Natural, which is itself a subclass of Fiber. Each subclass implements its own overriding code for the virtual method ShowMe.

fib1.ShowMe();    //returns  "Natural"

fib2.ShowMe();    //returns  "Cotton"

A subclass can inherit a virtual method without overriding it. If the Cotton class does not override ShowMe(), it uses the method defined in its base class Natural. In that case, the call to fib2.ShowMe() would return "Natural".

New Modifier and Versioning

There are situations where it is useful to hide inherited members of a base class. For example, your class may contain a method with the same signature as one in its base class. To notify the compiler that your subclass is creating its own version of the method, it should use the new modifier in the declaration.

ShowMe() is no longer virtual and cannot be overridden. For Natural to create its own version, it must use the new modifier in the method declaration to hide the inherited version. Observe the following:

  • An instance of the Natural class that calls ShowMe()invokes the new method:

Natural myFiber = new Natural();

string fibTtype = myFiber.ShowMe();  // returns "Natural"

  • Cotton inherits the new method from Natural:

Cotton myFiber = new Cotton();

string fibType = myFiber.ShowMe();   // returns "Natural"

  • If ShowMe were declared as private rather than public in Natural, Cotton would inherit ShowMe from Fiber, because it cannot inherit a method that is out of scope.

Listing 3-7. Using the New Modifier for Versioning

public class Fiber

{

   public string ShowMe()        {return("Base");}

   public virtual string GetID() {return("BaseID");}

}

public class Natural:Fiber

{

   // Hide Inherited version of ShowMe

   new public string ShowMe()     {return("Natural");}

   public override string GetID() {return("NaturalID");}

   }

public class Cotton:Natural

{  // Inherits two methods: ShowMe() and GetID()

}

Sealed and Abstract Modifiers

A sealed modifier indicates that a method cannot be overridden in an inheriting class; an abstract modifier requires that the inheriting class implement it. In the latter case, the base class provides the method declaration but no implementation.

This code sample illustrates how an inheriting class uses the sealed modifier to prevent a method from being overridden further down the inheritance chain. Note that sealed is always paired with the override modifier.

class A {

   public virtual void PrintID{....}

}

class B: A {

   sealed override public void PrintID{...}

}

class C:B {

   // This is illegal because it is sealed in B.

   override public void PrintID{...}

}

An abstract method represents a function with a signature—but no implementation code—that must be defined by any non-abstract class inheriting it. This differs from a virtual method, which has implementation code, but may be redefined by an inheriting class. The following rules govern the use abstract methods:

  • Abstract methods can only be declared in abstract classes; however, abstract classes may have non-abstract methods.

  • The method body consists of a semicolon:

public abstract void myMethod();

  • Although implicitly virtual, abstract methods cannot have the virtual modifier.

  • A virtual method can be overridden by an abstract method.

When facing the decision of whether to create an abstract class, a developer should also consider using an interface. The two are similar in that both create a blueprint for methods and properties without providing the implementation details. There are differences between the two, and a developer needs to be aware of these in order to make the better choice. The section on interfaces in this chapter offers a critical comparison.

Passing Parameters

By default, method parameters are passed by value, which means that a copy of the parameter's data—rather than the actual data—is passed to the method. Consequently, any change the target method makes to these copies does not affect the original parameters in the calling routine. If the parameter is a reference type, such as an instance of a class, a reference to the object is passed. This enables a called method to change or set a parameter value.

C# provides two modifiers that signify a parameter is being passed by reference: out and ref. Both of these keywords cause the address of the parameter to be passed to the target method. The one you use depends on whether the parameter is initialized by the calling or called method. Use ref when the calling method initializes the parameter value, and out when the called method assigns the initial value. By requiring these keywords, C# improves code readability by forcing the programmer to explicitly identify whether the called method is to modify or initialize the parameter.

The code in Listing 3-8 demonstrates the use of these modifiers.

Listing 3-8. Using the ref and out Parameter Modifiers

class TestParms

{

   public static void FillArray(out double[] prices)

   {

      prices = new double[4] {50.00,80.00,120.00,200.00};

   }

   public static void UpdateArray(ref double[] prices)

   {

      prices[0] = prices[0] * 1.50;

      prices[1] = prices[1] * 2.0;

   }

   public static double TaxVal(double ourPrice,

                               out double taxAmt)

   {

      double totVal = 1.10 * ourPrice;

      taxAmt = totVal – ourPrice;

      ourPrice = 0.0;  // Does not affect calling parameter

      return totVal;

   }

}

class MyApp

{

   public static void Main()

   {

      double[] priceArray;

      double taxAmt;

      //   (1) Call method to initialize array

      TestParms.FillArray(out priceArray);

      Console.WriteLine( priceArray[1].ToString());  // 80

      //   (2) Call method to update array

      TestParms.UpdateArray(ref priceArray);

      Console.WriteLine( priceArray[1].ToString());  // 160

      //   (3) Call method to calculate amount of tax.

      double ourPrice = 150.00;

      double newtax = TestParms.TaxVal(ourPrice, out taxAmt);

      Console.WriteLine( taxAmt.ToString());  // 15

      Console.WriteLine( ourPrice);           // 150.00

   }

}

In this example, the class MyApp is used to invoke three methods in the TestParms class:

  1. FillArray is invoked to initialize the array. This requires passing the array as a parameter with the out modifier.

  2. The returned array is now passed to a second method that modifies two elements in the array. The ref modifier indicates the array can be modified.

  3. ourPrice is passed to a method that calculates the amount of tax and assigns it to the parameter taxAmt. Although ourPrice is set to 0 within the method, its value remains unchanged in the calling method because it is passed by value.

C# includes one other parameter modifier, params, which is used to pass a variable number of arguments to a method. Basically, the compiler maps the variable number of arguments in the method invocation into a single parameter in the target method. To illustrate, let's consider a method that calculates the average for a list of numbers passed to it:

// Calculate average of variable number of arguments

public static double GetAvg(params double[] list)

{

   double tot = 0.0;

   for (int i = 0 ; i < list.Length; i++)

      tot += list[i];

   return tot / list.Length;

}

Except for the params modifier, the code for this method is no different than that used to receive an array. Rather than sending an array, however, the invoking code passes an actual list of arguments:

double avg;

avg = TestParms.GetAvg(12,15, 22, 5, 7 ,19);

avg = TestParms.GetAvg(100.50, 200, 300, 55,88,99,45);

When the compiler sees these method calls, it emits code that creates an array, populates it with the arguments, and passes it to the method. The params modifier is essentially a syntactical shortcut to avoid the explicit process of setting up and passing an array.

Core Note

The params keyword can only be used with the last parameter to a method.


3.6. Constructors

The Common Language Runtime (CLR) requires that every class have a constructor—a special-purpose method that initializes a class or class instance the first time it is referenced. There are three basic types of constructors: instance, private (a special case of instance), and static.

Instance Constructor

Syntax

[attributes] [modifiers]  <identifier> ( [parameter-list])

[:initializer] { constructor-body}

The syntax is the same as that of the method except that it does not have a return data type and adds an initializer option. There are a number of implementation and behavioral differences:

  • The identifier (constructor name) must be the same as the class name.

  • The initializer provides a way to invoke code prior to entering the constructor-body. It takes two forms:

    base(argument list)— Calls a base class

    this(argument list)— Calls another constructor in the same class

  • If no explicit constructor is provided for a class, the compiler creates a default parameterless constructor.

The instance constructor is called as part of creating a class instance. Because a class may contain multiple constructors, the compiler calls the constructor whose signature matches that of the call:

Fiber fib1 = new Cotton();

Fiber fib1 = new Cotton("Egyptian");

.NET constructs an object by allocating memory, zeroing out the memory, and calling the instance constructor. The constructor sets the state of the object. Any fields in the class that are not explicitly initialized are set to zero or null, depending on the associated member type.

Inheritance and Constructors

Constructors, unlike methods, are not inherited. It is up to each class to define its own constructor(s) or use the default parameterless constructor. Each constructor must call a constructor in its base class before the first line of the calling constructor is executed. Because C# generates a call to the base class's default constructor automatically, the programmer typically does not bother with this. But there are exceptions. Consider this code in which the base class Apparel defines a constructor:

public class Apparel

{

   private string color;

   private decimal price;

   // constructor

   public Apparel(string c, decimal p, string b)

   {

      color = c;

      price = p;

   }

}

// class inheriting from Apparel

class Coat: Apparel

{

   private string length;

   public Coat(string c, decimal p, string b, string l)

   {

      length = l;

      // ... other code

   }

}

If you try to compile this, you'll get an error stating that "no overload for Apparel takes 0 arguments". Two factors conspire to cause this: first, because Apparel has an explicit constructor, the compiler does not add a default parameterless constructor to its class definition; and second, as part of compiling the constructor in the derived class, the compiler includes a call to the base class's default constructor—which in this case does not exist. The solution is either to add a parameterless constructor to Apparel or to include a call in the derived class to the explicit constructor. Let's look at how a constructor can explicitly call a constructor in a base class.

Using Initializers

The C# compiler provides the base initializer as a way for a constructor in an inherited class to invoke a constructor in its base class. This can be useful when several classes share common properties that are set by the base class constructor. Listing 3-9 demonstrates how a derived class, Shirt, uses a base initializer to call a constructor in Apparel.

Listing 3-9. A Constructor with a Base Initializer

using System;

public  class Apparel

{

   private string color;

   private decimal price;

   private string brand;

   // Constructor

   public Apparel(string c,decimal p, string b)

   {

      color = c;

      price = p;

      brand = b;

   }

   public string ItemColor

   {

      get {return color;}

   }

   //  other properties and members go here

}

public class Shirt: Apparel

{

   private decimal mySleeve;

   private decimal myCollar;

   public Shirt (string c, decimal p, string b,

      decimal sleeve, decimal collar)  : base(c,p,b)

   {

      mySleeve = sleeve;

      myCollar = collar;

   }

}

public class TestClass

{

   static void Main()

   {

      Shirt shirtClass = new Shirt("white", 15.00m, "Arrow",

                                   32.0m, 15.5m);

      Console.WriteLine(shirtClass.ItemColor);  // "white"

   }

}

The compiler matches the signature of this initializer with the instance constructor in the base class that has a matching signature. Thus, when an instance of Shirt is created, it automatically calls the constructor in Apparel that has one parameter. This call is made before any code in Shirt is executed.

A second version of the initializer, one that uses the keyword this rather than base, also indicates which constructor is to be called when the class is instantiated. However, this refers to a constructor within the class instance, rather than the base class. This form of the initializer is useful for reducing the amount of compiler code generated in a class having multiple constructors and several fields to be initialized.

For example, if you examine the generated IL code for the following class, you would find that the fields fiberType and color are defined separately for each constructor.

public class Natural

{

   string fiberType = "Generic";

   string color = "white";

   // Constructors

   public Natural() { ... }

   public Natural(string cotton_type) { ... }

   public Natural(string cotton_type, string color) { ... }

}

For more efficient code, perform the field initialization in a single constructor and have the other constructors invoke it using the this initializer.

public Natural() {   // constructor initializes fields

    fiberType="Generic";

    color = "white";

}

// Following constructors use this() to call default

// constructor before constructor body is executed.

public Natural(string cotton_type): this() { ... }

public Natural(string cotton_type, string color):

   this() { ... }

Private Constructor

Recall that the private modifier makes a class member inaccessible outside its class. When applied to a class constructor, it prevents outside classes from creating instances of that class. Although somewhat non-intuitive (what good is a class that cannot be instantiated?), this turns out to be a surprisingly powerful feature.

Its most obvious use is with classes that provide functionality solely through static methods and fields. A classic example is the System.Math class found in the Framework Class Library.

It has two static fields, pi and the e (natural logarithmic base), as well as several methods that return trigonometric values. The methods behave as built-in functions, and there is no reason for a program to create an instance of the math class in order to use them.

In the earlier discussion of static methods, we presented a class (refer to Listing 3-5) that performs metric conversions. Listing 3-10 shows this class with the private constructor added.

Listing 3-10. Private Constructor Used with Class Containing Static Methods

using System;

class Conversions

{

   // class contains functions to provide metric conversions

   // static method can only work with static field.

   static cmPerInch = 2.54;

   private static double gmPerPound = 455;

   public static double inchesToMetric(double inches) {

      return(inches * cmPerInch);

   }

   public static double poundsToGrams(double pounds) {

      return(pounds*gmPerPound);

   }

   // Private constructor prevents creating class instance

   private Conversions()

   { ...   }

}

Although a simple example, this illustrates a class that does not require instantiation: The methods are static, and there is no state information that would be associated with an instance of the class.

A natural question that arises is whether it is better to use the private constructor or an abstract class to prevent instantiation. The answer lies in understanding the differences between the two. First, consider inheritance. Although an abstract class cannot be instantiated, its true purpose is to serve as a base for derived classes (that can be instantiated) to create their own implementation. A class employing a private constructor is not meant to be inherited, nor can it be. Secondly, recall that a private constructor only prevents outside classes from instantiating; it does not prevent an instance of the class from being created within the class itself.

The traits of the private constructor can also be applied to managing object creation. Although the private constructor prevents an outside method from instantiating its class, it does allow a public method in the class (sometimes called a factory method) to create an object. This means that a class can create instances of itself, control how the outside world accesses them, and control the number of instances created. This topic is discussed in Chapter 4, "Working with Objects in C#."

Static Constructor

Also known as a class or type constructor, the static constructor is executed after the type is loaded and before any one of the type members is accessed. Its primary purpose is to initialize static class members. This limited role results from the many restrictions that distinguish it from the instance constructor:

  • It cannot have parameters and cannot be overloaded. Consequently, a class may have only one static constructor.

  • It must have private access, which C# assigns automatically.

  • It cannot call other constructors.

  • It can only access static members.

Although it does not have a parameter, do not confuse it with a default base constructor, which must be an instance constructor. The following code illustrates the interplay between the static and instance constructors.

class BaseClass

{

   private static int callCounter;

   //  Static constructor

   static BaseClass(){

      Console.WriteLine("Static Constructor: "+callCounter);

   }

   //  Instance constructors

   public BaseClass()

   {

      callCounter+= 1;

      Console.WriteLine("Instance Constructor: "+callCounter);

   }

   // ... Other class operations

}

This class contains a static initializer, a static constructor, and an instance constructor. Let's look at the sequence of events that occur when the class is instantiated:

BaseClass myClass1 = new BaseClass();

BaseClass myClass2 = new BaseClass();

BaseClass myClass2 = new BaseClass();

Output:

Static Constructor: 0

Instance Constructor: 1

Instance Constructor: 2

Instance Constructor: 3

The compiler first emits code to initialize the static field to 0; it then executes the static constructor code that displays the initial value of callCounter. Next, the base constructor is executed. It increments the counter and displays its current value, which is now 1. Each time a new instance of BaseClass is created, the counter is incremented. Note that the static constructor is executed only once, no matter how many instances of the class are created.


3.7. Delegates and Events

Clicking a submit button, moving the mouse across a Form, pushing an Enter key, a character being received on an I/O port—each of these is an event that usually triggers a call to one or more special event handling routines within a program.

In the .NET world, events are bona fide class members—equal in status to properties and methods. Just about every class in the Framework Class Library has event members. A prime example is the Control class, which serves as a base class for all GUI components. Its events—including Click, DoubleClick, KeyUp, and GotFocus—are designed to recognize the most common actions that occur when a user interacts with a program. But an event is only one side of the coin. On the other side is a method that responds to, or handles, the event. Thus, if you look at the Control class methods, you'll find OnClick, OnDoubleClick, OnKeyUp, and the other methods that correspond to their events.

Figure 3-3 illustrates the fundamental relationship between events and event handlers that is described in this section. You'll often see this relationship referred to in terms of publisher/subscriber, where the object setting off the event is the publisher and the method handling it is the subscriber.

Figure 3-3. Event handling relationships

Delegates

Connecting an event to the handling method(s) is a delegate object. This object maintains a list of methods that it calls when an event occurs. Its role is similar to that of the callback functions that Windows API programmers are used to, but it represents a considerable improvement in safeguarding code.

In Microsoft Windows programming, a callback occurs when a function calls another function using a function pointer it receives. The calling function has no way of knowing whether the address actually refers to a valid function. As a result, program errors and crashes often occur due to bad memory references. The .NET delegate eliminates this problem. The C# compiler performs type checking to ensure that a delegate only calls methods that have a signature and return type matching that specified in the delegate declaration. As an example, consider this delegate declaration:

public delegate void MyString (string msg);

When the delegate is declared, the C# compiler creates a sealed class having the name of the delegate identifier (MyString). This class defines a constructor that accepts the name of a method—static or instance—as one of its parameters. It also contains methods that enable the delegate to maintain a list of target methods. This means that—unlike the callback approach—a single delegate can call multiple event handling methods.

A method must be registered with a delegate for it to be called by that delegate. Only methods that return no value and accept a single string parameter can be registered with this delegate; otherwise, a compilation error occurs. Listing 3-11 shows how to declare the MyString delegate and register multiple methods with it. When the delegate is called, it loops through its internal invocation list and calls all the registered methods in the order they were registered. The process of calling multiple methods is referred to as multicasting.

Listing 3-11. Multicasting Delegate

// file: delegate.cs

using System;

using System.Threading;

class DelegateSample

{

   public delegate void MyString(string s);

   public static void PrintLower(string s){

      Console.WriteLine(s.ToLower());

   }

   public static void PrintUpper(string s){

      Console.WriteLine(s.ToUpper());

   }

   public static void Main()

   {

      MyString myDel;

      // register method to be called by delegate

      myDel =  new MyString(PrintLower);

      // register second method

      myDel += new MyString(PrintUpper);

      // call delegate

      myDel("My Name is Violetta.");

      // Output: my name is violetta.

      //         MY NAME IS VIOLETTA.

   }

}

Note that the += operator is used to add a method to the invocation list. Conversely, a method can be removed using the -= operator:

myDel += new MyString(PrintUpper); // register for callback

myDel -= new MyString(PrintUpper); // remove method from list

In the preceding example, the delegate calls each method synchronously, which means that each succeeding method is called only after the preceding method has completed operation. There are two potential problems with this: a method could "hang up" and never return control, or a method could simply take a long time to process—blocking the entire application. To remedy this, .NET allows delegates to make asynchronous calls to methods. When this occurs, the called method runs on a separate thread than the calling method. The calling method can then determine when the invoked method has completed its task by polling it, or having it call back a method when it is completed. Asynchronous calls are discussed in Chapter 13, "Asynchronous Programming and Multithreading."

Delegate-Based Event Handling

In abstract terms, the .NET event model is based on the Observer Design Pattern. This pattern is defined as "a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically."[1] We can modify this definition to describe the .NET event handling model depicted in Figure 3-3: "when an event occurs, all the delegate's registered methods are notified and executed automatically." An understanding of how events and delegates work together is the key to handling events properly in .NET.

[1] Design Patterns by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides; Addison-Wesley, 1995.

To illustrate, let's look at two examples. We'll begin with built-in events that have a predefined delegate. Then, we'll examine how to create events and delegates for a custom class.

Working with Built-In Events

The example in Listing 3-12 displays a form and permits a user to draw a line on the form by pushing a mouse key down, dragging the mouse, and then raising the mouse key. To get the endpoints of the line, it is necessary to recognize the MouseDown and MouseUp events. When a MouseUp occurs, the line is drawn.

The delegate, MouseEventHandler, and the event, MouseDown, are predefined in the Framework Class Library. The developer's task is reduced to implementing the event handler code and registering it with the delegate. The += operator is used to register methods associated with an event.

this.MouseDown += new MouseEventHandler(OnMouseDown);

The underlying construct of this statement is

this.event += new delegate(event handler method);

To implement an event handler you must provide the signature defined by the delegate. You can find this in documentation that describes the declaration of the MouseEventHandler delegate:

public delegate void MouseEventHandler(

   object sender,

   MouseEventArgs e)

Listing 3-12. Event Handler Example

using System;

using System.Windows.Forms;

using System.Drawing;

class DrawingForm:Form

{

   private int lastX;

   private int lastY;

   private Pen myPen= Pens.Black; // defines color of drawn line

   public DrawingForm() {

      this.Text = "Drawing Pad";

      // Create delegates to call MouseUp and MouseDown

      this.MouseDown += new MouseEventHandler(OnMouseDown);

      this.MouseUp   += new MouseEventHandler(OnMouseUp);

   }

   private void OnMouseDown(object sender, MouseEventArgs e)

   {

      lastX = e.X;

      lastY = e.Y;

   }

   private void OnMouseUp(object sender, MouseEventArgs e)

   {

      // The next two statements draw a line on the form

      Graphics g = this.CreateGraphics();

      if (lastX >0 ){

         g.DrawLine(myPen, lastX,lastY,e.X,e.Y);

      }

      lastX = e.X;

      lastY = e.Y;

   }

   static void Main() {

      Application.Run(new DrawingForm());

   }

}

Using Anonymous Methods with Delegates

.NET 2.0 introduced a language construct known as anonymous methods that eliminates the need for a separate event handler method; instead, the event handling code is encapsulated within the delegate. For example, we can replace the following statement from Listing 3-12:

this.MouseDown += new MouseEventHandler(OnMouseDown);

with this code that creates a delegate and includes the code to be executed when the delegate is invoked:

this.MouseDown += delegate(object sender, EventArgs e)

{

   lastX = e.X;

   lastY = e.Y;

}

The code block, which replaces OnMouseDown, requires no method name and is thus referred to as an anonymous method. Let's look at its formal syntax:

delegate [(parameter-list)] {anonymous-method-block}

  • The delegate keyword is placed in front of the code that is executed when the delegate is invoked.

  • An optional parameter list may be used to pass data to the code block. These parameters should match those declared by the delegate. In this example, the parameters correspond to those required by the predefined delegate MouseEventHandler.

  • When the C# compiler encounters the anonymous code block, it creates a new class and constructs a method inside it to contain the code block. This method is called when the delegate is invoked.

To further clarify the use of anonymous methods, let's use them to simplify the example shown earlier in Listing 3-11. In the original version, a custom delegate is declared, and two callback methods are implemented and registered with the delegate. In the new version, the two callback methods are replaced with anonymous code blocks:

// delegate declaration

public delegate void MyString(string s);

// Register two anonymous methods with the delegate

MyString myDel;

myDel =  delegate(string s) { Console.WriteLine(s.ToLower()); };

myDel += delegate(string s) { Console.WriteLine(s.ToUpper()); };

// invoke delegate

myDel("My name is Violetta");

When the delegate is called, it executes the code provided in the two anonymous methods, which results in the input string being printed in all lower- and uppercase letters, respectively.

Defining Custom Events

When writing your own classes, it is often necessary to define custom events that signal when some change of state has occurred. For example, you may have a component running that monitors an I/O port and notifies another program about the status of data being received. You could use raw delegates to manage the event notification; but allowing direct access to a delegate means that any method can fire the event by simply invoking the delegate. A better approach—and that used by classes in the Framework Class Library—is to use the event keyword to specify a delegate that will be called when the event occurs.

The syntax for declaring an event is

public event  <delegate name>  <event name>

Let's look at a simple example that illustrates the interaction of an event and delegate:

public class IOMonitor

{

   // Declare delegate

   public delegate void IODelegate(String s);

   // Define event variable

   public event IODelegate DataReceived ;

   // Fire the event

   public void FireReceivedEvent (string msg)

   {

      if (DataReceived != null)  // Always check for null

      {

         DataReceived(msg);      // Invoke callbacks

      }

   }

}

This code declares the event DataReceived and uses it in the FireReceivedEvent method to fire the event. For demonstration purposes, FireReceivedEvent is assigned a public access modifier; in most cases, it would be private to ensure that the event could only be fired within the IOMonitor class. Note that it is good practice to always check the event delegate for null before publishing the event. Otherwise, an exception is thrown if the delegate's invocation list is empty (no client has subscribed to the event).

Only a few lines of code are required to register a method with the delegate and then invoke the event:

IOMonitor monitor = new IOMonitor();

// You must provide a method that handles the callback

monitor.DataReceived += new IODelegate(callback method);

monitor.FireReceivedEvent("Buffer Full");  // Fire event

Defining a Delegate to Work with Events

In the preceding example, the event delegate defines a method signature that takes a single string parameter. This helps simplify the example, but in practice, the signature should conform to that used by all built-in .NET delegates. The EventHandler delegate provides an example of the signature that should be used:

public delegate void EventHandler(object sender,

                                  EventArgs eventArgs);

The delegate signature should define a void return type, and have an object and EventArgs type parameter. The sender parameter identifies the publisher of the event; this enables a client to use a single method to handle and identify an event that may originate from multiple sources.

The second parameter contains the data associated with the event. .NET provides the EventArgs class as a generic container to hold a list of arguments. This offers several advantages, the most important being that it decouples the event handler method from the event publisher. For example, new arguments can be added later to the EventArgs container without affecting existing subscribers.

Creating an EventArgs type to be used as a parameter requires defining a new class that inherits from EventArgs. Here is an example that contains a single string property. The value of this property is set prior to firing the event in which it is included as a parameter.

public class IOEventArgs: EventArgs

{

   public IOEventArgs(string msg){

      this.eventMsg = msg;

   }

   public string Msg{

      get {return eventMsg;}

   }

   private string eventMsg;

}

IOEventArgs illustrates the guidelines to follow when defining an EventArgs class:

  • It must inherit from the EventArgs class.

  • Its name should end with EventArgs.

  • Define the arguments as readonly fields or properties.

  • Use a constructor to initialize the values.

If an event does not generate data, there is no need to create a class to serve as the EventArgs parameter. Instead, simply pass EventArgs.Empty.

Core Note

If your delegate uses the EventHandler signature, you can use EventHandler as your delegate instead of creating your own. Because it is part of the .NET Framework Class Library, there is no need to declare it.

An Event Handling Example

Let's bring these aforementioned ideas into play with an event-based stock trading example. For brevity, the code in Listing 3-13 includes only an event to indicate when shares of a stock are sold. A stock purchase event can be added using similar logic.

Listing 3-13. Implementing a Custom Event-Based Application

//File: stocktrader.cs

using System;

// (1) Declare delegate

public delegate void TraderDelegate(object sender,

                                    EventArgs e);

// (2) A class to define the arguments passed to the delegate

public class TraderEventArgs: EventArgs

{

   public TraderEventArgs(int shs, decimal prc, string msg,

                          string sym){

      this.tradeMsg   = msg;

      this.tradeprice = prc;

      this.tradeshs   = shs;

      this.tradesym   = sym;

   }

   public string Desc{

      get {return tradeMsg;}

   }

   public decimal SalesPrice{

      get {return tradeprice;}

   }

   public int Shares{

      get {return tradeshs;}

   }

   public string Symbol{

       get {return tradesym;}

   }

   private string  tradeMsg;

   private decimal tradeprice;

   private int tradeshs;

   private string tradesym;

}

// (3) class defining event handling methods

public class EventHandlerClass

{

   public void HandleStockSale(object sender,EventArgs e)

   {

      // do housekeeping for stock purchase

      TraderEventArgs ev = (TraderEventArgs) e;

      decimal totSale = (decimal)(ev.Shares * ev.SalesPrice);

      Console.WriteLine(ev.Desc);

   }

   public void LogTransaction(object sender,EventArgs e)

   {

      TraderEventArgs ev = (TraderEventArgs) e;

      Console.WriteLine(ev.Symbol+" "+ev.Shares.ToString()

                        +" "+ev.SalesPrice.ToString("###.##"));

   }

}

// (4) Class to sell stock and publish stock sold event

public class Seller

{

   // Define event indicating a stock sale

   public event TraderDelegate StockSold;

   public void StartUp(string sym, int shs, decimal curr)

   {

      decimal salePrice= GetSalePrice(curr);

      TraderEventArgs t = new TraderEventArgs(shs,salePrice,

            sym+" Sold at "+salePrice.ToString("###.##"), sym);

      FireSellEvent(t);  // Fire event

   }

   // method to return price at which stock is sold

   // this simulates a random price movement from current price

   private decimal GetSalePrice(decimal curr)

   {

      Random rNum = new Random();

      // returns random number between 0 and 1

      decimal rndSale = (decimal)rNum.NextDouble() * 4;

      decimal salePrice= curr - 2 + rndSale;

      return salePrice;

   }

   private void FireSellEvent(EventArgs e)

   {

      if (StockSold != null)  // Publish defensively

      {

         StockSold(this, e);  // Invoke callbacks by delegate

      }

   }

}

class MyApp

{

   public static void Main()

   {

      EventHandlerClass eClass= new EventHandlerClass();

      Seller sell = new Seller();

      // Register two event handlers for stocksold event

      sell.StockSold += new TraderDelegate(

                    eClass.HandleStockSale);

      sell.StockSold += new TraderDelegate(

                    eClass.LogTransaction);

      // Invoke method to sell stock(symbol, curr price,

                                     sell price)

      sell.StartUp("HPQ",100, 26);

   }

}

The class Seller is at the heart of the application. It performs the stock transaction and signals it by publishing a StockSold event. The client requesting the transaction registers two event handlers, HandleStockSale and LogTransaction, to be notified when the event occurs. Note also how the TRaderEvents class exposes the transaction details to the event handlers.


3.8. Operator Overloading

Built-in operators such as + and - are used so instinctively that one rarely thinks of them as a predefined implementation for manipulating intrinsic types. In C#, for example, the + operator is used for addition or concatenation depending on the data types involved. Clearly, each must be supported by different underlying code. Because the compiler knows what a numeric and string type represent, it is quite capable of doing this. It makes sense then that these operators cannot be applied to custom classes or structures of which the compiler has no knowledge.

It turns out that C# provides a mechanism referred to as operator overloading that enables a class to implement code that determines how the class responds to the operator. The code for overloading an operator is syntactically similar to that of a method:

public static <return type> operator <op> ( parameter list)

{ implementation code}

Several rules govern its usage:

  • The public and static modifiers are required.

  • The return type is the class type when working with classes. It can never be void.

  • op is a binary, unary, or relational operator. Both equals (==) and not equals (!=) must be implemented in a relational pair.

  • Binary operators require two arguments; unary operators require one argument.

Operator overloading with classes does not have to be limited to geometric or spatial objects. The example shown in Listing 3-14 demonstrates how to use the concept to maintain stocks in a portfolio. It contains two classes: one that represents a stock (defined by its price, number of shares, and risk factor) and one that represents the portfolio of stocks. Two overloaded operators (+ and -) add and remove stocks from the portfolio.

Listing 3-14. Operator Overloading for Classes

using System;

class Portfolio

{

   public decimal risk;

   public decimal totValue;

   // Overloaded operator to add stock to Portfolio

   public static Portfolio operator + (Portfolio p,Stock s)

   {

      decimal currVal  = p.totValue;

      decimal currRisk = p.risk;

      p.totValue = p.totValue + s.StockVal;

      p.risk = (currVal/p.totValue)*p.risk +

         (s.StockVal/p.totValue)* s.BetaVal;

      return p;

   }

   // Overloaded operator to remove stock from Portfolio

   public static Portfolio operator - (Portfolio p,Stock s)

   {

      p.totValue = p.totValue - s.StockVal;

      p.risk = p.risk - ((s.BetaVal-p.risk)

                       *(s.StockVal/p.totValue));

      return p;

   }

}

class Stock

{

   private decimal value;

   private decimal beta; // risk increases with value

   public  Stock(decimal myBeta, decimal myValue,

                 int shares)

   {

      value = (decimal) myValue * shares;

      beta = myBeta;

   }

   public decimal StockVal

      { get {return value; } }

   public decimal BetaVal

      { get {return beta; } }

}

class MyApp

{

   public static void Main()

   {

      Portfolio p = new Portfolio();

      // 200 shs of HPQ at $25, 100 shs of IBM @ $95

      Stock hpq  = new Stock(1.1M, 25M, 200);

      Stock ibm  = new Stock(1.05M, 95.0M, 100);

      p += hpq;   // Add hpq

      p += ibm;   // Add ibm

      Console.Write("value:{0} ",p.totValue.ToString());

      Console.WriteLine(" risk: {0}",

                        p.risk.ToString("#.00"));

      // value = 14,500 and risk = 1.07

      p -= ibm;   // Remove ibm from portfolio

      Console.Write("value:{0} ",p.totValue.ToString());

      Console.Write(" risk: {0}",p.risk.ToString("#.00"));

      // value = 5000 and risk = 1.10

   }

}

The addition or deletion of a stock causes the portfolio total value and weighted risk factor to be adjusted. For example, when both stocks are added to the portfolio, the risk is 1.07. Remove ibm and the risk is 1.10, the risk of hpq alone.

When choosing to implement operator overloading, be aware that .NET languages are not required to support it. The easiest way to provide interoperability for those languages (such as Visual Basic.NET) lacking this feature is to include an additional class member that performs the same function:

public static Portfolio AddStocks ( Portfolio p, Stock s)

{ return p + s; }

In this case, the code exposes a public method whose implementation calls the overloaded method.

Another approach is to take advantage of the fact that language interaction occurs at the Intermediate Language level and that each operator is represented in the IL by a hidden method. Thus, if a language knows how to invoke this method, it can access the operator.

The ECMA standard provides a list of method names that correspond to each operator. For example, the + and & used in the preceding code are represented by op_Addition and op_BitwiseAnd, respectively. A language would access the overloaded + operator with its own syntactic variation of the following code:

newPortfolio = P.op_Addition(P, s)

Either approach works, but relying on assembly language is probably less appealing than providing a custom public method.

Although the discussion has been on classes, operator overloading also can be applied to simple data types. For example, you could define your own exponentiation operator for integers.


3.9. Interfaces

Syntax:

[attributes] [modifiers] interface  identifier [:baselist]

{interface body} [;]

The syntax of the interface declaration is identical to that of a class except that the keyword interface replaces class. This should not be surprising, because an interface is basically a class that declares, but does not implement, its members. An instance of it cannot be created, and classes that inherit from it must implement all of its methods.

This sounds similar to an abstract class, which also cannot be instantiated and requires derived classes to implement its abstract methods. The difference is that an abstract class has many more capabilities: It may be inherited by subclasses, and it may contain state data and concrete methods.

One rule of thumb when deciding whether to use a class or interface type is the relationship between the type and its inheriting classes. An interface defines a behavior for a class—something it "can do." The built-in .NET ICloneable interface, which permits an object to create a copy of itself, is an example of this. Classes, on the other hand, should be used when the inheriting class is a "type of " the base class. For example, you could create a shape as a base class, a circle as a subclass of shape, and the capability to change the size of the shape as an interface method.

Aside from their differing roles, there are numerous implementation and usage differences between a class and an interface:

  • An interface cannot inherit from a class.

  • An interface can inherit from multiple interfaces.

  • A class can inherit from multiple interfaces, but only one class.

  • Interface members must be methods, properties, events, or indexers.

  • All interface members must have public access (the default).

  • By convention, an interface name should begin with an uppercase I.

Creating and Using a Custom Interface

Listing 3-15 demonstrates how to define, implement, and program against a simple custom interface:

Listing 3-15. Interface Basics

public interface IShapeFunction

{

   double GetArea();    //  public abstract method

}

class Circle : IShapeFunction   // Inherit Interface

{

   private double radius;

   public circle( double rad)

   {

      radius = rad;

   }

   public double GetArea()

   {

      return ( Math.PI*radius * radius);

   }

   public string ShowMe()

   {

      return ("Circle");

   }

}

class Rectangle: IShapeFunction      // Inherit interface

{

   private double width, height;

   public rectangle( double myWidth, double myHeight)

   {

      width= myWidth;

      height= myHeight;

   }

   public double GetArea()

   {

      return (width * height);

   }

}

class MyApp

{

   public static void Main()

   {

      Circle myCircle = new Circle(4);

      // Interface variable that references a circle object.

      IShapeFunction myICircle = myCircle;

      Rectangle myRectangle = new Rectangle(4,8);

      // Place shape instances in an array

      IShapeFunction[] myShapes = {myCircle, myRectangle};

      // The next two statements print the same results

      MessageBox.Show(myCircle.GetArea().ToString());

      MessageBox.Show(myShapes[0].GetArea().ToString());

      MessageBox.Show(myCircle.ShowMe());   // class method

      MessageBox.Show(myICircle.GetArea().ToString());

      // The following will not compile because myICircle can

      // access only members of the IShapeFunction interface.

      MessageBox.Show(myICircle.ShowMe());

   }

}

The declaration of the interface and implementation of the classes is straightforward: A Circle and Rectangle class inherit the IShapeFunction interface and implement its GetArea method.

Conceptually, it is useful to think of a class that implements one or more interfaces as having multiple types. It has its own innate type, but it also can be viewed as having the type of each interface it implements. Consider this code in the MyApp class:

Circle myCircle = new Circle(4);

IShapeFunction myICircle = myCircle;

The first statement creates an instance of the Circle class. The second statement creates a variable that refers to this circle object. However, because it is specified as an IShapeFunction type, it can only access members of that interface. This is why an attempt to reference the ShowMe method fails. By using the interface type, you effectively create a filter that restricts access to members of the interface only.

One of the most valuable aspects of working with interfaces is that a programmer can treat disparate classes in a similar manner, as long at they implement the same interface.

IShapeFunction[] myShapes = {myCircle, myRectangle};

MessageBox.Show(myShapes[0].GetArea().ToString());

This code creates an array that can contain any class that implements the IShapeFunction interface. Its only interest is in using the GetArea method, and it neither has nor requires any knowledge of other class members. We can easily extend this example to create a class to work with this array that has no knowledge of the Circle or Rectangle class.

public class ShapeUtil

{

   public static double SumAreas

         (IShapeFunction[] funArray)

   {

      double tot = 0.0;

      for (int i = 0; i < funArray.Length; i++)

         { tot += funArray[i].GetArea();  }

      return tot;

   }

}

// Access this method with ShapeUtil.SumAreas(myShapes);

The code in this class can be used with any concrete class that implements the IShapeFunction interface.

Core Approach

For maximum program flexibility, consider using interface types, rather than class types, when defining method parameters. This ensures there is no limit on the types of classes that can be passed to the method, as long as they implement the required interface.

Working with Interfaces

Determining Which Interface Members Are Available

You may not always know at compile time whether a class implements a specific interface. This is often the case when working with a collection that contains a number of types. To perform this check at runtime, use the as or is keyword in your code.

// (1)  as keyword to determine if interface is implemented

Circle myCircle = new Circle(5.0);

IShapeFunction myICircle;

myICircle = myCircle as IShapeFunction;

If (myICircle !=null) //interface is implemented

// (2)  is keyword to determine if interface is implemented

Circle myCircle = new Circle(5.0);

If (myCircle is IShapeFunction) // True if interface implemented

Accessing Interface Methods

Because a class may inherit methods from a base class and/or multiple interfaces, there is the possibility that inherited methods will have the same name. To avoid this ambiguity, specify an interface method declaration in the derived class with the interface and method name:

double IShapeFunction.GetArea() {  // <interface>.<method>

This not only permits a class to implement multiple methods, but has the added effect of limiting access to this method to interface references only. For example, the following would result in an error:

Circle myCircle = new Circle(5.0);

// cannot reference explicit method

double myArea = myCircle.GetArea();

Interfaces Versus Abstract Classes

In the overall design of a system, the real issue is not whether to use a class or interface, but how to best mix the two. The C# restriction on multiple implementation inheritance assures an expanded role for interfaces. It is just too burdensome to try to load all the needed functionality into one base class or a hierarchy of base classes.

A better solution is to define a base class and then expand its capabilities by adding corresponding interfaces. This permits developers to use the class directly without regard to whether the methods are interface implementations. Yet, it also permits the developer to make use of the interface and ignore the other class members.

Of course, you must consider the drawbacks to using interfaces. A well-designed base class reduces the burden on the inherited class to implement everything by promoting code reuse. Also, a non-abstract class can add a member without breaking any existing derived class; adding a member to an interface would break any class implementing the interface.


3.10. Generics

To understand and appreciate the concept of generics, consider the need to create a class that will manage a collection of objects. The objects may be of any type and are specified at compile time by a parameter passed to the class. Moreover, the collection class must be type-safe—meaning that it will accept only objects of the specified type.

In the 1.x versions of .NET, there is no way to create such a class. Your best option is to create a class that contains an array (or other container type) that treats everything as an object. As shown here, casting, or an as operator, is then required to access the actual object type. It is also necessary to include code that verifies the stored object is the correct type.

Object[] myStack = new object[50];

myStack[0] = new Circle(5.0);    // place Circle object in array

myStack[1] = "Circle";           // place string in array

Circle c1 = myStack[0] as Circle;

if( c1!=null) {                  // circle object obtained

Circle c2 = (Circle) myStack[1]; // invalid case exception

Generics, introduced with .NET 2.0, offer an elegant solution that eliminates the casting, explicit type checking, and boxing that occurs for value type objects. The primary challenge of working with generics is getting used to the syntax, which can be used with a class, interface, or structure.

The best way to approach the syntax is to think of it as a way to pass the data type you'll be working with as a parameter to the generic class. Here is the declaration for a generic class:

public class myCollection<T>

{

   T[] myStack = new T[50];

}

The type parameter T is placed in brackets and serves as a placeholder for the actual type. The compiler recognizes any reference to T within the body of the class and replaces it with the actual type requested. As this statement shows, creating an instance of a generic class is straightforward:

myCollection <string> = new myCollection<string>;

In this case, string is a type argument and specifies that the class is to work with string types only. Note that more than one type parameter may be used, and that the type parameter can be any name, although Microsoft uses (and recommends) single characters in its generic classes.

Although a class may be generic, it can restrict the types that it will accept. This is done by including an optional list of constraints for each type parameter. To declare a constraint, add the where keyword followed by a list of parameter/requirement pairs. The following declaration requires that the type parameter implement the ISerializable and IComparable interfaces:

public class myCollection<T> where

   T:ISerializable,

   T:IComparable

A parameter may have multiple interface constraints and a single class restraint. In addition, there are three special constraints to be aware of:

class— Parameter must be reference type.

struct— Parameter must be value type.

new()— Type parameter must have a parameterless constructor.

An example of a generic class is provided in Listing 3-16. The class implements an array that manages objects of the type specified in the type parameter. It includes methods to add items to the array, compare items, and return an item count. It includes one constraint that restricts the type parameter to a reference type.

Listing 3-16. A Generic Class to Hold Data

using System.Collections.Generic

public class GenStack<T>

   where T:class    // constraint restricts access to ref types

{

   private T[] stackCollection;

   private int count = 0;

   // Constructor

   public GenStack(int size)

   {

      stackCollection = new T[size];

   }

   public void Add(T item)

   {

      stackCollection[count] = item;

      count += 1;

   }

   // Indexer to expose elements in internal array

   public T this[int ndx]

   {

      get

      {

         if (!(ndx < 0 || ndx > count - 1))

            return stackCollection[ndx];

         // Return empty object

         else return (default(T));

      }

   }

   public int ItemCount

   {

      get {return count;}

   }

   public int Compare<C>(T value1, T value2)

   {

      // Case-sensitive comparison: -1, 0(match), 1

      return Comparer<T>.Default.Compare(value1, value2);

   }

}

The following code demonstrates how a client could access the generic class described in Listing 3-16:

// Create instance to hold 10 items of type string

GenStack<string> myStack = new GenStack<string>(10);

myStack.Add("Leslie");

myStack.Add("Joanna");

Console.WriteLine(myStack.ItemCount);  // 2

Console.WriteLine(myStack[1]);         // Joanna

int rel = myStack.Compare<string>("Joanna", "joanna");  // -1

Console.WriteLine(rel.ToString());

Generics and the .NET generic collection classes are discussed further in Chapter 4.


3.11. Structures

A .NET structure—struct in C# syntax—is often described as a lightweight class. It is similar to a class in that its members include fields, methods, properties, events, and constructors; and it can inherit from interfaces. But there are also implementation differences and restrictions that limit its capabilities vis-à-vis a class:

  • A struct is a value type. It implicitly inherits from the System.ValueType.

  • A struct cannot inherit from classes, nor can it be inherited.

  • An explicitly declared constructor must have at least one parameter.

  • struct members cannot have initializers. The field members must be initialized by the constructor or the client code that creates the constructor.

Because a struct is a value type, it is stored on the stack where a program works directly with its contents. This generally provides quicker access than indirectly accessing data through a pointer to the heap. On the downside, structs can slow things down when passed back and forth as parameters. Rather than passing a reference, the CLR copies a struct and sends the copy to the receiving method. Also, a struct faces the boxing and unboxing issue of value types.

When deciding whether to use a struct to represent your data, consider the fact that types that naturally have value semantics (objects that directly contain their value as opposed to a reference to a value) are often implemented underneath as a struct. Examples include the primitives discussed in Chapter 2, the color structure whose properties (red, aqua, and so on) define colors in .NET, the DateTime structure used for date-related operations, and various graphics structures used to represent objects such as points and rectangles.

Defining Structures

Syntax:

[attribute][modifier] struct identifier [:interfaces]

{struct-body}

Example:

public struct DressShirt

{

   public float CollarSz;

   public int SleeveLn;

   // constructor

   public DressShirt(float collar, int sleeve)

   {

      this.CollarSz = collar;

      this.SleeveLn = sleeve;

   }

}

The syntax clearly resembles a class. In fact, replace struct with class and the code compiles. It has a public modifier that permits access from any assembly. The default modifier is internal, which restricts access to the containing assembly. No interfaces are specified—although a struct can inherit from an interface—so the struct is not required to implement any specific methods.

Core Note

It is possible to specify the layout of a struct's fields in memory using the StructLayout attribute. This is most commonly used when the struct is to be accessed by unmanaged code that expects a specific physical layout of the data. By default, the fields in a struct are stored in sequential memory locations.

An instance of the structure can be created in two ways:

DressShirt dShirt = new DressShirt(16, 33);

DressShirt myShirt = new DressShirt();

myShirt.CollarSz = 16.5F;

myShirt.SleeveLn = 33;

The first statement creates an instance that relies on the user-defined constructor to initialize the field values. When designing such a constructor, be aware that you must initialize all fields within the constructor; otherwise, the compiler will issue an error.

The second statement also creates an instance of the struct by calling the default constructor, which initializes the fields to default values of zero (0). Note the difference here between a struct and a class: A struct always has a default parameterless constructor; a class has the default parameterless constructor only if there are no explicitly defined constructors.

Using Methods and Properties with a Structure

Methods and properties are usually associated with classes, but they play an equally important role in the use of structures. In fact, a client accessing a method has no syntactical clue as to whether the method or property is associated with a class or struct. Listing 3-17 extends the original example to add two properties and a method.

Listing 3-17. Basic Elements of a Struct

public struct DressShirt

{

   private float CollarSz;

   private int SleeveLn;

   public DressShirt(float collar, int sleeve)

   {

      this.CollarSz = collar;

      this.SleeveLn = sleeve;

   }

   // Properties to return sleeve and collar values

   public int Sleeve

   {

      get {return (SleeveLn);}

      set {SleeveLn = value;}

   }

   public float Collar

   {

      get {return (CollarSz); }

      // "value" is an implicit parameter

      set {CollarSz = value;   }

   }

   // Method to convert size to different scale

   public string ShirtSize()

   {

      string mySize = "S";

      if (CollarSz > 14.5) mySize = "M";

      if (CollarSz > 15.5) mySize = "L";

      if (CollarSz > 16.5) mySize = "XL";

      return (mySize);

   }

}

The most important thing to note about this code is that it could be cut and pasted into a class and run with no changes. Although a struct doesn't support all of the features of a class, the ones it does are implemented using identical syntax.


3.12. Structure Versus Class

Many developers instinctively select a class to represent data and the operations performed on it. However, there are cases where a struct is a better choice, as evidenced by its use in the Framework Class Library to represent simple data types. This section compares the two and offers general guidelines to consider when choosing between the two.

Table 3-5. Comparison of Structure and Class

Structure

Class

Default access level of the type

Internal

Internal

Default access level for data members

Public

Private

Default access level for properties and methods

Private

Private

Value or reference type

Value

Reference

Can be a base for new types

No

Yes

Implement interfaces

Yes

Yes

Raise and handle events

Yes

Yes

Scope of members

Structure

Class

Instance initialization

Constructor—with or without parameters. A struct cannot contain a custom parameterless constructor.

Constructor—with or without parameters.

Can be nested

Yes

Yes

Has a destructor

No

Yes (Finalizer)

It is clear from the table that structures possess many of the features and capabilities of classes. Consequently, a developer may have difficulty deciding which is the better choice. The answer lies in understanding the few—but significant—differences between the two.

Structures Are Value Types and Classes Are Reference Types

As mentioned, classes are allocated space from the managed heap when the object or class instance is created. The address of the object (on the heap) is returned to the variable representing the object. In contrast, a variable set to a struct type contains the structure's actual data—not a pointer. The ramifications of this are most pronounced when passing arguments to functions. Reference types simply require that a pointer be passed, whereas structures require that a copy of all fields be made and passed to the function.

The structure does have some advantages with regard to memory allocation and Garbage Collection. Structure instances are allocated in a thread's stack rather than the managed heap, thus avoiding the associated overhead of managing pointers. Memory management is also simpler. When a copy of a structure is no longer reachable, its memory is collected and made available. In contrast, classes often require special code to handle unmanaged resources or invoke the system's garbage collection routine. Garbage Collection is covered in the next chapter.

Unlike a Class, a Structure Cannot Be Inherited

This is a departure from C++ conventions that permit a class to use an existing structure as a base class and permit a structure to use a class as a base. Is the .NET lack of support for this a step backward? Not really. It better delineates the role of the structure versus the class—making the structure less of a pseudo-class and more of a data structure. In addition, it provides for a more efficient implementation of the structure. The restriction enables the compiler to minimize the amount of administrative code that would be required to support inheritance. For example, because the compiler knows that a structure's methods cannot be overridden by subclasses, it can optimize the method invocation by expanding the method code inline rather than executing a method call.

General Rules for Choosing Between a Structure and a Class

The easiest way to make the choice is to compare the features of the type that you are designing with the following checklist. If any of these are true, you should use a class.

  • The type needs to serve as a base for deriving other types.

  • The type needs to inherit from another class. Note that although a structure cannot explicitly specify a base class, it does implicitly inherit from the System.ValueType and may override the inherited methods.

  • The type is frequently passed as a method parameter. Performance degrades as copies of the structure are created with each call. An exception to this is when the structure exists inside an array. Because an array is a reference type, only a pointer to the array is passed.

  • The type is used as the return type of methods. In the case where the return type is a structure, the system must copy the structure from the called function to the calling program.


3.13. Summary

The goal of the C# language architects was to create a "component-oriented" language based on the traditional object-oriented principles of encapsulation, inheritance, and polymorphism. Toward this end, they included language features that make properties, events, and attributes first-class language constructs. Properties now have their own get and set syntax; events can be created with the event keyword and linked to delegates that call registered methods when the event occurs; custom or built-in attributes can be attached to a class or selected class members to add descriptive information to an assembly's metacode.

C# provides several forms of method declarations: a virtual method permits a derived class to implement its own version of the method; sealed prevents a derived class from overriding it; and abstract requires a derived class to implement its own version of the method.

In some cases, a structure or interface provides a better programming solution than a class. The C# struct is an efficient, simple way to represent self-contained data types that don't require the overhead of classes. An interface is a practical way to define a behavior that can be passed on to inheriting classes. In .NET, its value is enhanced because a class (or struct) can inherit from any number of interfaces—but from only one class.


3.14. Test Your Understanding

1:

What type of class cannot be inherited?

2:

How many classes can a class directly inherit? How many interfaces?

3:

Referring to the following class, answer the questions below:

public class ShowName {

   public static void ShowMe(string MyName)

   { Console.WriteLine(MyName); }

}

  1. Can the method ShowName be referenced from s?

ShowName s = new ShowName();

s.ShowMe("Giacomo");

  2. Write a code sample that will print "My Name is Ishmael".

4:

Can an abstract class have non-abstract methods?

5:

What keyword must a derived class use to replace a non-virtual inherited method?

6:

What are the results from running the following code?

public class ParmTest

{

   public static void GetCoordinates(ref int x, int y)

   {

      x= x+y;

      y= y+x;

   }

}

// calling method

int x=20;

int y=40;

ParmTest.GetCoordinates(ref x, y);

Console.WriteLine("x="+x+" y="+y);

  1.
x=60 y=40

  2.
x=20 y=60

  3.
x=20 y=40

  4.
x=60 y=60

7:

What is the best way to ensure that languages that do not recognize operator overloading can access C# code containing this feature?

8:

Name two ways that you can prevent a class from being instantiated.

9:

Your application contains a class StoreSales that fires an event when an item is sold. The event provides the saleprice (decimal), date (DateTime), and the itemnum (int) to its subscribers. Create an event handler that processes the ItemSold event by extracting and printing the sales data.

public delegate void SaleDelegate(object sender,

                                  SaleEvArgs e);

public event SaleDelegate ItemSold;

StoreSales mysale= new StoreSale();

mySale.ItemSold += new SaleDelegate(PrintSale);

10:

What happens if you attempt to compile and run the following code?

using System;

public class Base

{

   public void aMethod(int i, String s)

   {

      Console.WriteLine("Base Method");

   }

   public Base()

   {

      Console.WriteLine("Base Constructor");

   }

}

public class Child: Base

{

   string parm="Hello";

   public static void Main(String[] argv)

   {

      Child c = new Child();

      c.aMethod();

   }

   void aMethod(int i, string Parm)

  {

      Console.WriteLine(Parm);

   }

   public void aMethod()

   { }

}

  1. Error during compilation.

  2. "Base Constructor" is printed.

  3. "Base Constructor" and "Base Method" are printed.

  4. "Base Constructor" and "Hello" are printed.


Chapter 4. Working with Objects in C#

Topics in This Chapter

  • Creating Objects: Learn how to use a factory design pattern to create objects.

  • Exception Handling: Effective exception handling requires an understanding of how exceptions are thrown and caught in .NET. Along with an overview of exception handling, this section looks at how to create custom exception objects.

  • Using System.Object Methods: Familiarity with the System.Object methods is necessary if you want to create custom collection classes that implement the standard features found in classes contained in the Framework Class Library.

  • Collection Classes and Interfaces: .NET offers a variety of collection classes for managing multiple objects. This section looks at arrays, hash tables, and stacks, among others. Of particular interest are the 2.0 classes that support generics.

  • Object Serialization: Objects can be converted (serialized) into a binary stream or an XML formatted stream. An example using the binary serializer is presented.

  • Object Life Cycle Management: .NET Garbage Collection automatically removes unreferenced objects. This can produce unwanted results unless measures are taken to ensure that objects are destroyed properly.

The purpose of this chapter is to consider what happens to a class when it becomes an object. This metamorphosis raises some interesting questions: What is the best way to create an object? How do you ensure that an object handles errors gracefully? How do you prevent an object from wasting resources (the dreaded memory leak)? What is the best way to work with groups of objects? How do you dispose of an object? Although these questions are unlikely to keep a developer awake at night, their consideration should lead to a keener insight into class design.

In an attempt to answer these questions, a variety of topics are presented. These include how to create objects using established design patterns; how to implement the System.Object methods on custom classes; how to implement exception handling; how to persist objects using serialization; and how to use collection classes and interfaces to manage groups of objects. The chapter concludes with a look at how to design an object so that it shuts down properly when subject to .NET Garbage Collection.


4.1. Object Creation

The subject of creating objects typically receives less attention than that of designing classes; but it is important enough that a formal body of techniques known as creational patterns has been developed to provide object creation models. A popular approach—used throughout the Framework Class Library (FCL)—is to implement the factory creational pattern. As the name implies, the factory is an object whose sole purpose is to create other objects—much like a real-world factory. Its advantage is that it handles all the details of object creation; a client instructs the factory which object to create and is generally unaffected by any implementation changes that may occur.

There are a number of ways to implement the factory pattern. This section presents two logical approaches—illustrated in Figures 4-1 and 4-2.

Figure 4-1. Factory with one factory class

Figure 4-2. Factory with multiple factory classes

Figure 4-1 represents the case where one factory is used to produce all of the related products (objects). In Figure 4-2, each product has its own factory, and the client sends the request to the factory that produces the desired object. We'll look at examples of both, beginning with code for the single factory implementation (see Listing 4-1).

Listing 4-1. Using a Class Factory to Create Objects— Single Factory

public interface IApparel    // Interface representing product

{

   string ShowMe();

   bool Knit        // Property to indicate if Knit

   { get; }

}

public class SportsShirt : IApparel

{

   public string ShowMe()

   {

      return("Sports Shirt");

   }

   public bool Knit

   { get {return true;} }

}

public class DressShirt : IApparel

{

   public string ShowMe()

   {

      return("Dress Shirt");

   }

   public bool Knit

   { get {return false;} }

}

// Factory to return instances of apparel classes

public class ApparelFactory

{

   public IApparel CreateApparel( string apptype)

   {

      switch (apptype)

      {

         case "MDRSHIRT":

            return new DressShirt();

         case "MSPSHIRT":

            return new SportsShirt();

      }

      return null;

   }

}

In this example, the class factory implements a method named CreateApparel that returns objects. The most important thing to note is that this method is declared with IApparel as its return type, which enables it to return an instance of any class that implements that interface.

public IApparel CreateApparel( string apptype)

The same effect could be achieved by replacing the interface with an abstract class. This could yield better code reuse in situations where objects share common behavior, but should be weighed against other factors that were discussed in Chapter 3, "Class Design in C#."

With the factory and product classes defined, all the hard work has been done. It's a simple matter for clients to create objects:

ApparelFactory factory= new ApparelFactory();

IApparel ob1 = factory.CreateApparel("MDRSHIRT");

IApparel ob2 = factory.CreateApparel("MSPSHIRT");

string shirtType = ob1.ShowMe(); // Returns "Dress Shirt"

If the application needs to add any more products, the factory is supplied with the new code, but no changes are required on the client side. It only needs to be aware of how to request all available products.

Example: Creating Objects with Multiple Factories

This solution adds an abstract class (we could use an interface) for the factory and two concrete subclasses that implement it to produce specific apparel objects:

// abstract

public abstract class AppFactory

{

   public  abstract IApparel CreateApparel();

}

// Concrete factory classes

public class DressShirtFactory:AppFactory

{

   public override IApparel CreateApparel( )

   { return new DressShirt(); }

}

public class SportShirtFactory : AppFactory

{

   public override IApparel CreateApparel( )

   { return new SportsShirt(); }

}

We have created the abstract class so that its subclasses can be passed to a new ApparelCollector class that serves as an intermediary between the clients and the factories. Specifically, the client passes the factory to this class, and it is responsible for calling the appropriate factory.

public class ApparelCollector

{

   public void CollectApparel(AppFactory factory)

   {

      IApparel apparel = factory.CreateApparel();

   }

}

The code to use the new class is analogous to that in the first example:

AppFactory factory = new DressShirtFactory();

IApparel obj2 = new ApparelCollector().CollectApparel(factory);

For a simple example like this, the first approach using one factory is easier to implement. However, there are cases where it's preferable to have multiple factories. The objects may be grouped into families of products (a shirt factory and a dress factory, for example), or you may have a distributed application where it makes sense for different developers to provide their own factory classes.


4.2. Exception Handling

One of the most important aspects of managing an object is to ensure that its behavior and interaction with the system does not result in a program terminating in error. This means that an application must deal gracefully with any runtime errors that occur, whether they originate from faulty application code, the Framework Class Library, or hardware faults.

.NET provides developers with a technique called structured exception handling (SEH) to deal with error conditions. The basic idea is that when an exception occurs, an exception object is created and passed along with program control to a specially designated section of code. In .NET terms, the exception object is thrown from one section of code to another section that catches it.

Compared to error handling techniques that rely on error codes and setting bit values, SEH offers significant advantages:

  • The exception is passed to the application as an object whose properties include a description of the exception, the assembly that threw the exception, and a stack trace that shows the sequence of calls leading to the exception.

  • If an exception is thrown and an application does not catch it, the Common Language Runtime (CLR) terminates the application. This forces the developer to take error handling seriously.

  • The exception handling and detection code does not have to be located where the errors occur. This means, for example, that exception handling code could be placed in a special class devoted to that purpose.

  • Exceptions are used exclusively and consistently at both the application and system level. All methods in the .NET Framework throw exceptions when an error occurs.

Before looking at the actual mechanics of implementing exception handling, let's examine the exception itself. As previously mentioned, an exception is a class instance. All .NET exceptions derive from the System.Exception class. So, an understanding of this class is essential to working with exceptions.

System.Exception Class

As shown in Figure 4-3, System.Exception is the base class for two generic subclasses—SystemException and ApplicationException—from which all exception objects directly inherit. .NET Framework exceptions (such as IOException and ArithmeticException) derive directly from IOException, whereas custom application exceptions should inherit from ApplicationException. The sole purpose of these classes is to categorize exceptions, because they do not add any properties or methods to the base System.Exception class.

Figure 4-3. .NET exception classes hierarchy

The System.Exception class contains relatively few members. Table 4-1 summarizes the members discussed in this section.

Table 4-1. System.Exception Class Properties

Property

Type

Description

HelpLink

string

Contains a URL that points to help documentation.

InnerException

Exception

Is set to null unless the exception occurs while a previous exception is being handled. A GetBaseException method can be used to list a chain of previous inner exceptions.

Message

string

The text describing the exception.

Source

string

Name of the assembly that generated the exception.

StackTrace

string

Contains the sequence of method names and signatures that were called prior to the exception. It is invaluable for debugging.

TargetSite

MethodBase

Provides details about the method that threw the exception. The property is an object of type MethodBase. It returns the name of the method in which the exception occurred. It also has a DeclaringType property that returns the name of the class containing the method.

HResult

Int32

This is a protected property used when interoperating with COM code. When an exception is thrown to a COM client, this value is converted to an HRESULT in the COM world of unmanaged code.

Writing Code to Handle Exceptions

C# uses a TRy/catch/finally construct to implement exception handling (see Figure 4-4). When an exception occurs, the system searches for a catch block that can handle the current type of exception. It begins its search in the current method, working down the list of catch blocks sequentially. If none is found, it then searches the catch blocks in the calling method associated with the relevant try block. If the search yields no matching catch block, an unhandled exception occurs. As discussed later, the application is responsible for defining a policy to deal with this. Let's look at the details of using these three blocks.

Figure 4-4. Code blocks used for exception handling

The try Block

The code inside the try block is referred to as a guarded region because it has associated catch or finally blocks to handle possible exceptions or cleanup duties. Each try block must have at least one accompanying catch or finally block.

The catch Block

A catch block consists of the keyword catch followed by an expression in parentheses called the exception filter that indicates the type of exception to which it responds. Following this is the code body that implements the response.

The exception filter identifies the exception it handles and also serves as a parameter when an exception is thrown to it. Consider the following statement:

catch (DivideByZeroException ex)  {  ... }

The filter will be invoked if a System.DivideByZeroException occurs. The variable ex references the exception and provides access to its properties, such as ex.Message and ex.StackTrace.

When using multiple catch blocks, ordering is important. They should be listed hierarchically, beginning with the most specific exceptions and ending with the more general ones. In fact, the compiler generates an error if you do not order them correctly.

catch (DivideByZeroException ex)  {  ... }

catch (IndexOutOfRangeException ex) { ... }

catch (Exception ex)  {  ... }

This codes first looks for specific exceptions such as a division by zero or an index out of range. The final exception filter, Exception, catches any exception derived from System.Exception. When an exception is caught, the code in the block is executed and all other catch blocks are skipped. Control then flows to the finally block—if one exists.

Note that the catch block may include a throw statement to pass the exception further up the call stack to the previous caller. The throw statement has an optional parameter placed in parentheses that can be used to identify the type of exception being thrown. If tHRow is used without a parameter, it throws the exception caught by the current block. You typically throw an exception when the calling method is better suited to handle it.

The finally Block

Regarded as the "cleanup" block, the finally block is executed whether or not an exception occurs and is a convenient place to perform any cleanup operations such as closing files or database connections. This block must be included if there are no catch blocks; otherwise, it is optional.

Example: Handling Common SystemException Exceptions

Listing 4-2 illustrates the use of the TRy/catch/finally blocks in dealing with an exception generated by the CLR.

Listing 4-2. Handling Exceptions Generated by the CLR

using System;

// Class to illustrate results of division by zero

public class TestExcep

{

   public static int Calc(int j)

   {

      return (100 / j);

   }

}

class MyApp

{

   public static void Main()

   {

      TestExcep exTest = new TestExcep();

      try

      {

         // Create divide by zero in called method

         int dZero = TestExcep.Calc(0);

         // This statement is not executed

         Console.WriteLine("Result: {0}",dZero);

      }

      catch(DivideByZeroException ex)

      {

        Console.WriteLine("{0}\n{1}\n", ex.Message, ex.Source);

         Console.WriteLine(ex.TargetSite.ToString());

         Console.WriteLine(ex.StackTrace);

      }

      catch (Exception ex)

      {

         Console.WriteLine("General "+ex.Message);

      }

      finally

      {

         Console.WriteLine("Cleanup occurs here.");

      }

   }

}

In this example, TestExcep.Calc tHRows a division by zero exception when MyApp calls it with a zero value. Because Calc has no code to handle the exception, the exception is thrown back automatically to MyApp at the point where Calc was called. From there, control passes to the block provided to handle DivideByZeroException exceptions. For demonstration purposes, the statements in the catch block display the following information provided by the exception object:

Property | Value Printed
ex.Message | Attempted to divide by zero
ex.Source | zeroexcept (assembly name)
ex.TargetSite | Void Main()
ex.StackTrace | at MyApp.Main()

Core Recommendation

StackTrace displays only those methods on the call stack to the level where the exception is first handled—not where it occurs. Although you may be tempted to catch exceptions at the point where they occur in order to view the full call stack, this is discouraged. It may improve diagnostics; however, it takes time and space to throw an exception and its entire call stack. Usually, the lower on a call stack that an exception occurs, the more likely it is that the conditions causing it can be avoided by improved coding logic.

How to Create a Custom Exception Class

Custom exception classes are useful when you need to describe errors in terms of the class that issues the error. For example, you may want the exception to describe the specific behavior causing the error or to indicate a problem with a parameter that does not meet some required criteria. In general, first look to the most specific system exception available; if that is inadequate, consider creating your own.

In Listing 4-3, a method throws a custom exception if the object it receives does not implement the two required interfaces. The exception, NoDescException, returns a message describing the error and the name of the object causing the failure.

Listing 4-3. Building a Custom Exception Class

// Custom Exception Class

[Serializable]

public class NoDescException : ApplicationException

{  // Three constructors should be implemented

   public NoDescException(){}

   public NoDescException(string message):base(message){}

   public NoDescException(string message, Exception innerEx)

         :base(message, innerEx){ }

}

// Interfaces that shape objects are to implement

public interface IShapeFunction

   { double GetArea(); }

public interface IShapeDescription

   { string ShowMe();}

// Circle and Rectangle classes are defined

class Circle : IShapeFunction

{

   private double radius;

   public Circle (double rad)

   {

      radius= rad;

   }

   // Methods to implement both interfaces

   public double GetArea()

   {  return (3.14*radius*radius);  }

}

class Rectangle : IShapeFunction, IShapeDescription

{

   private int width, height;

   public Rectangle(int w, int h)

   {

      width= w;

      height=h;

   }

   // Methods to implement both interfaces

   public double GetArea()

   {  return (height*width);  }

   public string ShowMe()

   {  return("rectangle");    }

}

public class ObjAreas

{

   public static void ShowAreas(object ObjShape)

   {

      // Check to see if interfaces are implemented

      if (!(ObjShape is IShapeDescription &&

          ObjShape is IShapeFunction) )

      {

         // Throw custom exception

         string objName = ObjShape.ToString();

         throw new NoDescException

            ("Interface not implemented for "+objName);

      }

      // Continue processing since interfaces exist

      IShapeFunction myShape   = (IShapeFunction)ObjShape;

      IShapeDescription myDesc = (IShapeDescription) ObjShape;

      string desc = myDesc.ShowMe();

      Console.WriteLine(desc+" Area= "+

                        myShape.GetArea().ToString());

   }

}

To view the custom exception in action, let's create two shape objects and pass them via calls to the static ObjAreas.ShowAreas method.

Circle myCircle = new Circle(4.0);

Rectangle myRect = new Rectangle(5,2);

try

{

   ObjAreas.ShowAreas(myRect);

   ObjAreas.ShowAreas(myCircle);

}

catch (NoDescException ex)

{

   Console.WriteLine(ex.Message);

}

The ShowAreas method checks to ensure the object it has received implements the two interfaces. If not, it throws an instance of NoDescException and control passes to the calling code. In this example, the Circle object implements only one interface, resulting in an exception.

Pay particular attention to the design of NoDescException. It is a useful model that illustrates the rules to be followed in implementing a custom exception type:

  • The class should be derived from ApplicationException.

  • By convention, the exception name should end in Exception. The Exception base class defines three public constructors that should be included:

    1. A parameterless constructor to serve as the default.

    2. A constructor with one string parameter—usually the message.

    3. A constructor with a string parameter and an Exception object parameter that is used when an exception occurs while a previous exception is being handled.

  • Use the base initializer to call the base class to take care of the actual object creation. If you choose to add fields or properties, add a new constructor to initialize these values.

  • The Serializable attribute specifies that the exception can be serialized, which means it can be represented as XML for purposes of storing it or transmitting it. Typically, you can ignore this attribute, because it's only required if an exception object is being thrown from code in one application domain to another. Application domains are discussed Chapter 15, "Code Refinement, Security, and Deployment"; for now, think of them as logical partitions that .NET uses to isolate code segments.

Unhandled Exceptions

Unhandled exceptions occur when the CLR is unable to find a catch filter to handle the exception. The default result is that the CLR will handle it with its own methods. Although this provides a warning to a user or developer, it is not a recommended way to deal with it. The solution is in the problem: Take advantage of .NET's unhandled exception event handlers to funnel all of the exceptions to your own custom exception handling class.

The custom class provides a convenient way to establish a policy for dealing with unhandled exceptions. The code can be implemented to recognize whether it is dealing with a debug or release version, and respond accordingly. For example, in debug version, your main concern is to start the debugger; in a release version, you should log the error and provide a meaningful screen that allows the user to end the program.

Unfortunately, there is no single approach that applies to all C# programming needs. Your actual solution depends on whether you are working with a Console, Windows Form, Web Forms, or Web Services application. In this section, we will look at how to implement a Windows Forms solution, which is conceptually the same as for a Console application. Web Forms and Web Services are addressed in the Web Applications chapters, Chapters 16–18.

Unhandled Exceptions in a Windows Forms Application

Event handling was discussed in the previous chapter along with the important role of delegates. We can now use those techniques to register our own callback method that processes any unhandled exceptions thrown in the Windows application.

When an exception occurs in Windows, the application's OnThreadException method is ultimately called. It displays a dialog box describing the unhandled exception. You can override this by creating and registering your own method that matches the signature of the System.Threading.ThreadExceptionEventHandler delegate. Listing 4-4 shows one way this can be implemented.

MyUnhandledMethod is defined to handle the exception and must be registered to receive the callback. The following code registers the method for the THReadException event using the THReadExceptionEventHandler delegate and runs the application:

static void Main()

{

   Application.ThreadException += new

      ThreadExceptionEventHandler(UnForgiven.MyUnhandledMethod);

   Application.Run(new Form1());

}

The implementation code in the method is straightforward. The most interesting aspect is the use of preprocessor directives (#if DEBUG) to execute code based on whether the application is in release or debug mode.

Listing 4-4. Unhandled Exceptions in a Windows Form

// Class to receive callback to process unhandled exceptions

using System.Diagnostics;

using System.Windows.Forms;

using System.Threading;

public class UnForgiven

{

   // Class signature matches ThreadExceptionEventHandler

   public static void MyUnhandledMethod

      (object sender, ThreadExceptionEventArgs e)

   {

#if DEBUG

   // Statements for debug mode

   // Display trace and start the Debugger

   MessageBox.Show("Debug: "+e.ToString());

#else

   // Statements for release mode

   // Provide output to user and log errors

   MessageBox.Show("Release: "+e.ToString());

#endif

   }

}

For a Console application, the same approach is used except that the delegate and event names are different. You would register the method with the following:

Thread.GetDomain().UnhandledException += new

   UnhandledExceptionEventHandler(

      UnForgiven.MyUnhandledMethodAp);

Also, the method's EventArgs parameter changes to UnhandledExceptionEventArgs.

Exception Handling Guidelines

The use of exception handling is the key to implementing code that performs in a stable manner when unexpected conditions occur. But successful exception handling requires more than surrounding code with try and catch blocks. Thought must be given to which exceptions should be caught and what to do with a caught exception. Included here are some general rules to consider. For a more extensive list, consult online "best practices" information.

Only catch exceptions when you have a specific need to do the following:

  • Perform a recovery

  • Perform a cleanup, particularly to release resources

  • Log information

  • Provide extra debug information

Use exceptions to handle unexpected events, not as a way to implement logic.

Distinguish between error handling and exception handling. Incorrectly formatted credit card numbers or incorrect passwords, for example, are all occurrences that should be anticipated and handled by the logic of your code. An unavailable database table or hardware failure requires an exception.

Don't catch or throw base exception types.

The base exception type System.Exception is a catch-all exception that can be used to catch any exception this is not specifically identified and caught. Much like "fools gold," its intuitive appeal is deceptive. The objective of exception handling is to identify specific exceptions and handle with code appropriate for them; catching each exception prevents the unexpected ones from being identified. As a rule, catch specific exceptions and allow unidentified ones to propagate up the call stack.

Make liberal use of finally blocks.

The purpose of a finally block is to perform any housekeeping chores before a method is exited. It is implemented as part of both try/catch/finally and try/ finally constructs. Because code should be more liberal in its use of throws than catches, an application should contain more TRy/finally constructs than TRy/ catch constructs.

The inclusion of thorough, intelligent exception handling code in an application is one of the hallmarks of well-written code. Although you'll find code segments in this book where exception handling is ignored or used sparingly, understand that this is done only in the interest of simplifying code examples. In real-world programming, there are no excuses for leaving it out.


4.3. Implementing System.Object Methods in a Custom Class

When creating a custom class, particularly one that will be available to other developers, it is important to ensure that it observes the proper rules of object etiquette. It should be CLS compliant, provide adequate exception handling, and adhere to OOP principles of encapsulation and inheritance when providing member accessibility. A class should also implement the features that .NET developers are accustomed to when working with Framework classes. These features include a custom implementation of the System.Object methods that all classes inherit:

  • ToString(). By default, this method returns the name of the class. It should be overridden to display contents of the object that distinguish it from other instances of the class.

  • Equals(). This is used to compare instances of a class to determine if they are equal. It's up to the class to define what equality means. This could mean that two objects have matching field values or that two objects reference the same memory location.

  • MemberwiseClone(). This method returns a copy of an object that contains the object's value fields only—referred to as a shallow copy. A custom class can use this method to return a copy of itself or implement its own clone method to return a deep copy that also includes reference type values.

The remainder of this section provides examples of overriding or using these methods in custom classes. Note that System.Object.Finalize—a method to perform cleanup duties before an object is claimed by garbage collection—is discussed in Section 4.6, "Object Life Cycle Management."

ToString() to Describe an Object

This method is most commonly used with primitive types to convert numbers to a string format that can be displayed or printed. When used with objects, the default is to return the fully qualified name of the object: <namespace>.<classname>. It's common to override this and return a string containing selected values from members of the object. A good example of this is the exception object from the previous section (refer to Figure 4-3) that returns the Message and StackTrace values:

ex.ToString()   // Output:

                //  Attempted to divide by zero

                //  at TestExcep.Calc(Int32 j)

                //  at MyApp.Main()

The code shown in Listing 4-5 demonstrates how easy it is to implement ToString in your own class.

The StringBuilder class is used to create the text string returned by the method. It provides an efficient way of handling strings and is described in Chapter 5, "C# Text Manipulation and File I/O."

Listing 4-5. Overriding ToString()

using System.Text;

using System;

public class Chair

{

   private double myPrice;

   private string myVendor, myID;

   public Upholstery myUpholstery;

   public  Chair(double price, string vendor, string sku)

   {  myPrice = price;

      myVendor = vendor;

      myID = sku;

   }

   // Override System.Object ToString()

   public override string ToString()

   {

      StringBuilder chairSB = new StringBuilder();

      chairSB.AppendFormat("ITEM = Chair");

      chairSB.AppendFormat(" VENDOR = {0}", this.myVendor);

      chairSB.AppendFormat(" PRICE = {0}",

                           this.myPrice.ToString());

      return chairSB.ToString();

   }

   public string MyVen

   {

      get {return myVendor;}

   }

   //... other properties to expose myPrice, myID

}

public class Upholstery

{

   public string Fabric ;

   public Upholstery( string fab)

   { Fabric = fab; }

}

The following statements create an instance of the object and set desc to the more meaningful value returned by ToString():

Chair myChair = new Chair(120.0, "Broyhill", "60-1222");

string desc = myChair.ToString());

// Returns ITEM = Chair VENDOR = Broyhill PRICE = 120.0

Equals() to Compare Objects

When comparing two reference types, Equals returns true only if they point to the same object in memory. To compare objects based on their value (value equality rather than referential equality), you must override the default method. An example of this is the String class—a reference type that implements Equals to perform a value comparison based on the characters in the strings.

The code in Listing 4-6 illustrates how to override Equals in the Chair class to compare objects using value semantics. It also overrides the GetHashCode method—always recommended when overriding Equals.

Listing 4-6. Overriding Equals()

public override bool Equals(Object obj)

{

   // Include the following two statements if this class

   // derives from a class that overrides Equals()

   //if (!base.Equals(obj))

   //   return false;

   // (1) Null objects cannot be compared

   if (obj == null) return false;

   // (2) Check object types

   if (this.GetType() != obj.GetType()) return false;

   // (3) Cast object so we can access its fields

   Chair otherObj = (Chair) obj;

   // (4) Compare reference fields

   if (!Object.Equals(myUpholstery,

              otherObj.myUpholstery)) return false;

   // (5) Compare Value Type members

   if (!myVendor.Equals(otherObj.myVendor)) return false;

   if (!myPrice.Equals(otherObj.myPrice))   return false;

   if (!myID.Equals(otherObj.myID))         return false;

   return true;

}

// Override GetHashCode – Required if Equals overridden

public override int GetHashCode()

{

   return myID.GetHashCode();

}

This method compares an instance of the current object with the one passed to it. The first step is to ensure that the received object is not null. Next, following the steps in Figure 4-5, the types of the two objects are compared to make sure they match.

Figure 4-5. Steps in overriding Equals()

The heart of the method consists of comparing the field values of the two objects. To compare reference fields, it uses the static Object.Equals method, which takes two objects as arguments. It returns true if the two objects reference the same instance, if both objects are null, or if the object's Equals comparison returns true. Value types are compared using the field's Equals method:

if (!myID.Equals(otherObj.myID))

return false;

Here is an example that demonstrates the new Equals method. It creates two Chair objects, sets their fields to the same value, and performs a comparison:

Chair myChair = new Chair(150.0, "Lane", "78-0988");

myChair.myUpholstery = new Upholstery("Silk");

Chair newChair = new Chair(150.0, "Lane", "78-0988");

newChair.myUpholstery= new Upholstery("Silk");

// Next statement returns false. Why?

bool eq = ( myChair.Equals(newChair));

Although the two objects have identical field values, the comparison fails—which is probably not what you want. The reason is that the objects point to two different myUpholstery instances, causing their reference comparison to fail. The solution is to override the Equals method in the Upholstery class, so that it performs a value comparison of the Fabric fields. To do so, place this code inside its Equals method, in addition to the other overhead code shown in Listing 4-6:

Upholstery otherObj = (Upholstery) obj;

if (!Fabric.Equals(otherObj.Fabric)) return false;

Overriding GetHashCode

The GetHashCode method generates an Int32 hash code value for any object. This value serves as an identifier that can be used to place any object in a hash table collection. The Framework designers decreed that any two objects that are equal must have the same hash code. As a result, any new Equals method must be paired with a GetHashCode method to ensure that identical objects generate the same hash code value.

Ideally, the hash code values generated over the range of objects represent a wide distribution. This example used a simple algorithm that calls the base type's GetHashCode method to return a value based on the item's ID. This is a good choice because the IDs are unique for each item, the ID is an instance field, and the ID field value is immutable—being set only in the constructor.

Determining If References Point to the Same Object

After you override the original Equals method to compare object values, your application may still need to determine if reference variables refer to the same object. System.Object has a static method called ReferenceEquals for this purpose. It is used here with our Chair class:

Chair chair1 = new Chair(120.0, "Broyhill", "66-9888") )

Chair chair2 = new Chair(120.0, "Broyhill", "66-9933") )

Chair chair3 = chair1;

if (Object.ReferenceEquals(chair1, chair3) )

   { MessageBox.Show("Same object");}

else

   { MessageBox.Show("Different objects");}

The method returns true because chair1 and chair3 reference the same instance.

Cloning to Create a Copy of an Object

The usual way to create an object is by using the new operator to invoke a constructor. An alternative approach is to make a copy or clone of an existing object. The object being cloned may be a class instance, an array, a string, or any reference type; primitives cannot be cloned. For a class to be cloneable, it must implement the ICloneable interface. This is a simple interface defined as

public interface ICloneable

{

   Object Clone();

}

It consists of a single method, Clone, that is implemented to create a copy of the object. The cloned object may represent a shallow copy or a deep copy of the object's fields. A shallow copy creates a new instance of the original object type, and then copies the non-static fields from the original object to the new object. For a reference type, only a pointer to the value is copied. Thus, the clone points to the same reference object as the original. A deep copy duplicates everything. It creates a copy of reference objects and provides a reference to them in the copy.

Figure 4-6 depicts a shallow and deep copy for this instance of the Chair class:

Chair myChair = new Chair(150.0, "Lane", "78-0988");

Upholstery myFabric = new Upholstery("Silk");

myChair.myUpholstery = myFabric;

Figure 4-6. Comparison of shallow and deep copy

In both cases, the clone of the myChair object contains its own copy of the value type fields. However, the shallow copy points to the same instance of myUpholstery as the original; in the deep copy, it references a duplicate object.

Let's now look at how to implement shallow cloning on a custom object. Deep cloning (not discussed) is specific to each class, and it essentially requires creating an instance of the object to be cloned and copying all field values to the clone. Any reference objects must also be created and assigned values from the original referenced objects.

How to Create a Shallow Copy

A shallow copy is sufficient when the object to be copied contains no reference type fields needed by the copy. The easiest way to implement shallow copying is to use the System.Object.MemberwiseClone method, a protected, non-virtual method that makes a shallow copy of the object it is associated with. We use this to enable the Chair class to clone itself:

public class Chair: ICloneable

{

   //... other code from Listing 4-5

   public Object Clone()

   {

      return MemberwiseClone(); // from System.Object

   }

The only requirements are that the class inherit the ICloneable interface and implement the Clone method using MemberwiseClone. To demonstrate, let's use this code segment to create a shallow copy clone of myChair by calling its Clone method:

// Make clone of myChair

Chair chairClone = (Chair)myChair.Clone();

bool isEqual;

// (1) Following evaluates to false

isEqual = Object.ReferenceEquals(myChair,chairClone);

// (2) Following evaluates to true

isEqual = Object.ReferenceEquals(

          myChair.myUpholstery,chairClone.myUpholstery);

The results confirm this is a shallow copy: The reference comparison of myChair and its clone fails because chairClone is created as a copy of the original object; on the other hand, the comparison of the reference field myUpholstery succeeds because the original and clone objects point to the same instance of the myUpholstery class.


4.4. Working with .NET Collection Classes and Interfaces

In .NET, collection is a general term that applies to the set of classes that represent the classic data structures used to group related types of data. These classes include stacks, queues, hash tables, arrays, and dictionaries. All are contained in one of two namespaces: System.Collections or System.Collections.Generic. In .NET versions 1.0 and 1.1, System.Collections was home to all collection classes. In the .NET 2.0 release, these original classes were revised to support generics—a way to make collections type-safe.

The .NET developers left the original namespace in for backward compatibility and added the System.Collections.Generic namespace to contain the generic classes. With the exception of the generics features, the classes in the two namespaces offer the same functionality—although there are a few name changes. To avoid confusion, much of this section refers to the System.Collections namespace. The details of generics are presented at the end of the section.

To best work with container classes such as the ArrayList and Hashtable, a developer should be familiar with the interfaces they implement. Interfaces not only provide a uniform way of managing and accessing the contents of a collection, but they are the key to creating a custom collection. We'll begin the section by looking at the most useful interfaces and the behavior they impart to the collection classes. Then, we'll examine selected collection classes.

Collection Interfaces

Interfaces play a major role in the implementation of the concrete collection classes. All collections inherit from the ICollection interface, and most inherit from the IEnumerable interface. This means that a developer can use common code semantics to work with the different collections. For example, the foreach statement is used to traverse the elements of a collection whether it is a Hashtable,a SortedList, or a custom collection. The only requirement is that the class implements the IEnumerable interface.

Table 4-2 summarizes the most important interfaces inherited by the collection classes. IComparer provides a uniform way of comparing elements for the purpose of sorting; IDictionary defines the special members required by the Hashtable and Dictionary objects; similarly, IList is the base interface for the ArrayList collection.

Table 4-2. System.Collections

Interface

Description

ICollection

The base interface for the collection classes. It contains properties to provide the number of elements in the collection and whether it is thread-safe. It also contains a method that copies elements of the collection into an array.

IComparer

Exposes a method that is used to compare two objects and plays a vital role in allowing objects in a collection to be sorted.

IDictionary

Allows an object to represent its data as a collection of key-and-value pairs.

IDictionaryEnumerator

Permits iteration through the contents of an object that implements the IDictionary interface.

IEnumerator

IEnumerable

Supports a simple iteration through a collection. The iteration only supports reading of data in the collection.

IHashCodeProvider

Supplies a hash code for an object. It contains one method: GetHashCode(object obj)

IList

The base interface of all lists. It controls whether the elements in the list can be modified, added, or deleted.

The UML-like diagram in Figure 4-7 shows how these interfaces are related. Recall that interfaces may inherit from multiple interfaces. Here, for example, IDictionary and IList inherit from IEnumerable and ICollection. Also of special interest is the GetEnumerator method that is used by interfaces to return the IEnumerator interface.

Figure 4-7. System.Collections Interface diagram

Of these interfaces, IDictionary and IList are the most important when considering the built-in collections provided by the FCL. For this reason, we'll discuss them later in the context of the collection classes.

ICollection Interface

This interface provides the minimal information that a collection must implement. All classes in the System.Collections namespace inherit it (see Table 4-3).

Table 4-3. ICollection Members

Member

Description

int Count

Property that returns the number of entries in the collection.

bool IsSynchronized

Property that indicates if access to the collection is thread-safe.

object SyncRoot

Property that returns an object that can be used to synchronize access to a collection by different threads.

void CopyTo( array, index)

Method to copy the contents of the collection to an array.

The IsSynchronized and SyncRoot properties require explanation if you are not yet familiar with threading (discussed in Chapter 13, "Asynchronous Programming and Multithreading"). Briefly, their purpose is to ensure the integrity of data in a collection while it is being accessed by multiple clients. It's analogous to locking records in a database to prevent conflicting updates by multiple users.

IHashCodeProvider Interface

This interface has one member—the GetHashCode method—that uses a custom hash function to return a hash code value for an object. The Hashtable class uses this interface as a parameter type in one of its overloaded constructors, but other than that you will rarely see it.

Using the IEnumerable and IEnumerator Interfaces to List the Contents of a Collection

For a class to support iteration using the foreach construct, it must implement the IEnumerable and IEnumerator interfaces. IEnumerator defines the methods and properties used to implement the enumerator pattern; IEnumerable serves only to return an IEnumerator object. Table 4-4 summarizes the member(s) provided by each interface.

Table 4-4. IEnumerable and IEnumerator

Member

Description

IEnumerable.GetEnumerator()

Returns an IEnumerator object that is used to iterate through collection.

IEnumerator.Current

Property that returns the current value in a collection.

IEnumerator.MoveNext()

Advances the enumerator to the next item in the collection. Returns bool.

IEnumerator.Reset()

Sets the enumerator to its beginning position in the collection. This method is not included in the Generic namespace.

These members are implemented in a custom collection class as a way to enable users to traverse the objects in the collection. To understand how to use these members, let's look at the underlying code that implements the foreach construct.

The foreach loop offers a simple syntax:

foreach ( ElementType element in collection)

{

   Console.WriteLine(element.Name);

}

The compiler expands the foreach construct into a while loop that employs IEnumerable and IEnumerator members:

// Get enumerator object using collection's GetEnumerator method

IEnumerator enumerator =

         ((IEnumerable) (collection)).GetEnumerator();

try

{

   while (enumerator.MoveNext())

   {

      ElementType element = (ElementType)enumerator.Current;

      Console.WriteLine(element.Name);

   }

}

finally

//  Determine if enumerator implements IDisposable interface

//  If so, execute Dispose method to release resources

{

   IDisposable disposable = enumerator as System.IDisposable;

   If( disposable !=null) disposable.Dispose();

}

When a client wants to iterate across the members of a collection (enumerable), it obtains an enumerator object using the IEnumerable.GetEnumerator method. The client then uses the members of the enumerator to traverse the collection: MoveNext() moves to the next element in the collection, and the Current property returns the object referenced by the current index of the enumerator. Formally, this is an implementation of the iteration design pattern.

Core Note

Enumerating through a collection is intrinsically not a thread-safe procedure. If another client (thread) modifies the collection during the enumeration, an exception is thrown. To guarantee thread safety, lock the collection prior to traversing it:

lock( myCollection.SyncRoot )

{

   foreach ( Object item in myCollection )

   {

      // Insert your code here

   }

}

Iterators

The foreach construct provides a simple and uniform way to iterate across members of a collection. For this reason, it is a good practice—almost a de facto requirement—that any custom collection support foreach. Because this requires that the collection class support the IEnumerable and IEnumerator interfaces, the traditional approach is to explicitly implement each member of these interfaces inside the collection class. Referencing Table 4-4, this means writing code to support the GetEnumerator method of the IEnumerable interface, and the MoveNext, Current, and Reset members of IEnumerator. In essence, it is necessary to build a state machine that keeps track of the most recently accessed item in a collection and knows how to move to the next item.

C# 2.0 introduced a new syntax referred to as iterators that greatly simplifies the task of implementing an iterator pattern. It includes a yield return (also a yield break) statement that causes the compiler to automatically generate code to implement the IEnumerable and IEnumerator interfaces. To illustrate, let's add iterators to the GenStack collection class that was introduced in the generics discussion in Chapter 3. (Note that iterators work identically in a non-generics collection.)

Listing 4-7 shows part of the original GenStack class with two new members that implement iterators: a GetEnumerator method that returns an enumerator to traverse the collection in the order items are stored, and a Reverse property that returns an enumerator to traverse the collection in reverse order. Both use yield return to generate the underlying code that supports foreach iteration.

Listing 4-7. Using Iterators to Implement Enumeration

using System;

using System.Collections.Generic;

public class GenStack<T>: IEnumerable<T>

{

   // Use generics type parameter to specify array type

   private T[] stackCollection;

   private int count = 0;

   // Constructor

   public GenStack(int size)

   {

      stackCollection = new T[size];

   }

   // (1) Iterator

   public IEnumerator<T> GetEnumerator()

   {

      for (int i = 0; i < count; i++)

      {

         yield return stackCollection[i];

      }

   }

   // (2) Property to return the collection in reverse order

   public IEnumerable<T> Reverse

   {

      get

      {

         for (int i = count - 1; i >= 0; i--)

         {

            yield return stackCollection[i];

          }

       }

   }

   public void Add(T item)

   {

      stackCollection[count] = item;

      count += 1;

   }

   // other class methods go here ...

This code should raise some obvious questions about iterators: Where is the implementation of IEnumerator? And how can a method with an IEnumerator return type or a property with an IEnumerable return type seemingly return a string value?

The answer to these questions is that the compiler generates the code to take care of the details. If the member containing the yield return statement is an IEnumerable type, the compiler implements the necessary generics or non-generics version of both IEnumerable and IEnumerator; if the member is an IEnumerator type, it implements only the two enumerator interfaces. The developer's responsibility is limited to providing the logic that defines how the collection is traversed and what items are returned. The compiler uses this logic to implement the IEnumerator.MoveNext method.

The client code to access the GenStack collection is straightforward. An instance of the GenStack class is created to hold ten string elements. Three items are added to the collection and are then displayed in original and reverse sequence.

GenStack<string> myStack = new GenStack<string>(10);

myStack.Add("Aida");

myStack.Add("La Boheme");

myStack.Add("Carmen");

// uses enumerator from GetEnumerator()

foreach (string s in myStack)

   Console.WriteLine(s);

// uses enumerator from Reverse property

foreach (string s in myStack.Reverse)

   Console.WriteLine(s);

The Reverse property demonstrates how easy it is to create multiple iterators for a collection. You simply implement a property that traverses the collection in some order and uses tbe yield return statement(s) to return an item in the collection.

Core Note

In order to stop iteration before an entire collection is traversed, use the yield break keywords. This causes MoveNext() to return false.

There are some restrictions on using yield return or yield break:

  • It can only be used inside a method, property (get accessor), or operator.

  • It cannot be used inside a finally block, an anonymous method, or a method that has ref or out arguments.

  • The method or property containing yield return must have a return type of Collections.IEnumerable, Collections.Generic. IEnumerable<>, Collections.IEnumerator, or Collections. Generic.IEnumerator<>.

Using the IComparable and IComparer Interfaces to Perform Sorting

Chapter 2, "C# Language Fundamentals," included a discussion of the System.Array object and its associated Sort method. That method is designed for primitive types such as strings and numeric values. However, it can be extended to work with more complex objects by implementing the IComparable and IComparer interfaces on the objects to be sorted.

IComparable

Unlike the other interfaces in this section, IComparable is a member of the System namespace. It has only one member, the method CompareTo:

int CompareTo(Object obj)

Returned Value | Condition
Less than 0 | Current instance < obj
0 | Current instance = obj
Greater than 0 | Current instance > obj

The object in parentheses is compared to the current instance of the object implementing CompareTo, and the returned value indicates the results of the comparison. Let's use this method to extend the Chair class so that it can be sorted on its myPrice field. This requires adding the IComparable inheritance and implementing the CompareTo method.

public class Chair : ICloneable, IComparable

{

   private double myPrice;

   private string myVendor, myID;

   public Upholstery myUpholstery;

   //... Constructor and other code

   // Add implementation of CompareTo to sort in ascending

   int IComparable.CompareTo(Object obj)

   {

      if (obj is Chair) {

         Chair castObj = (Chair)obj;

         if (this.myPrice > castObj.myPrice)

               return 1;

         if (this.myPrice < castObj.myPrice)

               return -1;

         else return 0;

         // Reverse 1 and –1 to sort in descending order

      }

   throw new ArgumentException("object in not a Chair");

   }

The code to sort an array of Chair objects is straightforward because all the work is done inside the Chair class:

Chair[]chairsOrdered = new Chair[4];

chairsOrdered[0] = new Chair(150.0, "Lane","99-88");

chairsOrdered[1] = new Chair(250.0, "Lane","99-00");

chairsOrdered[2] = new Chair(100.0, "Lane","98-88");

chairsOrdered[3] = new Chair(120.0, "Harris","93-9");

Array.Sort(chairsOrdered);

// Lists in ascending order of price

foreach(Chair c in chairsOrdered)

   MessageBox.Show(c.ToString());

IComparer

The previous example allows you to sort items on one field. A more flexible and realistic approach is to permit sorting on multiple fields. This can be done using an overloaded form of Array.Sort that takes an object that implements IComparer as its second parameter.

IComparer is similar to IComparable in that it exposes only one member, Compare, that receives two objects. It returns a value of –1, 0, or 1 based on whether the first object is less than, equal to, or greater than the second. The first object is usually the array to be sorted, and the second object is a class implementing a custom Compare method for a specific object field. This class can be implemented as a separate helper class or as a nested class within the class you are trying to sort (Chair).

This code creates a helper class that sorts the Chair objects by the myVendor field:

public class CompareByVen : IComparer

{

   public CompareByVen() { }

   int IComparer.Compare(object obj1, object obj2)

   {

      // obj1 contains array being sorted

      // obj2 is instance of helper sort class

      Chair castObj1 = (Chair)obj1;

      Chair castObj2 = (Chair)obj2;

      return String.Compare

         (castObj1.myVendor,castObj2.myVendor);

   }

}

If you refer back to the Chair class definition (refer to Figure 4-6 on page 166), you will notice that there is a problem with this code: myVendor is a private member and not accessible in this outside class. To make the example work, change it to public. A better solution, of course, is to add a property to expose the value of the field.

In order to sort, pass both the array to be sorted and an instance of the helper class to Sort:

Array.Sort(chairsOrdered,new CompareByVen());

In summary, sorting by more than one field is accomplished by adding classes that implement Compare for each sortable field.

System.Collections Namespace

The classes in this namespace provide a variety of data containers for managing collections of data. As shown in Figure 4-8, it is useful to categorize them based on the primary interface they implement: ICollection, IList, or IDictionary.

Figure 4-8. Selected classes in System.Collections

The purpose of interfaces is to provide a common set of behaviors for classes. Thus, rather than look at the details of all the classes, we will look at the interfaces themselves as well as some representative classes that illustrate how the members of the interfaces are implemented. We've already looked at ICollection, so let's begin with two basic collections that inherit from it.

Stack and Queue

The Stack and the Queue are the simplest of the collection classes. Because they do not inherit from IList or IDictionary, they do not provide indexed or keyed access. The order of insertion controls how objects are retrieved from them. In a Stack, all insertions and deletions occur at one end of the list; in a Queue, insertions are made at one end and deletions at the other. Table 4-5 compares the two.

Table 4-5. Stack and Queue—Selected Members and Features

Description

Stack

Queue

Method of maintaining data

Last-in, first-out (LIFO)

First-in, first-out (FIFO)

Add an item

Push()

EnQueue()

Remove an item

Pop()

DeQueue()

Return the current item without removing it

Peek()

Peek()

Determine whether an item is in the collection

Includes()

Includes()

Constructors

Stack()

  • Empty stack with default capacity

Stack(ICollection)

  • Stack is filled with received collection

Stack(int)

  • Set stack to initial int capacity

Queue()

  • Default capacity and growth factor

Queue(ICollection)

  • Filled with received collection

Queue(int)

  • Set queue to initial int capacity

Queue(int, float)

  • Set initial capacity and growth factor

Stacks play a useful role for an application that needs to maintain state information in order to hold tasks to be "performed later." The call stack associated with exception handling is a classic example; stacks are also used widely in text parsing operations. Listing 4-8 provides an example of some of the basic stack operations.

Listing 4-8. Using the Stack Container

public class ShowStack  {

   public static void Main()

   {

      Stack myStack = new Stack();

      myStack.Push(new Chair(250.0, "Adams Bros.", "87-00" ));

      myStack.Push(new Chair(100.0, "Broyhill","87-04"  ));

      myStack.Push(new Chair(100.0, "Lane","86-09"  ));

      PrintValues( myStack );        // Adams – Broyhill - Lane

      // Pop top object and push a new one on stack

      myStack.Pop();

      myStack.Push(new Chair(300.0, "American Chair"));

      Console.WriteLine(myStack.Peek().ToString()); // American

   }

   public static void PrintValues( IEnumerable myCollection )

   {

      System.Collections.IEnumerator myEnumerator =

            myCollection.GetEnumerator();

      while ( myEnumerator.MoveNext() )

         Consle.WriteLine(myEnumerator.Current.ToString());

         // Could list specific chair fields with

         // myChair = (Chair) myEnumerator.Current;

   }

}

Three objects are added to the stack. PrintValues enumerates the stack and lists the objects in the reverse order they were added. The Pop method removes "Lane" from the top of the stack. A new object is pushed onto the stack and the Peek method lists it. Note that the foreach statement could also be used to list the contents of the Stack.

ArrayList

The ArrayList includes all the features of the System.Array, but also extends it to include dynamic sizing and insertion/deletion of items at a specific location in the list. These additional features are defined by the IList interface from which ArrayList inherits.

IList Interface

This interface, whose members are listed in Table 4-6, is used to retrieve the contents of a collection via a zero-based numeric index. This permits insertion and removal at random location within the list.

Table 4-6. IList Members

Interface

Description

bool IsFixedSize

Indicates whether the collection has a fixed size. A fixed size prevents the addition or removal of items after the collection is created.

bool IsReadOnly

Items in a collection can be read but not modified.

int IndexOf(object)

Determines the index of a specific item in the collection.

int Add(object)

Adds an item to the end of a list. It returns the value of the index where the item was added.

void Insert (index, object)

void RemoveAt (index)

void Remove (object)

Methods to insert a value at a specific index; delete the value at a specific index; and remove the first occurrence of an item having the specified value.

void Clear()

Remove all items from a collection.

bool Contains(object)

Returns true if a collection contains an item with a specified value.

The most important thing to observe about this interface is that it operates on object types. This means that an ArrayList—or any collection implementing IList—may contain types of any kind. However, this flexibility comes at a cost: Casting must be widely used in order to access the object's contents, and value types must be converted to objects (boxed) in order to be stored in the collection. As we see shortly, C# 2.0 offers a new feature—called generics—that addresses both issues. However, the basic functionality of the ArrayList, as illustrated in this code segment, remains the same.

ArrayList chairList = new ArrayList( );

// alternative: ArrayList chairList = new ArrayList(5);

chairList.Add(new Chair(350.0, "Adams", "88-00"));

chairList.Add(new Chair(380.0, "Lane", "99-33"));

chairList.Add(new Chair(250.0, "Broyhill", "89-01"));

PrintValues(chairList);  // Adams – Lane - Broyhill

//

chairList.Insert(1,new Chair(100,"Kincaid"));

chairList.RemoveAt(2);

Console.WriteLine("Object Count: {0}",chairList.Count);

//

PrintValues(chairList); // Adams – Kincaid - Broyhill

// Copy objects to an array

Object chairArray = chairList.ToArray();

PrintValues((IEnumerable) chairArray);

This parameterless declaration of ArrayList causes a default amount of memory to be initially allocated. You can control the initial allocation by passing a size parameter to the constructor that specifies the number of elements you expect it to hold. In both cases, the allocated space is automatically doubled if the capacity is exceeded.

Hashtable

The Hashtable is a .NET version of a dictionary for storing key-value pairs. It associates data with a key and uses the key (a transformation algorithm is applied) to determine a location where the data is stored in the table. When data is requested, the same steps are followed except that the calculated memory location is used to retrieve data rather than store it.

Syntax:

public class Hashtable : IDictionary, ICollection, IEnumerable,

   ISerializable, IDeserializationCallback, ICloneable

As shown here, the Hashtable inherits from many interfaces; of these, IDictionary is of the most interest because it provides the properties and methods used to store and retrieve data.

IDictionary Interface

Collections implementing the IDictionary interface contain items that can be retrieved by an associated key value. Table 4-7 summarizes the most important members for working with such a collection.

Table 4-7. IDictionary Member

Member

Description

bool IsFixedSize

Indicates whether IDictionary has a fixed size. A fixed size prevents the addition or removal of items after the collection is created.

bool IsReadOnly

Elements in a collection can be read but not modified.

ICollection Keys

ICollection Values

Properties that return the keys and values of the collection.

void Add (key, value)

void Clear ()

void Remove (key)

Methods to add a key-value pair to a collection, remove a specific key, and remove all items (clear) from the collection.

bool Contains

Returns TRue if a collection contains an element with a specified key.

IDictionaryEnumerator

   GetEnumerator ()

Returns an instance of the IDictionaryEnumerator type that is required for enumerating through a dictionary.

IDictionaryEnumerator Interface

As shown in Figure 4-7 on page 169, IDictionaryEnumerator inherits from IEnumerator. It adds properties to enumerate through a dictionary by retrieving keys, values, or both.

Table 4-8. IDictionaryEnumerator Members

Member

Description

DictionaryEntry Entry

The variable Entry is used to retrieve both the key and value when iterating through a collection.

object Key

object Value

Properties that return the keys and values of the current collection entry.

All classes derived from IDictionary maintain two internal lists of data: one for keys and one for the associated value, which may be an object. The values are stored in a location based on the hash code of the key. This code is provided by the key's System.Object.GetHashCode method, although it is possible to override this with your own hash algorithm.

This structure is efficient for searching, but less so for insertion. Because keys may generate the same hash code, a collision occurs that requires the code be recalculated until a free bucket is found. For this reason, the Hashtable is recommended for situations where a large amount of relatively static data is to be searched by key values.

Create a Hashtable

A parameterless constructor creates a Hashtable with a default number of buckets allocated and an implicit load factor of 1. The load factor is the ratio of values to buckets the storage should maintain. For example, a load factor of .5 means that a hash table should maintain twice as many buckets as there are values in the table. The alternate syntax to specify the initial number of buckets and load factor is

Hashtable chairHash = new Hashtable(1000, .6)

The following code creates a hash table and adds objects to it:

// Create HashTable

Hashtable chairHash = new Hashtable();

// Add key - value pair to Hashtable

chairHash.Add ("88-00", new Chair(350.0, "Adams", "88-00");

chairHash.Add ("99-03", new Chair(380.0, "Lane", "99-03");

// or this syntax

chairHash["89-01"] = new Chair(250.0, "Broyhill", "89-01");

There are many ways to add values to a Hashtable, including loading them from another collection. The preceding example shows the most straightforward approach. Note that a System.Argument exception is thrown if you attempt to add a value using a key that already exists in the table. To check for a key, use the ContainsKey method:

// Check for existence of a key

bool keyFound;

if (chairHash.ContainsKey("88-00"))

   { keyFound = true;}

else

   {keyFound = false;}

List Keys in a Hashtable

The following iterates through the Keys property collection:

// List Keys

foreach (string invenKey in chairHash.Keys)

   { MessageBox.Show(invenKey); }

List Values in a Hashtable

These statements iterate through the Values in a hash table:

// List Values

foreach (Chair chairVal in chairHash.Values)

   { MessageBox.Show(chairVal.myVendor);}

List Keys and Values in a Hashtable

This code lists the keys and values in a hash table:

foreach ( DictionaryEntry deChair in chairHash)

{

   Chair obj = (Chair) deChair.Value;

   MessageBox.Show(deChair.Key+" "+ obj.myVendor);

}

The entry in a Hashtable is stored as a DictionaryEntry type. It has a Value and Key property that expose the actual value and key. Note that the value is returned as an object that must be cast to a Chair type in order to access its fields.

Core Note

According to .NET documentation (1.x), a synchronized version of a Hashtable that is supposed to be thread-safe for a single writer and concurrent readers can be created using the Synchronized method:

Hashtable safeHT = Hashtable.Synchronized(newHashtable());

Unfortunately, the .NET 1.x versions of the Hashtable have been proven not to be thread-safe for reading. Later versions may correct this flaw.

This section has given you a flavor of working with System.Collections interfaces and classes. The classes presented are designed to meet most general-purpose programming needs. There are numerous other useful classes in the namespace as well as in the System.Collections.Specialized namespace. You should have little trouble working with either, because all of their classes inherit from the same interfaces presented in this section.

System.Collections.Generic Namespace

Recall from Chapter 3 that generics are used to implement type-safe classes, structures, and interfaces. The declaration of a generic type includes one (or more) type parameters in brackets (<>) that serve(s) as a placeholder for the actual type to be used. When an instance of this type is created, the client uses these parameters to pass the specific type of data to be used in the generic type. Thus, a single generic class can handle multiple types of data in a type-specific manner.

No classes benefit more from generics than the collections classes, which stored any type of data as an object in .NET 1.x. The effect of this was to place the burden of casting and type verification on the developer. Without such verification, a single ArrayList instance could be used to store a string, an integer, or a custom object. Only at runtime would the error be detected.

The System.Collections.Generic namespace provides the generic versions of the classes in the System.Collections namespace. If you are familiar with the non-generic classes, switching to the generic type is straightforward. For example, this code segment using the ArrayList:

ArrayList primes = new ArrayList();

primes.Add(1);

primes.Add(3);

int pSum = (int)primes[0] + (int)primes[1];

primes.Add("test");

can be replaced with the generics version:

List<int> primes = new List<int>();

primes.Add(1);

primes.Add(3);

int pSum = primes[0] + primes[1];

primes.Add("text");   // will not compile

The declaration of List includes a type parameter that tells the compiler what type of data the object may contain—int in this case. The compiler then generates code that expects the specified type. For the developer, this eliminates the need for casting and type verification at runtime. From a memory usage and efficiency standpoint, it also eliminates boxing (conversion to objects) when primitives are stored in the collection.

Comparison of System.Collections and System.Collections.Generic Namespaces

As the following side-by-side comparison shows, the classes in the two namespaces share the same name with three exceptions: Hashtable becomes Dictionary<>, ArrayList becomes List<>, and SortedList is renamed SortedDictionary<>.

System.Collections | System.Collections.Generic
Comparer | Comparer<T>
Hashtable | Dictionary<K,T>
ArrayList | List<T>
Queue | Queue<T>
SortedList | SortedDictionary<K,T>
Stack | Stack<T>
ICollection | ICollection<T>
IComparable | IComparable<T>
IComparer | IComparer<T>
IDictionary | IDictionary<K,T>
IEnumerable | IEnumerable<T>
IEnumerator | IEnumerator<T>
IKeyComparer | IKeyComparer<T>
IList | IList<T>
(not applicable) | LinkedList<T>

The only other points to note regard IEnumerator. Unlike the original version, the generics version inherits from IDisposable and does not support the Reset method.

An Example Using a Generics Collections Class

Switching to the generics versions of the collection classes is primarily a matter of getting used to a new syntax, because the functionality provided by the generics and non-generics classes is virtually identical. To demonstrate, here are two examples. The first uses the Hashtable to store and retrieve instances of the Chair class (defined in Listing 4-5); the second example performs the same functions but uses the Dictionary class—the generics version of the Hashtable.

This segment consists of a Hashtable declaration and two methods: one to store a Chair object in the table and the other to retrieve an object based on a given key value.

// Example 1: Using Hashtable

public Hashtable ht = new Hashtable();

// Store Chair object in table using a unique product identifier

private void saveHT(double price, string ven, string sku)

{

   if (!ht.ContainsKey(sku))

   {

      ht.Add(sku, new Chair(price,ven,sku));

   }

}

// Display vendor and price for a product identifier

private void showChairHT(string sku)

{

   if (ht.ContainsKey(key))

   {

      if (ht[key] is Chair)  // Prevent casting exception

      {

         Chair ch = (Chair)ht[sku];

         Console.WriteLine(ch.MyVen + " " + ch.MyPr);

      }

         else

      { Console.WriteLine("Invalid Type: " +

                          (ht[key].GetType().ToString()));

      }

   }

}

Observe how data is retrieved from the Hashtable. Because data is stored as an object, verification is required to ensure that the object being retrieved is a Chair type; casting is then used to access the members of the object. These steps are unnecessary when the type-safe Dictionary class is used in place of the Hashtable.

The Dictionary<K,V> class accepts two type parameters that allow it to be strongly typed: K is the key type and V is the type of the value stored in the collection. In this example, the key is a string representing the unique product identifier, and the value stored in the Dictionary is a Chair type.

// Example 2: Using Generics Dictionary to replace Hashtable

//   Dictionary accepts string as key and Chair as data type

Dictionary<string,Chair> htgen = new Dictionary<string,Chair>();

//

private void saveGen(double price, string ven, string sku)

{

   if (!htgen.ContainsKey(sku))

   {

      htgen.Add(sku, new Chair(price,ven,sku));

   }

}

private void showChairGen(string sku)

{

   if (htgen.ContainsKey(key))

   {

      Chair ch = htgen[sku];   // No casting required

      Console.WriteLine(ch.MyVen + " " + ch.MyPr);

   }

}

The important advantage of generics is illustrated in the showChairGen method. It has no need to check the type of the stored object or perform casting.

In the long run, the new generic collection classes will render the classes in the System.Collections namespace obsolete. For that reason, new code development should use the generic classes where possible.


4.5. Object Serialization

In .NET, serialization refers to the process of converting an object or collection of objects into a format suitable for streaming across a network—a Web Service, for example—or storing in memory, a file, or a database. Deserialization is the reverse process that takes the serialized stream and converts it back into its original object(s).

.NET support three primary types of serialization:

  • Binary. Uses the BinaryFormatter class to serialize a type into a binary stream.

  • SOAP. Uses the SoapFormatter class to serialize a type into XML formatted according to SOAP (Simple Object Access Protocol) standards.

  • XML. Uses the XmlSerializer class to serialize a type into basic XML (described in Chapter 10, "Working with XML in .NET"). Web Services uses this type of serialization.

Serialization is used primarily for two tasks: to implement Web Services and to store (persist) collections of objects to a medium from which they can be later resurrected. Web Services and their use of XML serialization are discussed in Chapter 18, "XML Web Services." This section focuses on how to use binary serialization to store and retrieve objects. The examples use File I/O (see Chapter 5) methods to read and write to a file, which should be easily understood within the context of their usage.

Binary Serialization

The BinaryFormatter object that performs binary serialization is found in the System.Runtime.Serialization.Formatters.Binary namespace. It performs serialization and deserialization using the Serialize and Deserialize methods, respectively, which it inherits from the IFormatter interface.

Listing 4-9 provides an example of binary serialization using simple class members. A hash table is created and populated with two Chair objects. Next, a FileStream object is instantiated that points to a file on the local drive where the serialized output is stored. A BinaryFormatter is then created, and its Serialize method is used to serialize the hash table's contents to a file. To confirm the process, the hash table is cleared and the BinaryFormatter object is used to deserialize the contents of the file into the hash table. Finally, one of the members from a restored object in the hash table is printed—verifying that the original contents have been restored.

Listing 4-9. Serialization of a Hashtable

using System;

using System.Runtime.Serialization;

using System.Runtime.Serialization.Formatters.Binary;

using System.IO;

// Store Chair objects in a Hashtable

Hashtable ht = new Hashtable();

// Chair and Upholstery must have [Serializable] attribute

Chair ch = new Chair(100.00D, "Broyhill", "10-09");

ch.myUpholstery = new Upholstery("Cotton");

ht.Add("10-09", ch);

// Add second item to table

ch = new Chair(200.00D, "Lane", "11-19");

ch.myUpholstery = new Upholstery("Silk");

ht.Add("11-19", ch);

// (1) Serialize

// Create a new file; if file exits it is overwritten

FileStream fs= new  FileStream("c:\\chairs.dat",

                               FileMode.Create);

BinaryFormatter bf= new BinaryFormatter();

bf.Serialize(fs,ht);

fs.Close();

// (2) Deserialize binary file into a Hashtable of objects

ht.Clear();  // Clear hash table.

fs = new FileStream("c:\\chairs.dat", FileMode.Open);

ht =  (Hashtable) bf.Deserialize(fs);

// Confirm objects properly recreated

ch = (Chair)ht["11-19"];

Console.WriteLine(ch.myUpholstery.Fabric);  // "Silk"

fs.Close();

Observe the following key points:

  • The serialization and IO namespaces should be declared.

  • The Chair and Upholstery classes must have the [Serializable] attribute; otherwise, a runtime error occurs when Serialize()is executed.

  • Serialization creates an object graph that includes references from one object to another. The result is a deep copy of the objects. In this example, the myUpholstery field of Chair is set to an instance of the Upholstery class before it is serialized. Serialization stores a copy of the object—rather than a reference. When deserialization occurs, the Upholstery object is restored.

Excluding Class Members from Serialization

You can selectively exclude class members from being serialized by attaching the [NonSerialized] attribute to them. For example, you can prevent the myUpholstery field of the Chair class from being serialized by including this:

[NonSerialized]

public Upholstery myUpholstery;

The primary reason for marking a field NonSerialized is that it may have no meaning where it is serialized. Because an object graph may be loaded onto a machine different from the one on which it was stored, types that are tied to system operations are the most likely candidates to be excluded. These include delegates, events, file handles, and threads.

Core Note

A class cannot inherit the Serializable attribute from a base class; it must explicitly include the attribute. On the other hand, a derived class can be made serializable only if its base class is serializable.

Binary Serialization Events

.NET 2.0 introduced support for four binary serialization and deserialization events, as summarized in Table 4-9.

Table 4-9. Serialization and Deserialization Events

Event

Attribute

Description

OnSerializing

[Serializing]

Occurs before objects are serialized. Event handler is called for each object to be serialized.

OnSerialized

[Serialized]

Occurs after objects are serialized. Event handler is called once for each object serialized.

OnDeserializing

[Deserializing]

Occurs before objects are deserialized. Event handler is called once for each object to be deserialized.

OnDeserialized

[Deserialized]

Occurs after objects have been deserialized. Event handler is called for each deserialized object.

An event handler for these events is implemented in the object being serialized and must satisfy two requirements: the attribute associated with the event must be attached to the method, and the method must have this signature:

void <event name>(StreamingContext context)

To illustrate, here is a method called after all objects have been deserialized. The binary formatter iterates the list of objects in the order they were deserialized and calls each object's OnDeserialized method. This example uses the event handler to selectively update a field in the object. A more common use is to assign values to fields that were not serialized.

public class Chair

   {

      // other code here

   [OnDeserialized]

   void OnDeserialized(StreamingContext context)

   {

      // Edit vendor name after object is created

      if (MyVen == "Lane") MyVen = "Lane Bros.";

   }

}

Note that more than one method can have the same event attribute, and that more than one attribute can be assigned to a method—although the latter is rarely practical.

Handling Version Changes to a Serialized Object

Suppose the Chair class from the preceding examples is redesigned. A field could be added or deleted, for example. What happens if one then attempts to deserialize objects in the old format to the new format? It's not an uncommon problem, and .NET offers some solutions.

If a field is deleted, the binary formatter simply ignores the extra data in the deserialized stream. However, if the formatter detects a new field in the target object, it throws an exception. To prevent this, an [OptionalField] attribute can be attached to the new field(s). Continuing the previous example, let's add a field to Chair that designates the type of wood finish:

[OptionalField]

private string finish;

The presence of the attribute causes the formatter to assign a default null value to the finish field, and no exception is thrown. The application may also take advantage of the deserialized event to assign a value to the new field:

void OnDeserialized(StreamingContext context)

{

   if (MyVen == "Lane") finish = "Oak"; else finish = "Cherry";

}


4.6. Object Life Cycle Management

Memory allocation and deallocation have always been the bane of developers. Even the most experienced C++ and COM programmer is faced with memory leaks and attempts to access nonexistent objects that either never existed or have already been destroyed. In an effort to remove these responsibilities from the programmer, .NET implements a memory management system that features a managed heap and automatic Garbage Collection.

Recall from Chapter 2, "C# Language Fundamentals," that the managed heap is a pre-allocated area of memory that .NET uses to store reference types and data. Each time an instance of a class is created, it receives memory from the heap. This is a faster and cleaner solution than programming environments that rely on the operating system to handle memory allocation.

Allocating memory from the stack is straightforward: A pointer keeps track of the next free memory address and allocates memory from the top of the heap. The important thing to note about the allocated memory is that it is always contiguous. There is no fragmentation or complex overhead to keep track of free memory blocks. Of course, at some point the heap is exhausted and unused space must be recovered. This is where the .NET automatic Garbage Collection comes into play.

.NET Garbage Collection

Each time a managed object is created, .NET keeps track of it in a tree-like graph of nodes that associates each object with the object that created or uses it. In addition, each time another client references an object or a reference is assigned to a variable, the graph is updated. At the top of this graph is a list of roots, or parts of the application that exist as long at the program is running (see Figure 4-9). These include static variables, CPU registers, and any local or parameter variables that refer to objects on the managed heap. These serve as the starting point from which the .NET Framework uses a reference-tracing technique to remove objects from the heap and reclaim memory.

Figure 4-9. .NET Garbage Collection process

[View full size image]

The Garbage Collection process begins when some memory threshold is reached. At this point, the Garbage Collector (GC) searches through the graph of objects and marks those that are "reachable." These are kept alive while the unreachable ones are considered to be garbage. The next step is to remove the unreferenced objects (garbage) and compact the heap memory. This is a complicated process because the collector must deal with the twin tasks of updating all old references to the new object addresses and ensuring that the state of the heap is not altered as Garbage Collection takes place.

The details of Garbage Collection are not as important to the programmer as the fact that it is a nondeterministic (occurs unpredictably) event that deals with managed resources only. This leaves the programmer facing two problems: how to dispose of unmanaged resources such as files or network connections, and how to dispose of them in a timely manner. The solution to the first problem is to implement a method named Finalize that manages object cleanup; the second is solved by adding a Dispose method that can be called to release resources before Garbage Collection occurs. As we will see, these two methods do not operate autonomously. Proper object termination requires a solution that coordinates the actions of both methods.

Core Note

Garbage Collection typically occurs when the CLR detects that some memory threshold has been reached. However, there is a static method GC.Collect that can be called to trigger Garbage Collection. It can be useful under controlled conditions while debugging and testing, but should not be used as part of an application.

Object Finalization

Objects that contain a Finalize method are treated differently during both object creation and Garbage Collection than those that do not contain a Finalize method. When an object implementing a Finalize method is created, space is allocated on the heap in the usual manner. In addition, a pointer to the object is placed in the finalization queue (see Figure 4-9). During Garbage Collection, the GC scans the finalization queue searching for pointers to objects that are no longer reachable. Those found are moved to the freachable queue. The objects referenced in this queue remain alive, so that a special background thread can scan the freachable queue and execute the Finalize method on each referenced object. The memory for these objects is not released until Garbage Collection occurs again.

To implement Finalize correctly, you should be aware of several issues:

  • Finalization degrades performance due to the increased overhead. Only use it when the object holds resources not managed by the CLR.

  • Objects may be placed in the freachable queue in any order. Therefore, your Finalize code should not reference other objects that use finalization, because they may have already been processed.

  • Call the base Finalize method within your Finalize method so it can perform any cleanup: base.Finalize().

  • Finalization code that fails to complete execution prevents the background thread from executing the Finalize method of any other objects in the queue. Infinite loops or synchronization locks with infinite timeouts are always to be avoided, but are particularly deleterious when part of the cleanup code.

It turns out that you do not have to implement Finalize directly. Instead, you can create a destructor and place the finalization code in it. The compiler converts the destructor code into a Finalize method that provides exception handling, includes a call to the base class Finalize, and contains the code entered into the destructor:

Public class Chair

{

   public Chair() { }

   ~Chair()   // Destructor

   {  // finalization code  }

}

Note that an attempt to code both a destructor and Finalize method results in a compiler error.

As it stands, this finalization approach suffers from its dependency on the GC to implement the Finalize method whenever it chooses. Performance and scalability are adversely affected when expensive resources cannot be released when they are no longer needed. Fortunately, the CLR provides a way to notify an object to perform cleanup operations and make itself unavailable. This deterministic finalization relies on a public Dispose method that a client is responsible for calling.

IDisposable.Dispose()

Although the Dispose method can be implemented independently, the recommended convention is to use it as a member of the IDisposable interface. This allows a client to take advantage of the fact that an object can be tested for the existence of an interface. Only if it detects IDisposable does it attempt to call the Dispose method. Listing 4-10 presents a general pattern for calling the Dispose method.

Listing 4-10. Pattern for Calling Dispose()

public class MyConnections: IDisposable

{

   public void Dispose()

   {

      // code to dispose of resources

      base.Dispose(); // include call to base Dispose()

   }

   public void UseResources() { }

}

// Client code to call Dispose()

class MyApp

{

   public static void Main()

   {

      MyConnections connObj;

      connObj = new MyConnections();

      try

      {

         connObj.UseResources();

      }

      finally   // Call dispose() if it exists

      {

         IDisposable testDisp;

         testDisp = connObj as IDisposable;

         if(testDisp != null)

            { testDisp.Dispose(); }

      }

   }

This code takes advantage of the finally block to ensure that Dispose is called even if an exception occurs. Note that you can shorten this code by replacing the try/finally block with a using construct that generates the equivalent code:

Using(connObj)

{ connObj.UseResources() }

Using Dispose and Finalize

When Dispose is executed, the object's unmanaged resources are released and the object is effectively disposed of. This raises a couple of questions: First, what happens if Dispose is called after the resources are released? And second, if Finalize is implemented, how do we prevent the GC from executing it since cleanup has already occurred?

The easiest way to handle calls to a disposed object's Dispose method is to raise an exception. In fact, the ObjectDisposedException exception is available for this purpose. To implement this, add a boolean property that is set to true when Dispose is first called. On subsequent calls, the object checks this value and throws an exception if it is true.

Because there is no guarantee that a client will call Dispose, Finalize should also be implemented when resource cleanup is required. Typically, the same cleanup method is used by both, so there is no need for the GC to perform finalization if Dispose has already been called. The solution is to execute the SuppressFinalize method when Dispose is called. This static method, which takes an object as a parameter, notifies the GC not to place the object on the freachable queue.

Listing 4-11 shows how these ideas are incorporated in actual code.

Listing 4-11. Pattern for Implementing Dispose() and Finalize()

public class MyConnections: IDisposable

{

   private bool isDisposed = false;

   protected bool Disposed

   {

      get{ return isDisposed;}

   }

   public void Dispose()

   {

      if (isDisposed == false)

      {

         CleanUp();

         IsDisposed = true;

         GC.SuppressFinalize(this);

       }

   }

   protected virtual void CleanUp()

   {

      // cleanup code here

   }

   ~MyConnections()   // Destructor that creates Finalize()

   { CleanUp(); }

   public void UseResources()

   {

      // code to perform actions

      if(Disposed)

      {

         throw new ObjectDisposedException

               ("Object has been disposed of");

      }

   }

}

// Inheriting class that implements its own cleanup

public class DBConnections: MyConnections

{

   protected override void CleanUp()

   {

      // implement cleanup here

      base.CleanUp();

   }

}

The key features of this code include the following:

  • A common method, CleanUp, has been introduced and is called from both Dispose and Finalize . It is defined as protected and virtual, and contains no concrete code.

  • Classes that inherit from the base class MyConnections are responsible for implementing the CleanUp. As part of this, they must be sure to call the Cleanup method of the base class. This ensures that cleanup code runs on all levels of the class hierarchy.

  • The read-only property Disposed has been added and is checked before methods in the base class are executed.

In summary, the .NET Garbage Collection scheme is designed to allow programmers to focus on their application logic and not deal with details of memory allocation and deallocation. It works well as long as the objects are dealing with managed resources. However, when there are valuable unmanaged resources at stake, a deterministic method of freeing them is required. This section has shown how the Dispose and Finalize methods can be used in concert to manage this aspect of an object's life cycle.


4.7. Summary

This chapter has discussed how to work with objects. We've seen how to create them, manipulate them, clone them, group them in collections, and destroy them. The chapter began with a description of how to use a factory design pattern to create objects. It closed with a look at how object resources are released through automatic Garbage Collection and how this process can be enhanced programmatically through the use of the Dispose and Finalize methods. In between, the chapter examined how to make applications more robust with the use of intelligent exception handling, how to customize the System.Object methods such as Equals and ToString to work with your own objects, how cloning can be used to make deep or shallow copies, and how to use the built-in classes available in the System.Collections and System.Collections.Generic namespaces.

As a by-product of this chapter, you should now have a much greater appreciation of the important role that interfaces play in application design. They represent the base product when constructing a class factory, and they allow you to clone (ICloneable), sort (IComparer), or enumerate (IEnumerable) custom classes. Knowing that an object implements a particular interface gives you an immediate insight into the capabilities of the object.


4.8. Test Your Understanding

1:

What are the advantages of using a class factory to create objects?

2:

Which class should custom exceptions inherit from? Which constructors should be included?

3:

How does the default System.Object.Equals method determine if two objects are equal?

4:

Which interface(s) must a class implement in order to support the foreach statement?

5:

What is the main advantage of using generics?

6:

What is the purpose of implementing IDisposable on a class?

7:

Refer to the following code:

public class test: ICloneable

{

   public int Age;

   public string Name;

   public test(string myname)

   { Name = myname;    }

   public Object Clone()

   { return MemberwiseClone(); }

}

// Create instances of class

test myTest = new test("Joanna");

myTest.Age = 36;

test clone1 = (test) mytest.Clone();

test clone2 = myTest;

Indicate whether the following statements evaluate to TRue or false:

  1.
Object.ReferenceEquals(myTest.Name, clone1.Name)

  2.
Object.ReferenceEquals(myTest.Age, clone1.Age)

  3.
myTest.Name = "Julie";

Object.ReferenceEquals(myTest.Name, clone1.Name)

  4.
Object.ReferenceEquals(myTest.Name, clone2.Name)

8:

How does implementing Finalize on an object affect its Garbage Collection?


    Part II: Creating Applications Using the .NET Framework Class Library

    Chapter 5 C# Text Manipulation and File I/O

    Chapter 6 Building Windows Forms Applications

    Chapter 7 Windows Forms Controls

    Chapter 8 .NET Graphics Using GDI+

    Chapter 9 Fonts, Text, and Printing

    Chapter 10 Working with XML in .NET

    Chapter 11 ADO.NET

    Chapter 12 Data Binding with Windows Forms Controls


Chapter 5. C# Text Manipulation and File I/O

Topics in This Chapter

  • Characters and Unicode: By default, .NET stores a character as a 16-bit Unicode character. This enables an application to support international character sets—a technique referred to as localization.

  • String Overview: In .NET, strings are immutable. To use strings efficiently, it is necessary to understand what this means and how immutability affects string operations.

  • String Operations: In addition to basic string operations, .NET supports advanced formatting techniques for numbers and dates.

  • StringBuilder: The StringBuilder class offers an efficient alternative to concatenation for constructing screens.

  • Regular Expressions: The .NET Regex class provides an engine that uses regular expressions to parse, match, and extract values in a string.

  • Text Streams: Stream classes permit data to be read and written as a stream of bytes that can be encrypted and buffered.

  • Text Reading and Writing: The StreamReader and StreamWriter classes make it easy to read from and write to physical files.

  • System.IO: Classes in this namespace enable an application to work with underlying directory structure of the host operating system.

This chapter introduces the string handling capabilities provided by the .NET classes. Topics include how to use the basic String methods for extracting and manipulating string content; the use of the String.Format method to display numbers and dates in special formats; and the use of regular expressions (regexes) to perform advanced pattern matching. Also included is a look at the underlying features of .NET that influence how an application works with text. Topics include how the Just-In-Time (JIT) compiler optimizes the use of literal strings; the importance of Unicode as the cornerstone of character and string representations in .NET; and the built-in localization features that permit applications to automatically take into account the culture-specific characteristics of languages and countries.

This chapter is divided into two major topics. The first topic focuses on how to create, represent, and manipulate strings using the System.Char, System.String, and Regex classes; the second takes up a related topic of how to store and retrieve string data. It begins by looking at the Stream class and how to use it to process raw bytes of data as streams that can be stored in files or transmitted across a network. The discussion then moves to using the Textreader/TextWriter classes to read and write strings as lines of text. The chapter concludes with examples of how members of the System.IO namespace are used to access the Microsoft Windows directory and file structure.


5.1. Characters and Unicode

One of the watershed events in computing was the introduction of the ASCII 7-bit character set in 1968 as a standardized encoding scheme to uniquely identify alphanumeric characters and other common symbols. It was largely based on the Latin alphabet and contained 128 characters. The subsequent ANSI standard doubled the number of characters—primarily to include symbols for European alphabets and currencies. However, because it was still based on Latin characters, a number of incompatible encoding schemes sprang up to represent non-Latin alphabets such as the Greek and Arabic languages.

Recognizing the need for a universal encoding scheme, an international consortium devised the Unicode specification. It is now a standard, accepted worldwide, that defines a unique number for every character "no matter what the platform, no matter what the program, no matter what the language."[1]

[1] Unicode Consortium—www.unicode.org.

Unicode

NET fully supports the Unicode standard. Its internal representation of a character is an unsigned 16-bit number that conforms to the Unicode encoding scheme. Two bytes enable a character to represent up to 65,536 values. Figure 5-1 illustrates why two bytes are needed.

Figure 5-1. Unicode memory layout of a character

The uppercase character on the left is a member of the Basic Latin character set that consists of the original 128 ASCII characters. Its decimal value of 75 can be depicted in 8 bits; the unneeded bits are set to zero. However, the other three characters have values that range from 310 (0x0136) to 56,609 (0xDB05), which can be represented by no less than two bytes.

Unicode characters have a unique identifier made up of a name and value, referred to as a code point. The current version 4.0 defines identifiers for 96,382 characters. These characters are grouped in over 130 character sets that include language scripts, symbols for math, music, OCR, geometric shapes, Braille, and many other uses.

Because 16 bits cannot represent the nearly 100,000 characters supported worldwide, more bytes are required for some character sets. The Unicode solution is a mechanism by which two sets of 16-bit units define a character. This pair of code units is known as a surrogate pair. Together, this high surrogate and low surrogate represent a single 32-bit abstract character into which characters are mapped. This approach supports over 1,000,000 characters. The surrogates are constructed from values that reside in a reserved area at the high end of the Unicode code space so that they are not mistaken for actual characters.

As a developer, you can pretty much ignore the details of whether a character requires 16 or 32 bits because the .NET API and classes handle the underlying details of representing Unicode characters. One exception to this—discussed later in this section—occurs if you parse individual bytes in a stream and need to recognize the surrogates. For this, .NET provides a special object to iterate through the bytes.

Core Note

Unicode characters can only be displayed if your computer has a font supporting them. On a Windows operating system, you can install a font extension (ttfext.exe) that displays the supported Unicode ranges for a .ttf font. To use it, right-click the .ttf font name and select Properties. Console applications cannot print Unicode characters because console output always displays in a non-proportional typeface.

Working with Characters

A single character is represented in .NET as a char (or Char) structure. The char structure defines a small set of members (see char in Chapter 2, "C# Language Fundamentals") that can be used to inspect and transform its value. Here is a brief review of some standard character operations.

Assigning a Value to a Char Type

The most obvious way to assign a value to a char variable is with a literal value. However, because a char value is represented internally as a number, you can also assign it a numeric value. Here are examples of each:

string klm = "KLM";

byte     b = 75;

char k;

// Different ways to assign 'K' to variable K

k = 'K';

k = klm[0];              // Assign "K" from first value in klm

k = (char) 75;           // Cast decimal

k = (char) b;            // cast byte

k = Convert.ToChar(75);  // Converts value to a char

Converting a Char Value to a Numeric Value

When a character is converted to a number, the result is the underlying Unicode (ordinal) value of the character. Casting is the most efficient way to do this, although Convert methods can also be used. In the special case where the char is a digit and you want to assign the linguistic value—rather than the Unicode value—use the static GetNumericValue method.

// '7' has Unicode value of 55

char k = '7';

int n = (int) k;             // n = 55

n = (int) char.GetNumericValue(k);   // n = 7

Characters and Localization

One of the most important features of .NET is the capability to automatically recognize and incorporate culture-specific rules of a language or country into an application. This process, known as localization, may affect how a date or number is formatted, which currency symbol appears in a report, or how string comparisons are carried out. In practical terms, localization means a single application would display the date May 9, 2004 as 9/5/2004 to a user in Paris, France and as 5/9/2004 to a user in Paris, Texas. The Common Language Runtime (CLR) automatically recognizes the local computer's culture and makes the adjustments.

The .NET Framework provides more than a hundred culture names and identifiers that are used with the CultureInfo class to designate the language/country to be used with culture sensitive operations in a program. Although localization has a greater impact when working with strings, the Char.ToUpper method in this example is a useful way to demonstrate the concept.

// Include the System.Globalization namespace

// Using CultureInfo – Azerbaijan

char i = 'i';

// Second parameter is false to use default culture settings

// associated with selected culture

CultureInfo myCI = new CultureInfo("az", false );

i = Char.ToUpper(i,myCI);

An overload of ToUpper() accepts a CultureInfo object that specifies the culture (language and country) to be used in executing the method. In this case, az stands for the Azeri language of the country Azerbaijan (more about this follows). When the Common Language Runtime sees the CultureInfo parameter, it takes into account any aspects of the culture that might affect the operation. When no parameter is provided, the CLR uses the system's default culture.

Core Note

On a Windows operating system, the .NET Framework obtains its default culture information from the system's country and language settings. It assigns these values to the THRead.CurrentThread.CurrentCulture property. You can set these options by choosing Regional Options in the Control Panel.

So why choose Azerbaijan, a small nation on the Caspian Sea, to demonstrate localization? Among all the countries in the world that use the Latin character set, only Azerbaijan and Turkey capitalize the letter i not with I (U+0049), but with an I that has a dot above it (U+0130). To ensure that ToUpper() performs this operation correctly, we must create an instance of the CultureInfo class with the Azeri culture name—represented by az—and pass it to the method. This results in the correct Unicode character—and a satisfied population of 8.3 million Azerbaijani.

Characters and Their Unicode Categories

The Unicode Standard classifies Unicode characters into one of 30 categories. .NET provides a UnicodeCategory enumeration that represents each of these categories and a Char.GetUnicodecategory() method to return a character's category. Here is an example:

Char k  = 'K';

int iCat = (int) char.GetUnicodeCategory(k);   // 0

Console.WriteLine(char.GetUnicodeCategory(k)); // UppercaseLetter

char cr = (Char)13;

iCat = (int) char.GetUnicodeCategory(cr);      // 14

Console.WriteLine(char.GetUnicodeCategory(cr)); // Control

The method correctly identifies K as an UppercaseLetter and the carriage return as a Control character. As an alternative to the unwieldy GetUnicodeCategory, char includes a set of static methods as a shortcut for identifying a character's Unicode category. They are nothing more than wrappers that return a TRue or false value based on an internal call to GetUnicodeCategory. Table 5-1 lists these methods.

Table 5-1. Char Methods That Verify Unicode Categories

Method

Unicode Category

Description

IsControl

4

Control code whose Unicode value is U+007F, or in the range U+0000 through U+001F, or U+0080 through U+009F.

IsDigit

8

Is in the range 0–9.

IsLetter

0, 1, 2, 4

Letter.

IsLetterorDigit

0, 1, 8,

Union of letters and digits.

IsLower

1

Lowercase letter.

IsUpper

0

Uppercase letter.

IsPunctuation

18, 19, 20, 21, 22, 23, 24

Punctuation symbol—for example, DashPunctuation(19) or OpenPunctuation(20), OtherPunctuation(24).

IsSeparator

11, 12, 13

Space separator, line separator, paragraph separator.

IsSurrogate

16

Value is a high or low surrogate.

IsSymbol

25, 26, 28

Symbol.

IsWhiteSpace

11

Whitespace can be any of these characters: space (0x20), carriage return (0x0D), horizontal tab (0x09), line feed (0x0A), form feed (0x0C), or vertical tab (0x0B).

Using these methods is straightforward. The main point of interest is that they have overloads that accept a single char parameter, or two parameters specifying a string and index to the character within the string.

Console.WriteLine(Char.IsSymbol('+'));         // true

Console.WriteLine(Char.IsPunctuation('+')):    // false

string str = "black magic";

Console.WriteLine(Char.IsWhiteSpace(str, 5));  // true

char p = '.';

Console.WriteLine(Char.IsPunctuation(p));      // true

Int iCat = (int) char.GetUnicodeCategory(p);   // 24

Char p = '(';

Console.WriteLine(Char.IsPunctuation(p));      // true

int iCat = (int) char.GetUnicodeCategory(p);   // 20


5.2. The String Class

The System.String class was introduced in Chapter 2. This section expands that discussion to include a more detailed look at creating, comparing, and formatting strings. Before proceeding to these operations, let's first review the salient points from Chapter 2:

  • The System.String class is a reference type having value semantics. This means that unlike most reference types, string comparisons are based on the value of the strings and not their location.

  • A string is a sequence of Char types. Any reference to a character within a string is treated as a char.

  • Strings are immutable. This means that after a string is created, it cannot be changed at its current memory location: You cannot shorten it, append to it, or change a character within it. The string value can be changed, of course, but the modified string is stored in a new memory location. The original string remains until the Garbage Collector removes it.

  • The System.Text.StringBuilder class provides a set of methods to construct and manipulate strings within a buffer. When the operations are completed, the contents are converted to a string. StringBuilder should be used when an application makes extensive use of concatenation and string modifications.

Creating Strings

A string is created by declaring a variable as a string type and assigning a value to it. The value may be a literal string or dynamically created using concatenation. This is often a perfunctory process and not an area that most programmers consider when trying to improve code efficiency. In .NET, however, an understanding of how literal strings are handled can help a developer improve program performance.

String Interning

One of the points of emphasis in Chapter 1, "Introduction to .NET and C#," was to distinguish how value and reference types are stored in memory. Recall that value types are stored on a stack, whereas reference types are placed on a managed heap. It turns out that that the CLR also sets aside a third area in memory called the intern pool, where it stores all the string literals during compilation. The purpose of this pool is to eliminate duplicate string values from being stored.

Consider the following code:

string poem1 = "Kubla Khan";

string poem2 = "Kubla Khan";

string poem3 = String.Copy(poem2); // Create new string object

string poem4 = "Christabel";

Figure 5-2 shows a simplified view of how the strings and their values are stored in memory.

Figure 5-2. String interning

The intern pool is implemented as a hash table. The hash table key is the actual string and its pointer references the associated string object on the managed heap. When the JITcompiler compiles the preceding code, it places the first instance of "Kubla Khan" (poem1) in the pool and creates a reference to the string object on the managed heap. When it encounters the second string reference to "Kubla Khan" (poem2), the CLR sees that the string already exists in memory and, instead of creating a new string, simply assigns poem2 to the same object as poem1. This process is known as string interning. Continuing with the example, the String.Copy method creates a new string poem3 and creates an object for it in the managed heap. Finally, the string literal associated with poem4 is added to the pool.

To examine the practical effects of string interning, let's extend the previous example. We add code that uses the equivalence (==) operator to compare string values and the Object.ReferenceEquals method to compare their addresses.

Console.WriteLine(poem1 == poem2);                // true

Console.WriteLine(poem1 == poem3);                // true

Console.WriteLine(ReferenceEquals(poem1, poem3)); // false

Console.WriteLine(ReferenceEquals(poem1,

                  "Kubla Khan"));                 // true

The first two statements compare the value of the variables and—as expected—return a true value. The third statement compares the memory location of the variables poem3 and poem2. Because they reference different objects in the heap, a value of false is returned.

The .NET designers decided to exclude dynamically created values from the intern pool because checking the intern pool each time a string was created would hamper performance. However, they did include the String.Intern method as a way to selectively add dynamically created strings to the literal pool.

string khan = " Khan";

string poem5 = "Kubla" + khan;

Console.WriteLine(ReferenceEquals(poem5, poem1)); // false

// Place the contents of poem5 in the intern pool—if not there

poem5 = String.Intern(poem5);

Console.WriteLine(ReferenceEquals(poem5, poem1)); // true

The String.Intern method searches for the value of poem5 ("Kubla Khan") in the intern pool; because it is already in the pool, there is no need to add it. The method returns a reference to the already existing object (Object1) and assigns it to poem5. Because poem5 and poem1 now point to the same object, the comparison in the final statement is true. Note that the original object created for poem5 is released and swept up during the next Garbage Collection.

Core Recommendation

Use the String.Intern method to allow a string variable to take advantage of comparison by reference, but only if it is involved in numerous comparisons.

Overview of String Operations

The System.String class provides a large number of static and instance methods, most of which have several overload forms. For discussion purposes, they can be grouped into four major categories based on their primary function:

  • String Comparisons. The String.Equals, String.Compare, and String.CompareOrdinal methods offer different ways to compare string values. The choice depends on whether an ordinal or lexical comparison is needed, and whether case or culture should influence the operation.

  • Indexing and Searching. A string is an array of Unicode characters that may be searched by iterating through it as an array or by using special index methods to locate string values.

  • String Transformations. This is a catchall category that includes methods for inserting, padding, removing, replacing, trimming, and splitting character strings.

  • Formatting. NET provides format specifiers that are used in conjunction with String.Format to represent numeric and DateTime values in a number of standard and custom formats.

Many of the string methods—particularly for formatting and comparisons—are culture dependent. Where applicable, we look at how culture affects the behavior of a method.


5.3. Comparing Strings

The most efficient way to determine if two string variables are equal is to see if they refer to the same memory address. We did this earlier using the ReferenceEquals method. If two variables do not share the same memory address, it is necessary to perform a character-by-character comparison of the respective values to determine their equality. This takes longer than comparing addresses, but is often unavoidable.

.NET attempts to optimize the process by providing the String.Equals method that performs both reference and value comparisons automatically. We can describe its operation in the following pseudo-code:

If string1 and string2 reference the same memory location

   Then strings must be equal

Else

   Compare strings character by character to determine equality

This code segment demonstrates the static and reference forms of the Equals method:

string poem1 = "Kubla Khan";

string poem2 = "Kubla Khan";

string poem3 = String.Copy(poem2);

string poem4 = "kubla khan";

//

Console.WriteLine(String.Equals(poem1,poem2));  // true

Console.WriteLine(poem1.Equals(poem3));         // true

Console.WriteLine(poem1 == poem3);      // equivalent to Equals

Console.WriteLine(poem1 == poem4);      // false – case differs

Note that the == operator, which calls the Equals method underneath, is a more convenient way of expressing the comparison.

Although the Equals method satisfies most comparison needs, it contains no overloads that allow it to take case sensitivity and culture into account. To address this shortcoming, the string class includes the Compare method.

Using String.Compare

String.Compare is a flexible comparison method that is used when culture or case must be taken into account. Its many overloads accept culture and case-sensitive parameters, as well as supporting substring comparisons.

Syntax:

int Compare (string str1, string str2)

Compare (string str1, string str2, bool IgnoreCase)

Compare (string str1, string str2, bool IgnoreCase,

         CultureInfo ci)

Compare (string str1, int index1, string str2, int index2,

         int len)

Parameters:

str1 and str2 | Specify strings to be compared.
IgnoreCase | Set true to make comparison case-insensitive (default is false).
index1 and index2 | Starting position in str1 and str2.
ci | A CultureInfo object indicating the culture to be used.

Compare returns an integer value that indicates the results of the comparison. If the two strings are equal, a value of 0 is returned; if the first string is less than the second, a value less than zero is returned; if the first string is greater than the second, a value greater than zero is returned.

The following segment shows how to use Compare to make case-insensitive and case-sensitive comparisons:

int result;

string stringUpper = "AUTUMN";

string stringLower = "autumn";

// (1) Lexical comparison: "A" is greater than "a"

result = string.Compare(stringUpper,stringLower);       // 1

// (2) IgnoreCase set to false

result = string.Compare(stringUpper,stringLower,false); // 1

// (3)Perform case-insensitive comparison

result = string.Compare(stringUpper,stringLower,true);  // 0

Perhaps even more important than case is the potential effect of culture information on a comparison operation. .NET contains a list of comparison rules for each culture that it supports. When the Compare method is executed, the CLR checks the culture associated with it and applies the rules. The result is that two strings may compare differently on a computer with a US culture vis-à-vis one with a Japanese culture. There are cases where it may be important to override the current culture to ensure that the program behaves the same for all users. For example, it may be crucial that a sort operation order items exactly the same no matter where the application is run.

By default, the Compare method uses culture information based on the THRead.CurrentThread.CurrentCulture property. To override the default, supply a CultureInfo object as a parameter to the method. This statement shows how to create an object to represent the German language and country:

CultureInfo ci = new CultureInfo("de-DE");  // German culture

To explicitly specify a default culture or no culture, the CultureInfo class has two properties that can be passed as parameters—CurrentCulture, which tells a method to use the culture of the current thread, and InvariantCulture, which tells a method to ignore any culture.

Let's look at a concrete example of how culture differences affect the results of a Compare() operation.

using System.Globalization;   // Required for CultureInfo

// Perform case-sensitive comparison for Czech culture

string s1 = "circle";

string s2 = "chair";

result = string.Compare(s1, s2,

         true, CultureInfo.CurrentCulture));       //  1

result = string.Compare(s1, s2,

         true, CultureInfo.InvariantCulture));     //  1

// Use the Czech culture

result = string.Compare(s1, s2,

         true, new CultureInfo("cs-CZ"));          //  -1

The string values "circle" and "chair" are compared using the US culture, no culture, and the Czech culture. The first two comparisons return a value indicating that "circle" > "chair", which is what you expect. However, the result using the Czech culture is the opposite of that obtained from the other comparisons. This is because one of the rules of the Czech language specifies that "ch" is to be treated as a single character that lexically appears after "c".

Core Recommendation

When writing an application that takes culture into account, it is good practice to include an explicit CultureInfo parameter in those methods that accept such a parameter. This provides a measure of self-documentation that clarifies whether the specific method is subject to culture variation.

Using String.CompareOrdinal

To perform a comparison that is based strictly on the ordinal value of characters, use String.CompareOrdinal. Its simple algorithm compares the Unicode value of two strings and returns a value less than zero if the first string is less than the second; a value of zero if the strings are equal; and a value greater than zero if the first string is greater than the second. This code shows the difference between it and the Compare method:

string stringUpper = "AUTUMN";

string stringLower = "autumn";

//

result = string.Compare(stringUpper,stringLower,

         false, CultureInfo.InvariantCulture);            // 1

result = string.CompareOrdinal(stringUpper,stringLower);  // -32

Compare performs a lexical comparison that regards the uppercase string to be greater than the lowercase. CompareOrdinal examines the underlying Unicode values. Because A (U+0041) is less than a (U+0061), the first string is less than the second.


5.4. Searching, Modifying, and Encoding a String's Content

This section describes string methods that are used to perform diverse but familiar tasks such as locating a substring within a string, changing the case of a string, replacing or removing text, splitting a string into delimited substrings, and trimming leading and trailing spaces.

Searching the Contents of a String

A string is an implicit zero-based array of chars that can be searched using the array syntax string[n], where n is a character position within the string. For locating a substring of one or more characters in a string, the string class offers the IndexOf and IndexOfAny methods. Table 5-2 summarizes these.

Table 5-2. Ways to Examine Characters Within a String

String Member

Description

[ n ]

Indexes a 16-bit character located at position n within a string.

int ndx= 0;

while (ndx < poem.Length)

{

   Console.Write(poem[ndx]); //Kubla Khan

   ndx += 1;

}

IndexOf/LastIndexOf

(string, [int start],

[int count])

count. Number of chars to examine.

Returns the index of the first/last occurrence of a specified string within an instance. Returns –1 if no match.

string poem = "Kubla Khan";

int n = poem.IndexOf("la");  // 3

n = poem.IndexOf('K');       // 0

n = poem.IndexOf('K',4);     // 6

IndexOfAny/LastIndexOfAny

Returns the index of the first/last character in an array of Unicode characters.

string poem = "Kubla Khan";

char[] vowels = new char[5]

      {'a', 'e', 'i', 'o', 'u'};

n = poem.IndexOfAny(vowels);     // 1

n = poem.LastIndexOfAny(vowels); // 8

n = poem.IndexOfAny(vowels,2);   // 4

Searching a String That Contains Surrogates

All of these techniques assume that a string consists of a sequence of 16-bit characters. Suppose, however, that your application must work with a Far Eastern character set of 32-bit characters. These are represented in storage as a surrogate pair consisting of a high and low 16-bit value. Clearly, this presents a problem for an expression such as poem[ndx], which would return only half of a surrogate pair.

For applications that must work with surrogates, .NET provides the StringInfo class that treats all characters as text elements and can automatically detect whether a character is 16 bits or a surrogate. Its most important member is the GetTextElementEnumerator method, which returns an enumerator that can be used to iterate through text elements in a string.

TextElementEnumerator tEnum =

       StringInfo.GetTextElementEnumerator(poem) ;

while (tEnum.MoveNext())  // Step through the string

{

   Console.WriteLine(tEnum.Current);  // Print current char

}

Recall from the discussion of enumerators in Chapter 4, "Working with Objects in C#," that MoveNext() and Current are members implemented by all enumerators.

String Transformations

Table 5-3 summarizes the most important string class methods for modifying a string. Because the original string is immutable, any string constructed by these methods is actually a new string with its own allocated memory.

Table 5-3. Methods for Manipulating and Transforming Strings

Tag

Description

Insert (int, string)

Inserts a string at the specified position.

string mariner = "and he stoppeth three";

string verse = mariner.Insert(

      mariner.IndexOf(" three")," one of");

// verse --> "and he stoppeth one of three"

PadRight/PadLeft

Pads a string with a given character until it is a specified width. If no character is specified, whitespace is used.

string rem = "and so on";

rem =rem.PadRight(rem.Length+3,'.);

// rem --> "and so on..."

Remove(p , n)

Removes n characters beginning at position p.

string verse = "It is an Ancient Mariner";

string newverse = (verse.Remove(0,9));

// newverse --> "Ancient Mariner"

Replace (A , B)

Replaces all occurrences of A with B, where A and B are chars or strings.

string aString = "nap ace sap path";

string iString = aString.Replace('a','i');

// iString --> "nip ice sip pith"

Split( char[])

The char array contains delimiters that are used to break a string into substrings that are returned as elements in a string array.

string words = "red,blue orange ";

string [] split = words.Split(new Char []

                  {' ', ','});

Console.WriteLine(split[2]); // orange

ToUpper()

ToUpper(CultureInfo)

ToLower()

ToLower(CultureInfo)

Returns an upper- or lowercase copy of the string.

string poem2="Kubla Khan";

poem2= poem2.ToUpper(

      CultureInfo.InvariantCulture);

Trim()

Trim(params char[])

Removes all leading and trailing whitespaces. If a char array is provided, all leading and trailing characters in the array are removed.

string name = "  Samuel Coleridge";

name = name.Trim(); // "Samuel Coleridge"

TrimEnd (params char[])

TrimStart(params char[])

Removes all leading or trailing characters specified in a char array. If null is specified, whitespaces are removed.

string name = "  Samuel Coleridge";

trimName    = name.TrimStart(null);

shortname   = name.TrimEnd('e','g','i');

// shortName --> "Samuel Colerid"

Substring(n)

Substring(n, l)

Extracts the string beginning at a specified position (n) and of length l, if specified.

string title="Kubla Khan";

Console.WriteLine(title.Substring(2,3));

//bla

ToCharArray()

ToCharArray(n, l)

Extracts characters from a string and places in an array of Unicode characters.

string myVowels = "aeiou";

char[] vowelArr;

vowelArr = myVowels.ToCharArray();

Console.WriteLine(vowelArr[1]);  // "e"

Most of these methods have analogues in other languages and behave as you would expect. Somewhat surprisingly, as we see in the next section, most of these methods are not available in the StringBuilder class. Only Replace, Remove, and Insert are included.

String Encoding

Encoding comes into play when you need to convert between strings and bytes for operations such as writing a string to a file or streaming it across a network. Character encoding and decoding offer two major benefits: efficiency and interoperability. Most strings read in English consist of characters that can be represented by 8 bits. Encoding can be used to strip an extra byte (from the 16-bit Unicode memory representation) for transmission and storage. The flexibility of encoding is also important in allowing an application to interoperate with legacy data or third-party data encoded in different formats.

The .NET Framework supports many forms of character encoding and decoding. The most frequently used include the following:

  • UTF-8. Each character is encoded as a sequence of 1 to 4 bytes, based on its underlying value. ASCII compatible characters are stored in 1 byte; characters between 0x0080 and 0x07ff are stored in 2 bytes; and characters having a value greater than or equal to 0x0800 are converted to 3 bytes. Surrogates are written as 4 bytes. UTF-8 (which stands for UCS Transformation Format, 8-bit form) is usually the default for .NET classes when no encoding is specified.

  • UTF-16. Each character is encoded as 2 bytes (except surrogates), which is how characters are represented internally in .NET. This is also referred to as Unicode encoding.

  • ASCII. Encodes each character as an 8-bit ASCII character. This should be used when all characters are in the ASCII range (0x00 to 0x7F). Attempting to encode a character outside of the ACII range yields whatever value is in the character's low byte.

Encoding and decoding are performed using the Encoding class found in the System.Text namespace. This abstract class has several static properties that return an object used to implement a specific encoding technique. These properties include ASCII, UTF8, and Unicode. The latter is used for UTF-16 encoding.

An encoding object offers several methods—each having several overloads—for converting between characters and bytes. Here is an example that illustrates two of the most useful methods: GetBytes, which converts a text string to bytes, and GetString, which reverses the process and converts a byte array to a string.

string text= "In Xanadu did Kubla Khan";

Encoding UTF8Encoder = Encoding.UTF8;

byte[] textChars = UTF8Encoder.GetBytes(text);

Console.WriteLine(textChars.Length);         // 24

// Store using UTF-16

textChars = Encoding.Unicode.GetBytes(text);

Console.WriteLine(textChars.Length);         // 48

// Treat characters as two bytes

string decodedText = Encoding.Unicode.GetString(textChars);

Console.WriteLine(decodedText); // "In Xanadu did ...  "

You can also instantiate the encoding objects directly. In this example, the UTF-8 object could be created with

UTF8Encoding UTF8Encoder = new UTF8Encoding();

With the exception of ASCIIEncoding, the constructor for these classes defines parameters that allow more control over the encoding process. For example, you can specify whether an exception is thrown when invalid encoding is detected.


5.5. StringBuilder

The primary drawback of strings is that memory must be allocated each time the contents of a string variable are changed. Suppose we create a loop that iterates 100 times and concatenates one character to a string during each iteration. We could end up with a hundred strings in memory, each differing from its preceding one by a single character.

The StringBuilder class addresses this problem by allocating a work area (buffer) where its methods can be applied to the string. These methods include ways to append, insert, delete, remove, and replace characters. After the operations are complete, the ToString method is called to convert the buffer to a string that can be assigned to a string variable. Listing 5-1 introduces some of the StringBuilder methods in an example that creates a comma delimited list.

Listing 5-1. Introduction to StringBuilder

using System;

using System.Text;

public class MyApp

{

   static void Main()

   {

      // Create comma delimited string with quotes around names

      string namesF = "Jan Donna Kim ";

      string namesM = "Rob James";

      StringBuilder sbCSV = new StringBuilder();

      sbCSV.Append(namesF).Append(namesM);

      sbCSV.Replace(" ","','");

      // Insert quote at beginning and end of string

      sbCSV.Insert(0,"'").Append("'");

      string csv = sbCSV.ToString();

      // csv = 'Jan','Donna','Kim','Rob','James'

   }

}

All operations occur in a single buffer and require no memory allocation until the final assignment to csv. Let's take a formal look at the class and its members.

StringBuilder Class Overview

Constructors for the StringBuilder class accept an initial string value as well as integer values that specify the initial space allocated to the buffer (in characters) and the maximum space allowed.

// Stringbuilder(initial value)

StringBuilder sb1 = new StringBuilder("abc");

// StringBuilder(initial value, initial capacity)

StringBuilder sb2 = new StringBuilder("abc", 16);

// StringBuiler(Initial Capacity, maximum capacity)

StringBuilder sb3 = new StringBuilder(32,128);

The idea behind StringBuilder is to use it as a buffer in which string operations are performed. Here is a sample of how its Append, Insert, Replace, and Remove methods work:

int i = 4;

char[] ch = {'w','h','i','t','e'};

string myColor = " orange";

StringBuilder sb = new StringBuilder("red blue green");

sb.Insert(0, ch);              // whitered blue green

sb.Insert(5," ");              // white red blue green

sb.Insert(0,i);                // 4white red blue green

sb.Remove(1,5);                // 4 red blue green

sb.Append(myColor);            // 4 red blue green orange

sb.Replace("blue","violet");   // 4 red violet green orange

string colors = sb.ToString();

StringBuilder Versus String Concatenation

Listing 5-2 tests the performance of StringBuilder versus the concatenation operator. The first part of this program uses the + operator to concatenate the letter a to a string in each of a loop's 50,000 iterations. The second half does the same, but uses the StringBuilder.Append method. The Environment.TickCount provides the beginning and ending time in milliseconds.

Listing 5-2. Comparison of StringBuilder and Regular Concatenation

using System;

using System.Text;

public class MyApp

{

   static void Main()

   {

      Console.WriteLine("String routine");

      string a = "a";

      string str = string.Empty;

      int istart, istop;

      istart = Environment.TickCount;

      Console.WriteLine("Start: "+istart);

      // Use regular C# concatenation operator

      for(int i=0; i<50000; i++)

      {

         str += a;

      }

      istop = Environment.TickCount;

      Console.WriteLine("Stop: "+istop);

      Console.WriteLine("Difference: " + (istop-istart));

      // Perform concatenation with StringBuilder

      Console.WriteLine("StringBuilder routine");

      StringBuilder builder = new StringBuilder();

      istart = Environment.TickCount;

      Console.WriteLine("Start: "+istart);

      for(int i=0; i<50000; i++)

      {

         builder.Append(a);

      }

      istop = Environment.TickCount;

      str = builder.ToString();

      Console.WriteLine("Stop: "+Environment.TickCount);

      Console.WriteLine("Difference: "+ (istop-istart));

   }

}

Executing this program results in the following output:

String routine

Start: 1422091687

Stop: 1422100046

Difference: 9359

StringBuilder routine

Start: 1422100046

Stop: 1422100062

Difference: 16

The results clearly indicate the improved performance StringBuilder provides: The standard concatenation requires 9,359 milliseconds versus 16 milliseconds for StringBuilder. When tested with loops of 1,000 iterations, StringBuilder shows no significant advantage. Unless your application involves extensive text manipulation, the standard concatenation operator should be used.


5.6. Formatting Numeric and DateTime Values

The String.Format method is the primary means of formatting date and numeric data for display. It accepts a string composed of text and embedded format items followed by one or more data arguments. Each format item references a data argument and specifies how it is to be formatted. The CLR creates the output string by converting each data value to a string (using ToString), formatting it according to its corresponding format item, and then replacing the format item with the formatted data value. Here is a simple example:

String s= String.Format("The square root of {0} is {1}.",64,8);

// output: The square root of 64 is 8.

The method has several overloads, but this is the most common and illustrates two features common to all: a format string and a list of data arguments. Note that Console.WriteLine accepts the same parameters and can be used in place of String.Format for console output.

Constructing a Format Item

Figure 5-3 breaks down a String.Format example into its basic elements. The most interesting of these is the format item, which defines the way data is displayed.

Figure 5-3. String.Format example

As we can see, each format item consists of an index and an optional alignment and format string. All are enclosed in brace characters:

  1. The index is a zero-based integer that indicates the argument to which it is to be applied. The index can be repeated to refer to the same argument more than once.

  2. The optional alignment is an integer that indicates the minimum width of the area that contains the formatted value. If alignment value is positive, the argument value is right justified; if the value is negative, it is left justified.

  3. The optional format string contains the formatting codes to be applied to the argument value. If it is not specified, the output of the argument's ToString method is used. .NET provides several standard format codes to be used with numbers and dates as well as codes that are used to create custom format strings.

Formatting Numeric Values

Nine characters, or format specifiers, are available to format numbers into currency, scientific, hexadecimal, and other representations. Each character can have an integer appended to it that specifies a precision particular to that format—usually this indicates the number of decimal places. C# recognizes the standard format specifiers[2] shown in Table 5-4.

[2] Microsoft Windows users can set formats using the Control Panel – Regional Options settings.

Table 5-4. Formatting Numeric Values with Standard Numeric Format Strings

Format Specifier

Description

Pattern

Output

C or c

Currency. Number is represented as a currency amount. The precision specifies the number of decimal places.

{0:C2}, 1458.75

$ 1,458.75

D or d

Decimal. Applies to integral values. The precision indicates the total number of spaces the number occupies; is padded with zeros on left if necessary.

{0:D5}, 455

{0:D5}, -455

00455

-00455

E or e

Scientific. The number is converted to a scientific notation: ddddE+nnn. The precision specifies the number of digits after the decimal point.

{0,10:E2}, 3298.78

{0,10:E4}, -54783.4

3.30+E003

-5.4783+E004

F or f

Fixed Point. The number is converted to format of: ddd.ddd. The precision indicates the number of decimal places.

{0,10:F0}, 162.57

{0,10:F2}, 8162.57

    162

8162.57

G or g

General. The number is converted to fixed point or scientific notation based on the precision and type of number. Scientific is used if the exponent is greater than or equal to the specified precision or less than –4.

{0,10:G}, .0000099

{0,10:G2}, 455.89

{0,10:G3}, 455.89

{0,10:G}, 783229.34

  9.9E-06

  4.6E+02

      456

783229.34

N or n

Number. Converts to a string that uses commas as thousands separators. The precision specifies the number of decimal places.

{0,10:N}, 1045.78

{0,10:N1}, 45.98

1,045.78

    45.9

P or p

Percent. Number is multiplied by 100 and presented as percent with number of decimal places specified by precision.

{0,10:P}, 0.78

{0,10:P3}, 0.7865

 78.00 %

78.650 %

R or r

Round-trip. Converts to a string that retains all decimal place accuracy. Then number to be converted must be floating point.

{0,10:R}, 1.62736

1.62736

X or x

Hexadecimal. Converts the number to its hex representation. The precision indicates the minimum number of digits shown. Number is padded with zeros if needed.

{0,10:X}, 25

{0,10:X4}, 25

{0,10:x4}, 31

  19

0019

001f

The patterns in this table can also be used directly with Console.Write and Console.WriteLine:

Console.WriteLine("The Hex value of {0} is {0:X} ",31); //1F

The format specifiers can be used alone to enhance output from the ToString method:

decimal pct = .758M;

Console.Write("The percent is "+pct.ToString("P2")); // 75.80 %

.NET also provides special formatting characters that can be used to create custom numeric formats. The most useful characters are pound sign (#), zero (0), comma (,), period (.), percent sign (%), and semi-colon (;). The following code demonstrates their use:

decimal dVal = 2145.88M;   // decimal values require M suffix

string myFormat;

myFormat = dVal.ToString("#####");           //  2146

myFormat = dVal.ToString("#,###.00");        //  2,145.88

myFormat = String.Format("Value is {0:#,###.00;

(#,###.00)}",-4567);

// semicolon specifies alternate formats. (4,567.00)

myFormat = String.Format("Value is {0:$#,###.00}", 4567);

                                             //  $4,567.00

Console.WriteLine("{0:##.00%}",.18);         //  18.00 %

The role of these characters should be self-explanatory except for the semicolon (;), which deserves further explanation. It separates the format into two groups: the first is applied to positive values and the second to negative. Two semicolons can be used to create three groups for positive, negative, and zero values, respectively.

Formatting Dates and Time

Date formatting requires a DateTime object. As with numbers, this object has its own set of standard format specifiers. Table 5-5 summarizes these.

Table 5-5. Formatting Dates with Standard Characters

Format Specifier

Description

Example—English

Example—German

d

Short date pattern

1/19/2004

19.1.2004

D

Long date pattern

Monday, January 19, 2004

Montag, 19 Januar, 2004

f

Full date/time pattern (short time)

Monday, January 19, 2004 4:05 PM

Montag, 19 Januar, 2004 16:05

F

Full date/time pattern (full time)

Monday, January 19, 2004 4:05:20 PM

Montag, 19 Januar, 2004 16:05:20

g

General date/time pattern (short time)

1/19/2004 4:05 PM

19/1/2004 16:05

G

General date/time pattern (long time)

1/19/2004 4:05:20 PM

19/1/2004 16:05:20

M, m

Month day pattern

January 19

19 Januar

Y, y

Year month pattern

January, 2004

Januar, 2004

t

Short time pattern

4:05 PM

16:05

T

Long time pattern

4:05:20 PM

16:05:20

s

Universal Sortable Date-Time pattern. Conforms to ISO 8601. Uses local time.

2004-01-19T16:05:20

2004-01-19T16:05:20

u

Universal Sortable Date-Time pattern

2004-01-19 16:05:20Z

2004-01-19 16:05:20Z

U

Universal Sortable Date-Time pattern. Uses universal time.

Monday, January 19, 2004 21:05:20 PM

Montag, 19. Januar, 2004 21:05:20

Here are some concrete examples that demonstrate date formatting. In each case, an instance of a DateTime object is passed an argument to a format string.

DateTime curDate = DateTime.Now;  // Get Current Date

Console.Writeline("Date: {0:d} ", curDate);   // 1/19/2004

// f: --> Monday, January 19, 2004 5:05 PM

Console.Writeline("Date: {0:f} ", curDate);

// g: --> 1/19/2004 5:05 PM

Console.Writeline("Date: {0:g} ", curDate);

If none of the standard format specifiers meet your need, you can construct a custom format from a set of character sequences designed for that purpose. Table 5-6 lists some of the more useful ones for formatting dates.

Table 5-6. Character Patterns for Custom Date Formatting

Format

Description

Example

d

Day of month. No leading zero.

5

dd

Day of month. Always has two digits.

05

ddd

Day of week with three-character abbreviation.

Mon

dddd

Day of week full name.

Monday

M

Month number. No leading zero.

1

MM

Month number with leading zero if needed.

01

MMM

Month name with three-character abbreviation.

Jan

MMMM

Full name of month.

January

y

Year. Last one or two digits.

5

yy

Year. Last one or two digits with leading zero if needed.

05

yyyy

Four-digit year.

2004

HH

Hour in 24-hour format.

15

mm

Minutes with leading zero if needed.

20

Here are some examples of custom date formats:

DateTime curDate = DateTime.Now;

f = String.Format("{0:dddd} {0:MMM} {0:dd}", curDate);

// output: Monday Jan 19

f = currDate.ToString("dd MMM yyyy")

// output: 19 Jan 2004

// The standard short date format (d) is equivalent to this:

Console.WriteLine(currDate.ToString("M/d/yyyy"));  // 1/19/2004

Console.WriteLine(currDate.ToString("d"));         // 1/19/2004

CultureInfo ci = new CultureInfo("de-DE");         // German

f = currDate.ToString("dd-MMMM-yyyy HH:mm", ci)

// output: 19-Januar-2004 23:07

ToString is recommended over String.Format for custom date formatting. It has a more convenient syntax for embedding blanks and special separators between the date elements; in addition, its second parameter is a culture indicator that makes it easy to test different cultures.

Dates and Culture

Dates are represented differently throughout the world, and the ability to add culture as a determinant in formatting dates shifts the burden to .NET from the developer. For example, if the culture on your system is German, dates are automatically formatted to reflect a European format: the day precedes the month; the day, month, and year are separated by periods (.) rather than slashes (/); and the phrase Monday, January 19 becomes Montag, 19. Januar. Here is an example that uses ToString with a German CultureInfo parameter:

CultureInfo ci = new CultureInfo("de-DE");       // German

Console.WriteLine(curDate.ToString("D",ci));

// output ---> Montag, 19. Januar 2004

Console.WriteLine(curDate.ToString("dddd",ci));  // -->Montag

The last statement uses the special custom format "dddd" to print the day of the week. This is favored over the DateTime.DayofWeek enum property that returns only an English value.

NumberFormatInfo and DateTimeFormatInfo Classes

These two classes govern how the previously described format patterns are applied to dates and numbers. For example, the NumberFormatInfo class includes properties that specify the character to be used as a currency symbol, the character to be used as a decimal separator, and the number of decimal digits to use when displaying a currency value. Similarly, DateTimeFormatInfo defines properties that correspond to virtually all of the standard format specifiers for dates. One example is the FullDateTimePattern property that defines the pattern returned when the character F is used to format a date.

NumberFormatInfo and DateTimeFormatInfo are associated with specific cultures, and their properties are the means for creating the unique formats required by different cultures. .NET provides a predefined set of property values for each culture, but they can be overridden.

Their properties are accessed in different ways depending on whether the current or non-current culture is being referenced (current culture is the culture associated with the current thread). The following statements reference the current culture:

NumberFormatInfo.CurrentInfo.<property>

CultureInfo.CurrentCulture.NumberFormat.<property>

The first statement uses the static property CurrentInfo and implicitly uses the current culture. The second statement specifies a culture explicitly (CurrentCulture) and is suited for accessing properties associated with a non-current CultureInfo instance.

CultureInfo ci = new CultureInfo("de-DE");

string f = ci.NumberFormat.CurrencySymbol;

NumberFormatInfo and DateTimeFormatInfo properties associated with a non-current culture can be changed; those associated with the current thread are read-only. Listing 5-3 offers a sampling of how to work with these classes.

Listing 5-3. Using NumberFormatInfo and DateTimeFormatInfo

using System

using System.Globalization

Class MyApp

{

   // NumberFormatInfo

   string curSym = NumberFormatInfo.CurrentInfo.CurrencySymbol;

   int dd  = NumberFormatInfo.CurrentInfo.CurrencyDecimalDigits;

   int pdd = NumberFormatInfo.CurrentInfo.PercentDecimalDigits;

   // --> curSym = "$"   dd = 2  pdd = 2

   // DateTimeFormatInfo

   string ldp= DateTimeFormatInfo.CurrentInfo.LongDatePattern;

   // --> ldp = "dddd, MMMM, dd, yyyy"

   string enDay = DateTimeFormatInfo.CurrentInfo.DayNames[1];

   string month = DateTimeFormatInfo.CurrentInfo.MonthNames[1];

   CultureInfo ci = new CultureInfo("de-DE");

   string deDay = ci.DateTimeFormat.DayNames[1];

   // --> enDay = "Monday"  month = February  deDay = "Montag"

   // Change the default number of decimal places

   // in a percentage

   decimal passRate = .840M;

   Console.Write(passRate.ToString("p",ci));  // 84,00%

   ci.NumberFormat.PercentDecimalDigits = 1;

   Console.Write(passRate.ToString("p",ci));  // 84,0%

}

In summary, .NET offers a variety of standard patterns that satisfy most needs to format dates and numbers. Behind the scenes, there are two classes, NumberFormatInfo and DateTimeFormatInfo, that define the symbols and rules used for formatting. .NET provides each culture with its own set of properties associated with an instance of these classes.


5.7. Regular Expressions

The use of strings and expressions to perform pattern matching dates from the earliest programming languages. In the mid-1960s SNOBOL was designed for the express purpose of text and string manipulation. It influenced the subsequent development of the grep tool in the Unix environment that makes extensive use of regular expressions. Those who have worked with grep or Perl or other scripting languages will recognize the similarity in the .NET implementation of regular expressions.

Pattern matching is based on the simple concept of applying a special pattern string to some text source in order to match an instance or instances of that pattern within the text. The pattern applied against the text is referred to as a regular expression, or regex, for short.

Entire books have been devoted to the topic of regular expressions. This section is intended to provide the essential knowledge required to get you started using regular expressions in the .NET world. The focus is on using the Regex class, and creating regular expressions from the set of characters and symbols available for that purpose.

The Regex Class

You can think of the Regex class as the engine that evaluates regular expressions and applies them to target strings. It provides both static and instance methods that use regexes for text searching, extraction, and replacement. The Regex class and all related classes are found in the System.Text.RegularExpressions namespace.

Syntax:

Regex( string pattern )

Regex( string pattern, RegexOptions)

Parameters:

Pattern | Regular expression used for pattern matching.
RegexOptions | An enum whose values control how the regex is applied. Values include: CultureInvariant— Ignore culture. IgnoreCase— Ignore upper- or lowercase. RightToLeft— Process string right to left.

Example:

Regex r1 = new Regex("  ");   // Regular expression is a blank

String words[] = r1.Split("red blue orange yellow");

// Regular expression matches upper- or lowercase "at"

Regex r2 = new Regex("at", RegexOptions.IgnoreCase);

As the example shows, creating a Regex object is quite simple. The first parameter to its constructor is a regular expression. The optional second parameter is one or more (separated by |) RegexOptions enum values that control how the regex is applied.

Regex Methods

The Regex class contains a number of methods for pattern matching and text manipulation. These include IsMatch, Replace, Split, Match, and Matches. All have instance and static overloads that are similar, but not identical.

Core Recommendation

If you plan to use a regular expression repeatedly, it is more efficient to create a Regex object. When the object is created, it compiles the expression into a form that can be used as long as the object exists. In contrast, static methods recompile the expression each time they are used.

Let's now examine some of the more important Regex methods. We'll keep the regular expressions simple for now because the emphasis at this stage is on understanding the methods—not regular expressions.

IsMatch()

This method matches the regular expression against an input string and returns a boolean value indicating whether a match is found.

string searchStr = "He went that a way";

Regex myRegex = new Regex("at");

// instance methods

bool match = myRegex.IsMatch(searchStr);         // true

// Begin search at position 12 in the string

match = myRegex.IsMatch(searchStr,12);           // false

// Static Methods – both return true

match = Regex.IsMatch(searchStr,"at");

match = Regex.IsMatch(searchStr,"AT",RegexOptions.IgnoreCase);

Replace()

This method returns a string that replaces occurrences of a matched pattern with a specified replacement string. This method has several overloads that permit you to specify a start position for the search or control how many replacements are made.

Syntax:

static Replace (string input, string pattern, string replacement

                [,RegexOptions])

Replace(string input, string replacement)

Replace(string input, string replacement, int count)

Replace(string input, string replacement, int count, int startat)

The count parameter denotes the maximum number of matches; startat indicates where in the string to begin the matching process. There are also versions of this method—which you may want to explore further—that accept a MatchEvaluator delegate parameter. This delegate is called each time a match is found and can be used to customize the replacement process.

Here is a code segment that illustrates the static and instance forms of the method:

string newStr;

newStr = Regex.Replace("soft rose","o","i");   // sift rise

// instance method

Regex myRegex = new Regex("o");                // regex = "o"

// Now specify that only one replacement may occur

newStr = myRegex.Replace("soft rose","i",1);   // sift rose

Split()

This method splits a string at each point a match occurs and places that matching occurrence in an array. It is similar to the String.Split method, except that the match is based on a regular expression rather than a character or character string.

Syntax:

String[] Split(string input)

String[] Split(string input, int count)

String[] Split(string input, int count, int startat)

Static String[] Split(string input, string pattern)

Parameters:

input | The string to split.
count | The maximum number of array elements to return. A count value of 0 results in as many matches as possible. If the number of matches is greater than count, the last match consists of the remainder of the string.
startat | The character position in input where the search begins.
pattern | The regex pattern to be matched against the input string.

This short example parses a string consisting of a list of artists' last names and places them in an array. A comma followed by zero or more blanks separates the names. The regular expression to match this delimiter string is: ",[ ]*". You will see how to construct this later in the section.

string impressionists = "Manet,Monet, Degas, Pissarro,Sisley";

// Regex to match a comma followed by 0 or more spaces

string patt = @",[ ]*";

// Static method

string[] artists = Regex.Split(impressionists, patt);

// Instance method is used to accept maximum of four matches

Regex myRegex = new Regex(patt);

string[] artists4 = myRegex.Split(impressionists, 4);

foreach (string master in artists4)

   Console.Write(master);

// Output --> "Manet" "Monet" "Degas" "Pissarro,Sisley"

Match() and Matches()

These related methods search an input string for a match to the regular expression. Match() returns a single Match object and Matches() returns the object MatchCollection, a collection of all matches.

Syntax:

Match Match(string input)

Match Match(string input, int startat)

Match Match(string input, int startat, int numchars)

static Match(string input, string pattern, [RegexOptions])

The Matches method has similar overloads but returns a MatchCollection object.

Match and Matches are the most useful Regex methods. The Match object they return is rich in properties that expose the matched string, its length, and its location within the target string. It also includes a Groups property that allows the matched string to be further broken down into matching substrings. Table 5-7 shows selected members of the Match class.

Table 5-7. Selected Members of the Match Class

Member

Description

Index

Property returning the position in the string where the first character of the match is found.

Groups

A collection of groups within the class. Groups are created by placing sections of the regex with parentheses. The text that matches the pattern in parentheses is placed in the Groups collection.

Length

Length of the matched string.

Success

TRue or False depending on whether a match was found.

Value

Returns the matching substring.

NextMatch()

Returns a new Match with the results from the next match operation, beginning with the character after the previous match, if any.

The following code demonstrates the use of these class members. Note that the dot (.) in the regular expression functions as a wildcard character that matches any single character.

string verse = "In Xanadu did Kubla Khan";

string patt = ".an...";       // "." matches any character

Match verseMatch = Regex.Match(verse, patt);

Console.WriteLine(verseMatch.Value);  // Xanadu

Console.WriteLine(verseMatch.Index);  // 3

//

string newPatt = "K(..)";             //contains group(..)

Match kMatch = Regex.Match(verse, newPatt);

while (kMatch.Success) {

   Console.Write(kMatch.Value);       // -->Kub -->Kha

   Console.Write(kMatch.Groups[1]);   // -->ub  -->ha

   kMatch = kMatch.NextMatch();

}

This example uses NextMatch to iterate through the target string and assign each match to kMatch (if NextMatch is left out, an infinite loop results). The parentheses surrounding the two dots in newPatt break the pattern into groups without affecting the actual pattern matching. In this example, the two characters after K are assigned to group objects that are accessed in the Groups collection.

Sometimes, an application may need to collect all of the matches before processing them—which is the purpose of the MatchCollection class. This class is just a container for holding Match objects and is created using the Regex.Matches method discussed earlier. Its most useful properties are Count, which returns the number of captures, and Item, which returns an individual member of the collection. Here is how the NextMatch loop in the previous example could be rewritten:

string verse = "In Xanadu did Kubla Khan";

String newpatt = "K(..)";

foreach (Match kMatch in Regex.Matches(verse, newpatt))

   Console.Write(kMatch.Value);  // -->Kub  -->Kha

// Could also create explicit collection and work with it.

MatchCollection mc = Regex.Matches(verse, newpatt);

Console.WriteLine(mc.Count);     // 2

Creating Regular Expressions

The examples used to illustrate the Regex methods have employed only rudimentary regular expressions. Now, let's explore how to create regular expressions that are genuinely useful. If you are new to the subject, you will discover that designing Regex patterns tends to be a trial-and-error process; and the endeavor can yield a solution of simple elegance—or maddening complexity. Fortunately, almost all of the commonly used patterns can be found on one of the Web sites that maintain a searchable library of Regex patterns (www.regexlib.com is one such site).

A regular expression can be broken down into four different types of metacharacters that have their own role in the matching process:

  • Matching characters. These match a specific type of character—for example, \d matches any digit from 0 to 9.

  • Repetition characters. Used to prevent having to repeat a matching character or item—for example, \d{3}can be used instead of \d\d\d to match three digits.

  • Positional characters. Designate the location in the target string where a match must occur—for example, ^\d{3} requires that the match occur at the beginning of the string.

  • Escape sequences. Use the backslash (\) in front of characters that otherwise have special meaning—for example, \} permits the right brace to be matched.

Table 5-8 summarizes the most frequently used patterns.

Table 5-8. Regular Expression Patterns

Pattern

Matching Criterion

Example

+

Match one or more occurrences of the previous item.

to+ matches too and tooo. It does not match t.

*

Match zero or more occurrences of the previous item.

to* matches t or too or tooo.

?

Match zero or one occurrence of the previous item. Performs "non-greedy" matching.

te?n matches ten or tn. It does not match teen.

{n}

Match exactly n occurrences of the previous character.

te{2}n matches teen. It does not match ten or teeen.

{n,}

Match at least n occurrences of the previous character.

te{1,}n matches ten and teen. It does not match tn.

{n,m}

Match at least n and no more than m occurrences of the previous character.

te{1,2}n matches ten and teen.

\

Treat the next character literally. Used to match characters that have special meaning such as the patterns +, *, and ?.

A\+B matches A+B. The slash (\) is required because + has special meaning.

\d \D

Match any digit (\d) or non-digit (\D). This is equivalent to [0-9] or [^0-9], respectively.

\d\d matches 55.

\D\D matches xx.

\w \W

Match any word plus underscore (\w) or non-word (\W) character. \w is equivalent to [a-zA-Z0-9_]. \W is equivalent to [^a-zA-Z0-9_].

\w\w\w\w matches A_19.

\W\W\W matches ($).

\n  \r  \t

\v  \f

Match newline, carriage return, tab, vertical tab, or form feed, respectively.

N/A

\s \S

Match any whitespace (\s) or non-whitespace (\S). A whitespace is usually a space or tab character.

\w\s\w\s\w matches A B C.

. (dot)

Matches any single character. Does not match a newline.

a.c matches abc.

It does not match abcc.

|

Logical OR.

"in|en" matches enquiry.

[. . . ]

Match any single character between the brackets. Hyphens may be used to indicate a range.

[aeiou] matches u. [\d\D] matches a single digit or non-digit.

[^. . .]

All characters except those in the brackets.

[^aeiou] matches x.

A Pattern Matching Example

Let's apply these character patterns to create a regular expression that matches a Social Security Number (SSN):

bool iMatch = Regex.IsMatch("245-09-8444",

                            @"\d\d\d-\d\d-\d\d\d\d");

This is the most straightforward approach: Each character in the Social Security Number matches a corresponding pattern in the regular expression. It's easy to see, however, that simply repeating symbols can become unwieldy if a long string is to be matched. Repetition characters improve this:

bool iMatch = Regex.IsMatch("245-09-8444",

                            @"\d{3}-\d{2}-\d{4}");

Another consideration in matching the Social Security Number may be to restrict where it exists in the text. You may want to ensure it is on a line by itself, or at the beginning or end of a line. This requires using position characters at the beginning or end of the matching sequence.

Let's alter the pattern so that it matches only if the Social Security Number exists by itself on the line. To do this, we need two characters: one to ensure the match is at the beginning of the line, and one to ensure that it is also at the end. According to Table 5-9, ^ and $ can be placed around the expression to meet these criteria. The new string is

@"^\d{3}-\d{2}-\d{4}$"

Table 5-9. Characters That Specify Where a Match Must Occur

Position Character

Description

^

Following pattern must be at the start of a string or line.

$

Preceding pattern must be at end of a string or line.

\A

Preceding pattern must be at the start of a string.

\b \B

Move to a word boundary (\b), where a word character and non-word character meet, or a non-word boundary.

\z \Z

Pattern must be at the end of a string (\z) or at the end of a string before a newline.

These positional characters do not take up any space in the expression—that is, they indicate where matching may occur but are not involved in the actual matching process.

As a final refinement to the SSN pattern, let's break it into groups so that the three sets of numbers separated by dashes can be easily examined. To create a group, place parentheses around the parts of the expression that you want to examine independently. Here is a simple code example that uses the revised pattern:

string ssn = "245-09-8444";

string ssnPatt = @"^(\d{3})-(\d{2})-(\d{4})$";

Match ssnMatch = Regex.Match(ssn, ssnPatt);

if (ssnMatch.Success){

   Console.WriteLine(ssnMatch.Value);         // 245-09-8444

   Console.WriteLine(ssnMatch.Groups.Count);  // 4

   // Count is 4 since Groups[0] is set to entire SSN

   Console.Write(ssnMatch.Groups[1]);         // 245

   Console.Write(ssnMatch.Groups[2]);         // 09

   Console.Write(ssnMatch.Groups[3]);         // 8444

}

We now have a useful pattern that incorporates position, repetition, and group characters. The approach that was used to create this pattern—started with an obvious pattern and refined it through multiple stages—is a useful way to create complex regular expressions (see Figure 5-4).

Figure 5-4. Regular expression

Working with Groups

As we saw in the preceding example, the text resulting from a match can be automatically partitioned into substrings or groups by enclosing sections of the regular expression in parentheses. The text that matches the enclosed pattern becomes a member of the Match.Groups[] collection. This collection can be indexed as a zero-based array: the 0 element is the entire match, element 1 is the first group, element 2 the second, and so on.

Groups can be named to make them easier to work with. The name designator is placed adjacent to the opening parenthesis using the syntax ?<name>. To demonstrate the use of groups, let's suppose we need to parse a string containing the forecasted temperatures for the week (for brevity, only two days are included):

string txt ="Monday Hi:88 Lo:56 Tuesday Hi:91 Lo:61";

The regex to match this includes two groups: day and temps. The following code creates a collection of matches and then iterates through the collection, printing the content of each group:

string rgPatt = @"(?<day>[a-zA-Z]+)\s*(?<temps>Hi:\d+\s*Lo:\d+)";

MatchCollection mc = Regex.Matches(txt, rgPatt); //Get matches

foreach(Match m in mc)

{

   Console.WriteLine("{0} {1}",

                     m.Groups["day"],m.Groups["temps"]);

}

//Output:   Monday Hi:88 Lo:56

//          Tuesday Hi:91 Lo:61

Core Note

There are times when you do not want the presence of parentheses to designate a group that captures a match. A common example is the use of parentheses to create an OR expression—for example, (an|in|on). To make this a non-capturing group, place ?: inside the parentheses—for example, (?:an|in|on).

Backreferencing a Group

It is often useful to create a regular expression that includes matching logic based on the results of previous matches within the expression. For example, during a grammatical check, word processors flag any word that is a repeat of the preceding word(s). We can create a regular expression to perform the same operation. The secret is to define a group that matches a word and then uses the matched value as part of the pattern. To illustrate, consider the following code:

string speech = "Four score and and seven years";

patt = @"(\b[a-zA-Z]+\b)\s\1";          // Match repeated words

MatchCollection mc = Regex.Matches(speech, patt);

foreach(Match m in mc) {

      Console.WriteLine(m.Groups[1]);   // --> and

}

This code matches only the repeated words. Let's examine the regular expression:

Text/Pattern

Description

and and

@"(\b[a-zA-Z]+\b)\s

Matches a word bounded on each side by a word boundary (\b) and followed by a whitespace.

and and

\1

The backreference indicator. Any group can be referenced with a slash (\) followed by the group number. The effect is to insert the group's matched value into the expression.

A group can also be referenced by name rather than number. The syntax for this backreference is \k followed by the group name enclosed in <>:

patt = @"(?<word>\b[a-zA-Z]+\b)\s\k<word>";

Examples of Using Regular Expressions

This section closes with a quick look at some patterns that can be used to handle common pattern matching challenges. Two things should be clear from these examples: There are virtually unlimited ways to create expressions to solve a single problem, and many pattern matching problems involve nuances that are not immediately obvious.

Using Replace to Reverse Words

string userName = "Claudel, Camille";

userName = Regex.Replace( userName, @"(\w+),\s*(\w+)", "$2 $1" );

Console.WriteLine(userName);   // Camille Claudel

The regular expression assigns the last and first name to groups 1 and 2. The third parameter in the Replace method allows these groups to be referenced by placing $ in front of the group number. In this case, the effect is to replace the entire matched name with the match from group 2 (first name) followed by the match from group 1 (last name).

Parsing Numbers

String myText = "98, 98.0, +98.0, +98";

string numPatt = @"\d+";                     // Integer

numPatt = @"(\d+\.?\d*)|(\.\d+)";            // Allow decimal

numPatt = @"([+-]?\d+\.?\d*)|([+-]?\.\d+)";  // Allow + or -

Note the use of the OR (|) symbol in the third line of code to offer alternate patterns. In this case, it permits an optional number before the decimal.

The following code uses the ^ character to anchor the pattern to the beginning of the line. The regular expression contains a group that matches four bytes at a time. The * character causes the group to be repeated until there is nothing to match. Each time the group is applied, it captures a 4-digit hex number that is placed in the CaptureCollection object.

string hex = "00AA001CFF0C";

string hexPatt =  @"^(?<hex4>[a-fA-F\d]{4})*";

Match hexMatch = Regex.Match(hex,hexPatt);

Console.WriteLine(hexMatch.Value); // --> 00AA001CFFOC

CaptureCollection cc = hexMatch.Groups["hex4"].Captures;

foreach (Capture c in cc)

   Console.Write(c.Value); // --> 00AA 001C FF0C

Figure 5-5 shows the hierarchical relationship among the Match, GroupCollection, and CaptureCollection classes.

Figure 5-5. Hex numbers captured by regular expression

    Previous Section < Day Day Up > Next Section


5.8. System.IO: Classes to Read and Write Streams of Data

The System.IO namespace contains the primary classes used to move and process streams of data. The data source may be in the form of text strings, as discussed in this chapter, or raw bytes of data coming from a network or device on an I/O port. Classes derived from the Stream class work with raw bytes; those derived from the TexTReader and TextWriter classes operate with characters and text strings (see Figure 5-6). We'll begin the discussion with the Stream class and look at how its derived classes are used to manipulate byte streams of data. Then, we'll examine how data in a more structured text format is handled using the TexTReader and TextWriter classes.

Figure 5-6. Selected System.IO classes

The Stream Class

This class defines the generic members for working with raw byte streams. Its purpose is to abstract data into a stream of bytes independent of any underlying data devices. This frees the programmer to focus on the data stream rather than device characteristics. The class members support three fundamental areas of operation: reading, writing, and seeking (identifying the current byte position within a stream). Table 5-10 summarizes some of its important members. Not included are methods for asynchronous I/O, a topic covered in Chapter 13, "Asynchronous Programming and Multithreading."

Table 5-10. Selected Stream Members

Member

Description

CanRead

CanSeek

CanWrite

Indicates whether the stream supports reading, seeking, or writing.

Length

Length of stream in bytes; returns long type.

Position

Gets or sets the position within the current stream; has long type.

Close()

Closes the current stream and releases resources associated with it.

Flush()

Flushes data in buffers to the underlying device—for example, a file.

Read(byte array, offset, count)

ReadByte()

Reads a sequence of bytes from the stream and advances the position within the stream to the number of bytes read. ReadByte reads one byte. Read returns number of bytes read; ReadByte returns –1 if at end of the stream.

SetLength()

Sets the length of the current stream. It can be used to extend or truncate a stream.

Seek()

Sets the position within the current stream.

Write(byte array, offset, count)

WriteByte()

Writes a sequence of bytes (Write) or one byte (WriteByte) to the current stream. Neither has a return value.

These methods and properties provide the bulk of the functionality for the FileStream, MemoryStream, and BufferedStream classes, which we examine next.

FileStreams

A FileStream object is created to process a stream of bytes associated with a backing store—a term used to refer to any storage medium such as disk or memory. The following code segment demonstrates how it is used for reading and writing bytes:

try

{

   // Create FileStream object

   FileStream fs = new FileStream(@"c:\artists\log.txt",

         FileMode.OpenOrCreate, FileAccess.ReadWrite);

   byte[] alpha = new byte[6] {65,66,67,68,69,70}; //ABCDEF

   // Write array of bytes to a file

   // Equivalent to: fs.Write(alpha,0, alpha.Length);

   foreach (byte b in alpha) {

      fs.WriteByte(b);}

   // Read bytes from file

   fs.Position = 0;         // Move to beginning of file

   for (int i = 0; i< fs.Length; i++)

      Console.Write((char) fs.ReadByte()); //ABCDEF

   fs.Close();

catch(Exception ex)

{

   Console.Write(ex.Message);

}

As this example illustrates, a stream is essentially a byte array with an internal pointer that marks a current location in the stream. The ReadByte and WriteByte methods process stream bytes in sequence. The Position property moves the internal pointer to any position in the stream. By opening the FileStream for ReadWrite, the program can intermix reading and writing without closing the file.

Creating a FileStream

The FileStream class has several constructors. The most useful ones accept the path of the file being associated with the object and optional parameters that define file mode, access rights, and sharing rights. The possible values for these parameters are shown in Figure 5-7.

Figure 5-7. Options for FileStream constructors

The FileMode enumeration designates how the operating system is to open the file and where to position the file pointer for subsequent reading or writing. Table 5-11 is worth noting because you will see the enumeration used by several classes in the System.IO namespace.

Table 5-11. FileMode Enumeration Values

Value

Description

Append

Opens an existing file or creates a new one. Writing begins at the end of the file.

Create

Creates a new file. An existing file is overwritten.

CreateNew

Creates a new file. An exception is thrown if the file already exists.

Open

Opens an existing file.

OpenOrCreate

Opens a file if it exists; otherwise, creates a new one.

truncate

Opens an existing file, removes its contents, and positions the file pointer to the beginning of the file.

The FileAccess enumeration defines how the current FileStream may access the file; FileShare defines how file streams in other processes may access it. For example, FileShare.Read permits multiple file streams to be created that can simultaneously read the same file.

MemoryStreams

As the name suggests, this class is used to stream bytes to and from memory as a substitute for a temporary external physical store. To demonstrate, here is an example that copies a file. It reads the original file into a memory stream and then writes this to a FileStream using the WriteTo method:

FileStream fsIn = new FileStream(@"c:\manet.bmp",

              FileMode.Open, FileAccess.Read);

FileStream fsOut = new FileStream(@"c:\manetcopy.bmp",

              FileMode.OpenOrCreate, FileAccess.Write);

MemoryStream ms = new MemoryStream();

// Input image byte-by-byte and store in memory stream

int imgByte;

while ((imgByte = fsIn.ReadByte())!=-1){

   ms.WriteByte((byte)imgByte);

}

ms.WriteTo(fsOut);              // Copy image from memory to disk

byte[] imgArray = ms.ToArray(); // Convert to array of bytes

fsIn.Close();

fsOut.Close();

ms.Close();

BufferedStreams

One way to improve I/O performance is to limit the number of reads and writes to an external device—particularly when small amounts of data are involved. Buffers have long offered a solution for collecting small amounts of data into larger amounts that could then be sent more efficiently to a device. The BufferedStream object contains a buffer that performs this role for an underlying stream. You create the object by passing an existing stream object to its constructor. The BufferedStream then performs the I/O operations, and when the buffer is full or closed, its contents are flushed to the underlying stream. By default, the BufferedStream maintains a buffer size of 4096 bytes, but passing a size parameter to the constructor can change this.

Buffers are commonly used to improve performance when reading bytes from an I/O port or network. Here is an example that associates a BufferedStream with an underlying FileStream. The heart of the code consists of a loop in which FillBytes (simulating an I/O device) is called to return an array of bytes. These bytes are written to a buffer rather than directly to the file. When fileBuffer is closed, any remaining bytes are flushed to the FileStream fsOut1. A write operation to the physical device then occurs.

private void SaveStream() {

   Stream fsOut1 = new FileStream(@"c:\captured.txt",

      FileMode.OpenOrCreate, FileAccess.Write);

   BufferedStream fileBuffer = new BufferedStream(fsOut1);

   byte[] buff;         // Array to hold bytes written to buffer

   bool readMore=true;

   while(readMore) {

      buff = FillBytes();         // Get array of bytes

      for (int j = 0;j<buff[16];j++){

        fileBuffer.WriteByte(buff[j]);   // Store bytes in buffer

      }

      if(buff[16]< 16) readMore=false;   // Indicates no more data

   }

   fileBuffer.Close();  // Flushes all remaining buffer content

   fsOut1.Close();      // Must close after bufferedstream

}

// Method to simulate I/O device receiving data

private static byte[] FillBytes() {

   Random rand = new Random();

   byte[] r = new Byte[17];

   // Store random numbers to return in array

   for (int j=0;j<16;j++) {

      r[j]= (byte) rand.Next();

      if(r[j]==171)        // Arbitrary end of stream value

      {

         r[16]=(byte)(j);  // Number of bytes in array

         return r;

      }

   }

   System.Threading.Thread.Sleep(500);  // Delay 500ms

   return r;

}

Using StreamReader and StreamWriter to Read and Write Lines of Text

Unlike the Stream derived classes, StreamWriter and StreamReader are designed to work with text rather than raw bytes. The abstract TextWriter and TexTReader classes from which they derive define methods for reading and writing text as lines of characters. Keep in mind that these methods rely on a FileStream object underneath to perform the actual data transfer.

Writing to a Text File

StreamWriter writes text using its Write and WriteLine methods. Note their differences:

  • WriteLine works only with strings and automatically appends a newline (carriage return\linefeed).

  • Write does not append a newline character and can write strings as well as the textual representation of any basic data type (int32, single, and so on) to the text stream.

The StreamWriter object is created using one of several constructors:

Syntax (partial list):

public StreamWriter(string path)

public StreamWriter(stream s)

public StreamWriter(string path, bool append)

public StreamWriter(string path, bool append, Encoding encoding)

Parameters:

path | Path and name of file to be opened.
s | Previously created Stream object—typically a FileStream.
append | Set to true to append data to file; false overwrites.
encoding | Specifies how characters are encoded as they are written to a file. The default is UTF-8 (UCS Transformation Format) that stores characters in the minimum number of bytes required.

This example creates a StreamWriter object from a FileStream and writes two lines of text to the associated file:

string filePath = @"c:\cup.txt";

// Could use: StreamWriter sw = new StreamWriter(filePath);

// Use FileStream to create StreamWriter

FileStream fs = new FileStream(filePath, FileMode.OpenOrCreate,

                FileAccess.ReadWrite);

StreamWriter sw2 = new StreamWriter(fs);

// Now that it is created, write to the file

sw2.WriteLine("The world is a cup");

sw2.WriteLine("brimming\nwith water.");

sw2.Close();  // Free resources

Reading from a Text File

A StreamReader object is used to read text from a file. Much like StreamWriter, an instance of it can be created from an underlying Stream object, and it can include an encoding specification parameter. When it is created, it has several methods for reading and viewing character data (see Table 5-12).

Table 5-12. Selected StreamReader Methods

Member

Description

Peek()

Returns the next available character without moving the position of the reader. Returns an int value of the character or –1 if none exists.

Read()

Read(char buff, int ndx,

    int count)

Reads next character (Read()) from a stream or reads next count characters into a character array beginning at ndx.

ReadLine()

Returns a string comprising one line of text.

ReadToEnd()

Reads all characters from the current position to the end of the Textreader. Useful for downloading a small text file over a network stream.

This code creates a StreamReader object by passing an explicit FileStream object to the constructor. The FileStream is used later to reposition the reader to the beginning of the file.

String path= @"c:\cup.txt";

if(File.Exists(path))

{

   FileStream fs = new FileStream(path,

         FileMode.OpenOrCreate, FileAccess.ReadWrite);

   StreamReader reader = new StreamReader(fs);

   // or StreamReader reader = new StreamReader(path);

   // (1) Read first line

   string line = reader.ReadLine();

   // (2) Read four bytes on next line

   char[] buff  = new char[4];

   int count = reader.Read(buff,0,buff.Length);

   // (3) Read to end of file

   string cup = reader.ReadToEnd();

   // (4) Reposition to beginning of file

   //     Could also use reader.BaseStream.Position = 0;

   fs.Position = 0;

   // (5) Read from first line to end of file

   line = null;

   while ((line = reader.ReadLine()) != null){

      Console.WriteLine(line);

   }

   reader.Close();

}

Core Note

A StreamReader has an underlying FileStream even if it is not created with an explicit one. It is accessed by the BaseStream property and can be used to reposition the reader within the stream using its Seek method. This example moves the reader to the beginning of a file:

reader.BaseStream.Seek(0, SeekOrigin.Begin);

StringWriter and StringReader

These two classes do not require a lot of discussion, because they are so similar in practice to the StreamWriter and StreamReader. The main difference is that these streams are stored in memory, rather than in a file. The following example should be self-explanatory:

StringWriter writer = new StringWriter();

writer.WriteLine("Today I have returned,");

writer.WriteLine("after long months ");

writer.Write("that seemed like centuries");

writer.Write(writer.NewLine);

writer.Close();

// Read String just written from memory

string myString = writer.ToString();

StringReader reader = new StringReader(myString);

string line = null;

while ((line = reader.ReadLine()) !=null) {

   Console.WriteLine(line);

}

reader.Close();

The most interesting aspect of the StringWriter is that it is implemented underneath as a StringBuilder object. In fact, StringWriter has a GetStringBuilder method that can be used to retrieve it:

StringWriter writer = new StringWriter();

writer.WriteLine("Today I have returned,");

// Get underlying StringBuilder

StringBuilder sb = writer.GetStringBuilder();

sb.Append("after long months ");

Console.WriteLine(sb.ToString());

writer.Close();

Core Recommendation

Use the StringWriter and StringBuilder classes to work with large strings in memory. A typical approach is to use the StreamReader.ReadToEnd method to load a text file into memory where it can be written to the StringWriter and manipulated by the StringBuilder.

Encryption with the CryptoStream Class

An advantage of using streams is the ability to layer them to add functionality. We saw earlier how the BufferedStream class performs I/O on top of an underlying FileStream. Another class that can be layered on a base stream is the CryptoStream class that enables data in the underlying stream to be encrypted and decrypted. This section describes how to use this class in conjunction with the StreamWriter and StreamReader classes to read and write encrypted text in a FileStream. Figure 5-8 shows how each class is composed from the underlying class.

Figure 5-8. Layering streams for encryption/decryption

CryptoStream is located in the System.Security.Cryptography namespace. It is quite simple to use, requiring only a couple of lines of code to apply it to a stream. The .NET Framework provides multiple cryptography algorithms that can be used with this class. Later, you may want to investigate the merits of these algorithms, but for now, our interest is in how to use them with the CryptoStream class.

Two techniques are used to encrypt data: assymmetric (or public key) and symmetric (or private key). Public key is referred to as asymmetric because a public key is used to decrypt data, while a different private key is used to encrypt it. Symmetric uses the same private key for both purposes. In our example, we are going to use a private key algorithm. The .NET Framework Class Library contains four classes that implement symmetric algorithms:

  • DESCryptoServiceProvider— Digital Encryption Standard (DES) algorithm

  • RC2CryptoServiceProvider— RC2 algorithm

  • RijndaelManaged— Rijndael algorithm

  • TRippleDESCryptoServiceProvider— TRipleDES algorithm

We use the DES algorithm in our example, but we could have chosen any of the others because implementation details are identical. First, an instance of the class is created. Then, its key and IV (Initialization Vector) properties are set to the same key value. DES requires these to be 8 bytes; other algorithms require different lengths. Of course, the key is used to encrypt and decrypt data. The IV ensures that repeated text is not encrypted identically. After the DES object is created, it is passed as an argument to the constructor of the CryptoStream class. The CryptoStream object simply treats the object encapsulating the algorithm as a black box.

The example shown here includes two methods: one to encrypt and write data to a file stream, and the other to decrypt the same data while reading it back. The encryption is performed by WriteEncrypt, which receives a FileStream object parameter encapsulating the output file and a second parameter containing the message to be encrypted; ReadEncrypt receives a FileStream representing the file to be read.

fs = new FileStream("C:\\test.txt", FileMode.Create,

                    FileAccess.Write);

MyApp.WriteEncrypt(fs, "Selected site is in Italy.");

fs= new FileStream("C:\\test.txt",FileMode.Open,

                   FileAccess.Read);

string msg = MyApp.ReadEncrypt(fs);

Console.WriteLine(msg);

fs.Close();

WriteEncrypt encrypts the message and writes it to the file stream using a StreamWriter object that serves as a wrapper for a CrytpoStream object. CryptoStream has a lone constructor that accepts the file stream, an object encapsulating the DES algorithm logic, and an enumeration specifying its mode.

// Encrypt FileStream

private static void WriteEncrypt(FileStream fs, string msg) {

   // (1) Create Data Encryption Standard (DES) object

   DESCryptoServiceProvider crypt = new

         DESCryptoServiceProvider();

   // (2) Create a key and Initialization Vector –

   // requires 8 bytes

   crypt.Key = new byte[] {71,72,83,84,85,96,97,78};

   crypt.IV  = new byte[] {71,72,83,84,85,96,97,78};

   // (3) Create CryptoStream stream object

   CryptoStream cs = new CryptoStream(fs,

      crypt.CreateEncryptor(),CryptoStreamMode.Write);

   // (4) Create StreamWriter using CryptoStream

   StreamWriter sw = new StreamWriter(cs);

   sw.Write(msg);

   sw.Close();

   cs.Close();

}

ReadEncrypt reverses the actions of WriteEncrypt. It decodes the data in the file stream and returns the data as a string object. To do this, it layers a CryptoStream stream on top of the FileStream to perform decryption. It then creates a StreamReader from the CryptoStream stream that actually reads the data from the stream.

// Read and decrypt a file stream.

private static string ReadEncrypt(FileStream fs) {

   // (1) Create Data Encryption Standard (DES) object

   DESCryptoServiceProvider crypt =

         new DESCryptoServiceProvider();

   // (2) Create a key and Initialization Vector

   crypt.Key = new byte[] {71,72,83,84,85,96,97,78};

   crypt.IV  = new byte[] {71,72,83,84,85,96,97,78};

   // (3) Create CryptoStream stream object

   CryptoStream cs = new CryptoStream(fs,

         crypt.CreateDecryptor(),CryptoStreamMode.Read);

   // (4) Create StreamReader using CryptoStream

   StreamReader sr = new StreamReader(cs);

   string msg = sr.ReadToEnd();

   sr.Close();

   cs.Close();

   return msg;

}


5.9. System.IO: Directories and Files

The System.IO namespace includes a set of system-related classes that are used to manage files and directories. Figure 5-9 shows a hierarchy of the most useful classes. Directory and DirectoryInfo contain members to create, delete, and query directories. The only significant difference in the two is that you use Directory with static methods, whereas a DirectoryInfo object must be created to use instance methods. In a parallel manner, File and FileInfo provide static and instance methods for working with files.

Figure 5-9. Directory and File classes in the System.IO namespace

FileSystemInfo

The FileSystemInfo class is a base class for DirectoryInfo and FileInfo. It defines a range of members that are used primarily to provide information about a file or directory. The abstract FileSystemInfo class takes advantage of the fact that files and directories share common features. Its properties include CreationTime, LastAccessTime, LastWriteTime, Name, and FullName. It also includes two important methods: Delete to delete a file or directory and Refresh that updates the latest file and directory information.

Here is a quick look at some of the FileSystemInfo members using DirectoryInfo and FileInfo objects. Note the use of the Refresh method before checking the directory and file attributes.

// DirectoryInfo

string dir  = @"c:\artists";

DirectoryInfo di = new DirectoryInfo(dir);

di.Refresh();

DateTime IODate = di.CreationTime;

Console.WriteLine("{0:d}",IODate)           // 10/9/2001

// FileInfo

string file = @"C:\artists\manet.jpg";

FileInfo fi = new FileInfo(file);

if (fi.Exists) {

   fi.Refresh();

   IODate = fi.CreationTime;

   Console.WriteLine("{0:d}",IODate);       // 5/15/2004

   Console.WriteLine(fi.Name);              // monet.txt

   Console.WriteLine(fi.Extension);         // .txt

   FileAttributes attrib = fi.Attributes;

   Console.WriteLine((int) attrib);         // 32

   Console.WriteLine(attrib);               // Archive

}

Working with Directories Using the DirectoryInfo, Directory, and Path Classes

When working with directories, you usually have a choice between using the instance methods of DirectoryInfo or the corresponding static methods of Directory. As a rule, if you are going to refer to a directory in several operations, use an instance of DirectoryInfo. Table 5-13 provides a comparison summary of the available methods.

Table 5-13. Comparison of Selected DirectoryInfo and Directory Members

DirectoryInfo

Directory

Member

Description

Member

Description

Create()

CreateSubdirectory()

Create a directory or subdirectory.

CreateDirectory()

Pass the string path to the method. Failure results in an exception.

Delete()

Delete a directory.

Delete(string)

Delete(string, bool)

First version deletes an empty directory. Second version deletes a directory and all subdirectories if boolean value is true.

Getdirectories()

Returns an array of DirectoryInfo type containing all subdirectories in the current directory.

GetDirectories(string)

GetDirectories(string,

  string filter)

Returns a string array containing the names of directories in the path. A filter may be used to specify directory names.

GetFiles()

Returns an array of FileInfo types containing all files in the directory.

GetFiles(string)

GetFiles(string, filter)

Returns string array of files in directory. A filter may be used to match against file names. The filter may contain wildcard characters ? or * to match a single character or zero or more characters.

Parent

Retrieves parent directory of current path.

GetParent()

Retrieves parent directory of specified path.

N/A

GetLogicalDrives()

Returns string containing logical drives on system. Format:

<drive>:\

Let's look at some examples using both static and instance methods to manipulate and list directory members. The sample code assumes the directory structure shown in Figure 5-10.

Figure 5-10. Directory structure used in Directory examples

Create a Subdirectory

This code adds a subdirectory named cubists below expressionists:

// Directory static method to create directory

string newPath =

      @"c:\artists\expressionists\cubists";

if (!Directory.Exists(newPath))

   Directory.CreateDirectory(newPath);

// DirectoryInfo

string curPath= @"c:\artists\expressionists";

di = new DirectoryInfo(curPath);

if (di.Exists) di.CreateSubdirectory(newPath);

Delete a Subdirectory

This code deletes the cubists subdirectory just created:

string newPath = @"c:\artists\expressionists\cubists";

// Directory

if (Directory.Exists(newPath)) Directory.Delete(newPath);

// The following fails because the directory still contains files

Directory.Delete(@"c:\artists\expressionists");

// The following succeeds because true is passed to the method

Directory.Delete(@"c:\artists\expressionists",true);

// DirectoryInfo

DirectoryInfo di = new DirectoryInfo(newPath);

If (di.Exists) di.Delete();

List Directories and Files

This code defines a method that recursively loops through and lists the subdirectories and selected files on the C:\artists path. It uses the static Directory methods GeTDirectories and GetFiles. Both of these return string values.

static readonly int Depth=4; // Specify directory level to search

ShowDir (@"c:\artists\", 0); // Call method to list files

// Display directories and files using recursion

public static void ShowDir(string sourceDir, int recursionLvl)

{

   if (recursionLvl<= Depth) // Limit subdirectory search depth

   {

      // Process the list of files found in the directory

      Console.WriteLine(sourceDir);

      foreach( string fileName in

               Directory.GetFiles(sourceDir,"s*.*"))

      {

         Console.WriteLine("  "+Path.GetFileName(fileName));

      // Use recursion to process subdirectories

      foreach(string subDir in

              Directory.GetDirectories(sourceDir))

         ShowDir(subDir,recursionLvl+1);  // Recursive call

   }

}

GetFiles returns a full path name. The static Path.GetFileName method is used to extract the file name and extension from the path. For demonstration purposes, a filter has been added to the GetFiles method to have it return only the path of files that begins with s.

Here is the same operation using the DirectoryInfo class. Its Getdirectories and GetFiles methods behave differently than the Directory versions: They return objects rather than strings, and they return the immediate directory or file name rather than the entire path.

// DirectoryInfo

public static void ShowDir(DirectoryInfo sourceDir,

                           int recursionLvl)

{

   if (recursionLvl<= Depth)  // Limit subdirectory search depth

   {

      // Process the list of files found in the directory

      Console.WriteLine(sourceDir.FullName);

      foreach( FileInfo fileName in

               sourceDir.GetFiles("s*.*"))

         Console.WriteLine("  "+fileName);

      // Use recursion to process subdirectories

      foreach(DirectoryInfo subDir in sourceDir.GetDirectories())

         ShowDir2(subDir,recursionLvl+1);  // Recursive call

   }

}

The method is called with two parameters: a DirectoryInfo object that encapsulates the path and an initial depth of 0.

DirectoryInfo dirInfo = new DirectoryInfo(@"c:\artists\");

ShowDir(dirInfo, 0);

Using the Path Class to Operate on Path Names

To eliminate the need for developers to create code that manipulates a path string, .NET provides a Path class that consists of static methods designed to operate on a path string. The methods—a shortcut for Regex patterns—extract selected parts of a path or return a boolean value indicating whether the path satisfies some criterion. Note that because the format of a path is platform dependent (a Linux path is specified differently than a Windows path), the .NET implementation of this class is tailored to the platform on which it runs.

To illustrate the static Path methods, let's look at the results of applying selected methods to this path:

string fullPath = @"c:\artists\impressionists\monet.htm";

Method | Returns
Path.GetDirectoryName(fullPath) | c:\artists\impressionists
Path.GetExtension(fullPath) | .htm
GetFileName(fullPath) | monet.htm
GetFullPath(fullPath) | c:\artists\impressionists\monet.htm
GetPathRoot(fullPath) | c:\
Path.HasExtension(fullPath) | TRue

Working with Files Using the FileInfo and File Classes

The FileInfo and File classes are used for two purposes: to provide descriptive information about a file and to perform basic file operations. The classes include methods to copy, move, and delete files, as well as open files for reading and writing. This short segment uses a FileInfo object to display a file's properties, and the static File.Copy method to copy a file:

string fname= @"c:\artists\impressionists\degas.txt";

// Using the FileInfo class to print file information

FileInfo fi = new FileInfo(fname);  // Create FileInfo object

if (fi.Exists)

{

   Console.Write("Length: {0}\nName: {1}\nDirectory: {2}",

   fi.Length, fi.Name, fi.DirectoryName);

   // output: --> 488  degas.txt  c:\artists\impressionists

}

// Use File class to copy a file to another directory

if (File.Exists(fname))

{

   try

   {

      // Exception is thrown if file exists in target directory

      // (source, destination, overwrite=false)

      File.Copy(fname,@"c:\artists\19thcentury\degas.txt",false);

   }

   catch(Exception ex)

   {

      Console.Write(ex.Message);

   }

}

Using FileInfo and File to Open Files

The File and FileInfo classes offer an alternative to creating FileStream, StreamWriter, and StreamReader objects directly. Table 5-14 summarizes the FileInfo methods used to open a file. The static File methods are identical except that their first parameter is always a string containing the name or path of the file to open.

Table 5-14. Selected FileInfo Methods for Opening a File

Member

Returns

Description

Open(mode)

Open(mode,access)

Open(mode,access,share)

FileStream

Opens a file with access and sharing privileges. The three overloads take FileMode, FileAccess, and FileShare enumerations.

Create()

FileStream

Creates a file and returns a FileStream object. If file exists, returns reference to it.

OpenRead()

FileStream

Opens the file in read mode.

OpenWrite()

FileStream

Opens a file in write mode.

AppendText()

StreamWriter

Opens the file in append mode. If file does not exist, it is created. Equivalent to StreamWriter(string, true).

CreateText()

StreamWriter

Opens a file for writing. If the file exists, its contents are overwritten. Equivalent to StreamWriter(string, false).

OpenText()

StreamReader

Opens the file in read mode. Equivalent to StreamReader(string).

The FileInfo.Open method is the generic and most flexible way to open a file:

public FileStream Open(FileMode mode, FileAccess access,

                       FileShare share)

Create, OpenRead, and OpenWrite are specific cases of Open that offer an easy-to-use method that returns a FileStream object and requires no parameters. Similarly, the OpenText, AppendText, and CreateText methods return a StreamReader or StreamWriter object.

The decision to create a FileStream (or StreamReader/StreamWriter) using FileInfo or the FileStream constructor should be based on how the underlying file is used in the application. If the file is being opened for a single purpose, such as for input by a StreamReader, creating a FileStream directly is the best approach. If multiple operations are required on the file, a FileInfo object is better. This example illustrates the advantages of using FileInfo. First, it creates a FileStream that is used for writing to a file; then, another FileStream is created to read the file's contents; finally, FileInfo.Delete is used to delete the file.

FileInfo     fi = new FileInfo(@"c:\temp.txt");

FileStream   fs = fi.Create();          // Create file

StreamWriter sw= new StreamWriter(fs);  // Create StreamWriter

sw.Write("With my crossbow\nI shot the Albatross. ");

sw.Close();                   // Close StreamWriter

// Now use fi to create a StreamReader

fs = fi.OpenRead();           // Open for reading

StreamReader sr = new StreamReader(fs);

while(( string l = sr.ReadLine())!= null)

{

   Console.WriteLine(l);      // --> With my crossbow

}                             // --> I shot the Albatross.

sr.Close();

fs.Close();

fi.Delete();                  // Delete temporary file


5.10. Summary

The demands of working with text have increased considerably from the days when it meant dealing with 7-bit ASCII or ANSII characters. Today, the Unicode standard defines the representation of more than 90,000 characters comprising the world's alphabets. We've seen that .NET fully embraces this standard with its 16-bit characters. In addition, it supports the concept of localization, which ensures that a machine's local culture information is taken into account when manipulating and representing data strings.

String handling is facilitated by a rich set of methods available through the String and StringBuilder classes. A variety of string comparison methods are available with options to include case and culture in the comparisons. The String.Format method is of particular note with its capacity to display dates and numbers in a wide range of standard and custom formats. String manipulation and concatenation can result in an inefficient use of memory. We saw how the StringBuilder class is an efficient alternative for basic string operations. Applications that require sophisticated pattern matching, parsing, and string extraction can use regular expressions in conjunction with the Regex class.

The System.IO namespace provides a number of classes for reading and writing data: The FileStream class is used to process raw bytes of data; MemoryStream and BufferedStream allow bytes to be written to memory or buffered; the StreamReader and StreamWriter classes support the more traditional line-oriented I/O. Operations related to managing files and directories are available as methods on the File, FileInfo, Directory, and DirectoryInfo classes. These are used to create, copy, delete, and list files and directories.


5.11. Test Your Understanding

1:

Which class encapsulates information about a specific culture?

2:

Name two ways to access the default culture within an application.

3:

Match each regular expression:

  1.
@"(\b[^\Wa-z0-9_][^\WA-Z0-9_]*\b)"

  2.
@"(\b[^\Wa-z0-9_]+\b)"

  3.
@"(\b[^\WA-Z0-9_]+\b)"

with the function it performs:

  1. Find all capitalized words.

  2. Find all lowercase words.

  3. Find all words with the initial letter capitalized.

4:

When is it more advantageous to use the instance methods of Regex rather than the static ones?

5:

Which string comparison method(s) is (are) used to implement this statement:

if (myString == "Monday") bool sw = true;

6:

Match each statement:

  1.
curdt.ToString("D")

  2.
curdt.ToString("M")

  3.
curdt.ToString("ddd MMM dd")

with its output:

  1. March 9

  2. Tuesday, March 9, 2004

  3. Tue Mar 09

7:

Which of these objects is not created from an existing FileStream?

  1.
FileInfo

  2.
StreamReader

  3.
BufferedStream

8:

You can create a FileStream object with this statement:

FileStream fs = new FileStream(fname,

                   FileMode.OpenOrCreate,

                   FileAccess.Write,FileShare.None);

Which one of the following statements creates an identical FileStream using an existing FileInfo object, fi?

  1.
fs = fi.OpenWrite();

  2.
fs = fi.Create();

  3.
fs = fi.CreateText();

9:

Indicate whether the following comparisons are true or false:

  1.
(string.Compare("Alpha","alpha") >0)

  2.
(string.Compare("Alpha","alpha",true) ==0)

  3.
(string.CompareOrdinal("Alpha","alpha")>0)

  4.
(string.Equals("alpha","Alpha"))


Chapter 6. Building Windows Forms Applications

Topics in This Chapter

  • Introduction: With just a few lines of code, you can build a Windows Forms (WinForms) application that demonstrates the basics of event handling and creating child forms.

  • Using Form Controls: All controls inherit from the base Control class. The members of this class provide a uniform way of positioning, sizing, and modifying a control's appearance.

  • The Form Class: The Form class includes custom properties that affect its appearance, and enable it to work with menus and manage child forms.

  • Message and Dialog Boxes: A pop-up window to provide user interaction or information can be created as a message or dialog box.

  • MDI Forms: A Multiple Document Interface (MDI) is a container that holds child forms. A main menu permits the forms to be organized, accessed, and manipulated.

  • Working with Menus: .NET supports two types of menus: a main form menu and a context menu that can be associated with individual controls.

  • Adding Help to a Form: Help buttons, ToolTips, and HTML Help are options for adding help to a form.

  • Form Inheritance: Visual inheritance enables a form to be created quickly, by inheriting the interface elements and functionality from another form.

This chapter is aimed at developers responsible for creating Graphical User Interface (GUI) applications for the desktop—as opposed to applications that run on a Web server or mobile device. The distinction is important because .NET provides separate class libraries for each type of application and groups them into distinct namespaces:

  • System.Windows.Forms. Windows Forms (WinForms).

  • System.Web.UIWebControls. Web Forms.

  • System.Web.UIMobileControls. Mobile Forms for hand-held and pocket devices.

Although this chapter focuses on Windows Forms, it is important to recognize that modern applications increasingly have to support multiple environments. Acknowledging this, .NET strives to present a uniform "look and feel" for applications developed in each. The forms and controls in all three namespaces provide similar, but not identical, functionality. The knowledge you acquire in this chapter will shorten the learning curve required to develop .NET applications for the Web and mobile devices.

Developers usually rely on an Integrated Development Environment (IDE), such as Visual Studio, to develop GUI applications. This makes it easy to overlook the fact that a form is a class that inherits from other classes and has its own properties and methods. To provide a true understanding of forms, this chapter peers beneath the IDE surface at the class members and how their implementation defines and affects the behavior of the form. The discussion includes how to display forms, resize them, make them scrollable or transparent, create inherited forms, and have them react to mouse and keyboard actions.

This is not a chapter about the principles of GUI design, but it does demonstrate how adding amenities, such as Help files and a tab order among controls, improves form usability. Controls, by the way, are discussed only in a generic sense. A detailed look at specific controls is left to Chapter 7, "Windows Forms Controls."


6.1. Programming a Windows Form

All Windows Forms programs begin execution at a designated main window. This window is actually a Form object that inherits from the System.Windows. Forms.Form class. The initial window is displayed by passing an instance of it to the static Application.Run method.

The challenge for the developer is to craft an interface that meets the basic rule of design—form follows function. This means that a form's design should support its functionality to the greatest extent possible. To achieve this, a developer must understand the properties, methods, and events of the Form class, as well as those of the individual controls that are placed on the form.

Building a Windows Forms Application by Hand

Let's create a simple Windows application using a text editor and the C# compiler from the command line. This application, shown in Listing 6-1, consists of a single window with a button that pops up a message when the button is clicked. The simple exercise demonstrates how to create a form, add a control to it, and set up an event handler to respond to an event fired by the control.

Listing 6-1. Basic Windows Forms Application Built by Hand

using System;

using System.Windows.Forms;

using System.Drawing;

class MyWinApp

{

   static void Main()

   {

   // (1) Create form and invoke it

   Form mainForm = new SimpleForm();

   Application.Run(mainForm);

   }

}

// User Form derived from base class Form

class SimpleForm:Form

{

   private Button button1;

   public SimpleForm() {

      this.Text = "Hand Made Form";

      // (2) Create a button control and set some attributes

      button1 = new Button();

      button1.Location = new Point(96,112);

      button1.Size = new Size(72,24);

      button1.Text= "Status";

      this.Controls.Add(button1);

      // (3) Create delegate to call routine when click occurs

      button1.Click += new EventHandler(button1_Click);

   }

   void button1_Click(object sender, EventArgs e) {

      MessageBox.Show("Up and Running");

   }

}

Recall from Chapter 1, "Introduction to .NET and C#," that command-line compilation requires providing a target output file and a reference to any required assemblies. In this case, we include the System.Windows.Forms assembly that contains the necessary WinForms classes. To compile, save the preceding source code as winform.cs and enter the following statement at the command prompt:

csc /t:winform.exe /r:System.Windows.Forms.dll winform.cs

After it compiles, run the program by typing winform; the screen shown in Figure 6-1 should appear. The output consists of a parent form and a second form created by clicking the button. An important point to note is that the parent form cannot be accessed as long as the second window is open. This is an example of a modal form, where only the last form opened can be accessed. The alternative is a modeless form, in which a parent window spawns a child window and the user can access either the parent or child window(s). Both of these are discussed later.

Figure 6-1. Introductory Windows application

The code breaks logically into three sections:

  1. Form Creation.

    The parent form is an instance of the class SimpleForm, which inherits from Form and defines the form's custom features. The form—and program—is invoked by passing the instance to the Application.Run method.

  2. Create Button Control.

    A control is placed on a form by creating an instance of the control and adding it to the form. Each form has a Controls property that returns a Control.Collection type that represents the collection of controls contained on a form. In this example, the Controls.Add method is used to add a button to the form. Note that a corresponding Remove method is also available to dynamically remove a control from its containing form. An IDE uses this same Add method when you drag a control onto a form at design time. However, if you want to add or delete controls at runtime, you will be responsible for the coding.

    Controls have a number of properties that govern their appearance. The two most basic ones are Size and Location. They are implemented as:

button1.Size = new Size(72,24);   // width, height

button1.Location = new Point(96,112); //x,y

    The struct Size has a constructor that takes the width and height as parameters; the constructor for Point accepts the x and y coordinates of the button within the container.

  3. Handle Button Click Event.

    Event handling requires providing a method to respond to the event and creating a delegate that invokes the method when the event occurs (refer to Chapter 3, "Class Design in C#," for details of event handling). In this example, button1_Click is the method that processes the event. The delegate associated with the Click event is created with the following statement:

button1.Click += new EventHandler(button1_Click);

    This statement creates an instance of the built-in delegate EventHandler and registers the method button1_Click with it.

Core Note

.NET 2.0 adds a feature known as Partial Types, which permits a class to be physically separated into different files. To create a partial class, place the keyword partial in front of class in each file containing a segment of the class. Note that only one class declaration should specify Forms inheritance. The compilation process combines all the files into one class—identical to a single physical class. For Windows applications, partial classes seem something of a solution in search of a problem. However, for Web applications, they serve a genuine need, as is discussed in Chapter 16, "ASP.NET Web Forms and Controls."

This exercise should emphasize the fact that working with forms is like working with any other classes in .NET. It requires gaining a familiarity with the class members and using standard C# programming techniques to access them.


6.2. Windows.Forms Control Classes

The previous examples have demonstrated how a custom form is derived from the Windows.Forms.Form class. Now, let's take a broader look at the class hierarchy from which the form derives and the functionality that each class offers (see Figure 6-2).

Figure 6-2. Windows Forms class hierarchy

The Control Class

It may seem counterintuitive that the Form class is below the Control class on the hierarchical chain, because a form typically contains controls. But the hierarchy represents inheritance, not containment. A Form is a container control, and the generic members of the Control class can be applied to it just as they are to a simple Label or TextBox.

System.Windows.Forms.dll contains more than fifty controls that are available for use on a Windows Form. A few that inherit directly from Control are listed in Figure 6-2. All controls share a core set of inherited properties, methods, and events. These members allow you to size and position the control; adorn it with text, colors, and style features; and respond to keyboard and mouse events. This chapter examines the properties that are common to all inheriting controls; Chapter 7 offers a detailed look at the individual controls and how to use their distinct features in applications.

Control Properties

Table 6-1 defines some of the properties common to most types that inherit from Control.

Table 6-1. Common Control Properties

Category

Property

Description

Size and position

Size

A Size object that exposes the width and height.

Location

Location is a Point object that specifies the x and y coordinates of the top left of the control.

Width, Height, Top, Left, Right

These int values are derived from the size and location of the object. For example, Right is equal to Left + Width; Bottom is equal to Top + Height.

Bounds

Bounds is a rectangle that defines a control's position and size:

Button.Bounds =

   new Rectangle (10,10,50,60)

ClientRectangle

ClientRectangle is a rectangle that represents the client area of the control—that is, the area excluding scroll bars, titles, and so on.

Anchor

Specifies which edges of the container the control is anchored to. This is useful when the container is resized.

Dock

Controls which edge of the container the control docks to.

Color and appearance

BackColor, ForeColor

Specifies the background and foreground color for the control. Color is specified as a static property of the Color structure.

BackGroundImage

BackGroundImage specifies an image to be used in the background of the control.

Text

Text

Text associated with the control.

Font

Font describes the characteristics of the text font: typeface, size, bold, and so on.

Focus

TabIndex

int value that indicates the tab order of this control within its container.

TabStop

Boolean value that indicates whether this control can receive focus from the Tab key.

Focused

Boolean value indicating whether a control has input focus.

Visible

Boolean value indicating whether the control is displayed.

Keyboard and mouse

MouseButtons

Returns the current state of the mouse buttons (left, right, and middle).

MousePosition

Returns a Point type that specifies the cursor position.

ModifierKeys

Indicates which of the modifier keys (Shift, Ctrl, Alt) is pressed.

Cursor

Specifies the shape of the mouse pointer when it is over the control. Assigned value is a static property of the Cursors class. These include:

.Hand    .Cross   UpArrow      Default

.Beam    .Arrow   WaitCursor

Runtime status

Handle

int value representing the handle of Windows control.

Focused

bool value indicating whether the control has focus.

Working with Controls

When you drag a control onto a form, position it, and size it, VisualStudio.NET (VS.NET) automatically generates code that translates the visual design to the underlying property values. There are times, however, when a program needs to modify controls at runtime to hide them, move them, and even resize them. In fact, size and position are often based on the user's screen size, which can only be detected at runtime. An IDE cannot do this, so it is necessary that a programmer understand how these properties are used to design an effective control layout.

Size and Position

As we saw in the earlier example, the size of a control is determined by the Size object, which is a member of the System.Drawing namespace:

button1.Size = new Size(80,40);  // (width, height)

button2.Size = button1.Size;     // Assign size to second button

A control can be resized during runtime by assigning a new Size object to it. This code snippet demonstrates how the Click event handler method can be used to change the size of the button when it is clicked:

private void button1_Click(object sender, System.EventArgs e)

{

   MessageBox.Show("Up and Running");

   Button button1 = (Button) sender;  //Cast object to Button

   button1.Size = new Size(90,20);    //Dynamically resize

The System.Drawing.Point object can be used to assign a control's location. Its arguments set the x and y coordinates—in pixels—of the upper-left corner of a control. The x coordinate is the number of pixels from the left side of the container. The y coordinate is the number of pixels from the top of the container.

button1.Location = new Point(20,40);  // (x,y) coordinates

It is important to recognize that this location is relative to the control's container. Thus, if a button is inside a GroupBox, the button's location is relative to it and not the form itself. A control's Location also can be changed at runtime by assigning a new Point object.

Another approach is to set the size and location in one statement using the Bounds property:

button1.Bounds = new Rectangle(20,40, 100,80);

A Rectangle object is created with its upper-left corner at the x,y coordinates (20,40) and its lower-right coordinates at (100,80). This corresponds to a width of 80 pixels and a height of 40 pixels.

How to Anchor and Dock a Control

The Dock property is used to attach a control to one of the edges of its container. By default, most controls have docking set to none; some exceptions are the StatusStrip/StatusBar that is set to Bottom and the ToolStrip/ToolBar that is set to Top. The options, which are members of the DockStyle enumeration, are Top, Bottom, Left, Right, and Fill. The Fill option attaches the control to all four corners and resizes it as the container is resized. To attach a TextBox to the top of a form, use

TextBox1.Dock = DockStyle.Top;

Figure 6-3 illustrates how docking affects a control's size and position as the form is resized. This example shows four text boxes docked, as indicated.

Figure 6-3. Control resizing and positioning using the Dock property

[View full size image]

Resizing the form does not affect the size of controls docked on the left or right. However, controls docked to the top and bottom are stretched or shrunk horizontally so that they take all the space available to them.

Core Note

The Form class and all other container controls have a DockPadding property that can be set to control the amount of space (in pixels) between the container's edge and the docked control.

The Anchor property allows a control to be placed in a fixed position relative to a combination of the top, left, right, or bottom edge of its container. Figure 6-4 illustrates the effects of anchoring.

Figure 6-4. How anchoring affects the resizing and positioning of controls

The distance between the controls' anchored edges remains unchanged as the form is stretched. The PictureBox (1) is stretched horizontally and vertically so that it remains the same distance from all edges; the Panel (2) control maintains a constant distance from the left and bottom edge; and the Label (3), which is anchored only to the top, retains its distance from the top, left, and right edges of the form.

The code to define a control's anchor position sets the Anchor property to values of the AnchorStyles enumeration (Bottom, Left, None, Right, Top). Multiple values are combined using the OR ( | ) operator:

btnPanel.Anchor = (AnchorStyles.Bottom | AnchorStyles.Left);

Tab Order and Focus

Tab order defines the sequence in which the input focus is given to controls when the Tab key is pressed. The default sequence is the order in which the controls are added to the container.

The tab order should anticipate the logical sequence in which users expect to input data and thus guide them through the process. The form in Figure 6-5 represents such a design: The user can tab from the first field down to subsequent fields and finally to the button that invokes the final action.

Figure 6-5. Tab order for controls on a form

Observe two things in the figure: First, even though labels have a tab order, they are ignored and never gain focus; and second, controls in a container have a tab order that is relative to the container—not the form.

A control's tab order is determined by the value of its TabIndex property:

TextBox1.TabIndex = 0;  //First item in tab sequence

In VS.NET, you can set this property directly with the Property Manager, or by selecting ViewTabOrder and clicking the boxes over each control to set the value. If you do not want a control to be included in the tab order, set its TabStop value to false. This does not, however, prevent it from receiving focus from a mouse click.

When a form is loaded, the input focus is on the control (that accepts mouse or keyboard input) with the lowest TabIndex value. During execution, the focus can be given to a selected control using the Focus method:

if(textBox1.CanFocus)

{ textBox1.Focus(); }

Iterating Through Controls on a Form

All controls on a form are contained in a Controls collection. By enumerating through this collection, it is possible to examine each control, identify it by name and type, and modify properties as needed. One common use is to clear the value of selected fields on a form in a refresh operation. This short example examines each control in Figure 6-5 and displays its name and type:

int ctrlCt = this.Controls.Count;   //  8

foreach (Control ct in this.Controls)

{

   object ob = ct.GetType();

   MessageBox.Show(ob.ToString());  //Displays type as string

   MessageBox.Show(ct.Name);

}

There are several things to be aware of when enumerating control objects:

  • The type of each control is returned as a fully qualified name. For example, a TextBox is referred to as System.Forms.Form.TextBox.

  • Only a container's top-level objects are listed. In this example, the Controls.Count value is 8 rather than 10 because the GroupBox is counted as one control and its child controls are excluded.

  • You can use a control's HasChildren property to determine if it is a container. Listing 6-2 uses recursion to list all child controls.

Listing 6-2. Enumerate All Controls on a Form Recursively

void IterateThroughControls(Control parent)

{

   foreach (Control c in parent.Controls)

   {

      MessageBox.Show(c.ToString());

      if(c.HasChildren)

      {

         IterateThroughControls(c);

      }

   }

}

Applying this code to Figure 6-5 results in all controls on the main form being listed hierarchically. A control is listed followed by any child it may have.

Control Events

When you push a key on the keyboard or click the mouse, the control that is the target of this action fires an event to indicate the specific action that has occurred. A registered event handling routine then responds to the event and formulates what action to take.

The first step in handling an event is to identify the delegate associated with the event. You must then register the event handling method with it, and make sure the method's signature matches the parameters specified by the delegate. Table 6-2 summarizes the information required to work with mouse and keyboard triggered events.

Table 6-2. Control Events

Event

Built-In Delegate/Parameters

Description

Click,

DoubleClick,

MouseEnter,

MouseLeave,

MouseHover,

MouseWheel

EventHandler (

   object sender, EventArgs e)

Events triggered by clicking, double clicking, or moving the mouse.

MouseDown,

MouseUp,

MouseMove

MouseEventHandler (

   object sender, MouseEventArgs)

Events triggered by mouse and mouse button motions. Note that this event is not triggered if the mouse action occurs within a control in the current container.

KeyUp,

KeyDown

KeyEventHandler (

   object sender, KeyEventArgs e)

Events triggered by key being raised or lowered.

KeyPress

KeyPressEventHandler (

   object sender,

   KeyPressEventArgs e)

Event triggered by pressing any key.

Handling Mouse Events

In addition to the familiar Click and DoubleClick events, all Windows Forms controls inherit the MouseHover, MouseEnter, and MouseLeave events. The latter two are fired when the mouse enters and leaves the confines of a control. They are useful for creating a MouseOver effect that is so common to Web pages.

To illustrate this, let's consider an example that changes the background color on a text box when a mouse passes over it. The following code sets up delegates to call OnMouseEnter and OnMouseLeave to perform the background coloring:

TextBox userID = new TextBox();

userID.MouseEnter += new EventHandler(OnMouseEnter);

userID.MouseLeave += new EventHandler(OnMouseLeave);

The event handler methods match the signature of the EventHandler delegate and cast the sender parameter to a Control type to access its properties.

private void OnMouseEnter(object sender, System.EventArgs e){

   Control ctrl = (Control) sender;

   ctrl.BackColor= Color.Bisque;

}

private void OnMouseLeave(object sender, System.EventArgs e){

   Control ctrl = (Control) sender;

   ctrl.BackColor= Color.White;

}

Core Note

It is possible to handle events by overriding the default event handlers of the base Control class. These methods are named using the pattern Oneventname—for example, OnMouseDown, OnMouseMove, and so on. To be consistent with .NET, the delegate approach is recommended over this. It permits a control to specify multiple event handlers for an event, and it also permits a single event handler to process multiple events.

The delegates for the MouseDown, MouseUp, and MouseMove events take a second argument of the MouseEventArgs type rather than the generic EventArgs type. This type reveals additional status information about a mouse via the properties shown in Table 6-3.

Table 6-3. Properties of MouseEventArgs

Property

Description

Button

Indicates which mouse button was pressed. The attribute value is defined by the MouseButtons enumeration: Left, Middle, None, Right

Clicks

Number of clicks since last event.

Delta

Number of detents the mouse wheel rotates. A positive number means it's moving forward; a negative number shows backward motion.

X, Y

Mouse coordinates relative to the container's upper-left corner. This is equivalent to the control's MousePosition property.

The properties in this table are particularly useful for applications that must track mouse movement across a form. Prime examples are graphics programs that rely on mouse location and button selection to control onscreen drawing. To illustrate, Listing 6-3 is a simple drawing program that draws a line on a form's surface, beginning at the point where the mouse key is pressed and ending where it is raised. To make it a bit more interesting, the application draws the line in black if the left button is dragged, and in red if the right button is dragged.

Listing 6-3. Using Mouse Events to Draw on a Form

using System;

using System.Windows.Forms;

using System.Drawing;

class MyWinApp

{

   static void Main() {

   Form mainForm = new DrawingForm();

   Application.Run(mainForm);

   }

}

// User Form derived from base class Form

class DrawingForm:Form

{

   Point lastPoint = Point.Empty; // Save coordinates

   public Pen myPen;      //Defines color of line

   public DrawingForm()

   {

      this.Text = "Drawing Pad";

      // reate delegates to call MouseUp and MouseDown

      this.MouseDown += new MouseEventHandler(OnMouseDown);

      this.MouseUp   += new MouseEventHandler(OnMouseUp);

   }

   private void OnMouseDown(object sender, MouseEventArgs e)

   {

      myPen = (e.Button==MouseButtons.Right)? Pens.Red:

            Pens.Black;

      lastPoint.X = e.X;

      lastPoint.Y = e.Y;

   }

   private void OnMouseUp(object sender, MouseEventArgs e)

   {

      // Create graphics object

      Graphics g = this.CreateGraphics();

      if (lastPoint != Point.Empty)

         g.DrawLine(myPen, lastPoint.X,lastPoint.Y,e.X,e.Y);

      lastPoint.X = e.X;

      lastPoint.Y = e.Y;

      g.Dispose();

   }

}

Even without an understanding of .NET graphics, the role of the graphics-related classes should be self-evident. A Graphics object is created to do the actual drawing using its DrawLine method. The parameters for this method are the Pen object that defines the color and the coordinates of the line to be drawn. When a button is pressed, the program saves the coordinate and sets myPen based on which button is pressed: a red pen for the right button and black for the left. When the mouse button is raised, the line is drawn from the previous coordinate to the current location. The MouseEventArgs object provides all the information required to do this.

Handling Keyboard Events

Keyboard events are also handled by defining a delegate to call a custom event handling method. Two arguments are passed to the event handler: the sender argument identifies the object that raised the event and the second argument contains fields describing the event. For the KeyPress event, this second argument is of the KeyPressEventArgs type. This type contains a Handled field that is set to TRue by the event handling routine to indicate it has processed the event. Its other property is KeyChar, which identifies the key that is pressed.

KeyChar is useful for restricting the input that a field accepts. This code segment demonstrates how easy it is to limit a field's input to digits. When a non-digit is entered, Handled is set to TRue, which prevents the form engine from displaying the character. Otherwise, the event handling routine does nothing and the character is displayed.

private void OnKeyPress(object sender,  KeyPressEventArgs e)

{

   if (! char.IsDigit(e.KeyChar)) e.Handled = true;

}

The KeyPress event is only fired for printable character keys; it ignores non-character keys such as Alt or Shift. To recognize all keystrokes, it is necessary to turn to the KeyDown and KeyUp events. Their event handlers receive a KeyEventArgs type parameter that identifies a single keystroke or combination of keystrokes. Table 6-4 lists the important properties provided by KeyEventArgs.

Table 6-4. KeyEventArgs Properties

Member

Description

Alt, Control, Shift

Boolean value that indicates whether the Alt, Control, or Shift key was pressed.

Handled

Boolean value that indicates whether an event was handled.

KeyCode

Returns the key code for the event. This code is of the Keys enumeration type.

KeyData

Returns the key data for the event. This is also of the Keys enumeration type, but differs from the KeyCode in that it recognizes multiple keys.

Modifiers

Indicates which combination of modifier keys (Alt, Ctrl, and Shift) was pressed.

A few things to note:

  • The Alt, Control, and Shift properties are simply shortcuts for comparing the KeyCode value with the Alt, Control, or Shift member of the Keys enumeration.

  • KeyCode represents a single key value; KeyData contains a value for a single key or combination of keys pressed.

  • The Keys enumeration is the secret to key recognition because its members represent all keys. If using Visual Studio.NET, Intellisense lists all of its members when the enum is used in a comparison; otherwise, you need to refer to online documentation for the exact member name because the names are not always what you expect. For example, digits are designated by D1, D2, and so on.

The preceding code segment showed how to use KeyPress to ensure a user presses only number keys (0–9). However, it does not prevent one from pasting non-numeric data using the Ctrl-V key combination. A solution is to use the KeyDown event to detect this key sequence and set a flag notifying the KeyPress event handler to ignore the attempt to paste.

In this example, two event handlers are registered to be called when a user attempts to enter data into a text box using the keyboard. KeyDown is invoked first, and it sets paste to true if the user is pushing the Ctrl-V key combination. The KeyPress event handler uses this flag to determine whether to accept the key strokes.

private bool paste;

//Register event handlers for TextBox t.

//They should be registered in this order,

//because the last registered is the

//first executed

t.KeyPress += new KeyPressEventHandler(OnKeyPress);

t.KeyDown  += new KeyEventHandler(OnKeyDown);

private void OnKeyDown(object sender, KeyEventArgs e)

{

if (e.Modifiers == Keys.Control  && e.KeyCode == Keys.V)

{

   paste=true; //Flag indicating paste attempt

   string msg = string.Format("Modifier: {0} \nKeyCode: {1}

       \nKeyData: {2}", e.Modifiers.ToString(),

        e.KeyCode.ToString(), e.KeyData.ToString());

   MessageBox.Show(msg);      //Display property values

}

private void OnKeyPress(object sender, KeyPressEventArgs e)

{

   if (paste==true)  e.Handled = true;

}

This program displays the following values for the selected KeyEventArgs properties when Ctrl-V is pressed:

Modifier:  Control

KeyCode:   V

KeyData:   Control, V


6.3. The Form Class

The Form object inherits all the members of the Control class as well as the ScrollableControl class, which provides properties that enable scrolling. To this it adds a large number of properties that enable it to control its appearance, work with child forms, create modal forms, display menus, and interact with the desktop via tool and status bars. Table 6-5 shows a selected list of these properties.

Table 6-5. Selected Form Properties

Category

Property

Description

Appearance

FormBorderStyle

Gets or sets the border style of the form. It is defined by the FormBorderStyle enumeration:

Fixed3D        None

FixedSingle    Sizable

FixedDialog

ControlBox

Boolean value that determines whether the menu icon in the left corner of a form and the close button on the upper right are shown.

MaximizeBox

MinimizeBox

Boolean value that indicates whether these buttons are displayed on the form.

Opacity

Gets or sets the opacity of the form and all of its controls. The maximum value (least transparent) is 1.0. Does not work with Windows 95/98.

transparencyKey

A color that represents transparent areas on the form. Any control or portion of the form that has a back color equal to this color is not displayed. Clicking this transparent area sends the event to any form below it.

Size and position

AutoScale

Indicates whether the form adjusts its size to accommodate the size of the font used.

ClientSize

Size of the form excluding borders and the title bar.

DesktopLocation

A Point type that indicates where the form is located on the desktop window.

StartPosition

Specifies the initial position of a form. It takes a FormStartPosition enum value:

CenterParent— Centered within bounds of parent form.

CenterScreen— Centered within the display.

Manual— Use the DeskTopLocation value.

Windows DefaultLocation— Windows sets value.

MinimumSize

MaximumSize

A Size object that designates the maximum and minimum size for the form. A value of (0,0) indicates no minimum or maximum.

ShowInTaskBar

Boolean value specifying whether application is represented in Windows task bar. Default is true.

TopLevel

TopMost

Indicates whether to display the form as a TopLevel window or TopMost window. A top-level window has no parent; top-most form is always displayed on top of all other non-TopMost forms.

WindowState

Indicates how the form is displayed on startup. It takes a value from the FormWindowState enumeration: Maximized, Minimized, or Normal.

Owner forms

Owner

The form designated as the owner of the form.

OwnedForms

A Form array containing the forms owned by a form.

Setting a Form's Appearance

The four properties shown in Figure 6-6 control which buttons and icon are present on the top border of a form. The Icon property specifies the .ico file to be used as the icon in the left corner; the ControlBox value determines whether the icon and close button are displayed (true) or not displayed (false); similarly, the MaximizeBox and MinimizeBox determine whether their associated buttons appear.

Figure 6-6. Properties to control what appears on the title bar

The purpose of these properties is to govern functionality more than appearance. For example, it often makes sense to suppress the minimize and maximize buttons on modal forms in order to prevent a user from maximizing the form and hiding the underlying parent form.

Form Opacity

A form's opacity property determines its level of transparency. Values ranges from 0 to 1.0, where anything less than 1.0 results in partial transparency that allows elements beneath the form to be viewed. Most forms work best with a value of 1.0, but adjusting opacity can be an effective way to display child or TopMost forms that hide an underlying form. A common approach is to set the opacity of such a form to 1.0 when it has focus, and reduce the opacity when it loses focus. This technique is often used with search windows that float on top of the document they are searching.

Let's look at how to set up a form that sets its opacity to 1 when it is active and to .8 when it is not the active form. To do this, we take advantage of the Deactivate and Activated events that are triggered when a form loses or gains focus. We first set up the delegates to call the event handling routines:

this.Deactivate += new System.EventHandler(Form_Deactivate);

this.Activated  += new System.EventHandler(Form_Activate);

The code for the corresponding event handlers is trivial:

void Form_Deactivate(object sender, EventArgs e)

{   this.Opacity= .8;   }

void Form_Activate(object sender, EventArgs e)

{   this.Opacity= 1;   }

Form Transparency

Opacity affects the transparency of an entire form. There is another property, TRansparencyKey, which can be used to make only selected areas of a form totally transparent. This property designates a pixel color that is rendered as transparent when the form is drawn. The effect is to create a hole in the form that makes any area below the form visible. In fact, if you click a transparent area, the event is recognized by the form below.

The most popular use of transparency is to create non-rectangular forms. When used in conjunction with a border style of FormBorderStyle.None to remove the title bar, a form of just about any geometric shape can be created. The next example illustrates how to create and use the cross-shaped form in Figure 6-7.

Figure 6-7. Form using transparency to create irregular appearance

The only requirement in creating the shape of the form is to lay out the transparent areas in the same color as the transparency key color. Be certain to select a color that will not be used elsewhere on the form. A standard approach is to set the back color of the form to the transparency key color, and draw an image in a different color that it will appear as the visible form.

To create the form in Figure 6-7, place Panel controls in each corner of a standard form and set their BackColor property to Color.Red. The form is created and displayed using this code:

CustomForm myForm = new CustomForm();

myForm.TransparencyKey = Color.Red;

myForm.FormBorderStyle= FormBorderStyle.None;

myForm.Show();

This achieves the effect of making the panel areas transparent and removing the title bar. With the title bar gone, it is necessary to provide a way for the user to move the form. This is where the mouse event handling discussed earlier comes to the rescue.

At the center of the form is a multiple arrow image displayed in a PictureBox that the user can click and use to drag the form. Listing 6-4 shows how the MouseDown, MouseUp, and MouseMove events are used to implement form movement.

Listing 6-4. Using Form Transparency to Create a Non-Rectangular Form

using System;

using System.Drawing;

using System.Collections;

using System.ComponentModel;

using System.Windows.Forms;

public class CustomForm : Form

{

   private Point lastPoint = Point.Empty;  //Save mousedown

   public CustomForm()

   {

      InitializeComponent();  // set up form

      //Associate mouse events with pictureBox

      pictureBox1.MouseDown += new MouseEventHandler(

            OnMouseDown);

      pictureBox1.MouseUp   += new MouseEventHandler(OnMouseUp);

      pictureBox1.MouseMove += new MouseEventHandler(

            OnMouseMove);

   }

   private void OnMouseDown(object sender, MouseEventArgs e)

   {

      lastPoint.X = e.X;

      lastPoint.Y = e.Y;

   }

   private void OnMouseUp(object sender, MouseEventArgs e)

   {

      lastPoint = Point.Empty;

   }

   //Move the form in response to the mouse being dragged

   private void OnMouseMove(object sender, MouseEventArgs e)

   {

      if (lastPoint != Point.Empty)

      {

         //Move form in response to mouse movement

         int xInc = e.X - lastPoint.X;

         int yInc = e.Y - lastPoint.Y;

         this.Location = new Point(this.Left + xInc,

                       this.Top+yInc);

      }

   }

   // Close Window

   private void button1_Click(object sender, System.EventArgs e)

   {

      this.Close();

   }

}

The logic is straightforward. When the user clicks the PictureBox, the coordinates are recorded as lastPoint. As the user moves the mouse, the Location property of the form is adjusted to reflect the difference between the new coordinates and the original saved location. When the mouse button is raised, lastPoint is cleared. Note that a complete implementation should also include code to handle form resizing.

Setting Form Location and Size

The initial location of a form is determined directly or indirectly by its StartPosition property. As described in Table 6-6, it takes its value from the FormStartPosition enumeration. These values allow it to be placed in the center of the screen, in the center of a parent form, at a Windows default location, or at an arbitrarily selected location. Manual offers the greatest flexibility because it allows the program to set the location.

Table 6-6. The Life Cycle of a Modeless Form

Action

Events Triggered

Description

Form object created

The form's constructor is called. In Visual Studio, the InitializeComponent method is called to initialize the form.

Form displayed:

Form.Show()

Form.Load

Form.Activated

The Load event is called first, followed by the Activated event.

Form activated

Form.Activated

This occurs when the user selects the form. This becomes an "active" form.

Form deactivated

Form.Deactivate

Form is deactivated when it loses focus.

Form closed

Form.Deactivate

Form.Closing

Form.Closed

Form is closed by executing Form.Close or clicking on the form's close button.

The initial location is normally set in the Form.Load event handler. This example loads the form 200 pixels to the right of the upper-left corner of the screen:

private void opaque_Load(object sender, System.EventArgs e)

{

   this.DesktopLocation = new Point(200,0);

}

The form's initial location can also be set by the form that creates and displays the form object:

opaque opForm = new opaque();

opForm.Opacity = 1;

opForm.TopMost = true;   //Always display form on top

opForm.StartPosition = FormStartPosition.Manual;

opForm.DesktopLocation = new Point(10,10);

opForm.Show();

This code creates an instance of the form opaque and sets its TopMost property so that the form is always displayed on top of other forms in the same application. The DeskTopLocation property sets the form's initial location. For it to work, however, the StartPostion property must first be set to FormStartPosition.Manual.

Core Note

The DesktopLocation property sets coordinates within a screen's working area, which is the area not occupied by a task bar. The Location property of the Control class sets the coordinates relative to the upper-left edge of the control's container.

A form's size can be set using either its Size or ClientSize property. The latter is usually preferred because it specifies the workable area of the form—the area that excludes the title bar, scrollbars, and any edges. This property is set to an instance of a Size object:

this.ClientSize = new System.Drawing.Size(208, 133);

It is often desirable to position or size a form relative to the primary (.NET supports multiple screens for an application) screen size. The screen size is available through the Screen.PrimaryScreen.WorkingArea property. This returns a rectangle that represents the size of the screen excluding task bars, docked toolbars, and docked windows. Here is an example that uses the screen size to set a form's width and height:

int w = Screen.PrimaryScreen.WorkingArea.Width;

int h = Screen.PrimaryScreen.WorkingArea.Height;

this.ClientSize = new Size(w/4,h/4);

After a form is active, you may want to control how it can be resized. The aptly named MinimumSize and MaximumSize properties take care of this. In the following example, the maximum form size is set to one-half the width and height of the working screen area:

//w and h are the screen's width and height

this.MaximumSize = new Size(w/2,h/2);

this.MinimumSize = new Size(200, 150);

Setting both width and height to zero removes any size restrictions.

Displaying Forms

After a main form is up and running, it can create instances of new forms and display them in two ways: using the Form.ShowDialog method or the Form.Show method inherited from the Control class. Form.ShowDialog displays a form as a modal dialog box. When activated, this type of form does not relinquish control to other forms in the application until it is closed. Dialog boxes are discussed at the end of this section.

Form.Show displays a modeless form, which means that the form has no relationship with the creating form, and the user is free to select the new or original form. If the creating form is not the main form, it can be closed without affecting the new form; closing the main form automatically closes all forms in an application.

The Life Cycle of a Modeless Form

A form is subject to a finite number of activities during its lifetime: It is created, displayed, loses and gains focus, and finally is closed. Most of these activities are accompanied by one or more events that enable the program to perform necessary tasks associated with the event. Table 6-6 summarizes these actions and events.

Let's look at some of the code associated with these events.

Creating and Displaying a Form

When one form creates another form, there are coding requirements on both sides. The created form must set up code in its constructor to perform initialization and create controls. In addition, delegates should be set up to call event handling routines. If using Visual Studio.NET, any user initialization code should be placed after the call to InitializeComponent.

For the class that creates the new form object, the most obvious task is the creation and display of the object. A less obvious task may be to ensure that only one instance of the class is created because you may not want a new object popping up each time a button on the original form is clicked. One way to manage this is to take advantage of the Closed event that occurs when a created form is closed (another way, using OwnedForms, is discussed shortly). If the form has not been closed, a new instance is not created. The code that follows illustrates this.

An EventHandler delegate is set up to notify a method when the new form, opForm, is closed. A flag controls what action occurs when the button to create or display the form is pushed. If an instance of the form does not exist, it is created and displayed; if it does exist, the Form.Activate method is used to give it focus.

//Next statement is at beginning of form's code

public opaque opForm;

bool closed = true;   //Flag to indicate if opForm exists

//Create new form or give focus to existing one

private void button1_Click(object sender, System.EventArgs e)

{

   if (closed)

   {

      closed = false;

      opForm = new opaque();

      //Call OnOpClose when new form closes

      opForm.Closed += new EventHandler(OnOpClose);

      opForm.Show();     //Display new form object

   } else {

      opForm.Activate();  //Give focus to form

   }

}

//Event handler called when child form is closed

private void OnOpClose(object sender, System.EventArgs e)

{

   closed = true;   //Flag indicating form is closed

}

Form Activation and Deactivation

A form becomes active when it is first shown or later, when the user clicks on it or moves to it using an Alt-Tab key to iterate through the task bar. This fires the form's Activated event. Conversely, when the form loses focus—through closing or deselection—the Deactivate event occurs. In the next code segment, the Deactivate event handler changes the text on a button to Resume and disables the button; the Activated event handler re-enables the button.

this.Deactivate += new System.EventHandler(Form_Deactivate);

this.Activated  += new System.EventHandler(Form_Activate);

//

void Form_Deactivate(object sender, EventArgs e)

{   button1.Enabled = false;

    button1.Text = "Resume";      }

void Form_Activate(object sender, EventArgs e)

{   button1.Enabled = true;   }

Closing a Form

The Closing event occurs as a form is being closed and provides the last opportunity to perform some cleanup duties or prevent the form from closing. This event uses the CancelEventHandler delegate to invoke event handling methods. The delegate defines a CancelEventArgs parameter that contains a Cancel property, which is set to true to cancel the closing. In this example, the user is given a final prompt before the form closes:

this.Closing += new CancelEventHandler(Form_Closing);

void Form_Closing(object sender, CancelEventArgs e)

{

   if(MessageBox.Show("Are you sure you want to Exit?", "",

      MessageBoxButtons.YesNo) == DialogResult.No)

   {

      //Cancel the closing of the form

      e.Cancel = true;

   }

}

Forms Interaction—A Sample Application

When multiple form objects have been created, there must be a way for one form to access the state and contents of controls on another form. It's primarily a matter of setting the proper access modifiers to expose fields and properties on each form. To illustrate, let's build an application that consists of two modeless forms (see Figure 6-8). The main form contains two controls: a Textbox that holds the document being processed and a Search button that, when clicked, passes control to a search form. The search form has a Textbox that accepts text to be searched for in the main form's document. By default, the search phrase is any highlighted text in the document; it can also be entered directly by the user.

Figure 6-8. Text search application using multiple forms

When the Find Next button is pushed, the application searches for the next occurrence of the search string in the main document. If an occurrence is located, it is highlighted. To make it more interesting, the form includes options to search forward or backward and perform a case-sensitive or case-insensitive search.

The main challenge in developing this application is to determine how each form makes the content of its controls available to the other form. DocForm, the main form, must expose the contents of documentText so that the search form can search the text in it and highlight an occurrence of matching text. The search form, SearchForm, must expose the contents of txtSearch, the TextBox containing the search phrase, so that the main form can set it to the value of any highlighted text before passing control to the form.

DocForm shares the contents of documentText through a text box field myText that is assigned the value of documentText when the form loads. Setting myText to public static enables the search form to access the text box properties by simply qualifying them with DocForm.myText.

public static TextBox myText;   //Declare public variable

private void docForm_Load(object sender, System.EventArgs e)

{

   myText = documentText;

}

SearchForm exposes the contents of txtSearch to other objects through a write-only string property.

public String SearchPhrase

{

   set { txtSearch.Text = value;}   //Write Only

}

DocForm, as well as any object creating an instance of SearchForm, can set this property. Now let's look at the remaining code details of the two forms.

Code for the Main Form

When the button on DocForm is clicked, the application either creates a new instance of SearchForm or passes control to an existing instance. In both cases, it first checks its text box and passes any highlighted text (SelectedText) to the SearchForm object via its SearchPhrase property (see Listing 6-5). Techniques described in earlier examples are used to create the object and set up a delegate to notify the DocForm object when the search form object closes.

Listing 6-5. Method to Pass Control to Search Form Instance

private void btnSearch_Click(object sender, System.EventArgs e) {

   //Create instance of search form if it does not exist

   if (closed)

   {

      closed= false;

      searchForm = new SearchForm();   //Create instance

      searchForm.TopMost = true;

      searchForm.Closed += new EventHandler(onSearchClosed);

      searchForm.StartPosition = FormStartPosition.Manual;

      searchForm.DesktopLocation = new Point(this.Right-200,

            this.Top-20);

      searchForm.SearchPhrase = documentText.SelectedText;

      searchForm.Show();

   } else {

      searchForm.SearchPhrase = documentText.SelectedText;

      searchForm.Activate();

   }

}

private void onSearchClosed(object sender, System.EventArgs e)

{   closed= true;   }

Code for the Search Form

Listing 6-6 displays the code executed when the Find Next button is clicked. The search for the next occurrence of the search string can proceed up the document using the LastIndexOf method or down the document using IndexOf. Logic is also included to ignore or recognize case sensitivity.

Listing 6-6. Search for Matching Text on Another Form

private void btnFind_Click(object sender, System.EventArgs e)

{

   int ib;      //Index to indicate position of match

   string myBuff = DocForm.myText.Text; //Text box contents

   string searchText= this.txtSearch.Text;  //Search string

   int ln = searchText.Length;          //Length of search string

   if (ln>0)

   {

      //Get current location of selected text

      int selStart = DocForm.myText.SelectionStart;

      if (selStart >= DocForm.myText.Text.Length)

      {

         ib = 0;

      } else {

         ib = selStart + ln;

      }

      if (!this.chkCase.Checked) //Case-insensitive search

      {

         searchText = searchText.ToUpper();

         myBuff = myBuff.ToUpper();

      }

      if (this.radDown.Checked)ib =

            myBuff.IndexOf(searchText,ib);

      if (this.radUp.Checked && ib>ln-1)ib =

                 myBuff.LastIndexOf(searchText,ib-2,ib-1);

      if (ib >= 0)        //Highlight text on main form

      {

         DocForm.myText.SelectionStart = ib;

         DocForm.myText.SelectionLength = txtSearch.Text.Length;

      }

   }

}

Owner and Owned Forms

When a form displays an instance of a modeless form, it does not by default create an explicit relationship between itself and the new form. The forms operate autonomously: They either can be closed (except for a main form, which causes all forms to be closed) or minimized without affecting the other; and the creator form has no easy way to distinguish among instances of the forms it has launched.

Often, however, one form does have a dependency on the other. In the preceding example, the floating search window exists only as a companion to a document that it searches. Its relationship to the form that created it is referred to as an owner-owned relationship. In .NET, this can be more than just a logical relationship. A form has an Owner property that can be set to the instance of the form that "owns" it. After this relationship is formally established, the behavior of the owner and owned form(s) is linked. For example, the owned form is always visually on top of its owner form. This eliminates the need to make SearchForm a TopMost form in our preceding example.

An owner-owned relationship is easily established by setting the Owner property of a newly created form to the form that is creating it.

opaque opForm = new opaque();

opForm.Owner = this;   //Current form now owns new form

opForm.Show();

This relationship affects the user's interaction with the form in three ways: The owned form is always on top of the owner form even if the owner is active; closing the owner form also closes the owned form; and minimizing the owner form minimizes all owned forms and results in only one icon in the task bar.

Another advantage of the owner-owned relationship is that an owner form has an OwnedForms collection that contains all the owned forms it creates. The following example demonstrates how an owner form creates two owned forms, opForm and opForm2, and then enumerates the collection to set the caption of each form before displaying it:

opaque opForm = new opaque();

opForm.Owner = this;  //Set current form to owner form

opaque opForm2 = new opaque();

opForm2.Owner = this; //Set current form to owner form

for (int ndx=0; ndx<this.OwnedForms.Length; ndx++)

{

   myForms.Text = "Owner: Form1 - Form"+ndx.ToString();

   myForms.Show();

}

Note that although modal forms exhibit the features of an owned form, the Owner property must be set to establish an explicit relationship.

Message and Dialog Boxes

.NET provides a set of classes and enumerations that make it easy to create a message or dialog window to interact with a user. The simplest approach is to use the MessageBox class and its versatile Show method. The other approach is to create a custom form and invoke it with the form's ShowDialog method. Both of these methods create modal forms.

MessageBox

The MessageBox class uses its Show method to display a message box that may contain text, buttons, and even an icon. The Show method includes these overloads:

Syntax:

static DialogResult Show(string msg)

static DialogResult Show(string msg, string caption)

static DialogResult Show(string msg, string caption,

      MessageBoxButtons buttons)

static DialogResult Show(string msg, string caption,

      MessageBoxButtons buttons, MessageBoxIcon icon,

      MessageBoxDefaultButton defBtn)

DialogResult. The method returns one of the enum members Abort, Cancel, Ignore, No, None, OK, Retry, and Yes.

MessageBoxIcon. This enumeration places an icon on the message box. Members include Asterisk, Error, Exclamation, Hand, Information, None, Question, Stop, and Warning.

MessageBoxButtons. This is an enumeration with values that determine which buttons are displayed in the message box. Its members are AbortRetryIgnore, OK, OKCancel, RetryCancel, YesNo, and YesNoCancel. The buttons correspond to the text in the member name. For example, YesNo results in a form with a Yes and No button.

MessageBoxDefaultButton. This an enumeration that defines the default button on the screen. Its members are Button1, Button2, and Button3.

Figure 6-9, which is created with the following statement, provides a visual summary of these parameter options:

MessageBox.Show("OK to Close", "Game Status",

      MessageBoxButtons.YesNoCancel,MessageBoxIcon.Question,

      MessageBoxDefaultButton.Button2 );

Figure 6-9. MessageBox.Show example

Clicking one of the three buttons returns a value of DialogResult.Yes, DialogResult.No, or DialogResult.Cancel, respectively.

ShowDialog

The ShowDialog method permits you to create a custom form that is displayed in modal mode. It is useful when you need a dialog form to display a few custom fields of information. Like the MessageBox, it uses buttons to communicate with the user.

The form used as the dialog box is a standard form containing any controls you want to place on it. Although not required, the form's buttons are usually implemented to return a DialogResult enum value. The following code handles the Click event for the two buttons shown on the form in Figure 6-10:

private void buttonOK_Click(object sender, System.EventArgs e)

{ this.DialogResult = DialogResult.OK; }

private void buttonCancel_Click(object sender, System.EventArgs e)

{ this.DialogResult = DialogResult.Cancel; }

Figure 6-10. Creating a menu with VS.NET

To complete the form, we also need to set a default button and provide a way for the form to be cancelled if the user presses the Esc key. This is done by setting the form's AcceptButton and CancelButton properties in the form's constructor.

AcceptButton = buttonOK;      //Button to receive default focus

CancelButton = buttonCancel;  //Fires when Esc pushed

The code that creates and displays the form is similar to previous examples. The only difference is that the new form instance calls its ShowDialog method and returns a DialogResult type result.

customer cust = new customer();

cust.MinimizeBox = false;

cust.MaximizeBox = false;

if (cust.ShowDialog() == DialogResult.OK)

{  MessageBox.Show("Returns OK"); }

else

{  MessageBox.Show("Returns Cancel"); }

Multiple Document Interface Forms

A Multiple Document Interface (MDI) application is characterized by one application window and several document windows. Structurally, a single container is used to hold multiple documents. To manage the collection of documents, the MDI application includes a menu system with options to open, save, and close a document; switch between documents; and arrange the documents in various visual layouts.

No special classes are required to create an MDI application. The only requirement is that one form be designated the container by setting its IsMdiContainer property to true. Child forms are designated by setting their MdiParent property to the container form.

The MDI form in Figure 6-11 shows the three elements that comprise an MDI form: the parent container; the child form(s); and a menu to manage the creation, selection, and arrangement of the child forms.

Figure 6-11. MDI form

The container form is created by including this statement in its constructor:

this.IsMdiContainer = true;

By tradition, child forms are created by selecting an option on the File menu such as File-New or File-Open. The supporting code creates an instance of the child form and sets its MdiParent property.

invForm myForm = new invForm();

myForm.MdiParent = this;

mdiCt += mdiCt;   //Count number of forms created

myForm.Text= "Invoice" + mdiCt.ToString();

myForm.Show();

A variable that counts the number of forms created is appended to each form's Text property to uniquely identify it.

Creating a Menu and MDI Form

A discussion of MDI forms is incomplete without considering the requirements for a menu to manage the windows within the container. Minimally, an MDI parent menu should contain a File section for creating and retrieving forms and a Windows section that lists all child forms and permits form selection.

A basic menu is constructed from two classes: the MainMenu class that acts as a container for the whole menu structure and the MenuItem class that represents the menu items in the menu. Both of these classes expose a MenuItems collection property that is used to create the menu hierarchy by adding subitems to each class that represent the menu options. After the menu items are in place, the next step is to tie them to appropriate event handling routines using the familiar delegate approach. Let's step through an example that demonstrates how to create the menu system shown in Figure 6-12. Afterwards, we'll look at creating the menu in Visual Studio.NET. It is certainly much quicker and easier to use VS.NET, but it is less flexible if you need to create menus at runtime.

Figure 6-12. MDI Form menu

The first step is to declare the main menu object and the menu items as class variables. (To avoid repetition, code for all menu items is not shown.)

private MainMenu mainMenu1;

private MenuItem menuItem1;   //File

private MenuItem menuItem2;   //Edit

private MenuItem menuItem3;   //Window

private MenuItem menuItem4;   //File - New

The main menu and menu items are created inside the class constructor:

this.mainMenu1 = new System.Windows.Forms.MainMenu();

this.menuItem1 = new System.Windows.Forms.MenuItem("File");

this.menuItem2 = new System.Windows.Forms.MenuItem("Edit");

this.menuItem3 = new System.Windows.Forms.MenuItem("Window");

this.menuItem4 = new System.Windows.Forms.MenuItem("New");

Next, the menu hierarchy is established by adding menu items to the main menu object to create the menu bar. The menu bar items then have menu items added to their MenuItems collection, which creates the drop-down menu.

//Add menu items to main menu object

this.mainMenu1.MenuItems.AddRange(new

      System.Windows.Forms.MenuItem[] {

         this.menuItem1,

         this.menuItem2,

         this.menuItem3});

//Add menu item below File

this.menuItem1.MenuItems.Add(this.menuItem4);

//Add menu items to Window menu item

this.menuItem3.MdiList = true;    //Causes child forms to display

this.menuItem3.MenuItems.AddRange(new

System.Windows.Forms.MenuItem[] {this.menuItem5,

      this.menuItem6, this.menuItem7, this.menuItem8});

//Set menu on form

this.Menu = this.mainMenu1;

The main points to observe in this code are:

  • The Add and AddRange methods add a single or multiple menu items to the MenuItems collection.

  • Setting a menu item's MdiList property to TRue causes a list of child forms to appear in the menu below that menu item (Invoice1 and Invoice2 are listed in Figure 6-12).

  • To place a menu on a form, set the form's Menu property to the MainMenu object.

The final step is to set up event handling code that provides logic to support the menu operations. Here is the code to define a delegate and method to support an event fired by clicking the File–New menu item. The code creates a new instance of invForm each time this menu item is clicked.

//Following is defined in constructor

MenuItem4.Click += new System.EventHandler(menuItem4_Click);

private void menuItem4_Click(object sender, System.EventArgs e)

{

   invForm myForm = new invForm();

   myForm.MdiParent = this;

   mdiCt += mdiCt;   //Count number of forms created

   myForm.Text= "Invoice" + mdiCt.ToString();

   myForm.Show();

}

The Window option on the menu bar has submenu items that let you rearrange the child forms within the MDI container. The LayoutMdi method of a form makes implementing this almost trivial. After setting up delegates in the usual manner, create the event handling routines:

private void menuItem6_Click(object sender, System.EventArgs e){

   this.LayoutMdi(MdiLayout.ArrangeIcons);

}

private void menuItem6_Click(object sender, System.EventArgs e){

   this.LayoutMdi(MdiLayout.Cascade);

}

private void menuItem7_Click(object sender, System.EventArgs e){

   this.LayoutMdi(MdiLayout.TileHorizontal);

}

private void menuItem8_Click(object sender, System.EventArgs e){

   this.LayoutMdi(MdiLayout.TileVertical);

}

The methods reorder the window by passing the appropriate MdiLayout enumeration value to the LayoutMdi method.

Creating an MDI Menu Using VisualStudio.NET

With the Form Designer open, double click the MainMenu icon in the Toolbox window. Two things happen: An icon appears in the component tray at the bottom and a menu template appears docked to the top of the form. Type the menu item titles into the cells that appear (see Figure 6-13). The top horizontal row of cells represents the menu bar; by moving vertically, you create drop-down menu items below the top-level items. After typing in the item name, double click the cell to create a Click event for the menu item. VS.NET creates the delegate and method stub automatically.

Figure 6-13. Creating a menu with VS.NET

Use the Property Window (press F4), which displays properties of the active cell, to change the default names assigned to menu items and set any other values.


6.4. Working with Menus

The previous section provided a solid introduction to menus. This section adds a checklist of MenuItem properties that affect an item's appearance and describes how to use the ContextMenu class.

MenuItem Properties

The .NET menu system is designed with the utilitarian philosophy that the value of a thing depends on its utility. Its menu item is not a thing of beauty, but it works. Here are some of its more useful properties:

Enabled. Setting this to false, grays out the button and makes it unavailable.

Checked. Places a checkmark beside the menu item text.

RadioCheck. Places a radio button beside the menu item text; Checked must also be true.

BreakBar or Break. Setting this to true places the menu item in a new column.

Shortcut. Defines a shortcut key from one of the Shortcut enum members. These members represent a key or key combination (such as Shortcut.AltF10) that causes the menu item to be selected when the keys are pressed. On a related matter, note that you can also place an & in front of a letter in the menu item text to produce a hot key that causes the item to be selected by pressing Alt-letter.

Context Menus

In addition to the MainMenu and MenuItem classes that have been discussed, there is a ContextMenu class that also inherits from the Menu class. This ContextMenu class is associated with individual controls and is used most often to provide a contextsensitive pop-up menu when the user right-clicks on a control.

The statements to construct a menu based on ContextMenu are the same as with a MainMenu. The only difference is that visually there is no top-level menu bar, and the menu is displayed near the control where it is invoked.

A menu can be associated with multiple controls, or each control can have its own menu. Typically, one menu is used for each control type. For example, you might have a context menu for all

TextBox controls, and another for buttons. To illustrate, let's create a menu that colors the background of a TextBox control (see Figure 6-14).

Figure 6-14. Context menu

Constructing a Context Menu

Creating a context menu is similar to creating a MainMenu. If using VS.NET, you drag the ContextMenu control to the form and visually add menu items. If coding by hand, you create an instance of the ContextMenu class and add menu items using the MenuItems.Add method. Following is a sampling of the code used to create the menu. Note that a single method handles the Click event on each menu item.

private ContextMenu contextMenu1;  //Context menu

private TextBox txtSearch;         //Text box that will use menu

// Following is in constructor

contextMenu1 = new ContextMenu();

// Add menu items and event handler using Add method

contextMenu1.MenuItems.Add("Azure Background",

      new System.EventHandler(this.menuItem_Click));

contextMenu1.MenuItems.Add("White Background",

      new System.EventHandler(this.menuItem_Click));

contextMenu1.MenuItems.Add("Beige Background",

      new System.EventHandler(this.menuItem_Click));

The completed menu is attached to a control by setting the control's ContextMenu property to the context menu:

//Associate text box with a context menu

this.txtSearch.ContextMenu = this.contextMenu1;

A right-click on txtSearch causes the menu to pop up. Click one of the menu items and this event handling routine is called:

private void menuItem_Click(object sender, System.EventArgs e)

{

   //Sender identifies specific menu item selected

   MenuItem conMi = (MenuItem) sender;

   string txt = conMi.Text;

   //SourceControl is control associated with this event

   if(txt == "Azure Background")        this.contextMenu1.SourceControl.BackColor = Color

  .Azure;

   if(txt == "White Background")        this.contextMenu1.SourceControl.BackColor = Color

  .White;

   if(txt == "Beige Background")        this.contextMenu1.SourceControl.BackColor = Color

  .Beige;

}

The two most important things to note in this example are that the argument sender identifies the selected menu item and that the context menu property SourceControl identifies the control associated with the event. This capability to identify the control and the menu item enables one method to handle events from any control on the form or any menu item in the context menu.


6.5. Adding Help to a Form

The majority of software users do not read documentation, except as a last resort. Users expect a program to be intuitive and provide context-sensitive documentation where it is needed. In addition to the Help option on the main menu bar, a polished program should provide assistance down to the individual controls on the form.

.NET offers multiple ways to configure an integrated help system:

  • Easy-to-use ToolTips that are displayed when the mouse moves over a control. These are specified as a control property provided by the ToolTip component.

  • The HelpProvider component is an "extender" that adds properties to existing controls. These properties enable the control to reference Microsoft HTML Help (.chm ) files.

  • A custom event handler can be written to implement code that explicitly handles the Control.HelpRequested event that is fired by pressing the F1 key or using a Help button.

ToolTips

This component adds mouseover capabilities to controls. If using VS.NET, you simply select the ToolTip control from the tool box and add it to your form. The effect of this is to add a string property (ToolTip on toolTip1) to each control, whose value is displayed when the mouse hovers over the control.

Of more interest is using ToolTips within a program to dynamically provide annotation for objects on a screen. Maps, which can be created dynamically in response to user requests, offer a prime example. They typically contain points of interest implemented as labels or picture boxes. As an example, consider the display in Figure 6-15, which shows a constellation and labels for its most important stars. When a user passes the cursor over the label, tool tip text describing the star is shown.

Figure 6-15. ToolTip information is displayed when mouse passes over name

Listing 6-7 shows a portion of the code that creates the form displayed in the figure. The form's BackGroundImage property is set to the image representing the constellation. Labels are placed on top of this that correspond to the position of three stars (code for only one star is shown). The Tag property of each label is set to the description of the star, and a ToolTip associates this information with the label using the SetToolTip method.

Listing 6-7. Using ToolTips to Annotate an Image

public class StarMap:Form

{

   public StarMap()

   {

      this.Text = "Star Search";

      this.Width=400;

      this.Height=220;

      // Place image of constellation on form

      this.BackgroundImage= new Bitmap(@"c:\dracoblack.gif");

      // Add name of star on Label

      Label star1 = new Label();

      Star1.Location = new Point(285,115);

      Star1.Size = new Size(60,16);

      star1.Text = "Thuban";

      star1.Tag  = "   Alpha Draconis\n> Magnitude: 3.67\n>"+

                   " 310 Light Years";

      star1.Font = new Font(star.Font, star.Font.Style |

                            FontStyle.Bold);

      this.Controls.Add(star1);

      ToolTip toolTip1 = new ToolTip();

      toolTip1.AutoPopDelay= 1500;  // Tool tip displays

                                    // for 1.5 secs.

      // Tool tip text comes from Tag property of Label

      toolTip1.SetToolTip(star1, star1.Tag.ToString());

      // Add labels for other stars Etamin and Gianfar here ...

   }

}

Core Note

To dynamically change a control's tool tip value, you must get an instance of the control's ToolTip, execute its RemoveAll method, and then use SetToolTip to reset the value of the tool tip string.

Responding to F1 and the Help Button

Many users regard the F1 key as a de facto way to invoke help. .NET provides built-in F1 support by causing a Control.HelpRequested event to fire when the user presses the F1 key. This event also fires when a user clicks the context-sensitive Help button at the top of a form and then clicks on a control using the Help cursor. See Figure 6-16.

Figure 6-16. The Help button

The Help button is displayed by setting these form properties:

  • Set MinimizeBox and MaxmizeBox to false.

  • Set HelpButton to true.

A recommended approach is to create one event handler routine and have each control invoke it. As an example, the following code defines delegates for two text boxes that notify the ShowHelp method when the HelpRequested event occurs. This method uses either a Tag property associated with each control or the control's name to specify help germane to that control.

this.date.HelpRequested  += new HelpEventHandler(ShowHelp);

this.ssn.HelpRequested   += new HelpEventHandler(ShowHelp);

this.ssn.Tag   = "Enter as: nnn-nn-nnnn";

this.date.Tag  = "Enter as: mm/dd/yyyy";

private void ShowHelp(object sender, HelpEventArgs e)

{

   Control reqControl = (Control)sender;

   // Technique 1: Use tag associated with control

   MessageBox.Show(reqControl.Tag.ToString());

   // Technique 2: Link to specific text within a CHM file

   string anchor = "#"+reqControl.Name;

   // ShowHelp invokes a compiled Help file

   Help.ShowHelp(reqControl,@"c:\ctest.chm",HelpNavigator.Topic,

                 "customers.htm"+anchor);

   e.Handled = true;  // Always set this

}

The event handler receives two arguments: the familiar sender that identifies the control that has focus when the event occurs and HelpEventArgs, which has Handled and MousePos as its only two properties. Handled is set to indicate the event has been handled. MousePos is a Point type that specifies the location of the cursor on the form.

The method provides context-sensitive help by identifying the active control and using this knowledge to select the Help text to be displayed. In this example, the first technique displays the tag property of a control as the Help message. The second—and more interesting technique—uses Help.ShowHelp to display a section of an HTML file that uses the control's name as an anchor tag. Specifically, it looks inside ctest.chm for the customers.htm page. Then, it searches that page for a named anchor tag such as <a name=ssn> . If found, it displays the HTML at that location.

ShowHelp is the most useful method of the Help class. It includes several overloads to show compiled Help files (.chm) or HTML files in an HTML Help format.

// URL may be .chm file or html file

public static void ShowHelp(Control parent, string url);

// HelpNavigator defines the type of .chm file to be displayed

public static void ShowHelp(Control parent, string url,

                            HelpNavigator navigator);

// Displays contents of Help file for a specified keyword

public static void ShowHelp(Control parent, string url,

                            string keyword);

// param is used with HelpNavigator.Topic to refine selection

public static void ShowHelp(Control parent, string url,

                            HelpNavigator navigator, object param);

The HelpNavigator enumeration specifies which part of a .chm file is displayed. It's values include TableofContents, Find, Index, and Topic. If you are unfamiliar with them, compiled Help files package multiple HTML files along with an optional table of contents and keyword indexes. The downloadable Microsoft HTML Help Workshop is the easiest way to learn how to use and create these files.

The HelpProvider Component

This component is a wrapper that is used primarily with Visual Studio.NET. Its main value is that it eliminates the need to explicitly handle the HelpRequested event. It is an extender that adds several properties to each control. These properties essentially correspond to the parameters in the ShowHelp method, which it calls underneath.

Add the HelpProvider to a form by selecting it from the tool box. Then, set its HelpNameSpace property to the name of the HTML or .chm file that the underlying ShowHelp method should reference (this corresponds to the URL parameter).

Each control on the form adds four extended properties:

  1. ShowHelp. Set to true to make activate Help feature.

  2. HelpNavigator. Takes the HelpNavigator enumeration value.

  3. HelpKeyword. Corresponds to the param or keyword parameter in ShowHelp.

  4. HelpString. This contains a message that is displayed when the Help button is used to click a control.

Help is not enabled on a control that has ShowHelp set to false. If it is set to true, but the other properties are not set, the file referenced in HelpNameSpace is displayed. A popular Help configuration is to set only the HelpString value so that the Help button brings up a short specific message and F1 brings up an HTML page.


6.6. Forms Inheritance

Just as a class inherits from a base class, a GUI form—which is also a class—can inherit the settings, properties, and control layout of a preexisting form. This means that you can create forms with standard features to serve as templates for derived forms. Before looking at the details of inheritance, let's first examine how to store a set of base forms in a code library and organize them by namespace.

Building and Using a Forms Library

Each form consists of a physical .cs file. A library of multiple forms is created by compiling each .cs file into a common .dll file. After this is done, the forms can be accessed by any compliant language—not just the one they are written in.

As an example, let's use the compiler from the command line to compile two forms into a single .dll file:

csc /t:library product.cs customer.cs  /out:ADCFormLib.dll

A base form must provide a namespace for the derived form to reference it. The following code defines a Products namespace for our example:

namespace Products

{

   public class ProductForm : System.Windows.Forms.Form

   {

To inherit this form, a class uses the standard inheritance syntax and designates the base class by its namespace and class name:

// User Form derived from base class Form

class NewProductForm: Products.ProductForm

{

As a final step, the compiler must be given a reference to the external assembly ADCFormLib so that the base class can be located. If using VS.NET, you use the Project-AddReference menu option to specify the assembly; from the command line, the reference flag is used.

csc /t:winexe /r:ADCFormLib.dll myApp.cs

Using the Inherited Form

If the derived form provides no additional code, it generates a form identical to its base form when executed. Of course, the derived form is free to add controls and supporting code. The only restriction is that menu items cannot be added to an existing menu; however, an entire menu can be added to the form and even replace an existing one on the base form.

The properties of inherited controls can be changed, but their default access modifier of private must first be changed to protected, and the base form then recompiled. The derived form is then free to make modifications: It may reposition the control or even set its Visible property to false to keep it from being displayed.

Overriding Events

Suppose the base form contains a button that responds to a click by calling event handler code to close the form. However, in your derived form, you want to add some data verification checks before the form closes. One's instinct is to add a delegate and event handler method to respond to the button Click event in the derived form. However, this does not override the original event handler in the base form, and both event handling routines get called. The solution is to restructure the original event handler to call a virtual method that can be overridden in the derived form. Here is sample code for the base form:

private void btn1_Clicked(object sender, System.EventArgs e)

{

   ButtonClicks();   // Have virtual method do actual work

}

protected virtual void ButtonClicks()

{

   this.Close();

}

The derived form simply overrides the virtual method and includes its own code to handle the event:

protected override void ButtonClicks() {

   // Code to perform any data validation

   this.Close();

}

Creating Inherited Forms with Visual Studio.NET

To create an inherited form using Visual Studio, open a project containing the form to serve as the base and compile it. Then, select Project-Add Inherited Form from the menu. Give the new form a name and open it. Next, an Inheritance Picker dialog box appears that lists the eligible base forms. Use the Browse feature to display forms that are in external libraries.

Select a base form and the new inherited form appears in the designer window. All of the controls on the form are marked with a plus (+), and you will find that only properties on the form itself are exposed. At this point, you can add controls or modify existing ones using the techniques just described.


6.7. Summary

Despite the migration of applications to the Internet, there is still a compelling need for Windows Forms programming. Windows Forms provide features and functionality superior to those of Web Forms. Moreover, the majority of real-world applications continue to run on local area networks. The .NET Framework Class Library provides a rich set of classes to support forms programming. The Control class at the top of the hierarchy provides the basic properties, methods, and events that allow controls to be positioned and manipulated on a form. Keyboard and mouse events enable a program to recognize any keys or mouse buttons that are clicked, as well as cursor position.

The Form class inherits all of the Control class members and adds to it properties that specify form appearance, position, and relationship with other forms. A form created by another form may be modal, which means it does not relinquish focus until it is closed, or modeless, in which case focus can be given to either form. In a multiple document interface (MDI) application, one form serves as a container to hold child forms. The container usually provides a menu for selecting or rearranging its child forms.

.NET includes a HelpRequested event that is fired when a user pushes the F1 key. This can be combined with the Help.ShowHelp method, which supports compiled HTML (.chm) files, to enable a developer to provide context-sensitive help on a form.


6.8. Test Your Understanding

1: | From which class must a Windows Forms application inherit?
2: | Describe the difference between anchoring and docking.
3: | What MouseEventArgs properties are used to identify the mouse coordinates and button clicked?
4: | Which Form property is used to create an irregular shaped form?
5: | What is the primary difference between a modal and modeless form?
6: | How does creating an owner-owned form relationship affect the behavior of the related forms?
7: | What form properties must be set in order to display a Help button?
8: | Compare using a Help button and a ToolTip.
9: | Describe how a base form can structure its event handling code so that an inherited form can override an event.


Chapter 7. Windows Forms Controls

Topics in This Chapter

  • Introduction: A class hierarchy diagram offers a natural way to group Windows Forms controls by their functionality.

  • Button Controls: The Button, CheckBox, and RadioButton controls are designed to permit users to make one or more selections on a form.

  • PictureBox and TextBoxt Controls: The PictureBox control is used to display and scale images; the TextBox control can be used to easily display and edit single or multiple lines of text.

  • List Controls: The ListBox, ComboBox, and CheckListBox offer different interfaces for displaying and manipulating data in a list format.

  • ListView and TreeView Controls: The ListView offers multiple views for displaying data items and their associated icons. The TReeView presents hierarchical information in an easy-to-navigate tree structure.

  • Timer and Progress Bar Controls: A timer can be used to control when an event is invoked, a ProgressBar to visually monitor the progress of an operation.

  • Building a User Control: When no control meets an application's needs, a custom one can be crafted by combining multiple controls or adding features to an existing one.

  • Moving Data Between Controls: Drag and drop provides an easy way for users to copy or move an item from one control to another. .NET offers a variety of classes and events required to implement this feature.

  • Using Resources: Resources required by a program, such as title, descriptive labels, and images, can be embedded within an application's assembly or stored in a satellite assembly. This is particularly useful for developing international applications.

The previous chapter introduced the Control class and the methods, properties, and events it defines for all controls. This chapter moves beyond that to examine the specific features of individual controls. It begins with a survey of the more important .NET controls, before taking an in-depth look at how to implement controls such as the TextBox, ListBox, treeView, and ListView. Also included is a discussion of the .NET drag-and-drop features that are used to move or copy data from one control to another.

Windows Forms (WinForms) are not restricted to using the standard built-in controls. Custom GUI controls can be created by extending an existing control, building a totally new control, or fashioning a user control from a set of related widgets. Examples illustrate how to extend a control and construct a user control. The chapter concludes with a look at resource files and how they are used to create GUI applications that support users from multiple countries and cultures.


7.1. A Survey of .NET Windows Forms Controls

The System.Windows.Forms namespace contains a large family of controls that add both form and function to a Windows-based user interface. Each control inherits a common set of members from the Control class. To these, it adds the methods, properties, and events that give the control its own distinctive behavior and appearance.

Figure 7-1 shows the inheritance hierarchy of the Windows Forms controls. The controls marked by an asterisk (*) exist primarily to provide backward compatibility between .NET 2.0 and .NET 1.x. Specifically, the DataGrid has been superseded by the DataGridView, the StatusBar by the StatusStrip, and the ToolBar by the ToolStrip. Table 7-1 provides a summary of the more frequently used controls in this hierarchy.

Figure 7-1. Windows Forms control hierarchy

Table 7-1. Selected Windows Forms Controls

Control

Use

Description

Button

Fires an event when a mouse click occurs or the Enter or Esc key is pressed.

Represents a button on a form. Its text property determines the caption displayed on the button's surface.

CheckBox

Permits a user to select one or more options.

Consists of a check box with text or an image beside it. The check box can also be represented as a button by setting:

checkBox1.Appearance =

   Appearance.Button

CheckedListBox

Displays list of items.

ListBox with checkbox preceding each item in list.

ComboBox

Provides TextBox and ListBox functionality.

Hybrid control that consists of a textbox and a drop-down list. It combines properties from both the TextBox and the ListBox.

DataGridView

GridView

Manipulates data in a grid format.

The DataGridView is the foremost control to represent relational data. It supports binding to a database. The DataGridView was introduced in .NET 2.0 and supersedes the DataGrid.

GroupBox

Groups controls.

Use primarily to group radio buttons; it places a border around the controls it contains.

ImageList

Manages a collection of images.

Container control that holds a collection of images used by other controls such as the ToolStrip, ListView, and TReeView.

Label

Adds descriptive information to a form.

Text that describes the contents of a control or instructions for using a control or form.

ListBox

Displays a list of items—one or more of which may be selected.

May contain simple text or objects. Its methods, properties, and events allow items to be selected, modified, added, and sorted.

ListView

Displays items and subitems.

May take a grid format where each row represents a different item and subitems. It also permits items to be displayed as icons.

MenuStrip

Adds a menu to a form.

Provides a menu and submenu system for a form. It supersedes the MainMenu control.

Panel

FlowPanelLayout

TablePanelLayout

Groups controls.

A visible or invisible container that groups controls. Can be made scrollable.

FlowPanelLayout automatically aligns controls vertically or horizontally.

TablePanelLayout aligns controls in a grid.

PictureBox

Contains a graphic.

Used to hold images in a variety of standard formats. Properties enable images to be positioned and sized within control's borders.

ProgressBar

Depicts an application's progress.

Displays the familiar progress bar that gives a user feedback regarding the progress of some event such as file copying.

RadioButton

Permits user to make one choice among a group of options.

Represents a Windows radio button.

StatusStrip

Provides a set of panels that indicate program status.

Provides a status bar that is used to provide contextual status information about current form activities.

TextBox

Accepts user input.

Can be designed to accept single- or multi-line input. Properties allow it to mask input for passwords, scroll, set letter casing automatically, and limit contents to read-only.

TReeView

Displays data as nodes in a tree.

Features include the ability to collapse or expand, add, remove, and copy nodes in a tree.

This chapter lacks the space to provide a detailed look at each control. Instead, it takes a selective approach that attempts to provide a flavor of the controls and features that most benefit the GUI developer. Notable omissions are the DataGridView control, which is included in the discussion of data binding in Chapter 12, "Data Binding with Windows Forms Controls," and the menu controls that were discussed in Chapter 6, "Building Windows Forms Applications."


7.2. Button Classes, Group Box, Panel, and Label

The Button Class

A button is the most popular way to enable a user to initiate some program action. Typically, the button responds to a mouse click or keystroke by firing a Click event that is handled by an event handler method that implements the desired response.

constructor: public Button()

The constructor creates a button instance with no label. The button's Text property sets its caption and can be used to define an access key (see Handling Button Events section); its Image property is used to place an image on the button's background.

Setting a Button's Appearance

Button styles in .NET are limited to placing text and an image on a button, making it flat or three-dimensional, and setting the background/foreground color to any available color. The following properties are used to define the appearance of buttons, check boxes, and radio buttons:

FlatStyle | This can take four values: FlatStyle.Flat, FlatStyle.Popup, FlatStyle.Standard, and FlatStyle.System. Standard is the usual three-dimensional button. Flat creates a flat button. Popup creates a flat button that becomes three-dimensional on a mouseover. System results in a button drawn to suit the style of the operating system.
Image | Specifies the image to be placed on the button. The Image.FromFile method is used to create the image object from a specified file: button1.Image = Image.FromFile("c:\\book.gif");
ImageAlign | Specifies the position of the image on the button. It is set to a value of the ContentAlignment enum: button1.ImageAlign = ContentAlignment.MiddleRight;
TextAlign | Specifies the position of text on the image using the ContentAlignment value.

Handling Button Events

A button's Click event can be triggered in several ways: by a mouse click of the button, by pressing the Enter key or space bar, or by pressing the Alt key in combination with an access key. An access key is created by placing an & in front of one of the characters in the control's Text property value.

The following code segment declares a button, sets its access key to C, and registers an event handler to be called when the Click event is triggered:

Button btnClose = new Button();

btnClose.Text= "&Close";  // Pushing ALT + C triggers event

btnClose.Click += new EventHandler(btnClose_Clicked);

// Handle Mouse Click, ENTER key, or Space Bar

private void btnClose_Clicked(object sender, System.EventArgs e)

{ this.Close();  }

Note that a button's Click event can also occur in cases when the button does not have focus. The AcceptButton and CancelButton form properties can specify a button whose Click event is triggered by pushing the Enter or Esc keys, respectively.

Core Suggestion

Set a form's CancelButton property to a button whose Click event handler closes the form. This provides a convenient way for users to close a window by pushing the Esc key.

The CheckBox Class

The CheckBox control allows a user to select a combination of options on a form—in contrast to the RadioButton, which allows only one selection from a group.

constructor: public CheckBox()

The constructor creates an unchecked check box with no label. The Text and Image properties allow the placement of an optional text description or image beside the box.

Setting a CheckBox's Appearance

Check boxes can be displayed in two styles: as a traditional check box followed by text (or an image) or as a toggle button that is raised when unchecked and flat when checked. The appearance is selected by setting the Appearance property to Appearance.Normal or Appearance.Button. The following code creates the two check boxes shown in Figure 7-2.

// Create traditional check box

this.checkBox1 = new CheckBox();

this.checkBox1.Location =

        new System.Drawing.Point(10,120);

this.checkBox1.Text = "La Traviata";

this.checkBox1.Checked = true;

// Create Button style check box

this.checkBox2 = new CheckBox();

this.checkBox2.Location =

        new System.Drawing.Point(10,150);

this.checkBox2.Text = "Parsifal";

this.checkBox2.Appearance = Appearance.Button;

this.checkBox2.Checked = true;

this.checkBox2.TextAlign = ContentAlignment.MiddleCenter;

Figure 7-2. CheckBox styles

The RadioButton Class

The RadioButton is a selection control that functions the same as a check box except that only one radio button within a group can be selected. A group consists of multiple controls located within the same immediate container.

constructor: public RadioButton()

The constructor creates an unchecked RadioButton with no associated text. The Text and Image properties allow the placement of an optional text description or image beside the box. A radio button's appearance is defined by the same properties used with the check box and button: Appearance and FlatStyle.

Placing Radio Buttons in a Group

Radio buttons are placed in groups that allow only one item in the group to be selected. For example, a 10-question multiple choice form would require 10 groups of radio buttons. Aside from the functional need, groups also provide an opportunity to create an aesthetically appealing layout.

The frequently used GroupBox and Panel container controls support background images and styles that can enhance a form's appearance. Figure 7-3 shows the striking effect (even more so in color) that can be achieved by placing radio buttons on top of a GroupBox that has a background image.

Figure 7-3. Radio buttons in a GroupBox that has a background image

Listing 7-1 presents a sample of the code that is used to place the radio buttons on the GroupBox control and make them transparent so as to reveal the background image.

Listing 7-1. Placing Radio Buttons in a GroupBox

using System.Drawing;

using System.Windows.Forms;

public class OperaForm : Form

{

   private RadioButton radioButton1;

   private RadioButton radioButton2;

   private RadioButton radioButton3;

   private GroupBox groupBox1;

   public OperaForm()

   {

      this.groupBox1 = new GroupBox();

      this.radioButton3 = new RadioButton();

      this.radioButton2 = new RadioButton();

      this.radioButton1 = new RadioButton();

      // All three radio buttons are created like this

      // For brevity only code for one button is included

      this.radioButton3.BackColor = Color.Transparent;

      this.radioButton3.Font = new Font("Microsoft Sans Serif",

                                        8.25F, FontStyle.Bold);

      this.radioButton3.ForeColor =

           SystemColors.ActiveCaptionText;

      this.radioButton3.Location = new Point(16, 80);

      this.radioButton3.Name = "radioButton3";

      this.radioButton3.Text = "Parsifal";

      // Group Box

      this.groupBox1 = new GroupBox();

      this.groupBox1.BackgroundImage =

           Image.FromFile("C:\\opera.jpg");

      this.groupBox1.Size = new Size(120, 112);

      // Add radio buttons to groupbox

      groupBox1.Add( new Control[]{radioButton1,radiobutton2,

                                   radioButton3});

   }

}

Note that the BackColor property of the radio button is set to Color.Transparent. This allows the background image of groupBox1 to be displayed. By default, BackColor is an ambient property, which means that it takes the color of its parent control. If no color is assigned to the radio button, it takes the BackColor of groupBox1 and hides the image.

The GroupBox Class

A GroupBox is a container control that places a border around its collection of controls. As demonstrated in the preceding example, it is often used to group radio buttons; but it is also a convenient way to organize and manage any related controls on a form. For example, setting the Enabled property of a group box to false disables all controls in the group box.

constructor:  public GroupBox()

The constructor creates an untitled GroupBox having a default width of 200 pixels and a default height of 100 pixels.

The Panel Class

The Panel control is a container used to group a collection of controls. It's closely related to the GroupBox control, but as a descendent of the ScrollableControl class, it adds a scrolling capability.

constructor:  public Panel()

Its single constructor creates a borderless container area that has scrolling disabled. By default, a Panel takes the background color of its container, which makes it invisible on a form.

Because the GroupBox and Panel serve the same purpose, the programmer is often faced with the choice of which to use. Here are the factors to consider in selecting one:

  • A GroupBox may have a visible caption, whereas the Panel does not.

  • A GroupBox always displays a border; a Panel's border is determined by its BorderStyle property. It may be set to BorderStyle.None, BorderStyle.Single, or BorderStyle.Fixed3D.

  • A GroupBox does not support scrolling; a Panel enables automatic scrolling when its AutoScroll property is set to TRue.

A Panel offers no features to assist in positioning or aligning the controls it contains. For this reason, it is best used when the control layout is known at design time. But this is not always possible. Many applications populate a form with controls based on criteria known only at runtime. To support the dynamic creation of controls, .NET offers two layout containers that inherit from Panel and automatically position controls within the container: the FlowLayoutPanel and the TableLayoutPanel.

The FlowLayoutPanel Control

Figure 7-4 shows the layout of controls using a FlowLayoutPanel.

Figure 7-4. FlowLayoutPanel

This "no-frills" control has a single parameterless constructor and two properties worth noting: a FlowDirection property that specifies the direction in which controls are to be added to the container, and a WrapControls property that indicates whether child controls are rendered on another row or truncated.

The following code creates a FlowLayoutPanel and adds controls to its collection:

FlowLayoutPanel flp = new FlowLayoutPanel();

flp.FlowDirection = FlowDirection.LefttoRight;

// Controls are automatically positioned left to right

flp.Controls.Add(Button1);

flp.Controls.Add(Button2);

flp.Controls.Add(TextBox1);

flp.Controls.Add(Button3);

this.Controls.Add(flp);     // Add container to form

The FlowDirection enumerator members are BottomUp, LeftToRight, RighttoLeft, and TopDown. LefttoRight is the default.

TableLayoutPanel Control

Figure 7-5 shows the grid layout that results from using a TableLayoutPanel container.

Figure 7-5. TableLayoutPanel organizes controls in a grid

This code segment creates a TableLayoutPanel and adds the same four controls used in the previous example. Container properties are set to define a layout grid that has two rows, two columns, and uses an Inset border style around each cell. Controls are always added to the container moving left-to-right, top-to-bottom.

TableLayoutPanel tlp = new TableLayoutPanel();

// Causes the inset around each cell

tlp.CellBorderStyle = TableLayoutPanelCellBorderStyle.Inset;

tlp.ColumnCount = 2;      // Grid has two columns

tlp.RowCount    = 2;      // Grid has two rows

// If grid is full add extra cells by adding column

tlp.GrowStyle = TableLayoutPanelGrowStyle.AddColumns;

// Padding (pixels)within each cell (left, top, right, bottom)

tlp.Padding = new Padding(1,1,4,5);

tlp.Controls.Add(Button1);

tlp.Controls.Add(Button2);

// Other controls added here

The GrowStyle property is worth noting. It specifies how controls are added to the container when all of its rows and columns are filled. In this example, AddColumns specifies that a column be added to accommodate new controls. The other options are AddRows and None; the latter causes an exception to be thrown if an attempt is made to add a control when the panel is filled.

The Label Class

The Label class is used to add descriptive information to a form.

constructor: public Label()

The constructor creates an instance of a label having no caption. Use the Text property to assign a value to the label. The Image, BorderStyle, and TextAlign properties can be used to define and embellish the label's appearance.

The following code creates the label shown in Figure 7-6:

Label imgLabel = new Label();

imgLabel.BackColor= Color.White;

Image img = Image.FromFile("c:\\rembrandt.jpg");

imgLabel.Image= img;

imgLabel.ImageAlign= ContentAlignment.TopCenter;

imgLabel.Text="Rembrandt";

imgLabel.TextAlign= ContentAlignment.BottomCenter;

imgLabel.BorderStyle= BorderStyle.Fixed3D;

imgLabel.Size = new Size(img.Width+10, img.Height+25);

Figure 7-6. Label containing an image and text

One of its less familiar properties is UseMnemonic. By setting it to TRue and placing a mnemonic (& followed by a character) in the label's text, you can create an access key. For example, if a label has a value of &Sum, pressing Alt-S shifts the focus to the control (based on tab order) following the label.


7.3. PictureBox and TextBox Controls

The PictureBox Class

The PictureBox control is used to display images having a bitmap, icon, metafile, JPEG, GIF, or PNG format. It is a dynamic control that allows images to be selected at design time or runtime, and permits them to be resized and repositioned within the control.

constructor: public PictureBox()

The constructor creates an empty (Image = null) picture box that has its SizeMode property set so that any images are displayed in the upper-left corner of the box.

The two properties to be familiar with are Image and SizeMode. Image, of course, specifies the graphic to be displayed in the PictureBox. SizeMode specifies how the image is rendered within the PictureBox. It can be assigned one of four values from the PictureBoxSizeMode enumeration:

  1. AutoSize. PictureBox is sized to equal the image.

  2. CenterImage. Image is centered in box and clipped if necessary.

  3. Normal. Image is place in upper-left corner and clipped if necessary.

  4. StretchImage. Image is stretched or reduced to fit in box.

Figure 7-7 illustrates some of the features of the PictureBox control. It consists of a form with three small picture boxes to hold thumbnail images and a larger picture box to display a full-sized image. The large image is displayed when the user double-clicks on a thumbnail image.

Figure 7-7. Thumbnail images in small picture boxes are displayed at full size in a larger viewing window

The code, given in Listing 7-2, is straightforward. The event handler ShowPic responds to each DoubleClick event by setting the Image property of the large PictureBox ( bigPictur e ) to the image contained in the thumbnail. Note that the original images are the size of bigPicture and are automatically reduced (by setting SizeMode) to fit within the thumbnail picture boxes.

Listing 7-2. Working with Picture Boxes

using System;

using System.Drawing;

using System.Windows.Forms;

public class ArtForm : Form

{

   private PictureBox bigPicture;

   private PictureBox tn1;

   private PictureBox tn2;

   private PictureBox tn3;

   private Button btnClear;

   public ArtForm()

   {

      bigPicture = new PictureBox();

      tn1 = new PictureBox();

      tn2 = new PictureBox();

      tn3 = new PictureBox();

      btnClear = new Button();

      bigPicture.Location = new Point(90, 30);

      bigPicture.Name = "bigPicture";

      bigPicture.Size = new Size(160, 160);

      this.Controls.Add(bigPicture);

      // Define picturebox to hold first thumbnail image

      tn1.BorderStyle = BorderStyle.FixedSingle;

      tn1.Cursor = Cursors.Hand;

      tn1.Image = Image.FromFile("C:\\schiele1.jpg");

      tn1.Location = new Point(8, 16);

      tn1.Name = "tn1";

      tn1.Size = new Size(56, 56);

      tn1.SizeMode = PictureBoxSizeMode.StretchImage;

      this.Controls.Add(tn1);

      // Code for other thumbnails would go here

      // Button to clear picture box

      btnClear.Location = new Point(136, 192);

      btnClear.Name = "btnClear";

      btnClear.Size = new Size(88, 24);

      btnClear.Text = "Clear Image";

      this.Controls.Add(btnClear);

      btnClear.Click += new EventHandler(this.btnClear_Click);

      // Set up event handlers for double click events

      tn1.DoubleClick += new EventHandler(ShowPic);

      tn2.DoubleClick += new EventHandler(ShowPic);

      tn3.DoubleClick += new EventHandler(ShowPic);

   }

   static void Main()

   {

      Application.Run(new ArtForm());

   }

   private void btnClear_Click(object sender, EventArgs e)

   {

      bigPicture.Image = null;   // Clear image

   }

   private void ShowPic (object sender, EventArgs e)

   {

      // Sender is thumbnail image that is double clicked

      bigPicture.Image = ((PictureBox) sender).Image;

   }

}

The TextBox Class

The familiar TextBox is an easy-to-use control that has several properties that affect its appearance, but few that control its content. This leaves the developer with the task of setting up event handlers and data verification routines to control what is entered in the box.

constructor: public TextBox()

The constructor creates a TextBox that accepts one line of text and uses the color and font assigned to its container. From such humble origins, the control is easily transformed into a multi-line text handling box that accepts a specific number of characters and formats them to the left, right, or center. Figure 7-8 illustrates some of the properties used to do this.

Figure 7-8. TextBox properties

The text is placed in the box using the Text property and AppendText method:

txtPoetry.Text =

   "In Xanadu did Kubla Khan\r\na stately pleasure dome decree,";

txtPoetry.AppendText("\r\nWhere Alph the sacred river ran");

A couple of other TextBox properties to note are ReadOnly, which prevents text from being modified, and PasswordChar, which is set to a character used to mask characters entered—usually a password.

TextBoxes and Carriage Returns

When storing data from a TextBox into a database, you want to make sure there are no special characters embedded in the text, such as a carriage return. If you look at the TextBox properties, you'll find AcceptsReturn, which looks like a simple solution. Setting it to false should cause a TextBox to ignore the user pressing an Enter key. However, the name of this property is somewhat misleading. It only works when the form's AcceptButton property is set to a button on the form. Recall that this property causes the associated button's Click handler to be executed when the Enter key is pressed. If AcceptButton is not set (and the MultiLine property of the text box is set to TRue), the TextBox receives a newline (\r\n) when the Enter key is pushed.

This leaves the developer with the task of handling unwanted carriage returns. Two approaches are available: capture the keystrokes as they are entered or extract the characters before storing the text. The first approach uses a keyboard event handler, which you should be familiar with from the previous chapter.

// Set up event handler in constructor for TextBox txtPoetry

txtPoetry.KeyPress += new KeyPressEventHandler(onKeyPress);

private void onKeyPress( object sender, KeyPressEventArgs e)

{

   if(e.KeyChar == (char)13) e.Handled = true;

}

Setting Handled to TRue prevents the carriage return/linefeed from being added to the text box. This works fine for keyboard entry but has no effect on a cut-and-paste operation. To cover this occurrence, you can use the keyboard handling events described in Chapter 6 to prevent pasting, or you can perform a final verification step that replaces any returns with a blank or any character of your choice.

txtPoetry.Text = txtPoetry.Text.Replace(Environment.NewLine," ");

Core Note

Two common approaches for entering a carriage return/linefeed programmatically into a TextBox are

txtPoetry.Text = "Line 1\r\nLine 2";

txtPoetry.Text = "Line 1"+Environment.NewLine+"Line 2";


7.4. ListBox, CheckedListBox, and ComboBox Classes

The ListBox Class

The ListBox control is used to provide a list of items from which the user may select one or more items. This list is typically text but can also include images and objects. Other features of the ListBox include methods to perform text-based searches, sorting, multi-column display, horizontal and vertical scroll bars, and an easy way to override the default appearance and create owner-drawn ListBox items.

constructor: public ListBox()

The constructor creates an empty ListBox. The code to populate a ListBox is typically placed in the containing form's constructor or Form.Load event handler. If the ListBox.Sorted property is set to TRue, ListBox items are sorted alphabetically in ascending order. Also, vertical scroll bars are added automatically if the control is not long enough to display all items.

Adding Items to a ListBox

A ListBox has an Items collection that contains all elements of the list. Elements can be added by binding the ListBox to a data source (described in Chapter 11, "ADO.NET") or manually by using the Add method. If the Sorted property is false, the items are listed in the order they are entered. There is also an Insert method that places an item at a specified location.

lstArtists.Items.Add("Monet");

lstArtists.Items.Add("Rembrandt");

lstArtists.Items.Add("Manet");

lstArtists.Items.Insert(0, "Botticelli"); //Place at top

Core Note

To prevent a ListBox from repainting itself each time an item is added, execute the ListBox.BeginUpdate method prior to adding and ListBox.EndUpdate after the last item is added.

List boxes may also contain objects. Because an object may have many members, this raises the question of what is displayed in the TextBox list. Because by default a ListBox displays the results of an item's ToString method, it is necessary to override this System.Object method to return the string you want displayed. The following class is used to create ListBox items:

// Instances of this class will be placed in a ListBox

public class Artist

{

   public string BDate, DDate, Country;

   private string firstname;

   private string lastname;

   public Artist(string birth, string death, string fname,

                 string lname, string ctry)

   {

      BDate = birth;

      DDate = death;

      Country = ctry;

      firstname = fname;

      lastname = lname;

   }

   public override string ToString() {

      return (lastname+" , "+firstname);

   }

   public string GetLName    {

      get{ return lastname;}

   }

   public string GetFName    {

      get{ return firstname;}

   }

}

ToString has been overridden to return the artist's last and first names, which are displayed in the ListBox. The ListBox (Figure 7-9) is populated using these statements:

lstArtists.Items.Add

   (new Artist("1832", "1883", "Edouard", "Manet","Fr" ));

lstArtists.Items.Add

   (new Artist("1840", "1926", "Claude", "Monet","Fr"));

lstArtists.Items.Add

   (new Artist("1606", "1669", "Von Rijn", "Rembrandt","Ne"));

lstArtists.Items.Add

   (new Artist("1445", "1510", "Sandre", "Botticelli","It"));

Figure 7-9. ListBox items: (A) Default and (B) Custom drawn

Selecting and Searching for Items in a ListBox

The SelectionMode property determines the number of items a ListBox allows to be selected at one time. It takes four values from the SelectionMode enumeration: None, Single, MultiSingle, and MultiExtended. MultiSingle allows selection by clicking an item or pressing the space bar; MultiExtended permits the use of the Shift and Ctrl keys.

The SelectedIndexChanged event provides an easy way to detect when an item in a ListBox is selected. It is fired when the user clicks on an item or uses the arrow keys to traverse a list. A common use is to display further information about the selection in other controls on the form. Here is code that displays an artist's dates of birth and death when the artist's name is selected from the ListBox in Figure 7-9:

// Set up event handler in constructor

lstArtists.SelectedIndexChanged += new EventHandler(ShowArtist);

//

private void ShowArtist(object sender, EventArgs e)

{

   // Cast to artist object in order to access properties

   Artist myArtist = lstArtists.SelectedItem as Artist;

   if (myArtist != null) {

      txtBirth.Text = myArtist.Dob; // Place dates in text boxes

      txtDeath.Text = myArtist.Dod;

   }

}

The SelectedItem property returns the item selected in the ListBox. This object is assigned to myArtist using the as operator, which ensures the object is an Artist type. The SelectedIndex property can also be used to reference the selected item:

myArtist = lstArtists.Items[lstArtists.SelectedIndex] as Artist;

Working with a multi-selection ListBox requires a different approach. You typically do not want to respond to a selection event until all items have been selected. One approach is to have the user click a button to signal that all choices have been made and the next action is required. All selections are exposed as part of the SelectedItems collection, so it is an easy matter to enumerate the items:

foreach (Artist a in lstArtists.SelectedItems)

   MessageBox.Show(a.GetLName);

The SetSelected method provides a way to programatically select an item or items in a ListBox. It highlights the item(s) and fires the SelectedIndexChanged event. In this example, SetSelected is used to highlight all artists who were born in France:

lstArtists.ClearSelected();   // Clear selected items

for (int ndx =0; ndx < lstArtists.Items.Count-1; ndx ++)

{

   Artist a = lstArtists.Items[ndx] as Artist;

   if (a.country == "Fr") lstArtists.SetSelected(ndx,true);

}

Customizing the Appearance of a ListBox

The ListBox, along with the ComboBox, MenuItem, and TabControl controls, is an owner-drawn control. This means that by setting a control property, you can have it fire an event when the control's contents need to be drawn. A custom event handler takes care of the actual drawing.

To enable owner drawing of the ListBox, the DrawMode property must be set to one of two DrawMode enumeration values: OwnerDrawFixed or OwnerDrawVariable. The former draws each item a fixed size; the latter permits variable-sized items. Both of these cause the DrawItem event to be fired and rely on its event handler to perform the drawing.

Using the ListBox from the previous example, we can use the constructor to set DrawMode and register an event handler for the DrawItem event:

lstArtists.DrawMode = DrawMode.OwnerDrawFixed;

lstArtists.ItemHeight = 16;  // Height (pixels) of item

lstArtists.DrawItem += new DrawItemEventHandler(DrawList);

The DrawItemEventHandler delegate has two parameters: the familiar sender object and the DrawItemEventArgs object. The latter is of more interest. It contains properties related to the control's appearance and state as well as a couple of useful drawing methods. Table 7-2 summarizes these.

Table 7-2. DrawItemEventArgs Properties

Member

Description

BackColor

Background color assigned to the control.

Bounds

Defines the coordinates of the item to be drawn as a Rectangle object.

Font

Returns the font assigned to the item being drawn.

ForeColor

Foreground color of the control. This is the color of the text displayed.

Graphics

Represents the surface (as a Graphics object) on which the drawing occurs.

Index

The index in the control where the item is being drawn.

State

The state of the item being drawn. This value is a DrawItemState enumeration. For a ListBox, its value is Selected (1) or None(0).

DrawBackground()

Draws the default background.

DrawFocusRectangle()

Draws the focus rectangle around the item if it has focus.

Index is used to locate the item. Font, BackColor, and ForeColor return the current preferences for each. Bounds defines the rectangular area circumscribing the item and is used to indicate where drawing should occur. State is useful for making drawing decisions based on whether the item is selected. This is particularly useful when the ListBox supports multiple selections. We looked at the Graphics object briefly in the last chapter when demonstrating how to draw on a form. Here, it is used to draw in the Bounds area. Finally, the two methods, DrawBackground and DrawFocusRectangle, are used as their name implies.

The event handler to draw items in the ListBox is shown in Listing 7-3. Its behavior is determined by the operation being performed: If an item has been selected, a black border is drawn in the background to highlight the selection; if an item is added, the background is filled with a color corresponding to the artist's country, and the first and last names of the artist are displayed.

The routine does require knowledge of some GDI+ concepts (see Chapter 8, ".NET Graphics Using GDI+"). However, the purpose of the methods should be clear from their name and context: FillRectangle fills a rectangular area defined by the Rectangle object, and DrawString draws text to the Graphics object using a font color defined by the Brush object. Figure 7-9(B) shows the output.

Listing 7-3. Event Handler to Draw Items in a ListBox

private void DrawList(object sender, DrawItemEventArgs e)

{

   // Draw ListBox Items

   string ctry;

   Rectangle rect = e.Bounds;

   Artist a = lstArtists.Items[e.Index] as Artist;

   string artistName = a.ToString();

   if ( (e.State & DrawItemState.Selected) ==

                   DrawItemState.Selected )

   {

      // Draw Black border around the selected item

      e.Graphics.DrawRectangle(Pens.Black,rect);

   } else {

      ctry = a.Country;

      Brush b;   // Object used to define backcolor

      // Each country will have a different backcolor

      b = Brushes.LightYellow;      // Netherlands

      if (ctry == "Fr")  b = Brushes.LightGreen;

      if (ctry == "It")  b = Brushes.Yellow;

      e.Graphics.FillRectangle(b,rect);}

      e.Graphics.DrawString(artistName,e.Font,

                            Brushes.Black,rect);

   }

}

Other List Controls: the ComboBox and the CheckedListBox

The ComboBox control is a hybrid control combining a ListBox with a TextBox (see Figure 7-10). Like the ListBox, it derives from the ListControl and thus possesses most of the same properties.

Figure 7-10. ComboBox and CheckedListBox controls are variations on ListBox

Visually, the ComboBox control consists of a text box whose contents are available through its Text property and a drop-down list from which a selected item is available through the SelectedItem property. When an item is selected, its textual representation is displayed in the text box window. A ComboBox can be useful in constructing questionnaires where the user selects an item from the drop-down list or, optionally, types in his own answer. Its construction is similar to the ListBox:

ComboBox cbArtists = new ComboBox();

cbArtists.Size = new System.Drawing.Size(120, 21);

cbArtists.MaxDropDownItems= 4;  // Max number of items to display

cbArtists.DropDownWidth = 140;  // Width of drop-down portion

cbArtists.Items.Add(new Artist("1832", "1883",

                               "Edouard", "Manet","Fr" ));

// Add other items here...

The CheckedListBox is a variation on the ListBox control that adds a check box to each item in the list. The default behavior of the control is to select an item on the first click, and check or uncheck it on the second click. To toggle the check on and off with a single click, set the CheckOnClick property to TRue.

Although it does not support multiple selections, the CheckedListBox does allow multiple items to be checked and includes them in a CheckedItems collection. The code here loops through a collection of Artist objects that have been checked on the control:

// List all items with checked box.

foreach (Artist a in clBox.CheckedItems)

   MessageBox.Show(a.ToString()); //   –> Monet, Claude

You can also iterate through the collection and explicitly determine the checked state:

For (int i=0; I< clBox.Items.Count; i++)

{

   if(clBox.GetItemCheckState(i) == CheckState.Checked)

   { Do something }  else {do something if not checked }

}


7.5. The ListView and TreeView Classes

The ListView Class

ListView is another control that displays lists of information. It represents data relationally as items and subitems. The data can be represented in a variety of formats that include a multi-column grid and large or small icons to represent item data. Also, images and check boxes can adorn the control.

Figure 7-11 illustrates the basic properties and methods used to lay out a Details view of the control—a format obviously tailored to displaying database tables. The first column contains text for an item—as well as a picture—the remaining columns contain subitems for the parent item.

Figure 7-11. ListView control

Let's look at how this style of the ListView is constructed.

Creating a ListView Object

The ListView is created with a parameterless constructor:

ListView listView1 = new ListView();

Define Appearance of ListView Object

// Set the view to show details

listView1.View = View.Details;

The View property specifies one of five layouts for the control:

  • Details. An icon and item's text are displayed in column one. Subitems are displayed in the remaining columns.

  • LargeIcon. A large icon is shown for each item with a label below the icon.

  • List. Each item is displayed as a small icon with a label to its right. The icons are arranged in columns across the control.

  • SmallIcon. Each item appears in a single column as a small icon with a label to its right.

  • *Tile. Each item appears as a full-size icon with the label and subitem details to the right of it. Only available for Windows XP and 2003.

Core Note

The ListView.View property can be changed at runtime to switch among the possible views. In fact, you may recognize that the view options correspond exactly to the View menu options available in Windows Explorer.

After the Details view is selected, other properties that define the control's appearance and behavior are set:

// Allow the user to rearrange columns

listView1.AllowColumnReorder = true;

// Select the entire row when selection is made

listView1.FullRowSelect = true;

// Display grid lines

listView1.GridLines = true;

// Sort the items in the list in ascending order

listView1.Sorting = SortOrder.Ascending;

These properties automatically sort the items, permit the user to drag columns around to rearrange their order, and cause a whole row to be highlighted when the user selects an item.

Set Column Headers

In a Details view, data is not displayed until at least one column is added to the control. Add columns using the Columns.Add method. Its simplest form is

ListView.Columns.Add(caption, width, textAlign)

Caption is the text to be displayed. Width specifies the column's width in pixels. It is set to –1 to size automatically to the largest item in the column, or –2 to size to the width of the header.

// Create column headers for the items and subitems

   listView1.Columns.Add("Artist", -2, HorizontalAlignment.Left);

   listView1.Columns.Add("Born", -2, HorizontalAlignment.Left);

   listView1.Columns.Add("Died", -2, HorizontalAlignment.Left);

   listView1.Columns.Add("Country", -2, HorizontalAlignment.Left);

The Add method creates and adds a ColumnHeader type to the ListView's Columns collection. The method also has an overload that adds a ColumnHeader object directly:

ColumnHeader cHeader:

cHeader.Text = "Artist";

cHeader.Width = -2;

cHeader.TextAlign = HorizontalAlignment.Left;

ListView.Columns.Add(ColumnHeader cHeader);

Create ListView Items

Several overloaded forms of the ListView constructor are available. They can be used to create a single item or a single item and its subitems. There are also options to specify the icon associated with the item and set the foreground and background colors.

Constructors:

public ListViewItem(string text);

public ListViewItem(string[] items );

public ListViewItem(string text,int imageIndex );

public ListViewItem(string[] items,int imageIndex );

public ListViewItem(string[] items,int imageIndex,

         Color foreColor,Color backColor,Font font);

The following code demonstrates how different overloads can be used to create the items and subitems shown earlier in Figure 7-8:

// Create item and three subitems

ListViewItem item1 = new ListViewItem("Manet",2);

item1.SubItems.Add("1832");

item1.SubItems.Add("1883");

item1.SubItems.Add("France");

// Create item and subitems using a constructor only

ListViewItem item2 = new ListViewItem

       (new string[] {"Monet","1840","1926","France"}, 3);

// Create item and subitems with blue background color

ListViewItem item3 = new ListViewItem

       (new string[] {"Cezanne","1839","1906","France"}, 1,

       Color.Empty, Color.LightBlue, null);

To display the items, add them to the Items collection of the ListView control:

// Add the items to the ListView

   listView1.Items.AddRange(

      new ListViewItem[]{item1,item2,item3,item4,item5});

Specifying Icons

Two collections of images can be associated with a ListView control as ImageList properties: LargeImageList, which contains images used in the LargeIcon view; and SmallImageList, which contains images used in all other views. Think of these as zero-based arrays of images that are associated with a ListViewItem by the imageIndex parameter in the ListViewItem constructor. Even though they are referred to as icons, the images may be of any standard graphics format.

The following code creates two ImageList objects, adds images to them, and assigns them to the LargeImageList and SmallImageList properties:

// Create two ImageList objects

   ImageList imageListSmall = new ImageList();

   ImageList imageListLarge = new ImageList();

   imageListLarge.ImageSize = new Size(50,50); // Set image size

   // Initialize the ImageList objects

   // Can use same images in both collections since they're resized

   imageListSmall.Images.Add(Bitmap.FromFile("C:\\botti.gif"));

   imageListSmall.Images.Add(Bitmap.FromFile("C:\\cezanne.gif"));

   imageListLarge.Images.Add(Bitmap.FromFile("C:\\botti.gif"));

   imageListLarge.Images.Add(Bitmap.FromFile("C:\\cezanne.gif"));

   // Add other images here

   // Assign the ImageList objects to the ListView.

   listView1.LargeImageList = imageListLarge;

   listView1.SmallImageList = imageListSmall;

   ListViewItem lvItem1 = new ListViewItem("Cezanne",1);

An index of 1 selects the cezanne.gif images as the large and small icons. Specifying an index not in the ImageList results in the icon at index 0 being displayed. If neither ImageList is defined, no icon is displayed. Figure 7-12 shows the ListView from Figure 7-11 with its view set to View.LargeIcon:

listView1.View = View.LargeIcon;

Figure 7-12. LargeIcon view

Working with the ListView Control

Common tasks associated with the ListView control include iterating over the contents of the control, iterating over selected items only, detecting the item that has focus, and—when in Details view—sorting the items by any column. Following are some code segments to perform these tasks.

Iterating over All Items or Selected Items

You can use foreach to create nested loops that select an item and then iterate through the collection of subitems for the item in the outside loop:

foreach (ListViewItem lvi in listView1.Items)

{

   string row = "";

   foreach(ListViewItem.ListViewSubItem sub in lvi.SubItems)

   {

      row += " " + sub.Text;

   }

   MessageBox.Show(row); // List concatenated subitems

}

There are a couple of things to be aware of when working with these collections. First, the first subitem (index 0) element actually contains the text for the item—not a subitem. Second, the ordering of subitems is not affected by rearranging columns in the ListView control. This changes the appearance but does not affect the underlying ordering of subitems.

The same logic is used to list only selected items (MultiSelect = true permits multiple items to be selected). The only difference is that the iteration occurs over the ListView.SelectedItems collection:

foreach (ListViewItem lvisel in listView1.SelectedItems)

Detecting the Currently Selected Item

In addition to the basic control events such as Click and DoubleClick, the ListView control adds a SelectedIndexChanged event to indicate when focus is shifted from one item to another. The following code implements an event handler that uses the FocusedItem property to identify the current item:

// Set this in the constructor

listView1.SelectedIndexChanged +=

      new EventHandler(lv_IndexChanged);

// Handle SelectedIndexChanged Event

private void lv_IndexChanged(object sender, System.EventArgs e)

{

   string ItemText = listView1.FocusedItem.Text;

}

Note that this code can also be used with the Click events because they also use the EventHandler delegate. The MouseDown and MouseUp events can also be used to detect the current item. Here is a sample MouseDown event handler:

private void listView1_MouseDown(object sender, MouseEventArgs e)

{

   ListViewItem selection = listView1.GetItemAt(e.X, e.Y);

   if (selection != null)

   {

      MessageBox.Show("Item Selected: "+selection.Text);

   }

}

The ListView.GetItemAt method returns an item at the coordinates where the mouse button is pressed. If the mouse is not over an item, null is returned.

Sorting Items on a ListView Control

Sorting items in a ListView control by column values is a surprisingly simple feature to implement. The secret to its simplicity is the ListViewItemSorter property that specifies the object to sort the items anytime the ListView.Sort method is called. Implementation requires three steps:

1. | Set up a delegate to connect a ColumnClick event with an event handler.
2. | Create an event handler method that sets the ListViewItemSorter property to an instance of the class that performs the sorting comparison.
3. | Create a class to compare column values. It must inherit the IComparer interface and implement the IComparer.Compare method.

The following code implements the logic: When a column is clicked, the event handler creates an instance of the ListViewItemComparer class by passing it the column that was clicked. This object is assigned to the ListViewItemSorter property, which causes sorting to occur. Sorting with the IComparer interface is discussed in Chapter 4, "Working with Objects in C#").

// Connect the ColumnClick event to its event handler

listView1.ColumnClick +=new ColumnClickEventHandler(ColumnClick);

// ColumnClick event handler

private void ColumnClick(object o, ColumnClickEventArgs e)

{

   // Setting this property immediately sorts the

   // ListView using the ListViewItemComparer object

   this.listView1.ListViewItemSorter =

       new ListViewItemComparer(e.Column);

}

// Class to implement the sorting of items by columns

class ListViewItemComparer : IComparer

{

   private int col;

   public ListViewItemComparer()

   {

      col = 0;   // Use as default column

   }

   public ListViewItemComparer(int column)

      {

      col = column;

   }

   // Implement IComparer.Compare method

   public int Compare(object x, object y)

   {

      string xText = ((ListViewItem)x).SubItems[col].Text;

      string yText = ((ListViewItem)y).SubItems[col].Text;

      return String.Compare(xText, yText);

   }

}

The TreeView Class

As the name implies, the TReeView control provides a tree-like view of hierarchical data as its user interface. Underneath, its programming model is based on the familiar tree structure consisting of parent nodes and child nodes. Each node is implemented as a treeNode object that can in turn have its own Nodes collection. Figure 7-13 shows a treeView control that is used in conjunction with a ListView to display enum members of a selected assembly. (We'll look at the application that creates it shortly.)

Figure 7-13. Using TReeView control (left) and ListView (right) to list enum values

The TreeNode Class

Each item in a tree is represented by an instance of the TReeNode class. Data is associated with each node using the TReeNode's Text, Tag, or ImageIndex properties. The Text property holds the node's label that is displayed in the TReeView control. Tag is an object type, which means that any type of data can be associated with the node by assigning a custom class object to it. ImageIndex is an index to an ImageList associated with the containing TReeView control. It specifies the image to be displayed next to the node.

In addition to these basic properties, the treeNode class provides numerous other members that are used to add and remove nodes, modify a node's appearance, and navigate the collection of nodes in a node tree (see Table 7-3).

Table 7-3. Selected Members of the treeNode Class

Use

Member

Description

Appearance

BackColor,

ForeColor

Sets the background color and text color of the node.

Expand(),

Collapse()

Expands the node to display child nodes or collapses the tree so no child nodes are shown.

Navigation

FirstNode,

LastNode,

NextNode,

PrevNode

Returns the first or last node in the collection.

Returns the next or previous node (sibling) relative to the current node.

Index

The index of the current node in the collection.

Parent

Returns the current node's parent.

Node

Manipulation

Nodes.Add(),

Nodes.Remove(),

Nodes.Insert(),

Nodes.Clear()

Adds or removes a node to a Nodes collection. Insert adds a node at an indexed location, and Clear removes all tree nodes from the collection.

Clone()

Copies a tree node and entire subtree.

Let's look at how treeView and treeNode members are used to perform fundamental treeView operations.

Adding and Removing Nodes

The following code creates the tree in Figure 7-14 using a combination of Add, Insert, and Clone methods. The methods are performed on a preexisting TReeView1 control.

TreeNode tNode;

// Add parent node to treeView1 control

tNode = treeView1.Nodes.Add("A");

// Add child node: two overloads available

tNode.Nodes.Add(new TreeNode("C"));

tNode.Nodes.Add("D"));

// Insert node after C

tNode.Nodes.Insert(1,new TreeNode("E"));

// Add parent node to treeView1 control

tNode = treeView1.Nodes.Add("B");

Figure 7-14. TReeView node representation

At this point, we still need to add a copy of node A and its subtree to the parent node B. This is done by cloning the A subtree and adding it to node B. Node A is referenced as treeView1.Nodes[0] because it is the first node in the control's collection. Note that the Add method appends nodes to a collection, and they can be referenced by their zero-based position within the collection:

// Clone first parent node and add to node B

TreeNode clNode = (TreeNode) treeView1.Nodes[0].Clone();

tNode.Nodes.Add(clNode);

// Add and remove node for demonstration purposes

tNode.Nodes.Add("G");

tNode.Nodes.Remove(tNode.LastNode);

Iterating Through the Nodes in a TreeView

As with any collection, the foreach statement provides the easiest way to loop through the collection's members. The following statements display all the top-level nodes in a control:

foreach (TreeNode tn in treeView1.Nodes)

{

   MessageBox.Show(tn.Text);

   // If (tn.IsVisible) true if node is visible

   // If (tn.IsSelected) true if node is currently selected

}

An alternate approach is to move through the collection using the treeNode.NextNode property:

tNode = treeView1.Nodes[0];

while (tNode != null) {

   MessageBox.Show(tNode.Text);

   tNode = tNode.NextNode;

}

Detecting a Selected Node

When a node is selected, the TReeView control fires an AfterSelect event that passes a TReeViewEventArgs parameter to the event handling code. This parameter identifies the action causing the selection and the node selected. The TReeView example that follows illustrates how to handle this event.

You can also handle the MouseDown event and detect the node using the GetNodeAt method that returns the node—if any—at the current mouse coordinates.

private void treeView1_MouseDown(object sender, MouseEventArgs e)

{

   TreeNode tn = treeView1.GetNodeAt(e.X, e.Y);

   // You might want to remove the node:  tn.Remove()

}

A TreeView Example That Uses Reflection

This example demonstrates how to create a simple object browser (refer to Figure 7-13) that uses a treeView to display enumeration types for a specified assembly. When a node on the tree is clicked, the members for the selected enumeration are displayed in a ListView control.

Information about an assembly is stored in its metadata, and .NET provides classes in the System.Reflection namespace for exposing this metadata. The code in Listing 7-4 iterates across the types in an assembly to build the treeView. The parent nodes consist of unique namespace names, and the child nodes are the types contained in the namespaces. To include only enum types, a check is made to ensure that the type inherits from System.Enum.

Listing 7-4. Using a treeView and Reflection to List Enums in an Assembly

using System.Reflection;

//

private void GetEnums()

{

   TreeNode tNode=null;

   Assembly refAssembly ;

   Hashtable ht= new Hashtable();  // Keep track of namespaces

   string assem = AssemName.Text;  // Textbox with assembly name

   tvEnum.Nodes.Clear();           // Remove all nodes from tree

   // Load assembly to be probed

   refAssembly = Assembly.Load(assem);

   foreach (Type t in refAssembly.GetTypes())

   {

      // Get only types that inherit from System.Enum

      if(t.BaseType!=null && t.BaseType.FullName=="System.Enum")

      {

         string myEnum  = t.FullName;

         string  nSpace =

                   myEnum.Substring(0,myEnum.LastIndexOf("."));

         myEnum= myEnum.Substring(myEnum.LastIndexOf(".")+1) ;

         // Determine if namespace in hashtable

         if( ht.Contains(nSpace))

         {

            // Find parent node representing this namespace

            foreach (TreeNode tp in tvEnum.Nodes)

            {

              if(tp.Text == myEnum) { tNode=tp; break;}

            }

         }

         else

         {

            // Add parent node to display namespace

            tNode = tvEnum.Nodes.Add(nSpace);

            ht.Add(nSpace,nSpace);

         }

         // Add Child - name of enumeration

         TreeNode cNode = new TreeNode();

         cNode.Text= myEnum;

         cNode.Tag = t;     // Contains specific enumeration

         tNode.Nodes.Add(cNode);

      }

   }

}

Notice how reflection is used. The static Assembly.Load method is used to create an Assembly type. The Assembly.GetTypes is then used to return a Type array containing all types in the designated assembly.

refAssembly = Assembly.Load(assem);

foreach (Type t in refAssembly.GetTypes())

The Type.FullName property returns the name of the type, which includes the namespace. This is used to extract the enum name and the namespace name. The Type is stored in the Tag field of the child nodes and is used later to retrieve the members of the enum.

After the treeView is built, the final task is to display the field members of an enumeration when its node is clicked. This requires registering an event handler to be notified when an AfterSelect event occurs:

tvEnum.AfterSelect += new

   TreeViewEventHandler(tvEnum_AfterSelect);

The event handler identifies the selected node from the treeViewEventArgs.Node property. It casts the node's Tag field to a Type class (an enumerator in this case) and uses the GetMembers method to retrieve the type's members as MemberInfo types. The name of each field member—exposed by the MemberInfo.Name property—is displayed in the ListView:

// ListView lView;

// lView.View = View.List;

private void tvEnum_AfterSelect(Object sender,

                                TreeViewEventArgs e)

{

   TreeNode tn = e.Node;   // Node selected

   ListViewItem lvItem;

   if(tn.Parent !=null)    // Exclude parent nodes

   {

      lView.Items.Clear(); // Clear ListView before adding items

      Type cNode = (Type) tn.Tag;

      // Use Reflection to iterate members in a Type

      foreach (MemberInfo mi in cNode.GetMembers())

      {

         if(mi.MemberType==MemberTypes.Field &&

                           mi.Name != "value__" )  // skip this

         {

            lView.Items.Add(mi.Name);

         }

      }

   }

}


7.6. The ProgressBar, Timer, and StatusStrip Classes

The ProgressBar and Timer are lightweight controls that have complementary roles in an application: The Timer initiates action and the ProgressBar reflects the status of an operation or action. In fact, the Timer is not a control, but a component that inherits from the ComponentModel.Component class. It is used most often in processes to regulate some background activity. This may be a periodic update to a log file or a scheduled backup of data. A ProgressBar, on the other hand, provides visual feedback regarding the progress of an operation—such as file copying or steps in an installation.

The third class discussed in this section is the StatusStrip, which is often used in conjunction with a timer and ProgressBar. It's rendered on a form as a strip divided into one or more sections or panes that provide status information. Each section is implemented as a control that is added to the StatusStrip container. For a control to be included in the StatusStrip, it must inherit from the ToolStripItem class.

Building a StatusStrip

Let's now build a form that includes a multi-pane StatusStrip. As shown in Figure 7-15, the strip consists of a label, progress bar, and panel controls. The label (ToolStripLabel) provides textual information describing the overall status of the application. The progress bar is implemented as a ToolStripProgressBar object. It is functionally equivalent to a ProgressBar, but inherits from ToolStripItem. A StatusStripPanel shows the elapsed time since the form was launched. An event handler that is triggered by a timer updates both the progress bar and clock panel every five seconds.

Figure 7-15. StatusStrip with Label, ProgressBar, and Panel

Listing 7-5 contains the code to create the StatusStrip. The left and right ends of the progress bar are set to represent the values 0 and 120, respectively. The bar is set to increase in a step size of 10 units each time the PerformStep method is executed. It recycles every minute.

The Timer controls when the bar is incremented and when the elapsed time is updated. Its Interval property is set to a value that controls how frequently its Tick event is fired. In this example, the event is fired every 5 seconds, which results in the progress bar being incremented by 10 units and the elapsed time by 5 seconds.

Listing 7-5. StatusStrip That Uses a ProgressBar and Timer

// These variables have class scope

Timer currTimer;

StatusStrip statusStrip1;

StatusStripPanel panel1;

ToolStripProgressBar pb;

DateTime startDate = DateTime.Now;

private void BuildStrip()

{

   currTimer = new Timer();

   currTimer.Enabled = true;

   currTimer.Interval = 5000; // Fire tick event every 5 seconds

   currTimer.Tick += new EventHandler(timer_Tick);

   // Panel to contain elapsed time

   panel1 = new StatusStripPanel();

   panel1.BorderStyle = Border3DStyle.Sunken;

   panel1.Text = "00:00:00";

   panel1.Padding = new Padding(2);

   panel1.Name = "clock";

   panel1.Alignment = ToolStripItemAlignment.Tail; //Right align

   // Label to display application status

   ToolStripLabel ts = new ToolStripLabel();

   ts.Text = "Running...";

   // ProgressBar to show time elapsing

   pb = new ToolStripProgressBar();

   pb.Step    = 10;      // Size of each step or increment

   pb.Minimum = 0;

   pb.Maximum = 120;     // Allow 12 steps

   // Status strip to contain components

   statusStrip1 = new StatusStrip();

   statusStrip1.Height = 20;

   statusStrip1.AutoSize = true;

   // Add components to strip

   statusStrip1.Items.AddRange(new ToolStripItem[] {

      ts, pb, panel1 } );

   this.Controls.Add(statusStrip1);

}

private void timer_Tick(object sender, EventArgs e)

{

   // Get difference between current datetime

   // and form startup time

   TimeSpan ts = DateTime.Now.Subtract(startDate);

   string elapsed = ts.Hours.ToString("00") + ":" +

         ts.Minutes.ToString("00") +

         ":" + ts.Seconds.ToString("00");

   ((StatusStripPanel)statusStrip1.Items[

         "clock"]).Text= elapsed;

   // Advance progress bar

   if (pb.Value == pb.Maximum) pb.Value = 0;

   pb.PerformStep();   // Increment progress bar

}

The StatusStripPanel that displays the elapsed time has several properties that control its appearance and location. In addition to those shown here, it has an Image property that allows it to display an image. The StatusStripPanel class inherits from the ToolStripLabel class that is used in the first pane. Both can be used to display text, but the panel includes a BorderStyle property that ToolStripLabel lacks.


7.7. Building Custom Controls

At some point, you will face a programming task for which a standard WinForms control does not provide the functionality you need. For example, you may want to extend a TextBox control so that its background color changes according to its content, group a frequently used set of radio buttons into a single control, or create a new control that shows a digital clock face with the date underneath. These needs correspond to the three principal types of custom controls:

  1. A control that derives from an existing control and extends its functionality.

  2. A control that can serve as container to allow multiple controls to interact. This type of control is referred to as a user control. It derives directly from System.Windows.Forms.UserControl rather than Control, as do standard controls.

  3. A control that derives directly from the Control class. This type of control is built "from scratch," and it is the developer's responsibility to draw its GUI interface and implement the methods and properties that allow it to be manipulated by code.

Let's now look at how to extend an existing control and create a user control.

Extending a Control

The easiest way to create a custom control is to extend an existing one. To demonstrate this, let's derive a TextBox that accepts only digits. The code is quite simple. Create a new class NumericTextBox with TextBox as its base class. The only code required is an event handler to process the KeyPress event and accept only a digit.

class NumericTextBox: TextBox

{

   public NumericTextBox()

   {

      this.KeyPress += new KeyPressEventHandler(TextBoxKeyPress);

   }

   protected void TextBoxKeyPress(object sender,

                                  KeyPressEventArgs e)

   {

      if (! char.IsDigit(e.KeyChar)) e.Handled = true;

   }

}

After the extended control is compiled into a DLL file, it can be added to any form.

Building a Custom UserControl

Think of a user control as a subform. Like a form, it provides a container surface on which related widgets are placed. When compiled, the entire set of controls is treated as a single user control. Of course, users still can interact directly with any of the member controls. Programmatic and design-time access to control members is available through methods and properties defined on the user control.

The easiest way to design a control is with an IDE such as Visual Studio.NET (VS.NET), which makes it easy to position and size controls. The usual way to create a user control in VS.NET is to open a project as a Windows Control Library type. This immediately brings up a control designer window. The design window can also be accessed in a Windows Application by selecting Project – Add User Control from the top menu bar or right-clicking on the Solution Explorer and selecting Add – Add User Control. Although VS.NET can speed up the process of creating a control, it does not generate any proprietary code that cannot be duplicated using a text editor.

A UserControl Example

As an example, let's create a control that can be used to create a questionnaire. The control consists of a label whose value represents the question, and three radio buttons contained on a panel control that represent the user's choice of answers. The control exposes three properties: one that assigns the question to the label, one to set the background color of the panel control, and another that identifies the radio button associated with the user's answer.

Figure 7-16 shows the layout of the user control and the names assigned to each contained control.

Figure 7-16. Layout of a custom user control

Here is how the members are represented as fields within the UserControl1 class:

public class UserControl1 : System.Windows.Forms.UserControl

{

   private Panel panel1;

   private RadioButton radAgree;

   private RadioButton radDisagree;

   private RadioButton radUn;

   private Label qLabel;

Listing 7-6 contains the code for three properties: SetQ that sets the label's text property to the question, PanelColor that sets the color of the panel, and Choice, which returns the answer selected by the user as a Choices enum type.

Listing 7-6. Implementing Properties for a Custom User Control

public enum Choices

{

   Agree      = 1,

   DisAgree   = 2,

   Undecided     = 3,

}

public string SetQ

{

   set {qLabel.Text = value;}

   get {return(qLabel.Text);}

}

public Color PanelColor

{

   set {panel1.BackColor= value;}

   get {return(panel1.BackColor);}

}

public Choices Choice

{

   get

   {

      Choices usel;

      usel = Choices.Undecided;

      if (radDisagree.Checked) usel= Choices.DisAgree;

      if (radAgree.Checked) usel = Choices.Agree;

      return(usel);}

   }

}

Using the Custom User Control

If the user control is developed as part of a VS.NET Windows Application project, it is automatically added to the tool box under the Windows Forms tab. Simply select it and drop it onto the form. Otherwise, you have to right-click on a tool box tab, select Customize ToolBox, browse for the control, and add it to the tool box.

Figure 7-17 provides an example of using this new control. In this example, we place two control instances on the form and name them Q1 and Q2:

private usercontrol.UserControl1 Q1;

private usercontrol.UserControl1 Q2;

Figure 7-17. Custom user controls on a form

The properties can be set in the constructor or at runtime in the Form.Load event handler. If using VS.NET, the properties can be set at design time using the Property Browser.

Q1.SetQ = "The economy is performing well";

Q2.SetQ = "I'm not worried about the budget deficit.";

Q1.PanelColor = Color.Beige;

The final step in the application is to do something with the results after the questionnaire has been completed. The following code iterates through the controls on the form when the button is clicked. When a UserControl1 type is encountered, its Choice property is used to return the user's selection.

private void button1_Click(object sender, System.EventArgs e)

{

   foreach (Control ct in this.Controls)

   {

      if (ct is usercontrol.UserControl1)

      {

         UserControl1 uc = (UserControl1)ct;

         // Display control name and user's answer

         MessageBox.Show(ct.Name+" "+

              uc.Choice.ToString());

      }

   }

 }

Working with the User Control at Design Time

If you are developing an application with VS.NET that uses this custom control, you will find that the Property Browser lists all of the read/write properties. By default, they are placed in a Misc category and have no description associated with them. To add a professional touch to your control, you should create a category for the control's events and properties and add a textual description for each category member.

The categories and descriptions available in the Property Browser come from metadata based on attributes attached to a type's members. Here is an example of attributes added to the PanelColor property:

[Browsable(true),

Category("QControl"),

Description("Color of panel behind question block")]

public Color PanelColor

{

   set {panel1.BackColor = value;}

   get {return (panel1.BackColor);}

}

The Browsable attribute indicates whether the property is to be displayed in the browser. The default is TRue. The other two attributes specify the category under which the property is displayed and the text that appears below the Property Browser when the property is selected.

Always keep in mind that the motive for creating custom user controls is reusability. There is no point in spending time creating elaborate controls that are used only once. As this example illustrates, they are most effective when they solve a problem that occurs repeatedly.


7.8. Using Drag and Drop with Controls

The ability to drag data from one control and drop it onto another has long been a familiar feature of GUI programming. .NET supports this feature with several classes and enumerations that enable a control to be the target and/or source of the drag-and-drop operation.

Overview of Drag and Drop

The operation requires a source control that contains the data to be moved or copied, and a target control that receives the dragged data. The source initiates the action in response to an event—usually a MouseDown event. The source control's event handler begins the actual operation by invoking its DoDragDrop method. This method has two parameters: the data being dragged and a DragDropEffects enum type parameter that specifies the effects or actions the source control supports (see Table 7-4).

Table 7-4. DragDropEffects Enumeration

Member

Description

All

The data is moved to the target control, and scrolling occurs in the target control to display the newly positioned data.

Copy

Data is copied from target to source.

Link

Data from the source is linked to the target.

Move

The data is moved from the source to the target control.

None

The target control refuses to accept data.

Scroll

Scrolling occurs or will occur on the target control.

As the mouse moves across the form, the DoDragDrop method determines the control under the current cursor location. If this control has its AllowDrop property set to TRue, it is a valid drop target and its DragEnter event is raised. The DragEnter event handler has two tasks: to verify that the data being dragged is an acceptable type and to ensure the requested action (Effect) is acceptable. When the actual drop occurs, the destination control raises a DragDrop event. This event handler is responsible for placing the data in the target control (see Figure 7-18).

Figure 7-18. Sequence of events in drag-and-drop operation

After the DragDrop event handler finishes, the source control performs any cleanup operations. For example, if the operation involves moving data—as opposed to copying—the data must be removed from the source control.

To demonstrate these ideas, let's create an application that assigns players to a team from a roster of available players (see Figure 7-19). Team A is created by dragging names from the Available Players to the Team A list. Both lists are implemented with list boxes, and the Available Players list is set for single selection.

Figure 7-19. Drag-and-drop example

A name is selected by pressing the right mouse button and dragging the name to the target list. To add some interest, holding the Ctrl key copies a name rather than moving it.

After the form and controls are created, the first step is to set up the source control (lstPlayers) to respond to the MouseDown event and the target control (lstTeamA) to handle the DragEnter and DragDrop events:

lstPlayers.MouseDown +=

      new MouseEventHandler(Players_MouseDown);

lstTeamA.DragEnter   += new DragEventHandler(TeamA_DragEnter);

lstTeamA.DragDrop    += new DragEventHandler(TeamA_Drop);

The next step is to code the event handlers on the source and target control(s) that implement the drag-and-drop operation.

Source Control Responsibilities

The MouseDown event handler for the source ListBox first checks to ensure that an item has been selected. It then calls DoDragDrop, passing it the value of the selected item as well as the acceptable effects: Move and Copy. The DragDropEffects enumeration has a FlagsAttribute attribute, which means that any bitwise combination of its values can be passed. The value returned from this method is the effect that is actually used by the target. The event handler uses this information to perform any operations required to implement the effect. In this example, a move operation means that the dragged value must be removed from the source control.

Listing 7-7. Initiating a Drag-and-Drop Operation from the Source Control

private void Players_MouseDown(object sender, MouseEventArgs e)

{

   if ( lstPlayers.SelectedIndex >=0)

   {

      string players;

      int ndx = lstPlayers.SelectedIndex;

      DragDropEffects effect;

      players = lstPlayers.Items[ndx].ToString();

      if(players != "")

      {

         // Permit target to move or copy data

         effect = lstPlayers.DoDragDrop(players,

               DragDropEffects.Move | DragDropEffects.Copy);

         // Remove item from ListBox since move occurred

         if (effect == DragDropEffects.Move)

         lstPlayers.Items.RemoveAt(ndx);

      }

   }

}

Target Control Responsibilities

The destination control must implement the event handlers for the DragEnter and DragDrop events. Both of these events receive a DragEventArgs type parameter (see Table 7-5) that contains the information required to process the drag-and-drop event.

Table 7-5. DragEventArgs Properties

Member

Description

AllowedEffect

The effects that are supported by the source control.

Example to determine if Move is supported:

if ((e.AllowedEffect & DragDropEffects.Move) ==

   DragDropEffects.Move)

Data

Returns the IDataObject that contains data associated with this operation. This object implements methods that return information about the data. These include Getdata, which fetches the data, and GeTDataPresent, which checks the data type.

Effect

Gets or sets the target drop effect.

KeyState

Returns the state of the Alt key, Ctrl key, Shift key, and mouse buttons as an integer:

1—Left mouse button

8—Ctrl key

2—Right mouse button

16—Middle mouse button

4—Shift key

32—Alt key

X, Y

x and y coordinates of the mouse pointer.

The Data, Effect, and KeyState members are used as follows:

  • Data.GetDataPresent is used by the DragEnter event handler to ensure that the data is a type the target control can process.

  • The DragDrop event handler uses Data.GetData to access the data being dragged to it. The parameter to this method is usually a static field of the DataFormats class that specifies the format of the returned data.

  • The DragEnter event handler uses KeyState to determine the status of the mouse and keys in order to determine the effect it will use to process the data. Recall that in this example, pressing the Ctrl key signals that data is to copied rather than moved.

  • Effect is set by the DragEnter event handler to notify the source as to how—or if—it processed the data. A setting of DragDropEffects.None prevents the DragDrop event from firing.

Listing 7-8 shows the code for the two event handlers.

Listing 7-8. Handling the DragEnter and DragDrop Events

[FlagsAttribute]

enum KeyPushed

{

   // Corresponds to DragEventArgs.KeyState values

   LeftMouse   = 1,

   RightMouse  = 2,

   ShiftKey    = 4,

   CtrlKey     = 8,

   MiddleMouse = 16,

   AltKey      = 32,

}

private void TeamA_DragEnter(object sender, DragEventArgs e)

{

   KeyPushed kp = (KeyPushed) e.KeyState;

   // Make sure data type is string

   if (e.Data.GetDataPresent(typeof(string)))

   {

      // Only accept drag with left mouse key

      if ( (kp & KeyPushed.LeftMouse) == KeyPushed.LeftMouse)

      {

         if ((kp & KeyPushed.CtrlKey) == KeyPushed.CtrlKey)

         {

            e.Effect = DragDropEffects.Copy;    // Copy

         }

         else

         {

            e.Effect = DragDropEffects.Move;  // Move

         }

      }

      else  // Is not left mouse key

      {

         e.Effect = DragDropEffects.None;

      }

   } else    // Is not a string

   {

      e.Effect = DragDropEffects.None;

   }

}

// Handle DragDrop event

private void TeamA_Drop(object sender, DragEventArgs e)

{

   // Add dropped data to TextBox

   lstTeamA.Items.Add(

      (string) e.Data.GetData(DataFormats.Text));

}

An enum is created with the FlagsAttributes attribute to make checking the KeyState value easier and more readable. The logical "anding" of KeyState with the value of the CtrlKey (8) returns a value equal to the value of the CtrlKey if the Ctrl key is pressed.

A control can serve as source and target in the same application. You could make this example more flexible by having the list boxes assume both roles. This would allow you to return a player from lstTeamA back to the lstPlayers ListBox. All that is required is to add the appropriate event handlers.

Core Note

Drag and drop is not just for text. The DataFormats class predefines the formats that can be accepted as static fields. These include Bitmap, PenData, WaveAudio, and numerous others.


7.9. Using Resources

Figure 7-7, shown earlier in the chapter, illustrates the use of PictureBox controls to enlarge and display a selected thumbnail image. Each thumbnail image is loaded into the application from a local file:

tn1 = new PictureBox();

tn1.Image = Image.FromFile("c:\\schiele1.jpg");

This code works fine as long as the file schiele1.jpg exists in the root directory of the user's computer. However, relying on the directory path to locate this file has two obvious disadvantages: The file could be deleted or renamed by the user, and it's an external resource that has to be handled separately from the code during installation. Both problems can be solved by embedding the image in the assembly rather than treating it as an external resource.

Consider a GUI application that is to be used in multiple countries with different languages. The challenge is to adapt the screens to each country. At a minimum, this requires including text in the native language, and may also require changing images and the location of controls on the form. The ideal solution separates the logic of the program from the user interface. Such a solution treats the GUI for each country as an interchangeable resource that is loaded based on the culture settings (the country and language) of the computer.

The common denominator in these two examples is the need to bind an external resource to an application. .NET provides special resource files that can be used to hold just about any nonexecutable data such as strings, images, and persisted data. These resource files can be included in an assembly—obviating the need for external files—or compiled into satellite assemblies that can be accessed on demand by an application's main assembly.

Let's now look at the basics of working with resource files and how to embed them in assemblies; then, we will look at the role of satellite assemblies in localized applications.

Working with Resource Files

Resource files come in three formats: *.txt files in name/value format, *.resx files in an XML format, and *.resources files in a binary format. Why three? The text format provides an easy way to add string resources, the XML version supports both strings and other objects such as images, and the binary version is the binary equivalent of the XML file. It is the only format that can be embedded in an assembly—the other formats must be converted into a .resources file before they can be linked to an assembly. Figure 7-20 illustrates the approaches that can be used to create a .resources file.

Figure 7-20. A .resources file can be created from a text file, resources, or a .resx file

The System.Resources namespace contains the types required to manipulate resource files. It includes classes to read from and write to both resource file formats, as well as load resources from an assembly into a program.

Creating Resource Strings from a Text File

Resource files containing string values are useful when it is necessary for a single application to present an interface that must be customized for the environment in which it runs. A resource file eliminates the need to code multiple versions of an application; instead, a developer creates a single application and multiple resource files that contain the interface captions, text, messages, and titles. For example, an English version of an application would have the English resource file embedded in its assembly; a German version would embed the German resource file. Creating resource strings and accessing them in an application requires four steps:

1.
Create a text file with the name/value strings to be used in the application. The file takes this format:

;German version   (this is a comment)

Language=German

Select=Wählen Sie aus

Page=Seite

Previous=Vorherig

Next=Nächst

2.
Convert the text file to a .resources file using the Resource File Generator utility resgen.exe:

> resgen german.txt  german.resources

Note that the text editor used to create the text file should save it using UTF-8 encoding, which resgen expects by default.

3.
Use the System.Resources.ResourceManager class to read the strings from the resource file. As shown here, the ResourceManager class accepts two arguments: the name of the resource file and the assembly containing it. The Assembly class is part of the System.Reflection namespace and is used in this case to return the current assembly. After the resource manager is created, its GetString method is used by the application to retrieve strings from the resource file by their string name:

// new ResourceManager(resource file, assembly)

ResourceManager rm = new ResourceManager(

      "german",Assembly.GetExecutingAssembly());

nxtButton.Text= rm.GetString("Next");

4.
For this preceding code to work, of course, the resource file must be part of the application's assembly. It's bound to the assembly during compilation:

csc /t:exe /resource:german.resources myApp.cs

Using the ResourceWriter Class to Create a .resources File

The preceding solution works well for adding strings to a resource file. However, a resource file can also contain other objects such as images and cursor shapes. To place these in a .resources file, .NET offers the System.Resources.ResourceWriter class. The following code, which would be placed in a utility or helper file, shows how to create a ResourceWriter object and use its AddResource method to store a string and image in a resource file:

IResourceWriter writer = new ResourceWriter(

        "myResources.resources");  // .Resources output file

Image img = Image.FromFile(@"c:\schiele1.jpg");

rw.AddResource("Page","Seite");    // Add string

rw.AddResource("artistwife",img);  // Add image

rw.Close();                        // Flush resources to the file

Using the ResourceManager Class to Access Resources

As we did with string resources, we use the ResourceManager class to access object resources from within the application. To illustrate, let's return to the code presented at the beginning of this section:

tn1.Image = Image.FromFile("C:\\schiele1.jpg");

The ResourceManager allows us to replace the reference to an external file, with a reference to this same image that is now part of the assembly. The GetString method from the earlier example is replaced by the GetObject method:

ResourceManager rm = new

      ResourceManager("myresources",

                      Assembly.GetExecutingAssembly());

// Extract image from resources in assembly

tn1.Image = (Bitmap) rm.GetObject("artistwife");

Using the ResXResourceWriter Class to Create a .resx File

The ResXResourceWriter class is similar to the ResourceWriter class except that it is used to add resources to a .resx file, which represents resources in an intermediate XML format. This format is useful when creating utility programs to read, manage, and edit resources—a difficult task to perform with the binary .resources file.

ResXResourceWriter rwx = new

      ResXResourceWriter(@"c:\myresources.resx");

Image img = Image.FromFile(@"c:\schiele1.jpg");

rwx.AddResource("artistwife",img);  // Add image

rwx.Generate();   // Flush all added resources to the file

The resultant file contains XML header information followed by name/value tags for each resource entry. The actual data—an image in this case—is stored between the value tags. Here is a section of the file myresources.resx when viewed in a text editor:

<data name="face" type="System.Drawing.Bitmap, System.Drawing,

   Version=1.0.3300.0,Culture=neutral,

   PublicKeyToken=b03f5f7f11d50a3a" mimetype="application/x-

   microsoft.net.object.bytearray.base64">

<value>       ----   Actual Image bytes go here ----

</value>

Note that although this example stores only one image in the file, a .resx file can contain multiple resource types.

Using the ResXResourceReader Class to Read a .resx file

The ResXResourceReader class provides an IDictionaryEnumerator (see Chapter 4) that is used to iterate through the tag(s) in a .resx file. This code segment lists the contents of a resource file:

ResXResourceReader rrx = new

      ResXResourceReader("c:\\myresources.resx");

// Enumerate the collection of tags

foreach (DictionaryEntry de in rrx)

{

   MessageBox.Show("Name: "+de.Key.ToString()+"\nValue: " +

                   de.Value.ToString());

   // Output --> Name:  artistwife

   //        --> Value: System.Drawing.Bitmap

}

rrx.Close();

Converting a .resx File to a .resources File

The .resx file is converted to a .resources file using resgen.exe:

resgen myresources.resx  myresources.resources

If the second parameter is not included, the output file will have the same base name as the source file. Also, note that this utility can be used to create a .resources file from a .resx file. The syntax is the same as in the preceding example—just reverse the parameters.

VS.NET and Resources

Visual Studio.NET automatically creates a .resx file for each form in a project and updates them as more resources are added to the project. You can see the resource file(s) by selecting the Show All Files icon in the Solution Explorer.

When a build occurs, .resources files are created from the .resx files. In the code itself, a ResourceManager object is created to provide runtime access to the resources:

ResourceManager resources = new ResourceManager(typeof(Form1));

Using Resource Files to Create Localized Forms

In .NET vernacular, a localized application is one that provides multi-language support. This typically means providing user interfaces that display text and images customized for individual countries or cultures. The .NET resource files are designed to support such applications.

In a nutshell, resource files can be set up for each culture being supported. For example, one file may have all the control labels and text on its interface in German; another may have the same controls with French text. When the application runs, it looks at the culture settings of the computer it is running on and pulls in the appropriate resources. This little bit of magic is accomplished by associating resource files with the CultureInfo class that designates a language, or language and culture. The resource files are packaged as satellite assemblies, which are resource files stored as DLLs.

Resource Localization Using Visual Studio.NET

To make a form localized, you must set its Localizable property to TRue. This has the effect of turning each control on a form into a resource that has its properties stored in the form's .resx file. This sets the stage for creating separate .resx files for each culture a form supports.

Recall from Chapter 5, "C# Text Manipulation and File I/O," that a culture is specified by a two-character language code followed by an optional two-character country code. For example, the code for English in the United States is en-US. The terms neutral culture and specific culture are terms to describe a culture. A specific culture has both the language and country specified; a neutral culture has only the language. Consult the MSDN documentation on the CultureInfo class for a complete list of culture names.

To associate other cultures with a form, set the form's Language property to another locale from the drop-down list in the Properties window. This causes a .resx file to be created for the new culture. You can now customize the form for this culture by changing text, resizing controls, or moving controls around. This new property information is stored in the .resx file for this culture only—leaving the .resx files for other cultures unaffected.

The resource files are stored in folders, as shown in Figure 7-21. When the project is built, a satellite assembly is created to contain the resources for each culture, as shown in Figure 7-22. This DLL file has the same name in each folder.

Figure 7-21. VS.NET resource files for multiple cultures

Figure 7-22. Satellite assembly

Determining Localization Resources at Runtime

By default, an application's thread has its CurrentThread.CurrentUICulture property set to the culture setting of the machine it is running on. Instances of the ResourceManager, in turn, use this value to determine which resources to load. They do this by searching for the satellite assembly in the folder associated with the culture—a reason why the naming and location of resource folders and files is important. If no culture-specific resources are found, the resources in the main assembly are used.

Core Note

The easiest way to test an application with other culture settings is to set the CurrentUICulture to the desired culture. The following statement, for example, is placed before InitializeComponent() in VS.NET to set the specific culture to German:

System.Threading.Thread.CurrentThread.CurrentUICulture =

   new System.Globalization.CultureInfo("de-DE");

Creating a Satellite Assembly Without VS.NET

One of the advantages of using satellite assemblies is that they can be added to an application, or modified, without recompiling the application. The only requirements are that a folder be set up along the proper path, and that the folder and satellite assembly have the proper name.

Suppose you have a .resx file that has been converted by your translator to French Canadian. You can manually create and add a satellite assembly to the application in three steps:

1.
Convert the.resx file to a .resources file:

filmography.Form1.fr-CA.resources

2.
Convert the .resources file to a satellite assembly using the Assembly Linker (Al.exe):

Al.exe

      /t:lib

      /embed:filmography.Form1.fr-CA.resources

      /culture:fr-CA

      /out:filmography.resources.dll

3.
Create the fr-CA folder beneath Release folder and copy the new assembly file into it.

Placing the satellite assembly in the proper folder makes it immediately available to the executable and does not require compiling the application.


7.10. Summary

There are more than 50 GUI controls available in the .NET Framework Class Library. This chapter has taken a selective look at some of the more important ones. They all derive from the System.Windows.Forms.Control class that provides the inherited properties and methods that all the controls have in common.

Although each control is functionally unique, it is possible to create a taxonomy of controls based on similar characteristics and behavior. The button types, which are used to intitiate an action or make a selection, include the simple Button, CheckBox, and RadioButton. These are often grouped using a GroupBox or Panel control. The TextBox can be used to hold a single line of text or an entire document. Numerous methods are available to search the box and identify selected text within it. The PictureBox is available to hold images and has a SizeMode property that is used to position and size an image within the box.

Several controls are available for presenting lists of data. The ListBox and ComboBox display data in a simple text format. However, the underlying data may be a class object with multiple properties. The TReeView and ListView are useful for displaying data with a hierarchical relationship. The ListView can display data in multiple views that include a grid layout and icon representation of data. The TReeView presents a tree metaphor to the developer, with data represented as parent and child nodes.

Most of the controls support the drag-and-drop operation that makes it easy to move or copy data from one control to another. The source control initiates the action by calling a DoDragDrop method that passes the data and permissible effects to the target control.

For applications that require nonstandard controls, .NET lets you create custom controls. They may be created from scratch, derived from an existing control, or created as a combination of controls in a user control container.


7.11. Test Your Understanding

1:

Why is a container control such as a GroupBox used with radio buttons?

2:

What is the SizeMode property set to in order to automatically resize and fill an image in a PictureBox?

3:

Suppose you place objects in a ListBox that have these properties:

string Vendor, string ProductID, int Quantity

How do you have the ListBox display the ProductID and Quantity?

4:

What event is fired when an item in a ListBox is selected? What ListBox properties are used to identify the selected item?

5:

What property and value are set on a ListView to display its full contents in a grid layout?

6:

Which TReeNode property can be used to store object data in a TReeView node?

7:

Which two events must the destination control in a drag-and-drop operation support?

8:

The Property Browser in VS.NET uses metadata to categorize a control's properties and events and assign default values. How do you generate this information for the properties in a custom control?

9:

What class is used to read text from a text resource file embedded in an assembly? What method is used to read values from the file?


Chapter 8. .NET Graphics Using GDI+

Topics in This Chapter

  • Graphics Overview: The first step in working with GDI+ is to understand how to create and use a Graphics object. This section looks at how this object is created and used to handle the Paint event.

  • Using the Graphics Object to Create Shapes: .NET offers a variety of standard geometric shapes that can be drawn in outline form or filled in. The GraphicsPath class serves as a container that enables geometric shapes to be connected.

  • Using Pens and Brushes: Pens are used to draw shapes in outline form in different colors and widths; a brush is used to fill in shapes and create solid and gradient patterns.

  • Color: Colors may be defined according to red/green/blue (RGB) values or hue/saturation/brightness (HSB) values. Our project example illustrates RGB and HSB color spaces. By representing a color as an object, .NET permits it to be transformed by changing property values.

  • Images: .NET includes methods to load, display, and transform images. The most useful of these is the Graphics.DrawImage method that allows images to be magnified, reduced, and rotated.

Very few programmers are artists, and only a minority of developers is involved in the world of gaming where graphics have an obvious justification. Yet, there is something compelling about writing an application that draws on a computer screen. For one thing, it's not difficult. An array of built-in functions makes it easy to create geometric objects, color them, and even animate them. In this regard, .NET should satisfy the would-be artist that resides in many programmers.

To understand the .NET graphics model, it is useful to look at its predecessor—the Win32 Graphical Device Interface (GDI). This API introduced a large set of drawing objects that could be used to create device independent graphics. The idea was to draw to a logical coordinate system rather than a device specific coordinate system—freeing the developer to concentrate on the program logic and not device details. .NET essentially takes this API, wraps it up in classes that make it easier to work with, and adds a wealth of new features.

The graphics classes are collectively called GDI+. This chapter looks at the underlying principles that govern the use of the GDI+, and then examines the classes and the functionality they provide. Several programming examples are included that should provide the tools you will need to further explore the .NET graphics namespaces.

Keep in mind that GDI+ is not restricted to WinForms applications. Its members are also available to applications that need to create images dynamically for the Internet (Web Forms and Web Services).You should also recognize that GDI+ is useful for more than just games or graphics applications. Knowledge of its classes is essential if you want to design your own controls or modify the appearance of existing ones.


8.1. GDI+ Overview

The types that make up GDI+ are contained in the gdiplus.dll file. .NET neatly separates the classes and enumerations into logically named namespaces that reflect their use. As Figure 8-1 shows, the GDI+ functions fall into three broad categories: two-dimensional vector graphics, image manipulation, and typography (the combining of fonts and text strings to produce text output).

Figure 8-1. GDI+ namespaces

This figure does not depict inheritance, but a general hierarchical relationship between the GDI+ namespaces. System.Drawing is placed at the top of the chart because it contains the basic objects required for any graphic output: Pen, Brush, Color, and Font. But most importantly, it contains the Graphics class. This class is an abstract representation of the surface or canvas on which you draw. The first requirement for any drawing operation is to get an instance of this class, so the Graphics object is clearly a fruitful place to begin the discussion of .NET graphics.

The Graphics Class

Drawing requires a surface to draw on, a coordinate system for positioning and aligning shapes, and a tool to perform the drawing. GDI+ encapsulates this functionality in the System.Drawing.Graphics class. Its static methods allow a Graphics object to be created for images and controls; and its instance methods support the drawing of various shapes such as circles, triangles, and text. If you are familiar with using the Win32 API for graphics, you will recognize that this corresponds closely to a device context in GDI. But the Graphics object is of a simpler design. A device context is a structure that maintains state information about a drawing and is passed as an argument to drawing functions. The Graphics object represents the drawing surface and provides methods for drawing on it.

Let's see how code gains access to a Graphics object. Your application most likely will work with a Graphics object inside the scope of an event handler, where the object is passed as a member of an EventArgs parameter. The Paint event, which occurs each time a control is drawn, is by far the most common source of Graphics objects. Other events that have a Graphics object sent to their event handler include PaintValue, BeginPrint, EndPrint, and PrintDocument.PrintPage. The latter three are crucial to printing and are discussed in the next chapter.

Although you cannot directly instantiate an object from the Graphics class, you can use methods provided by the Graphics and Control classes to create an object. The most frequently used is Control.CreateGraphics—an instance method that returns a graphics object for the control calling the method. The Graphics class includes the FromHwnd method that relies on passing a control's Handle to obtain a Graphics object related to the control. Let's look at both approaches.

How to Obtain a Graphics Object from a Control Using CreateGraphics

The easiest way to create a Graphics object for a control is to use its CreateGraphics method. This method requires no parameters and is inherited from the Control class by all controls. To demonstrate, let's create an example that draws on a Panel control when the top button is clicked and refreshes all or part of the panel in response to another button click. The user interface to this program is shown in Figure 8-2 and will be used in subsequent examples.

Figure 8-2. Interface to demonstrate using Graphics object to draw on a control

Listing 8-1 contains the code for the Click event handlers associated with each button. When the Decorate Panel button (btnDecor) is clicked, a Graphics object is created and used to draw a rectangle around the edge of the panel as well as a horizontal line through the middle. When the Refresh button (btnRefresh) is clicked, the panel's Invalidate method is called to redraw all or half of the panel. (More on the Invalidate command is coming shortly.)

Listing 8-1. Using Control.CreateGraphics to Obtain a Graphics Object

using System.Drawing;

//

private void btnDecor_Click(object sender, System.EventArgs e)

{

   // Create a graphics object to draw on panel1

   Graphics cg = this.panel1.CreateGraphics();

   try (

      int pWidth  = panel1.ClientRectangle.Width;

      int pHeight = panel1.ClientRectangle.Height;

      // Draw a rectangle around border

      cg.DrawRectangle(Pens.Black,2,2,pWidth-4, pHeight-4);

      // Draw a horizontal line through the middle

      cg.DrawLine(Pens.Red,2,(pHeight-4)/2,pWidth-4,

                  (pHeight-4)/2);

   }

   finally {

      cg.Dispose();  // You should always dispose of object

   }

}

private void btnRefresh_Click(object sender,

                              System.EventArgs e)

{

// Invokes Invalidate to repaint the panel control

   if (this.radAll.Checked)   // Radio button - All

   {

      // Redraw panel1

      this.panel1.Invalidate();

      } else {

      // Redraw left half of panel1

      Rectangle r = new

            Rectangle(0,0,panel1.ClientRectangle.Width/2,

            ClientRectangle.Height);

      this.panel1.Invalidate(r); // Repaint area r

      this.panel1.Update();      // Force Paint event

   }

}

The btnDecor Click event handler uses the DrawRectangle and DrawLine methods to adorn panel1. Their parameters—the coordinates that define the shapes—are derived from the dimensions of the containing panel control. When the drawing is completed, the Dispose method is used to clean up system resources held by the object. (Refer to Chapter 4, "Working with Objects in C#," for a discussion of the IDisposable interface.) You should always dispose of the Graphics object when finished with it. The try-finally construct ensures that Dispose is called even if an interrupt occurs. As shown in the next example, a using statement provides an equivalent alternative to try-finally.

The btnRefresh Click event handler is presented as a way to provide insight into how forms and controls are drawn and refreshed in a WinForms environment. A form and its child controls are drawn (displayed) in response to a Paint event. Each control has an associated method that is responsible for drawing the control when the event occurs. The Paint event is triggered when a form or control is uncovered, resized, or minimized and restored.

How to Obtain a Graphics Object Using Graphics Methods

The Graphics class has three static methods that provide a way to obtain a Graphics object:

  • Graphics.FromHdc. Creates the Graphics object from a specified handle to a Win32 device context. This is used primarily for interoperating with GDI.

  • Graphics.FromImage. Creates a Graphics object from an instance of a .NET graphic object such as a Bitmap or Image. It is often used in ASP.NET (Internet) applications to dynamically create images and graphs that can be served to a Web browser. This is done by creating an empty Bitmap object, obtaining a Graphics object using FromImage, drawing to the Bitmap, and then saving the Bitmap in one of the standard image formats.

  • Graphics.FromHwnd. Creates the Graphics object from a handle to a Window, Form, or control. This is similar to GDI programming that requires a handle to a device context in order to display output to a specific device.

Each control inherits the Handle property from the Control class. This property can be used with the FromHwnd method as an alternative to the Control.CreateGraphics method. The following routine uses this approach to draw lines on panel1 when a MouseDown event occurs on the panel (see Figure 8-3).

Figure 8-3. Output for MouseDown example

Note that the Graphics object is created inside a using statement. This statement generates the same code as a TRy-finally construct that includes a g.Dispose() statement in the finally block.

private void panel1OnMouseDown(object sender, MouseEventArgs e)

{

   // The using statement automatically calls g.Dispose()

   using( Graphics g= Graphics.FromHwnd(panel1.Handle))

   {

      g.DrawLine(Pens.Red,e.X,e.Y,20,20);

   }

}

The Paint Event

A Paint event is triggered in a WinForms application when a form or control needs to be partially or fully redrawn. This normally occurs during the natural use of a GUI application as the window is moved, resized, and hidden behind other windows. Importantly, a Paint event can also be triggered programatically by a call to a control's Invalidate method.

Using Invalidate() to Request a Paint Event

The Control.Invalidate method triggers a Paint event request. The btnRefresh_Click event handler in Listing 8-1 showed two overloads of the method. The parameterless version requests that the entire panel control be redrawn; the second specifies that only the portion of the control's region specified by a rectangle be redrawn.

Here are some of the overloads for this method:

public void Invalidate()

public void Invalidate(bool invalidatechildren)

public void Invalidate(Rectangle rc)

public void Invalidate(Rectangle rc, bool invalidatechildren)

Note: Passing a true value for the invalidatechildren parameter causes all child controls to be redrawn.

Invalidate requests a Paint event, but does not force one. It permits the operating system to take care of more important events before invoking the Paint event. To force immediate action on the paint request, follow the Invalidate statement with a call to Control.Update.

Let's look at what happens on panel1 after a Paint event occurs. Figure 8-4 shows the consequences of repainting the left half of the control. The results are probably not what you desire: half of the rectangle and line are now gone. This is because the control's paint event handler knows only how to redraw the control. It has no knowledge of any drawing that may occur outside of its scope. An easy solution in this case is to call a method to redraw the rectangle and line after calling Invalidate. But what happens if Windows invokes the Paint event because half of the form is covered and uncovered by another window? This clears the control and our code is unaware it needs to redraw the rectangle and line. The solution is to handle the drawing within the Paint event handler.

Figure 8-4. Effects of invalidating a region

Core Note

When a form is resized, regions within the original area are not redrawn. To force all of a control or form to be redrawn, pass the following arguments to its SetStyle method. Only use this when necessary, because it slows down the paint process.

this.SetStyle(ControlStyles.ResizeRedraw, true);

Implementing a Paint Event Handler

After the Paint event occurs, a data class PaintEventArgs is passed as a parameter to the Paint event handler. This class provides access to the Graphics object and to a rectangle ClipRectangle that defines the area where drawing may occur. Together, these properties make it a simple task to perform all the painting within the scope of the event handler.

Let's see how to rectify the problem in the preceding example, where our drawing on panel1 disappears each time the paint event occurs. The solution, of course, is to perform the drawing inside the paint event handler. To do this, first register our event handler with the PaintEventHandler delegate:

this.panel1.Paint += new PaintEventHandler(paint_Panel);

Next, set up the event handler with the code to draw a rectangle and horizontal line on the panel. The Graphics object is made available through the PaintEventArgs parameter.

private void paint_Panel( object sender, PaintEventArgs e)

{

   Graphics cg = e.Graphics;

   int pWidth  = panel1.ClientRectangle.Width;

   int pHeight = panel1.ClientRectangle.Height;

   cg.DrawRectangle(Pens.Black,2,2,pWidth-4, pHeight-4);

   cg.DrawLine(Pens.Red,2,(pHeight-4)/2,pWidth-4,

               (pHeight-4)/2);

   base.OnPaint(e);   // Call base class implementation

}

The Control.OnPaint method is called when a Paint event occurs. Its role is not to implement any functionality, but to invoke the delegates registered for the event. To ensure these delegates are called, you should normally invoke the OnPaint method within the event handler. The exception to this rule is: To avoid screen flickering, do not call this method if painting the entire surface of a control.

Painting is a slow and expensive operation. For this reason, PaintEventArgs provides the ClipRectangle property to define the area that is displayed when drawing occurs. Any drawing outside this area is automatically clipped. However, it is important to realize that clipping affects what is displayed—it does not prevent the drawing code from being executed. Thus, if you have a time-consuming custom paint routine, the entire painting process will occur each time the routine is called, unless you include logic to paint only what is needed.

The following example illustrates how to draw selectively. It paints a pattern of semi-randomly colored rectangles onto a form's panel (see Figure 8-5). Before each rectangle is drawn, a check is made to confirm that the rectangle is in the clipping area.

private void paint_Panel( object sender,  PaintEventArgs e)

{

   Graphics g = e.Graphics;

   for (int i = 0; i< this.panel1.Width;i+=20)

   {

      for (int j=0; j< his.panel1.Height;j+=20)

      {

         Rectangle r= new Rectangle(i,j,20,20);

         if (r.IntersectsWith(e.ClipRectangle))

         {

            // FromArgb is discussed in Color section

            Brush b = new SolidBrush(Color.FromArgb((i*j)%255,

                  (i+j)%255, ((i+j)*j)%255));

            g.FillRectangle(b,r);

            g.DrawRectangle(Pens.White,r);

         }

      }

   }

}

Figure 8-5. Pattern used in paint example

The key to this code is the Rectangle.IntersectsWith method that checks for the intersection of two rectangles. In this case, it tests for overlap between the rectangle to be drawn and the clip area. If the rectangle intersects the clip area, it needs to be drawn. Thus, the method can be used to limit the portion of the screen that has to be repainted. To test the effects, this code was run with and without the IntersectsWith method. When included, the event handler required 0 to 17 milliseconds—depending on the size of the area to be repainted. When run without IntersectsWith, the event handler required 17 milliseconds to redraw all the rectangles.

Another approach to providing custom painting for a form or control is to create a subclass that overrides the base class's OnPaint method. In this example, myPanel is derived from the Panel class and overrides the OnPaint method to draw a custom diagonal line through the center of the panel.

// New class myPanel inherits from base class Panel

public class myPanel: Panel

{

   protected override void OnPaint(PaintEventArgs e)

   {

      Graphics g = e.Graphics;

      g.DrawLine(Pens.Aqua,0,0,this.Width,this.Height);

      base.OnPaint(e);

   }

}

Unless the new subclass is added to a class library for use in other applications, it is simpler to write an event handler to provide custom painting.


8.2. Using the Graphics Object

Let's look at the details of actually drawing with the Graphics object. The Graphics class contains several methods for rendering basic geometric patterns. In addition to the DrawLine method used in previous examples, there are methods for drawing rectangles, polygons, ellipses, curves, and other basic shapes. In general, each shape can be drawn as an outline using the Pen class, or filled in using the Brush class. The DrawEllipse and FillEllipse methods are examples of this. Let's look at some examples.

Basic 2-D Graphics

Figure 8-6 demonstrates some of the basic shapes that can be drawn with the Graphics methods. The shapes vary, but the syntax for each method is quite similar. Each accepts a drawing object—either a pen or brush—to use for rendering the shape, and the coordinates that determine the size and positions of the shape. A Rectangle object is often used to provide a shape's boundary.

Figure 8-6. Basic 2-D shapes

The following code segments draw the shapes shown in Figure 8-6. To keep things simple, the variables x and y are used to specify the location where the shape is drawn. These are set to the coordinates of the upper-left corner of a shape.

Pen blkPen = new Pen(Brushes.Black,2 );  // width=2

// Set this to draw smooth lines

g.SmoothingMode =

      System.Drawing.Drawing2D.SmoothingMode.AntiAlias;

// (1) Draw Circle and Draw Filled Circle

Rectangle r = new Rectangle(new Point(x,y),new Size(40,40));

g.DrawEllipse(blkPen, r);

g.FillEllipse(Brushes.Black,x,y+60,40,40);

// (2) Draw Ellipse and Filled Ellipse

int w = 60;

int h= 40;

Rectangle r = new Rectangle(new Point(x,y),new Size(w,h));

g.DrawEllipse(blkPen, r);

r = new Rectangle(new Point(x,y+60), new Size(w,h));

g.FillEllipse(Brushes.Red, r);

// (3) Draw Polygon

Point pt1 = new Point(x, y);

Point pt2 = new Point(x+22, y+12);

Point pt3 = new Point(x+22, y+32);

Point pt4 = new Point(x, y+44);

Point pt5 = new Point(x-22, y+32);

Point pt6 = new Point(x-22, y+12);

Point[] myPoints = {pt1, pt2, pt3, pt4, pt5, pt6};

g.DrawPolygon(blkPen, myPoints);

// Points would be changed so as not to draw over

// original polygon

g.FillPolygon(Brushes.Black, myPoints);

// (4)Draw Pie Shape and filled pie

Rectangle r = new Rectangle( new Point(x,y),new Size(80,80));

// Create start and sweep angles

int startAngle =  0;    // Clockwise from x-axis

int sweepAngle = -60;   // Clockwise from start angle

g.DrawPie(blkPen, r, startAngle, sweepAngle);

g.FillPie(Brushes.Black, x,y+60,80,80,startAngle, sweepAngle);

// (5) Draw Rectangle and Rectangle with beveled edges

blkPen.Width=5;   // make pen thicker to show bevel

g.DrawRectangle(blkPen,x,y,50,40);

blkPen.LineJoin = LineJoin.Bevel;

g.DrawRectangle(blkPen,x,y+60,50,40);

// (6) Draw Arc and Filled Pie

startAngle=45;

sweepAngle=180;

g.DrawArc(blkPen, x,y,40,40,startAngle, sweepAngle);

g.FillPie(Brushes.Black, x,y+60,40,40,startAngle,sweepAngle);

These code segments illustrate how easy it is to create simple shapes with a minimum of code. .NET also makes it easy to create more complex shapes by combining primitive shapes using the GraphicsPath class.

Creating Shapes with the GraphicsPath Class

The GraphicsPath class, which is a member of the System.Drawing.Drawing2D namespace, is used to create a container for a collection of primitive shapes. Succinctly, it permits you to add basic shapes to its collection and then to treat the collection as a single entity for the purpose of drawing and filling the overall shape. Before looking at a code example, you should be aware of some of the basic features of the GraphicsPath class:

  • It automatically connects the last point of a line or arc to the first point of a succeeding line or arc.

  • Its CloseFigure method can be used to automatically close open shapes, such as an arc. The first and last points of the shape are connected.

  • Its StartFigure method prevents the previous line from being automatically connected to the next line.

  • Its Dispose method should always be called when the object is no longer in use.

The following code creates and displays the Infinity Cross shown in Figure 8-7. It is constructed by adding five polygons to the GraphicsPath container object. The Graphics object then draws the outline and fills in the cross.

// g is the Graphics object

g.SmoothingMode = SmoothingMode.AntiAlias;

// Define five polygons

Point[] ptsT= {new Point(120,20),new Point(160,20),

      new Point(140,50)};

Point[] ptsL= {new Point(90,50),new Point(90,90),

      new Point(120,70)};

Point[] ptsB= {new Point(120,120),new Point(160,120),

      new Point(140,90)};

Point[] ptsR= {new Point(190,90), new Point(190,50),

      new Point(160, 70)};

Point[] ptsCenter = {new Point(140,50), new Point(120,70),

      new Point(140,90), new Point(160,70)};

// Create the GraphicsPath object and add the polygons to it

GraphicsPath gp = new GraphicsPath();

gp.AddPolygon(ptsT);   // Add top polygon

gp.AddPolygon(ptsL);   // Add left polygon

gp.AddPolygon(ptsB);   // Add bottom polygon

gp.AddPolygon(ptsR);   // Add right polygon

gp.AddPolygon(ptsCenter);

g.DrawPath(new Pen(Color.Red,2),gp); // Draw GraphicsPath

g.FillPath(Brushes.Gold,gp); // Fill the polygons

Figure 8-7. Infinity Cross

Instead of drawing and filling each polygon separately, we use a single DrawPath and FillPath statement to do the job.

The GraphicsPath class has several methods worth exploring—AddCircle, AddArc, AddEllipse, AddString, Warp, and others—for applications that require the complex manipulation of shapes. One of the more interesting is the transform method that can be used to rotate or shift the coordinates of a DrawPath object. This following code segment offers a taste of how it works. A transformation matrix is created with values that shift the x coordinates by 50 units and leave the y coordinates unchanged. This TRansform method applies the matrix to the DrawPath and shifts the coordinates; the shape is then drawn 50 units to the right of the first shape.

Matrix translateMatrix = new Matrix();

translateMatrix.Translate(50, 0);  // Offset x coordinate by 50

gp.Transform(translateMatrix);     // Transform path

g.DrawPath(Pens.Orange,gp);        // Display at new location

Hit Testing with Shapes

One of the reasons for placing shapes on a form is to permit a user to trigger an action by clicking a shape—as if she had clicked a button. Unlike a control, you cannot associate an event with a shape. Instead, you associate a MouseDown event with the container that holds the shape(s). Recall that a MouseDown event handler receives the x and y coordinates where the event occurs. After it has these, it is a simple process to use the rectangle and GraphicsPath methods to verify whether a point falls within their area:

bool Rectangle.Contains(Point(x,y))

bool GraphicsPath.IsVisible(Point(x,y))

To illustrate, consider an application that displays a map of US states and responds to a click on a state by displaying the name of the state capital. The map image is placed in a PictureBox, and rectangles and polygons are drawn on the states to set up the hit areas that respond to a MouseDown event. Figure 8-8 shows how the picture box's paint handler routine draws rectangles on three states and a polygon on Florida. (Of course, the shapes would not be visible in the actual application.) To respond to a pressed mouse key, set up a delegate to call an event handler when the MouseDown event occurs:

this.pictureBox1.MouseDown += new

      MouseEventHandler(down_Picture);

Figure 8-8. Hit test example

The following code implements event handler logic to determine if the mouse down occurs within the boundary of any shape.

private void down_Picture( object sender, MouseEventArgs e)

{

   // Rectangles and GraphicsPath gp are defined

   // as class variables

   if (rectNC.Contains(e.X,e.Y) )

   { MessageBox.Show("Capital: Raleigh"); }

   else if(rectSC.Contains(e.X,e.Y))

   { MessageBox.Show("Capital: Columbia");}

   else if(rectGA.Contains(e.X,e.Y))

   { MessageBox.Show("Capital: Atlanta");}

   else if(gp.IsVisible(e.X,e.Y))

   {MessageBox.Show("Capital: Tallahassee");}

}

After you have a basic understanding of how to create and use shapes, the next step is to enhance these shapes with eye catching graphical effects such as gradients, textured colors, and different line styles and widths. This requires an understanding of the System.Drawing classes: Pen, Brush, and Color.

Pens

The Graphics object must receive an instance of the Pen class to draw a shape's outline. Our examples thus far have used a static property of the Pens class—Pens.Blue, for example—to create a Pen object that is passed to the Graphics object. This is convenient, but in many cases you will want to create your own Pen object in order to use non-standard colors and take advantage of the Pen properties.

Constructors:

public Pen (Color color);

public Pen (Color color, single width);

public Pen (Brush brush);

public Pen (Brush brush, single width);

Example:

Pen p1 = new Pen(Color.Red, 5);

Pen p2 = new Pen(Color.Red); // Default width of 1

The constructors allow you to create a Pen object of a specified color and width. You can also set its attributes based on a Brush object, which we cover later in this section. Note that the Pen class inherits the IDisposable interface, which means that you should always call the Pen object's Dispose method when finished with it.

Besides color and width, the Pen class offers a variety of properties that allow you to control the appearance of the lines and curves the Pen object draws. Table 8-1 contains a partial list of these properties.

Table 8-1. Selected Pen Properties

Member

Description

Alignment

Determines how a line is drawn for closed shapes. Specifically, it specifies whether the line is drawn on the bounding perimeter or inside it.

Color

Color used to draw the shape or text.

DashCap

The cap style used at the beginning and end of dashes in a dashed line. A cap style is a graphic shape such as an arrow.

DashOffset

Distance from start of a line to the beginning of its dash pattern.

DashStyle

The type of dashed lines used. This is based on the DashStyle enumeration.

PenType

Specifies how a line is filled—for example, textured, solid, or gradient. It is determined by the Brush property of the Pen.

StartCap EndCap

The cap style used at the beginning and end of lines. This comes from the LineCap enumeration that includes arrows, diamonds, and squares—for example, LineCap.Square.

Width

Floating point value used to set width of Pen.

Let's look at some of the more interesting properties in detail.

DashStyle

This property defines the line style, which can be Solid, Dash, Dot, DashDot, DashDotDot, or Custom (see Figure 8-9). The property's value comes from the DashStyle enumeration.

Pen p1 = new Pen(Color.Black, 3);

p1.DashStyle = DashStyle.Dash;

g.DrawLine(p1,20,20,180,20);

Figure 8-9. DashStyles and LineCaps

StartCap and EndCap

These properties define the shape used to begin and end a line. The value comes from the LineCap enumeration, which includes ArrowAnchor, DiamondAnchor, Round, RoundAnchor, Square, SquareAnchor, and triangle. Examples of the DiamondAnchor and RoundAnchor are shown in Figure 8-9. The following code is used to create the lines in the figure:

Graphics g = pictureBox1.CreateGraphics();

Pen p1 = new Pen(Color.Black, 5);

p1.StartCap = LineCap.DiamondAnchor;

p1.EndCap   = LineCap.RoundAnchor;

int yLine = 20;

foreach(string ds in Enum.GetNames(typeof(DashStyle)))

{

   if (ds != "Custom") // Ignore Custom DashStyle type

   {

      // Parse creates an enum type from a string

      p1.DashStyle = (DashStyle)Enum.Parse(

                     typeof(DashStyle), ds);

      g.DrawLine(p1,20,yLine,120,yLine);

      g.DrawString(ds,new Font("Arial",10),Brushes.Black,

                   140,yLine-8);

      yLine += 20;

   }

}

The code loops through the DashStyle enumeration and draws a line for each enum value except Custom. It also uses the DrawString method to display the name of the enumeration values. This method is discussed in Chapter 9.

Brushes

Brush objects are used by these Graphics methods to create filled geometric shapes:

FillClosedCurve    FillEllipse      FillPath           FillPie

FillPolygon        FillRectangle    FillRectangles     FillRegion

All of these receive a Brush object as their first argument. As with the Pen class, the easiest way to provide a brush is to use a predefined object that represents one of the standard colors—for example, Brushes.AntiqueWhite. To create more interesting effects, such as fills with patterns and gradients, it is necessary to instantiate your own Brush type. Unlike the Pen class, you cannot create an instance of the abstract Brush class; instead, you use one of its inheriting classes summarized in Table 8-2.

Table 8-2. Brush Types That Derive from the Brush Class

Brush Type

Description

SolidBrush

Defines a brush of a single color. It has a single constructor:

Brush b = new SolidBrush(Color.Red);

TextureBrush

Uses a preexisting image (*.gif, *.bmp, or *.jpg) to fill a shape.

Image img = Image.FromFile("c:\\flower.jpg");

Brush b = new TextureBrush(img);

HatchBrush

Defines a rectangular brush with a foreground color, background color, and hatch style. Located in the System.Drawing.Drawing2D namespace.

LinearGradientBrush

Supports either a two-color or multi-color gradient. All linear gradients occur along a line defined by two points or a rectangle. Located in the Drawing2D namespace.

PathGradientBrush

Fills the interior of a GraphicsPath object with a gradient. Located in the Drawing2D namespace.

Note that all Brush classes have a Dispose method that should be called to destroy the Brush object when it is no longer needed.

The two most popular of these classes are HatchBrush, which is handy for creating charts, and LinearGradientBrush, for customizing the background of controls. Let's take a closer look at both of these.

The HatchBrush Class

As the name implies, this class fills the interior of a shape with a hatched appearance.

Constructors:

public HatchBrush(HatchStyle hStyle, Color forecolor)

public HatchBrush(HatchStyle hstyle, Color forecolor,

                  Color backcolor)

Parameters:

hStyle | HatchStyle enumeration that specifies the hatch pattern.
forecolor | The color of the lines that are drawn.
backcolor | Color of the space between the lines (black is default).

The predefined HatchStyle patterns make it a simple process to create elaborate, multi-color fill patterns. The following code is used to create the DarkVertical and DottedDiamond rectangles at the top of each column in Figure 8-10.

Graphics g = pictureBox1.CreateGraphics();

// Fill Rectangle with DarkVertical pattern

Brush b = new HatchBrush(HatchStyle.DarkVertical,

                         Color.Blue,Color.LightGray);

g.FillRectangle(b,20,20,80,60);

// Fill Rectangle with DottedDiamond pattern

b = new HatchBrush(HatchStyle.DottedDiamond,

                   Color.Blue,Color.LightGray);

g.FillRectangle(b,120,20,80,60);

Figure 8-10. Using HatchBrush with some of the available hatch styles

The LinearGradientBrush Class

In its simplest form, this class creates a transition that takes one color and gradually blends it into a second color. The direction of the transition can be set to horizontal, vertical, or any specified angle. The location where the transition begins can be set to a focal point other than the beginning of the area to be filled in. In cases where the gradient must be tiled to completely fill an area, options are available to control how each repeat is displayed. These options can be confusing, so let's begin with how to create a gradient brush and then work with examples that demonstrate the more useful properties and methods of the LinearGradientBrush class.

Constructors:

public LinearGradientBrush(Rectangle rect, Color color1,

   Color color2, LinearGradientMode linearGradientMode)

public LinearGradientBrush(Rectangle rect, Color color1,

   Color color2, float angle)

Parameters:

rect | Rectangle specifying the bounds of the gradient.
color1 | The start color in the gradient.
color2 | The end color in the gradient.
angle | The angle in degrees moving clockwise from the x axis.
LinearGradientMode | A LinearGradientMode enum value: Horizontal, Vertical, BackwardDiagonal, ForwardDiagonal

There is no substitute for experimentation when it comes to understanding graphics related concepts. Figure 8-11 shows the output from filling a rectangle with various configurations of a LinearGradientBrush object. Here is the code that creates these examples:

// Draw rectangles filled with gradient in a pictureBox

Graphics g = pictureBox1.CreateGraphics();

Size sz = new Size(100,80);

Rectangle rb = new Rectangle(new Point(20,20),sz);

// (1) Vertical Gradient (90 degrees)

LinearGradientBrush b = new

      LinearGradientBrush(rb,Color.DarkBlue,Color.LightBlue,90);

g.FillRectangle(b,rb);

rb.X=140;

// (2) Horizontal Gradient

b = new LinearGradientBrush(rb,Color.DarkBlue,

                            Color.LightBlue,0);

g.FillRectangle(b,rb);

rb.Y = 120;

rb.X = 20;

// (3) Horizontal with center focal point

b = new LinearGradientBrush(rb,Color.DarkBlue,

                            Color.LightBlue,0);

// Place end color at position (0-1) within brush

b.SetBlendTriangularShape(.5f);

g.FillRectangle(b,rb);

Figure 8-11. LinearGradientBrush examples: (1) Vertical, (2) Horizontal, (3) Focus Point, (4) Tiling

The main point of interest in this code is the use of the SetBlendTriangularShape method to create the blending effect shown in the third rectangle in Figure 8-11. This method takes an argument between 0 and 1.0 that specifies a relative focus point where the end color is displayed. The gradient then "falls off" on either side of this point to the start color.

The fourth rectangle in the figure is created by repeating the original brush pattern. The following code defines a small gradient brush that is used to fill a larger rectangle:

// Tiling Example – create small rectangle for gradient brush

Rectangle rb1 = new Rectangle(new Point(0,0),new Size(20,20));

b = new LinearGradientBrush(rb,Color.DarkBlue,

                            Color.LightBlue,0);

b.WrapMode = WrapMode.TileFlipX;

// Fill larger rectangle with repeats of small gradient rectangle

g.FillRectangle(b,rb);

Notice how the light and dark colors are reversed horizontally before each repeat occurs: [light-dark][dark-light]. The WrapMode property determines how the repeated gradient is displayed. In this example, it is set to the WrapMode enum value of TileFlipX, which causes the gradient to be reversed horizontally before repeating. The most useful enum values include the following:

Tile | Repeats the gradient.
TileFlipX | Reverses the gradient horizontally before repeating.
TileFlipXY | Reverses the gradient horizontally and vertically before repeating.
TileFlipY | Reverses the gradient vertically before repeating.

Creating a Multi-Color Gradient

It takes only a few lines of code to create a LinearGradientBrush object that creates a multi-color gradient. The key is to set the LinearGradientBrush.InterpolationColors property to an instance of a ColorBlend class that specifies the colors to be used. As the following code shows, the ColorBlend class contains an array of colors and an array of values that indicate the relative position (0–1) of each color on the gradient line. This example creates a gradient with a transition from red to white to blue—with white in the middle.

b = new LinearGradientBrush(rb,Color.Empty,Color.Empty,0);

ColorBlend myBlend = new ColorBlend();

// Specify colors to include in gradient

myBlend.Colors = new Color[]

      {Color.Red, Color.White, Color.Blue,};

// Position of colors in gradient

myBlend.Positions = new float[] {0f, .5f, 1f};

b.InterpolationColors = myBlend;  // Overrides constructor colors

Colors

.NET implements the Color object as a structure that includes a large number of colors predefined as static properties. For example, when a reference is made to Color.Indigo, the returned value is simply the Indigo property. However, there is more to the structure than just a list of color properties. Other properties and methods permit you to deconstruct a color value into its internal byte representation or build a color from numeric values. To appreciate this, let's look at how colors are represented.

Computers—as opposed to the world of printing—use the RGB (red/green/blue) color system to create a 32-bit unsigned integer value that represents a color. Think of RGB as a three-dimensional space with the red, green, and blue values along each axis. Any point within that space represents a unique RGB coordinate value. Throw in a fourth component, the alpha value—that specifies the color's transparency—and you have the 4-byte AlphaRGB (ARGB) value that defines a color. For example, Indigo has RGB values of 75, 0, 130, and an alpha value of 255 (no transparency). This is represented by the hex value 4B0082FF.

Colors can also be represented by the HSL (hue/saturation/luminosity) and HSB (hue/saturation/brightness) color spaces. While RGB values follow no easily discernible pattern, HSL and HSB are based on the standard color wheel (see Figure 8-12) that presents colors in an orderly progression that makes it easy to visualize the sequence. Hue is represented as an angle going counterclockwise around the wheel. The saturation is the distance from the center of the wheel toward the outer edge. Colors on the outer edge have full saturation. Brightness measures the intensity of a color. Colors shown on the wheel have 100% brightness, which decreases as they are darkened (black has 0% brightness). There is no standard for assigning HSB/HSL values. Programs often use values between 0 and 255 to correspond to the RGB numbering scheme. As we will see, .NET assigns the actual angle to the hue, and values between 0 and 1 to the saturation and brightness.

Figure 8-12. Color wheel

How to Create a Color Object

The Color structure provides three static methods for creating a Color object: FromName, FromKnownColor, and FromArgb. FromName takes the name of a color as a string argument and creates a new struct: Color magenta = Color.FromName("Magenta"). The name must match one in the KnownColor enumeration values, which is an enumeration of all the colors represented as properties in the Color and SystemColor structures.

FromKnownColor takes a KnownColor enumeration value as its argument and produces a struct for that color:

Color magenta = Color.FromKnownColor(KnownColor.Magenta);

FromArgb allows you to specify a color by RGB and alpha values, which makes it easy to change the transparency of an existing color. Here are some of its overloads:

// (r, g, b)

Color slate1 = Color.FromArgb (112, 128, 144);

// (alpha, r, g, b)

Color slate2 = Color.FromArgb (255, 112, 128, 144);

// (alpha, Color)

Color lightslate = Color.FromArgb(50, slate2 );

Examining the Characteristics of a Color Object

The Color structure has four properties that return the ARGB values of a color: Color.A, Color.R, Color.G, and Color.B. All of these properties have a value in the range 0 to 255.

Color slateGray = Color.FromArgb(255,112,128,144);

byte a = slateGray.A;              // 255

byte r = slateGray.R;              // 112

byte g = slateGray.G;              // 128

byte b = slateGray.B;              // 144

The individual HSB values of a color can be extracted using the Color.GetHue, GetSaturation, and GetBrightness methods. The hue is measured in degrees as a value from 0.0 to 360.0. Saturation and brightness have values between 0 and 1.

Color slateGray = Color.FromArgb(255,112,128,144);

float hue = slateGray.GetHue();            //  210 degrees

float sat = slateGray.GetSaturation();     // .125

float brt = slateGray.GetBrightness();     // .501

Observe in Figure 8-12 that the hue of 210 degrees (moving clockwise from 0) falls between cyan and blue on the circle—which is where you would expect to find a slate gray color.

A Sample Project: Building a Color Viewer

The best way to grasp how the color spaces relate to actual .NET colors is to visually associate the colors with their RGB and HSB values. .NET offers a ColorDialog class that can be used to display available colors; however, it does not identify colors by the system-defined names that most developers work with. So, let's build a simple color viewer that displays colors by name and at the same time demonstrates how to use the GDI+ types discussed in this section.

Figure 8-13 shows the user interface for this application. It consists of a TReeView on the right that contains all the colors in the KnownColor enumeration organized into 12 color groups. These groups correspond to the primary, secondary, and tertiary[1] colors of the color wheel. In terms of hues, each section is 30 degrees on the circle. The interface also contains two panels, a larger one in which a selected color is displayed, and a smaller one that displays a brightness gradient for the selected color. This is created using a multi-color gradient comprising black and white at each end, and the color at a focus point determined by its Brightness value. The remainder of the screen displays RGB and HSB values obtained using the properties and methods discussed earlier.

[1] Tertiary colors are red-orange, yellow-orange, yellow-green, blue-green, blue-violet, and red-violet.

Figure 8-13. Example: Color viewer demonstrates working with colors and gradients

The code for this application is shown in Listings 8-2 and 8-3. The former contains code to populate the treeNode structure with the color nodes; Listing 8-3 shows the methods used to display the selected color and its color space values. The routine code for laying out controls on a form is excluded.

Listing 8-2. Color Viewer: Populate treeNode with All Colors

using System.Drawing;

using System.Windows.Forms;

using System.Drawing.Drawing2D;

public class Form1 : Form

{

   public Form1()

   {

      InitializeComponent();  // Lay out controls on Form1

      // Set up event handler to be fired when node is selected

      colorTree.AfterSelect += new

            TreeViewEventHandler(ColorTree_AfterSelect);

      BuildWheel();   // Create Parent Nodes for 12 categories

   }

   [STAThread]

   static void Main()

   {

      Application.Run(new Form1());

   }

   // Parent Nodes in TreeView for each segment of color wheel

   private void BuildWheel()

   {

      TreeNode tNode;

      tNode = colorTree.Nodes.Add("Red");

      tNode = colorTree.Nodes.Add("Orange");

      tNode = colorTree.Nodes.Add("Yellow");

      // Remainder of nodes are added here ....

   }

   private void button1_Click(object sender, System.EventArgs e)

   {

      // Add Colors to TreeNode Structure

      // Loop through KnownColor enum values

      Array objColor = Enum.GetValues(typeof(KnownColor));

      for (int i=0; i < objColor.Length; i++)

      {

         KnownColor kc = (KnownColor) objColor.GetValue(i);

         Color c = Color.FromKnownColor(kc);

         if (!c.IsSystemColor) // Exclude System UI colors

         {

            InsertColor(c, c.GetHue());

         }

      }

   }

   private void InsertColor(Color myColor, float hue)

   {

      TreeNode tNode;

      TreeNode cNode = new TreeNode();

      // Offset is used to start color categories at 345 degrees

      float hueOffset = hue + 15;

      if (hueOffset >360) hueOffset -= 360;

      // Get one of 12 color categories

      int colorCat = (int)(hueOffset -.1)/30;

      tNode = colorTree.Nodes[colorCat];  // Get parent node

      // Add HSB values to node's Tag

      HSB nodeHSB = new HSB(hue, myColor.GetSaturation(),

            myColor.GetBrightness());

      cNode.Tag  = nodeHSB;      // Tag contains HSB values

      cNode.Text = myColor.Name;

      int nodeCt = tNode.Nodes.Count;

      bool insert=false;

      // Insert colors in ascending hue value

      for (int i=0; i< nodeCt && insert==false ;i++)

      {

         nodeHSB = (HSB)tNode.Nodes[i].Tag;

         if (hue < nodeHSB.Hue)

         {

            tNode.Nodes.Insert(i,cNode);

            insert = true;

         }

      }

      if (!insert) tNode.Nodes.Add(cNode);

   }

   public struct HSB

   {

      public float Hue;

      public float Saturation;

      public float Brightness;

      public HSB(float H, float S, float B)

      {

         Hue = H;

         Saturation = S;

         Brightness = B;

      }

   }

   // ---> Methods to Display Colors go here

}

When the application is executed, the form's constructor calls BuildWheel to create the tree structure of parent nodes that represent the 12 color categories. Then, when the Build Color Tree button is clicked, the Click event handler loops through the KnownColor enum value (excluding system colors) and calls InsertColor to insert the color under the correct parent node. The Tag field of the added node (color) is set to an HSB struct that contains the hue, saturation, and brightness for the color. Nodes are stored in ascending order of Hue value. (See Chapter 7, "Windows Forms Controls," for a discussion of the TReeNode control.)

Listing 8-3 contains the code for both the node selection event handler and for ShowColor, the method that displays the color, draws the brightness scale, and fills all the text boxes with RGB and HSB values.

Listing 8-3. Color Viewer: Display Selected Color and Values

private void ColorTree_AfterSelect(Object sender,

      TreeViewEventArgs e)

// Event handler for AfterSelect event

{

   // Call method to display color and info

   if (e.Node.Parent != null) ShowColor(e.Node);

}

private void ShowColor(TreeNode viewNode)

{

   Graphics g  = panel1.CreateGraphics();  // Color panel

   Graphics g2 = panel2.CreateGraphics();  // Brightness panel

   try

   {

      // Convert node's text value to Color object

      Color myColor = Color.FromName(viewNode.Text);

      Brush b = new SolidBrush(myColor);

      // Display selected color

      g.FillRectangle(b, 0,0,panel1.Width,panel1.Height);

      HSB hsbVal= (HSB) viewNode.Tag;

      // Convert hue to value between 0 and 255 for displaying

      int huescaled = (int) (hsbVal.Hue / 360 * 255);

      hText.Text = huescaled.ToString();

      sText.Text = hsbVal.Saturation.ToString();

      lText.Text = hsbVal.Brightness.ToString();

      rText.Text = myColor.R.ToString();

      gText.Text = myColor.G.ToString();

      bText.Text = myColor.B.ToString();

      // Draw Brightness scale

      Rectangle rect = new Rectangle(new Point(0,0),

            new Size(panel2.Width, panel2.Height));

      // Create multi-color brush gradient for brightness scale

      LinearGradientBrush bg = new LinearGradientBrush(rect,

            Color.Empty, Color.Empty,90);

      ColorBlend myBlend = new ColorBlend();

      myBlend.Colors = new Color[] {Color.White, myColor,

             Color.Black};

      myBlend.Positions = new float[]{0f,

             1-hsbVal.Brightness,1f};

      bg.InterpolationColors = myBlend;

      g2.FillRectangle(bg, rect);

      // Draw marker on brightness scale showing current color

      int colorPt = (int)((1–hsbVal.Brightness)* panel1.Height);

      g2.FillRectangle(Brushes.White,0,colorPt,10,2);

      b.Dispose();

      bg.Dispose();

   }

   finally

   {

      g.Dispose();

      g2.Dispose();

   }

}

The code incorporates several of the concepts already discussed in this section. Its main purpose is to demonstrate how Color, Graphics, and Brush objects work together. A SolidBrush is used to fill panel1 with a color sample, a gradient brush creates the brightness scale, and Color properties provide the RGB values displayed on the screen.


8.3. Images

GDI+ provides a wide range of functionality for working with images in a runtime environment. It includes support for the following:

  • The standard image formats such as GIF and JPG files.

  • Creating images dynamically in memory that can then be displayed in a WinForms environment or served up as images on a Web server or Web Service.

  • Using images as a surface to draw on.

The two most important classes for handling images are the Image and Bitmap class. Image is an abstract class that serves as a base class for the derived Bitmap class. It provides useful methods for loading and storing images, as well as gleaning information about an image, such as its height and width. But for the most part, working with images requires the creation of objects that represent raster images. This responsibility devolves to the Bitmap class, and we use it exclusively in this section.

Tasks associated with using images in applications fall into three general categories:

  • Loading and storing images. Images can be retrieved from files, from a stream of bytes, from the system clipboard, or from resource files. After you have loaded or created an image, it is easily saved into a specified image format using the Save method of the Image or Bitmap class.

  • Displaying an image. Images are dynamically displayed by writing them to the surface area of a form or control encapsulated by a Graphics object.

  • Manipulating an image. An image is represented by an array of bits in memory. These bits can be manipulated in an unlimited number of ways to transform the image. Traditional image operations include resizing, cropping, rotating, and skewing. GDI+ also supports changing an image's overall transparency or resolution and altering individual bits within an image.

Loading and Storing Images

The easiest way to bring an image into memory is to pass the name of the file to a Bitmap constructor. Alternatively, you can use the FromFile method inherited from the Image class.

string fname = "c:\\globe.gif";

Bitmap bmp = new Bitmap(fname);

bmp = (Bitmap)Bitmap.FromFile(fname); // Cast to convert Image

In both cases, the image stored in bmp is the same size as the image in the file. Another Bitmap constructor can be used to scale the image as it is loaded. This code loads and scales an image to half its size:

int w = Image.FromFile(fname).Width;

int h = Image.FromFile(fname).Height;

Size sz= new Size(w/2,h/2);

bmp = new Bitmap(Image.FromFile(fname), sz); //Scales

GDI+ support images in several standard formats: bitmaps (BMP), Graphics Interchange Format (GIF), Joint Photographic Experts Group (JPEG), Portable Network Graphics (PNG), and the Tag Image File Format (TIFF). These are used for both loading and storing images and, in fact, make it quite simple to convert one format to another. Here is an example that loads a GIF file and stores it in JPEG format:

string fname = "c:\\globe.gif";

bmp = new Bitmap(Image.FromFile(fname));

bmp.Save("c:\\globe.jpg",

         System.Drawing.Imaging.ImageFormat.Jpeg);

// Compare size of old and new file

FileInfo fi= new FileInfo(fname);

int old = (int) fi.Length;

fi = new FileInfo("c:\\globe.jpg");

string msg = String.Format("Original: {0} New: {1}",old,fi.Length);

MessageBox.Show(msg);  // ---> Original: 28996 New: 6736

The Save method has five overloads; its simplest forms take the name of the file to be written to as its first parameter and an optional ImageFormat type as its second. The ImageFormat class has several properties that specify the format of the image output file. If you have any experience with image files, you already know that the format plays a key role in the size of the file. In this example, the new JPEG file is less than one-fourth the size of the original GIF file.

To support multiple file formats, GDI+ uses encoders to save images to a file and decoders to load images. These are referred to generically as codecs (code-decode). An advantage of using codecs is that new image formats can be supported by writing a decoder and encoder for them.

.NET provides the ImageCodecInfo class to provide information about installed image codecs. Most applications allow GDI+ to control all aspects of loading and storing image files, and have no need for the codecs information. However, you may want to use it to discover what codecs are available on your machine. The following code loops through and displays the list of installed encoders (see Figure 8-14):

// Using System.Drawing.Imaging

string myList="";

foreach(ImageCodecInfo co in ImageCodecInfo.GetImageEncoders())

   myList = myList +"\n"+co.CodecName;

Console.WriteLine(myList);

Figure 8-14. Codecs

The DrawImage method of the Graphics object is used to display an image on the Graphics object's surface. These two statements load an image and draw it full size at the upper-left corner of the graphics surface (0,0). If the Graphics object surface is smaller than the image, the image is cropped (see the first figure in the following example).

Bitmap bmp = new Bitmap("C:\\globe.gif");

g.DrawImage(bmp,0,0);    // Draw at coordinates 0,0

DrawImage has some 30 overloaded versions that give you a range of control over sizing, placement, and image selection. Many of these include a destination rectangle, which forces the source image to be resized to fit the rectangle. Other variations include a source rectangle that permits you to specify a portion of the source image to be displayed; and some include both a destination and source rectangle.

The following examples capture most of the basic effects that can be achieved. Note that the source image is 192x160 pixels for all examples, and the destination panel is 96x80 pixels.

  1. The source image is drawn at its full size on the target surface. Cropping occurs because the destination panel is smaller than the source image.

Graphics g = panel1.CreateGraphics();

g.DrawImage(bmp,0,0);

  2. The source image is scaled to fit the destination rectangle.

Rectangle dRect = new

   Rectangle(new Point(0,0),

   new Size(panel1.Width,panel1.Height));

g.DrawImage(bmp, dRect);

//Or panel1.ClientRectangle

  3. Part of the source rectangle (left corner = 100,0) is selected.

Rectangle sRect = new

   Rectangle(new Point(100,0),

   new Size(192,160));

g.DrawImage(bmp,0,0,sRect,GraphicsUnit.Pixel);

  4. Combines examples 2 and 3: A portion of the source rectangle is scaled to fit dimensions of destination rectangle.

g.DrawImage(bmp,dRect,sRect,

            GraphicsUnit.Pixel);

  5. The destination points specify where the upper-left, upper-right, and lower-left point of the original are placed. The fourth point is determined automatically in order to create a parallelogram.

Point[]dp = {new Point(10,0),new Point(80,10),

    new Point(0,70)};   // ul, ur, ll

g.DrawImage(bmp,dp);

The DrawImage variations shown here illustrate many familiar image effects: zoom in and zoom out are achieved by defining a destination rectangle larger (zoom in) or smaller (zoom out) than the source image; image skewing and rotation are products of mapping the corners of the original image to three destination points, as shown in the figure to the left of Example 5.

A Note on Displaying Icons

Icons (.ico files) do not inherit from the Image class and cannot be rendered by the DrawImage method; instead, they use the Graphics.DrawIcon method. To display one, create an Icon object and pass the file name or Stream object containing the image to the constructor. Then, use DrawIcon to display the image at a desired location.

Icon icon = new Icon("c:\\clock.ico");

g.DrawIcon(icon,120,220);  // Display at x=120, y=220

icon.Dispose();            // Always dispose of object

g.Dispose();

Manipulating Images

.NET also supports some more advanced image manipulation techniques that allow an application to rotate, mirror, flip, and change individual pixels in an image. We'll look at these techniques and also examine the advantage of building an image in memory before displaying it to a physical device.

Rotating and Mirroring

Operations that rotate or skew an image normally rely on the DrawImage overload that maps three corners of the original image to destination points that define a parallelogram.

void DrawImage(Image image, Point destPoints[])

Recall from an earlier example that the destination points are the new coordinates of the upper-left, upper-right, and lower-left corners of the source image. Figure 8-15 illustrates the effects that can be achieved by altering the destination points.

Figure 8-15. Manipulation using DrawImage

The following code is used to create a mirrored image from the original image. Think of the image as a page that has been turned over from left to right: points a and b are switched, and point c is now the lower-right edge.

Bitmap bmp = new Bitmap(fname);               // Get image

// Mirror Image

Point ptA = new Point(bmp.Width,0);           // Upper left

Point ptB = new Point(0,0);                   // Upper right

Point ptC = new Point(bmp.Width, bmp.Height); // Lower left

Point[]dp = {ptA,ptB,ptC};

g.DrawImage(bmp,dp);

Many of these same effects can be achieved using the Bitmap.RotateFlip method, which has this signature:

Public void RotateFlip(RotateFlipType rft)

RotateFlipType is an enumeration that indicates how many degrees to rotate the image and whether to "flip" it after rotating (available rotations are 90, 180, and 270 degrees). Here are a couple of examples:

// Rotate 90 degrees

bmp.RotateFlip(RotateFlipType.Rotate90FlipNone);

// Rotate 90 degrees and flip along the vertical axis

bmp.RotateFlip(RotateFlipType.Rotate90FlipY);

// Flip horizontally (mirror)

bmp.RotateFlip(RotateFlipType.RotateNoneFlipX);

The most important thing to recognize about this method is that it changes the actual image in memory—as opposed to DrawImage, which simply changes it on the drawing surface. For example, if you rotate an image 90 degrees and then rotate it 90 degrees again, the image will be rotated a total of 180 degrees in memory.

Working with a Buffered Image

All of the preceding examples are based on drawing directly to a visible panel control on a form. It is also possible to load an image into, or draw your own image onto, an internal Bitmap object before displaying it. This can offer several advantages:

  • It permits you to create images such as graphs dynamically and display them in the application or load them into Web pages for display.

  • It improves performance by permitting the application to respond to a Paint event by redrawing a stored image, rather than having to reconstruct the image from scratch.

  • It permits you to keep the current "state" of the image. As long as all transformations, such as rotating or changing colors, are made first to the Bitmap object in memory, it will always represent the current state of the image.

To demonstrate, let's input a two-color image, place it in memory, change pixels in it, and write it to a panel. Figure 8-16 shows the initial image and the final image after pixels are swapped.

Figure 8-16. Use GetPixel() and SetPixel() to swap pixels

The following code creates a Bitmap object bmpMem that serves as a buffer where the pixels are swapped on the flag before it is displayed. We use the Graphics.FromImage method to obtain a Graphics object that can write to the image in memory. Other new features to note are the use of GetPixel and SetPixel to read and write pixels on the image.

Graphics g = pan.CreateGraphics();  // Create from a panel

Bitmap bmp = new Bitmap("c:\\flag.gif");

g.DrawImage(bmp,0,0);         // Draw flag to panel

Bitmap bmpMem = new Bitmap(bmp.Width,bmp.Height);

Graphics gMem = Graphics.FromImage(bmpMem);

gMem.DrawImage(bmp,0,0);      // Draw flag to memory

// Define a color object for the red pixels

Color cnRed = Color.FromArgb(255,214,41,33);  // a,r,g,b

// Loop through all pixels in image and swap them

for (int y=0; y<bmpMem.Height; y++)

{

   for (int x=0; x<bmpMem.Width; x++)

   {

      Color px = bmpMem.GetPixel(x,y);

      if(px.G > 240)

         bmpMem.SetPixel(x,y, cnRed);         // Set white to red

      else  bmpMem.SetPixel(x,y,Color.White); // Set red to white

   }

}

g.DrawImage(bmpMem,0,0);        // Display reversed flag on panel

gMem.Dispose();

g.Dispose();

Core Note

Applications that dynamically create images for display should draw them offscreen onto a Bitmap and then render them to the screen when completed.

Sample Project: Working with Images

This application brings together many of the concepts presented in this chapter: handling the Paint event; using Invalidation to clear portions of a screen; and using DrawImage to rotate, flip, and zoom in and out on an image.

The screen for the program is shown in Figure 8-17. It consists of a menu with three main selections: File is used to load an image; Image has options to mirror, flip, or copy an image; and Screen refreshes the screen. The panel control on the left serves as the main viewing window into which an image is loaded. The Image menu options are applied to this panel. The smaller panel is where part of the main image is copied. The + and – buttons zoom in and out, respectively.

Figure 8-17. User interface for Image Viewer [Source: Lourve, Paris]

The copying process is the most interesting part of the application. A user selects a rectangular area of the image by pressing the mouse button and dragging the mouse over the image. When the mouse button is raised, the selected area can be copied to the smaller panel by choosing Image-Copy from the menu.

The code for this project is presented in three sections: the menu operations, drawing the selection rectangle, and the functions associated with the small panel (panel2).

Implementing Menu Operations

The following fields are defined at the form level to make them available for menu operations:

private Bitmap bmp;          // Holds original loaded image

private Bitmap newbmp;       // Holds latest version of image

private bool imageStatus = false; // Indicates image is loaded

private int resizeLevel;     // Level image magnified/reduced

Listing 8-4 contains the code for the menu options to load an image into the viewing panel, flip an image, mirror an image, and refresh the viewing panel. The Image-Copy option is discussed in the code related to manipulating the image on panel2.

Listing 8-4. Image Viewer: Menu Items

Private void menuItem6_Click(object sender, System.EventArgs e)

{

   // Load image from file

   OpenFileDialog fd = new OpenFileDialog();

   fd.InitialDirectory = "c:\\" ;

   fd.Filter = "Image Files | *.JPG;*.GIF";

   if (fd.ShowDialog() == DialogResult.OK)

   {

      string fname= fd.FileName;

      using(Graphics g = panel1.CreateGraphics())

      {

         bmp = new Bitmap(fname); // Load image from file

         newBmp = bmp;            // Save copy of image

         // Clear main panel before drawing to it

         g.FillRectangle(Brushes.White,0,0,

               panel1.Width,panel1.Height );

         Rectangle r = new Rectangle(0,0,bmp.Width,

               bmp.Height);

         g.DrawImage(bmp,r);      // Draw image on panel

         ImageStatus = true;      // Indicates image exists

         // Clear small panel

         Graphics gClear= panel2.CreateGraphics();

         g.FillRectangle(Brushes.White,0,0,

               panel2.Width,panel2.Height );

         gClear.Dispose();

      }

   }

}

private void menuItem4_Click(object sender, System.EventArgs e)

{

   // Mirror image

   Graphics g= panel1.CreateGraphics();

   int h = newBmp.Height;

   int w = newBmp.Width;

   Point[] destPts = {

               new Point(w,0),

               new Point(0,0),

               new Point(w,h) };

   Bitmap tempBmp = new Bitmap(w,h);

   Graphics gr= Graphics.FromImage(tempBmp);

   gr.DrawImage(newBmp, destPts);  // Mirror temporary image

   g.DrawImage(tempBmp,0,0);       // Draw image on panel

   newBmp = tempBmp;               // Set to mirrored image

   g.Dispose();

   gr.Dispose();

}

private void menuItem3_Click(object sender, System.EventArgs e)

{

   // Flip image vertically

   newBmp.RotateFlip(RotateFlipType.RotateNoneFlipY);

   Graphics g = panel1.CreateGraphics();

   g.DrawImage(newBmp,0,0);

   g.Dispose();

}

private void menuItem9_Click(object sender, System.EventArgs e)

{

   // Refresh Screen

   panel1.Invalidate();    // Redraw entire panel

   panel1.Update();

   selectStatus = false;   // Refreshing removes selected area

}

The file loading routine displays a dialog box for the user to enter the image file name. If this image file exists, it is opened and displayed in panel1. If the image is larger than the panel, it is cropped.

The method that mirrors an image first creates a temporary Bitmap and uses DrawImage, as described earlier, to mirror the image to its surface. The mirrored image is displayed in the panel and saved in newBmp. Flipping could be done in a similar way, but for demonstration purposes, we use the RotateFlip method to directly transform newBmp before it is displayed.

The screen refresh routine simply calls Invalidate and Update to redraw the image on panel1. The main effect of this is to remove any selection rectangle (discussed next) that has been drawn on the image.

Drawing a Selection Rectangle on the Image

Listing 8-5 contains event handlers for Paint, MouseDown, MouseUp, and MouseMove. These routines permit the user to select a rectangular area on the image that can then be copied to panel2. The event handling routines are associated with the events using this code in the constructor:

panel1.MouseDown += new MouseEventHandler(Mouse_Down);

panel1.MouseUp   += new MouseEventHandler(Mouse_Up);

panel1.MouseMove += new MouseEventHandler(Mouse_Move);

panel1.Paint     += new PaintEventHandler(RePaint);

The following fields, defined at the form level, are used to keep status information:

private Point lastPoint = Point.Empty; // Tracks mouse movement

private Point origPoint = Point.Empty; // Mouse down coordinates

private Rectangle rectSel;             // Selected area

private bool selectStatus = false;     // True if area selected

When a MouseDown occurs, origPoint is set to the x,y coordinates and serves as the origin of the rectangle that is to be drawn. Dragging the mouse results in a rectangle being displayed that tracks the mouse movement. The MouseMove event handler must draw the rectangle at the new position and erase the previous rectangle. It uses lastPoint and origPoint to determine the part of the image to redraw in order to erase the previous rectangle. The new rectangle is determined by the current mouse coordinates and origPoint. When the MouseUp event occurs, rectSel is set to the final rectangle.

Listing 8-5. Image Viewer: Select Area of Image to Copy

private void RePaint(object sender, PaintEventArgs e)

{

   Graphics g = e.Graphics;

   // Redraw part of current image to panel

   if (ImageStatus) g.DrawImage(newBmp,

       e.ClipRectangle,e.ClipRectangle, GraphicsUnit.Pixel);

   base.OnPaint(e);

}

private void Mouse_Down(object sender, MouseEventArgs e)

{

   if (lastPoint != Point.Empty)

   {

      panel1.Invalidate(rectSel); // Clear previous rect.

      panel1.Update();

   }

   lastPoint.X= e.X;

   lastPoint.Y= e.Y;

   origPoint = lastPoint;   // Save origin of selected area

   selectStatus=true;

}

private void Mouse_Up(object sender, MouseEventArgs e)

{

   // Selected area complete. Define it as a rectangle.

   rectSel.X = e.X;

   if (e.X > origPoint.X) rectSel.X = origPoint.X;

   rectSel.Y = origPoint.Y;

   rectSel.Width = Math.Abs(e.X- origPoint.X)+1;

   rectSel.Height= Math.Abs(e.Y - origPoint.Y)+1;

   origPoint = Point.Empty;

   if (rectSel.Width < 2) selectStatus=false;

}

private void Mouse_Move(object sender, MouseEventArgs e)

{

   // Tracks mouse movement to draw bounding rectangle

   if (origPoint != Point.Empty)

   {

      Rectangle r;

      Rectangle rd;

      // Get rectangle area to invalidate

      int xop = origPoint.X;

         if (xop > lastPoint.X) xop= lastPoint.X;

         int w = Math.Abs(origPoint.X - lastPoint.X)+1;

         int h = lastPoint.Y - origPoint.Y+1;

         r = new Rectangle(xop,origPoint.Y,w,h);

         // Get rectangle area to draw

         xop = e.X >= origPoint.X ? origPoint.X:e.X;

         w = Math.Abs(origPoint.X - e.X);

         h = e.Y - origPoint.Y;

         rd = new Rectangle(xop, origPoint.Y,w,h);

         Graphics g = panel1.CreateGraphics();

         // Redraw image over previous rectangle

         g.DrawImage(newBmp,r,r);

         // Draw rectangle around selected area

         g.DrawRectangle(Pens.Red,rd);

         g.Dispose();

         lastPoint.X= e.X;

         lastPoint.Y= e.Y;

   }

}

The logic for creating the rectangles is based on establishing a point of origin where the first MouseDown occurs. The subsequent rectangles then attempt to use that point's coordinates for the upper left corner. However, if the mouse is moved to the left of the origin, the upper left corner must be based on this x value. This is why the MouseUp and MouseMove routines check to see if the current x coordinate e.x is less than that of the origin.

Copying and Manipulating the Image on the Small Panel

The following code is executed when Image – Copy is selected from the menu. The selected area is defined by the rectangle rectSel. The image bounded by this rectangle is drawn to a temporary Bitmap, which is then drawn to panel2. A copy of the contents of panel2 is always maintained in the Bitmap smallBmp.

if (selectStatus)

{

   Graphics g = panel2.CreateGraphics();

   g.FillRectangle(Brushes.White,panel2.ClientRectangle);

   Rectangle rd = new

         Rectangle(0,0,rectSel.Width,rectSel.Height);

   Bitmap temp = new Bitmap(rectSel.Width,rectSel.Height);

   Graphics gi = Graphics.FromImage(temp);

   // Draw selected portion of image onto temp

   gi.DrawImage(newBmp,rd,rectSel,GraphicsUnit.Pixel);

   smallBmp = temp;   // save image displayed on panel2

   // Draw image onto panel2

   g.DrawImage(smallBmp,rd);

   g.Dispose();

    resizeLevel = 0;  // Keeps track of magnification/reduction

}

The plus (+) and minus (–) buttons are used to enlarge or reduce the image on panel2. The actual enlargement or reduction is performed in memory on smallBmp, which holds the original copied image. This is then drawn to the small panel. As shown in the code here, the magnification algorithm is quite simple: The width and height of the original image are increased in increments of .25 and used as the dimensions of the target rectangle.

// Enlarge image

Graphics g = panel2.CreateGraphics();

if (smallBmp != null)

{

   resizeLevel= resizeLevel+1;

   float fac= (float) (1.0+(resizeLevel*.25));

   int w = (int)(smallBmp.Width*fac);

   int h = (int)(smallBmp.Height*fac);

   Rectangle rd= new Rectangle(0,0,w,h);  // Destination rect.

   Bitmap tempBmp = new Bitmap(w,h);

   Graphics gi = Graphics.FromImage(tempBmp);

   // Draw enlarged image to tempBmp Bitmap

   gi.DrawImage(smallBmp,rd);

   g.DrawImage(tempBmp,rd);   // Display enlarged image

   gi.Dispose();

}

g.Dispose();

The code to reduce the image is similar, except that the width and height of the target rectangle are decremented by a factor of .25:

resizeLevel= (resizeLevel>-3)?resizeLevel-1:resizeLevel;

float fac= (float) (1.0+(resizeLevel*.25));

int w = (int)(smallBmp.Width*fac);

int h =(int) (smallBmp.Height*fac);

A Note on GDI and BitBlt for the Microsoft Windows Platform

As we have seen in the preceding examples, Graphics.DrawImage is an easy-to-use method for drawing to a visible external device or to a Bitmap object in memory. As a rule, it meets the graphics demands of most programs. However, there are situations where a more flexible or faster method is required. One of the more common graphics requirements is to perform a screen capture of an entire display or a portion of a form used as a drawing area. Unfortunately, GDI+ does not provide a direct way to copy bits from the screen memory. You may also have a graphics-intensive application that requires the constant redrawing of complex images. In both cases, the solution may well be to use GDI—specifically the BitBlt function.

If you have worked with the Win32 API, you are undoubtedly familiar with BitBlt. If not, BitBlt, which is short for Bit Block Transfer, is a very fast method for copying bits to and from a screen's memory, usually with the support of the graphics card. In fact, the DrawImage method uses BitBlt underneath to perform its operations.

Even though it is part of the Win32 API, .NET makes it easy to use the BitBlt function. The first step is to use the System.Runtime.InteropServices namespace to provide the DllImportAttribute for the function. This attribute makes the Win32 API available to managed code.

[System.Runtime.InteropServices.DllImportAttribute("gdi32.dll")]

private static extern int BitBlt(

   IntPtr hDestDC,     // Handle to target device context

   int xDest,          // x coordinate of destination

   int yDest,          // y coordinate of destination

   int nWidth,         // Width of memory being copied

   int nHeight,        // Height of memory being copied

   IntPtr hSrcDC,      // Handle to source device context

   int xSrc,           // x coordinate of image source

   int ySrc,           // y coordinate of image source

   System.Int32 dwRop  // Copy is specified by 0x00CC0020

);

This function copies a rectangular bitmap from a source to a destination. The source and destination are designated by handles to their device context. (In Windows, a device context is a data structure that describes the object's drawing surface and where to locate it in memory.) The type of bit transfer performed is determined by the value of the dwRop parameter. A simple copy takes the value shown in the declaration. By changing this value, you can specify that the source and target bits be combined by AND, OR, XOR, and other logical operators.

Using bitBlt is straightforward. In this example, the contents of a panel are copied to a Bitmap object in memory. Creating the Graphics object for the panel and Bitmap should be familiar territory. Next, use the Graphics object's GetHdc method to obtain a handle for the device context for the panel and Bitmap. These are then passed to the bitBlt function along with a ropType argument that tells the function to perform a straight copy operation.

// Draw an image on to a panel

Graphics g = panel1.CreateGraphics();

g.DrawLine(Pens.Black,10,10,50,50);

g.DrawEllipse(Pens.Blue,0,30,40,30);

// Create a memory Bitmap object to be the destination

Bitmap fxMem = new Bitmap(panel1.Width,panel1.Height);

Graphics gfxMem = Graphics.FromImage(fxMem);

int ropType= 0x00CC0020;   // perform a copy operation

// Get references to the device context for the source and target

IntPtr HDCSrc= g.GetHdc();

IntPtr HDCMem= gfxMem.GetHdc();

// Copy a rectangular area from the panel of size 100 x 100

bitBlt(HDCMem,0,0,100,100,HDCSrc,0,0,ropType);

// Release resources when finished

g.ReleaseHdc(HDCSrc);

gfxMem.ReleaseHdc(HDCMem);

g.Dispose();

gfxMem.Dispose();

Always pair each GetHdc with a ReleaseHdc, and only place calls to GDI functions within their scope. GDI+ operations between the statements are ignored.


8.4. Summary

GDI+ supports a wide range of graphics-related operations ranging from drawing to image manipulation. All require use of the Graphics object that encapsulates the drawing surface and provides methods for drawing geometric shapes and images to the abstract surface. The graphic is typically rendered to a form or control on the form to be displayed in a user interface.

The task of drawing is simplified by the availability of methods to create predefined geometric shapes such as lines, ellipses, and rectangles. These shapes are outlined using a Pen object that can take virtually any color or width, and can be drawn as a solid line or broken into a series of dashes, dots, and combinations of these. The shapes are filled with special brush objects such as SolidBrush, TextureBrush, LinearGradientBrush, and HatchBrush.

The Image and Bitmap classes are used in .NET to represent raster-based images. Most of the standard image formats—BMP, GIF, JPG, and PNG—are supported. After an image is loaded or drawn, the Graphics.DrawImage method may be used to rotate, skew, mirror, resize, and crop images as it displays them to the Graphics object's surface.

When custom graphics are included in a form or control, it is necessary to respond to the Paint event, to redraw all or part of the graphics on demand. A program can force this event for a control by invoking the Control.Invalidate method.


8.4. Test Your Understanding

1:

What two properties does PaintEventArgs provide a Paint handler routine? Describe the role of each.

2:

What method is called to trigger a Paint event for a control?

3:

Which image is drawn by the following code?

GraphicsPath gp = new GraphicsPath();

gp.AddLine(0,0,60,0);

gp.AddLine(0,20,60,20);

g.DrawPath(new Pen(Color.Black,2),gp);

4:

Which of these statements will cause a compile-time error?

  1.
Brush sb = new SolidBrush(Color.Chartreuse);

  2.
Brush b = new Brush(Color.Red);

  3.
Brush h = new HatchBrush(HatchStyle.DottedDiamond,

                         Color.Blue,Color.Red);

5:

Which of these colors is more transparent?

Color a = FromArgb(255,112,128,144);

Color b = FromArgb(200,212,128,200);

6:

You are drawing an image that is 200x200 pixels onto a panel that is 100x100 pixels. The image is contained in the Bitmap bmp, and the following statement is used:

g.DrawImage(bmp, panel1.ClientRectangle);

What percent of the image is displayed?

  1. 25%

  2. 50%

  3. 100%

7:

The Russian artist Wassily Kandinsky translated the dance movements of Gret Palucca into a series of schematic diagrams consisting of simple geometric shapes. The following is a computer generated schematic (approximating Kandinsky's) that corresponds to the accompanying dance position. The schematic is created with a GraphicsPath object and the statements that follow. However, the statements have been rearranged, and your task is to place them in a sequence to draw the schematic. Recall that a GraphicsPath object automatically connects objects.

Graphics g = panel1.CreateGraphics();

g.SmoothingMode = SmoothingMode.AntiAlias;

GraphicsPath gp = new GraphicsPath();

gp.AddLine(10,170,30,170);

gp.AddLine(40,50,50,20);

gp.StartFigure();

gp.AddLine(16,100,100,100);

gp.AddLine(50,20,145,100);

gp.AddLine(100,100,190,180);

gp.StartFigure();

gp.AddArc(65,10,120,180,180,80);

g.DrawPath(new Pen(Color.Black,2),gp); gp.StartFigure();

gp.AddArc(65,5,120,100,200,70);

8:

The following statements are applied to the original image A:

Point ptA = new Point(bmp.Height,0);

Point ptB = new Point(bmp.Height,bmp.Width);

Point ptC = new Point(0,0);

Point[]dp = {ptA,ptB,ptC};

g.DrawImage(bmp,dp);

Which image is drawn by the last statement?


Chapter 9. Fonts, Text, and Printing

Topics in This Chapter

  • Fonts: .NET classes support the creation of individual font objects and font families. Treating a font as an object allows an application to set a font's style and size through properties.

  • Text: The Graphics.DrawString method and StringFormat class are used together to draw and format text on a drawing surface.

  • Printing: The PrintDocument class provides the methods, properties, and events used to control .NET printing. It exposes properties that allow printer selection, page setup, and choice of pages to be printed.

  • Creating a Custom PrintDocument Class: A custom PrintDocument class provides an application greater control over the printing process and can be used to ensure a consistent format for reports.

Chapter 8, ".NET Graphics Using GDI+," focused on the use of GDI+ for creating graphics and working with images. This chapter continues the exploration of GDI+ with the focus shifting to its use for "drawing text." One typically thinks of text as being printed—rather than drawn. In the .NET world, however, the display and rendering of text relies on techniques similar to those required to display any graphic: a Graphics object must be created, and its methods used to position and render text strings—the shape of which is determined by the font used.

This chapter begins with a look at fonts and an explanation of the relationship between fonts, font families, and typefaces. It then looks at the classes used to create fonts and the options for sizing them and selecting font styles. The Graphics.DrawString method is the primary tool for rendering text and has several overloads that support a number of formatting features: text alignment, text wrapping, the creation of columnar text, and trimming, to name a few.

The chapter concludes with a detailed look at printing. An incremental approach is taken. A simple example that illustrates the basic classes is presented. Building on these fundamentals, a more complex and realistic multi-page report with headers, multiple columns, and end-of-page handling is presented. Throughout, the emphasis is on understanding the GDI+ classes that support printer selection, page layout, and page creation.


9.1. Fonts

GDI+ supports only OpenType and TrueType fonts. Both of these font types are defined by mathematical representations that allow them to be scaled and rotated easily. This is in contrast to raster fonts that represent characters by a bitmap of a predetermined size. Although prevalent a few years ago, these font types are now only a historical footnote in the .NET world.

The term font is often used as a catchall term to describe what are in fact typefaces and font families. In .NET, it is important to have a precise definition for these terms because both Font and FontFamily are .NET classes. Before discussing how to implement these classes, let's look at the proper definition of these terms.

  • A font family is at the top of the hierarchy and consists of a group of typefaces that may be of any style, but share the same basic appearance. Tahoma is a font family.

  • A typeface is one step up the hierarchy and refers to a set of fonts that share the same family and style but can be any size. Tahoma bold is a typeface.

  • A font defines how a character is represented in terms of font family, style, and size. For example (see Figure 9-1), Tahoma Regular10 describes a font in the Tahoma font family having a regular style—as opposed to bold or italic—and a size of 10 points.

    Figure 9-1. Relationship of fonts, typefaces, and font families

Font Families

The FontFamily class has two primary purposes: to create a FontFamily object that is later used to create an instance of a Font, or to provide the names of all font families available on the user's computer system.

Creating a FontFamily Object

Constructors:

public FontFamily (string name);

public FontFamily (GenericFontFamilies genfamilies);

Parameters:

name

A font family name such as Arial or Tahoma.

FontFamily ff = new FontFamily("Arial");  // Arial family

GenericFontFamilies

An enumeration (in the System.Drawing.Text namespace) that has three values: Monospace, Serif, and SansSerif. This constructor is useful when you are interested in a generic font family rather than a specific one. The following constructor specifies a font family in which each character takes an equal amount of space horizontally. (Courier New is a common monospace family.)

FontFamily ff = new FontFamily(GenericFontFamilies.Monospace);

As a rule, the more specific you are about the font family, the more control you have over the appearance of the application, which makes the first constructor preferable in most cases. However, use a generic font family if you are unsure about the availability of a specific font on a user's computer.

Listing Font Families

The FontFamily.Families property returns an array of available font families. This is useful for allowing a program to select a font internally to use for displaying fonts to users, so they can select one. The following code enumerates the array of font families and displays the name of the family using the Name property; it also uses the IsStyleAvailable method to determine if the family supports a bold style font.

string txt = "";

foreach (FontFamily ff in FontFamily.Families)

{

   txt += ff.Name;

   if (ff.IsStyleAvailable(FontStyle.Bold))txt= += " (B)";

   txt += "\r\n"; // Line feed

}

textBox1.Text = txt;   // Place font family names in textbox

The Font Class

Although several constructors are available for creating fonts, they fall into two categories based on whether their first parameter is a FontFamily type or a string containing the name of a FontFamily. Other parameters specify the size of the font, its style, and the units to be used for sizing. Here are the most commonly used constructors:

Constructors:

public Font(FontFamily ff, Float emSize);

public Font(FontFamily ff, Float emSize, FontStyle style);

public Font(FontFamily ff, Float emSize, GraphicsUnit unit);

public Font(FontFamily ff, Float emSize, FontStyle style,

            GraphicsUnit unit);

public Font(String famname, Float emSize);

public Font(String famname, Float emSize, FontStyle style);

public Font(String famname, Float emSize, GraphicsUnit unit);

public Font(String famname, Float emSize, FontStyle style,

            GraphicsUnit unit);

Parameters:

emSize | The size of the font in terms of the GraphicsUnit. The default GraphicsUnit is Point.
style | A FontStyle enumeration value: Bold, Italic, Regular, Strikeout, or Underline. All font families do not support all styles.
unit | The units in which the font is measured. This is one of the GraphicsUnit enumeration values:
 | Point—1/72nd inch | Inch
 | Display—1/96th inch | Millimeter
 | Document—1/300th inch | Pixel (based on device resolution)

GraphicsUnits and sizing are discussed later in this section.

Creating a Font

Creating fonts is easy; the difficulty lies in deciding which of the many constructors to use. If you have already created a font family, the simplest constructor is

FontFamily ff = new FontFamily("Arial");

Font normFont = new Font(ff,10);      // Arial Regular 10 pt.

The simplest approach, without using a font family, is to pass the typeface name and size to a constructor:

Font normFont = new Font("Arial",10)  // Arial Regular 10 point

By default, a Regular font style is provided. To override this, pass a FontStyle enumeration value to the constructor:

Font boldFont = new Font("Arial",10,FontStyle.Bold);

Font bldItFont = new Font("Arial",10,

      FontStyle.Bold | FontStyle.Italic); // Arial bold italic 10

The second example illustrates how to combine styles. Note that if a style is specified that is not supported by the font family, an exception will occur.

The Font class implements the IDisposable interface, which means that a font's Dispose method should be called when the font is no longer needed. As we did in the previous chapter with the Graphics object, we can create the Font object inside a using construct, to ensure the font resources are freed even if an exception occurs.

using (Font normFont = new Font("Arial",12)) {

Using Font Metrics to Determine Font Height

The term font metrics refers to the characteristics that define the height of a font family. .NET provides both Font and FontClass methods to expose these values. To best understand them, it is useful to look at the legacy of typesetting and the terms that have been carried forward from the days of Gutenberg's printing press to the .NET Framework Class Library.

In typography, the square grid (imagine graph paper) used to lay out the outline (glyph) of a font is referred to as the em square. It consists of thousands of cells—each measured as one design unit. The outline of each character is created in an em square and consists of three parts: a descent, which is the part of the character below the established baseline for all characters; the ascent, which is the part above the baseline; and the leading, which provides the vertical spacing between characters on adjacent lines (see Figure 9-2).

Figure 9-2. Font metrics: the components of a font

Table 9-1 lists the methods and properties used to retrieve the metrics associated with the em height, descent, ascent, and total line space for a font family. Most of the values are returned as design units, which are quite easy to convert to a GraphicsUnit. The key is to remember that the total number of design units in the em is equivalent to the base size argument passed to the Font constructor. Here is an example of retrieving metrics for an Arial 20-point font:

FontFamily ff = new FontFamily("Arial");

Font myFont   = new Font(ff,20);   // Height is 20 points

int emHeight  = ff.GetEmHeight(FontStyle.Regular);     // 2048

int ascHeight = ff.GetCellAscent(FontStyle.Regular);   // 1854

int desHeight = ff.GetCellDescent(FontStyle.Regular);  // 434

int lineSpace = ff.GetLineSpacing(FontStyle.Regular);  // 2355

// Get Line Height in Points (20 x (2355/2048))

float guHeight = myFont.Size * (lineSpace / emHeight); // 22.99

float guHeight2 = myFont.GetHeight(); // 30.66 pixels

Table 9-1. Using Font and FontFamily to Obtain Font Metrics

Member

Units

Description

FontFamily.GetEmHeight

Design units

The height of the em square used to design the font family. TrueType fonts usually have a value of 2,048.

FontFamily.GetCellAscent

Design units

The height of a character above the base line.

FontFamily.GetCellDescent

Design units

The height of a character below the base line.

FontFamily.GetLineSpacing

Design units

The total height reserved for a character plus line spacing. The sum of CellAscent, CellDescent, and Leading (see Figure 9-2). This value is usually 12 to 15 percent greater than the em height.

Font.Size

Graphics unit

The base size (size passed to constructor).

Font.SizeInPoints

Points

The base size in points.

Font.GetHeight

Pixels

The total height of a line. Calculated by converting LineSpacing value to pixels.

The primary value of this exercise is to establish familiarity with the terms and units used to express font metrics. Most applications that print or display lines of text are interested primarily in the height of a line. This value is returned by the Font.GetHeight method and is also available through the Graphics.MeasureString method described in the next section.

Definitions:
cell height | = | ascent + descent
em height | = | cell height – internal leading
line spacing | = | cell height + external leading


9.2. Drawing Text Strings

The Graphics.DrawString method is the most straightforward way to place text on a drawing surface. All of its overloaded forms take a string to be printed, a font to represent the text, and a brush object to paint the text. The location where the text is to be printed is specified by a Point object, x and y coordinates, or a Rectangle object. The most interesting parameter is an optional StringFormat object that provides the formatting attributes for the DrawString method. We'll examine it in detail in the discussion on formatting.

Here are the overloads for DrawString. Note that StringFormat is optional in each.

Overloads:

public DrawString(string, font, brush, PointF

   [,StringFormat]);

public DrawString(string, font, brush, float, float

   [,StringFormat]);

public DrawString(string, font, brush, RectangleF

   [,StringFormat]);

Example:

Font regFont = new Font("Tahoma",12);

String s = "ice mast high came floating by as green as emerald.";

// Draw text beginning at coordinates  (20,5)

g.DrawString(s, regFont, Brushes.Black, 20,5);

regFont.Dispose();

In this example, the upper-left corner of the text string is located at the x,y coordinate 20 pixels from the left edge and 5 pixels from the top of the drawing surface. If the printed text extends beyond the boundary of the drawing surface, the text is truncated. You may want this in some cases, but more often you'll prefer that long lines be broken and printed as multiple lines.

Drawing Multi-Line Text

Several Drawstring overloads receive a rectangle to define where the output string is drawn. Text drawn into these rectangular areas is automatically wrapped to the next line if it reaches beyond the rectangle's boundary. The following code displays the fragment of poetry in an area 200 pixels wide and 50 pixels high.

String s = "and ice mast high came floating by as green

   as emerald."

// All units in pixels

RectangleF rf = new RectangleF(20,5,200,50);

// Fit text in rectangle

g.Drawstring(s,regFont,Brushes.Black, rf);

Word wrapping is often preferable to line truncation, but raises the problem of determining how many lines of text must be accommodated. If there are more lines of text than can fit into the rectangle, they are truncated. To avoid truncation, you could calculate the height of the required rectangle by taking into account the font (f), total string length(s), and rectangle width (w). It turns out that .NET Graphics.MeasureString method performs this exact operation. One of its overloads takes the string, font, and desired line width as arguments, and returns a SizeF object whose Width and Height properties provide pixel values that define the required rectangle.

SizeF sf = g.MeasureString(String s, Font f, int w);

Using this method, the preceding code can be rewritten to handle the dynamic creation of the bounding rectangle:

Font regFont = new Font("Tahoma",12);

String s = "and ice mast high came floating by as green

   as emerald."

int lineWidth = 200;

SizeF sf = g.MeasureString(s, regFont, lineWidth);

// Create rectangular drawing area based on return

// height and width

RectangleF rf = new RectangleF(20,5,sf.Width, sf.Height);

// Draw text in rectangle

g.Drawstring(s,regFont,Brushes.Black, rf);

// Draw rectangle around text

g.DrawRectangle(Pens.Red,20F,5F,rf.Width, rf.Height);

Note that DrawString recognizes newline (\r\n) characters and creates a line break when one is encountered.

Formatting Strings with the StringFormat Class

When passed as an argument to the DrawString method, a StringFormat object can provide a number of formatting effects. It can be used to define tab positions, set column widths, apply right or left justification, and control text wrapping and truncation. As we will see in the next section, it is the primary tool for creating formatted reports. The members that we will make heaviest use of are shown in Table 9-2.

Table 9-2. Important StringFormat Members

Member

Description

Alignment

A StringAlignment enumeration value:

StringAlignment.Center— Text is centered in layout rectangle.

StringAlignment.Far— Text is aligned to the right for left-to-right text.

StringAlignment.Near— Text is aligned to the left for left-to-right text.

TRimming

A StringTrimming enumeration value that specifies how to trim characters that do not completely fit in the layout rectangle:

StringTrimming.Character— Text is trimmed to the nearest character.

StringTrimming.EllipsisCharacter— Text is trimmed to the nearest

character and an ellipsis (...) is placed at the end of the line.

StringTrimming.Word— Text is trimmed to the nearest word.

SetTabStops

Takes two parameters: SetTabStops(firstTabOffset, tabStops)

FirstTabOffset— Number of spaces between beginning of line and first tab stop.

TabStops— Array of distances between tab stops.

FormatFlags

This bit-coded property provides a variety of options for controlling print layout when printing within a rectangle.

StringFormatFlags.DirectionVertical— Draws text from top-to-bottom.

StringFormatFlags.LineLimit— Only entire lines are displayed within the rectangle.

StringFormatFlags.NoWrap— Disables text wrapping. The result is that text is printed on one line only, irrespective of the rectangle's height

Using Tab Stops

Tab stops provide a way to align proportionate-spaced font characters into columns. To set up tab stops, you create a StringFormat object, use its SetTabStops method to define an array of tab positions, and then pass this object to the DrawString method along with the text string containing tab characters (\t).

Core Note

If no tab stops are specified, default tab stops are set up at intervals equal to four times the size of the font. A 10-point font would have default tabs every 40 points.

As shown in Table 9-2, the SetTabStops method takes two arguments: the offset from the beginning of the line and an array of floating point values that specify the distance between tab stops. Here is an example that demonstrates various ways to define tab stops:

float[] tStops = {50f, 100f, 100f};  //Stops at: 50, 150, and 250

float[] tStops = {50f};        // Stops at: 50, 100, 150

You can see that it is not necessary to specify a tab stop for every tab. If a string contains a tab for which there is no corresponding tab stop, the last tab stop in the array is repeated. Listing 9-1 demonstrates using tabs to set column headers.

Listing 9-1. Using Tab Stops to Display Columns of Data

private void RePaint(object sender, PaintEventArgs e)

{

   Graphics g = e.Graphics;

   Font hdrFont = new Font("Arial", 10,FontStyle.Bold);

   Font bdyFont = new Font("Arial", 10);

   // (1) Create StringFormat Object

   StringFormat strFmt = new StringFormat();

   // (2) Define Tab stops

   float[] ts = {140,60,40};

   strFmt.SetTabStops(0, ts);

   // (3) Define column header text to be printed with tabs

   string header = "Artist\tCountry\tBorn\tDied";

   // (4) Print column headers

   g.DrawString(header, hdrFont, Brushes.Black,10,10,strFmt);

   // Print one line below header

   string artist = "Edouard Manet\tEngland\t1832\t1892";

   g.DrawString(artist,bdyFont,Brushes.Black,10,

                10 + bdyFont.GetHeight(), strFmt);

   bdyFont.Dispose();

   hdrFont.Dispose();

}

Figure 9-3 shows the four-column output from this code. Note that the second column begins at the x coordinate 150, which is the first tab stop (140) plus the x coordinate (10) specified in DrawString.

Figure 9-3. Printing with tab stops

The unit of measurement in this example is a pixel. This unit of measurement is determined by the Graphics.PageUnit property. To override the default (pixels), set the property to a GraphicsUnit enumeration value—for example, g.PageUnit = GraphicsUnit.Inch. Be aware that all subsequent drawing done with the Graphics object will use these units.

Core Note

The use of tab spaces only supports left justification for proportionate fonts. If you need right justification—a virtual necessity for displaying financial data—pass a rectangle that has the appropriate coordinates to the DrawString method. Then, set the Alignment property of StringFormat to StringAlignment.Far.

String Trimming, Alignment, and Wrapping

The StringFormat Trimming and Alignment properties dictate how text is placed within a RectangleF object. Alignment works as you would expect, allowing you to center, right justify, or left justify a string. Trimming specifies how to truncate a string that extends beyond the boundaries of a rectangle when wrapping is not in effect. The basic options are to truncate on a word or character.

The following code segments demonstrate some of the common ways these properties can be used to format text strings.

Example 1: Printing Without a StringFormat Object

Font fnt = new Font("Tahoma",10,FontStyle.Bold);

RectangleF r = new RectangleF(5,5,220,60);

string txt = "dew drops are the gems of morning";

g.DrawString(txt,fnt,Brushes.Black,r);

g.DrawRectangle(Pens.Red,r.X,r.Y,r.Width,r.Height);

Example 2: Printing with NoWrap Option

StringFormat strFmt = new StringFormat();

strFmt.FormatFlags = StringFormatFlags.NoWrap;

g.DrawString(txt,fnt,Brushes.Black,r,strFmt);

Example 3: Printing with NoWrap and Clipping on a Word

StringFormat strFmt = new StringFormat();

strFmt.FormatFlags = StringFormatFlags.NoWrap;

strFmt.Trimming = StringTrimming.Word;

g.DrawString(txt,fnt,Brushes.Black,r,strFmt);

Example 4: Printing with NoWrap, Clipping on Word, and Right Justification

StringFormat strFmt = new StringFormat();

strFmt.FormatFlags = StringFormatFlags.NoWrap;

strFmt.Trimming = StringTrimming.Word;

strFmt.Alignment = StringAlignment.Far;

g.DrawString(txt,fnt,Brushes.Black,r,strFmt);

StringFormat also has a LineAlignment property that permits a text string to be centered vertically within a rectangle. To demonstrate, let's add two statements to Example 4:

strFmt.Alignment = StringAlignment.Center;

strFmt.LineAlignment = StringAlignment.Center;


9.3. Printing

The techniques discussed in Sections 9.1 and 9.2 are device independent, which means they can be used for drawing to a printer as well as a screen. This section deals specifically with the task of creating reports intended for output to a printer. For complex reporting needs, you may well turn to Crystal Reports—which has special support in Visual Studio.NET—or SQL Server Reports. However, standard reports can be handled quite nicely using the native .NET classes available. Moreover, this approach enables you to understand the fundamentals of .NET printing and apply your knowledge of event handling and inheritance to customize the printing process.

Overview

The PrintDocument class—a member of the System.Drawing.Printing namespace—provides the methods, properties, and events that control the print process. Consequently, the first step in setting up a program that sends output to a printer is to create a PrintDocument object.

PrintDocument pd = new PrintDocument();

The printing process in initiated when the PrintDocument.Print method is invoked. As shown in Figure 9-4, this triggers the BeginPrint and PrintPage events of the PrintDocument class.

Figure 9-4. PrintDocument events that occur during the printing process

An event handler wired to the PrintPage event contains the logic and statements that produce the output. This routine typically determines how many lines can be printed—based on the page size—and contains the DrawString statements that generate the output. It is also responsible for notifying the underlying print controller whether there are more pages to print. It does this by setting the HasMorePages property, which is passed as an argument to the event handler, to true or false.

The basic PrintDocument events can be integrated with print dialog boxes that enable a user to preview output, select a printer, and specify page options. Listing 9-2 displays a simple model for printing that incorporates the essential elements required. Printing is initiated when btnPrint is clicked.

Listing 9-2. Basic Steps in Printing

using System.Drawing;

using System.Drawing.Printing;

using System.Windows.Forms;

// Code for Form class goes here

   // Respond to button click

   private void btnPrint_Click(object sender,

                               System.EventArgs e)

   { PrintReport();   }

   // Set up overhead for printing

   private void PrintReport()

   {

      // (1) Create PrintDocument object

      PrintDocument pd = new PrintDocument();

      // (2) Create PrintDialog

      PrintDialog pDialog = new PrintDialog();

      pDialog.Document = pd;

      // (3) Create PrintPreviewDialog

      PrintPreviewDialog prevDialog = new PrintPreviewDialog();

      prevDialog.Document = pd;

      // (4) Tie event handler to PrintPage event

      pd.PrintPage += new PrintPageEventHandler(Inven_Report);

      // (5) Display Print Dialog and print if OK received

      if (pDialog.ShowDialog()== DialogResult.OK)

      {

         pd.Print(); // Invoke PrintPage event

      }

   }

   private void Inven_Report(object sender,

                             PrintPageEventArgs e)

   {

      Graphics g = e.Graphics;

      Font myFont = new Font("Arial",10);

      g.DrawString("Sample Output",myFont,Brushes.Black,10,10);

      myFont.Dispose();

   }

}

This simple example illustrates the rudiments of printing, but does not address issues such as handling multiple pages or fitting multiple lines within the boundaries of a page. To extend the code to handle these real-world issues, we need to take a closer look at the PrintDocument class.

PrintDocument Class

Figure 9-5 should make it clear that the PrintDocument object is involved in just about all aspects of printing: It provides access to paper size, orientation, and document margins through the DefaultPageSettings class; PrinterSettings allows selection of a printer as well as the number of copies and range of pages to be printed; and event handlers associated with the PrintDocument events take care of initialization and cleanup chores related to the printing process. The PrintController class works at a level closer to the printer. It is used behind the scenes to control the print preview process and tell the printer exactly how to print a document.

Figure 9-5. Selected PrintDocument properties and events

An understanding of these classes and events is essential to implementing a robust and flexible printing application that provides users full access to printing options.

Printer Settings

The PrinterSettings object maintains properties that specify the printer to be used and how the document is to be printed—page range, number of copies, and whether collating or duplexing is used. These values can be set programmatically or by allowing the user to select them from the Windows PrintDialog component. Note that when a user selects an option on the PrintDialog dialog box, he is actually setting a property on the underlying PrinterSettings object.

Selecting a Printer

The simplest approach is to display the PrintDialog window that contains a drop-down list of printers:

PrintDocument pd = new PrintDocument();

PrintDialog pDialog = new PrintDialog();

pDialog.Document = pd;

if (pDialog.ShowDialog()== DialogResult.OK)

{

   pd.Print(); // Invoke PrintPage event

}

You can also create your own printer selection list by enumerating the InstalledPrinters collection:

// Place names of printer in printerList ListBox

foreach(string pName in PrinterSettings.InstalledPrinters)

printerList.Items.Add(pName);

After the printer is selected, it is assigned to the PrinterName property:

string printer=

      printerList.Items[printerList.SelectedIndex].ToString();

pd.PrinterSettings.PrinterName = printer;

Selecting Pages to Print

The PrinterSettings.PrintRange property indicates the range of pages to be printed. Its value is a PrintRange enumeration value—AllPages, Selection, or SomePages—that corresponds to the All, Pages, and Selection print range options on the PrintDialog form. If Pages is selected, the PrinterSettings.FromPage and ToPage properties specify the first and last page to print. There are several things to take into consideration when working with a print range:

  • To make the Selection and Pages radio buttons available on the PrintDialog form, set PrintDialog.AllowSomePages and PrintDialog.AllowSelection to TRue.

  • The program must set the FromPage and ToPage values before displaying the Print dialog box. In addition, it's a good practice to set the MinimumPage and MaximumPage values to ensure the user does not enter an invalid page number.

  • Keep in mind that the values entered on a PrintDialog form do nothing more than provide parameters that are available to the application. It is the responsibility of the PrintPage event handler to implement the logic that ensures the selected pages are printed.

The following segment includes logic to recognize a page range selection:

pDialog.AllowSomePages = true;

pd.PrinterSettings.FromPage =1;

pd.PrinterSettings.ToPage = maxPg;

pd.PrinterSettings.MinimumPage=page 1;

pd.PrinterSettings.MaximumPage= maxPg;

if (pDialog.ShowDialog()== DialogResult.OK)

{

   maxPg = 5;    // Last page to print

   currPg= 1;    // Current page to print

   if (pDialog.PrinterSettings.PrintRange ==

       PrintRange.SomePages)

   {

       currPg = pd.PrinterSettings.FromPage;

       maxPg  = pd.PrinterSettings.ToPage;

   }

   pd.Print(); // Invoke PrintPage event

}

This code assigns the first and last page to be printed to currPg and maxPg. These both have class-wide scope and are used by the PrintPage event handler to determine which pages to print.

Setting Printer Resolution

The PrinterSettings class exposes all of the print resolutions available to the printer through its PrinterResolutions collection. You can loop through this collection and list or select a resolution by examining the Kind property of the contained PrinterResolution objects. This property takes one of five PrinterResolutionKind enumeration values: Custom, Draft, High, Low, or Medium. The following code searches the PrinterResolutions collection for a High resolution and assigns that as a PrinterSettings value:

foreach (PrinterResolution pr in

         pd.PrinterSettings.PrinterResolutions)

{

   if (pr.Kind == PrinterResolutionKind.High)

   {

      pd.PageSettings.PrinterResolution = pr;

      break;

   }

}

Page Settings

The properties of the PageSettings class define the layout and orientation of the page being printed to. Just as the PrinterSettings properties correspond to the PrintDialog, the PageSettings properties reflect the values of the PageSetupDialog.

PageSetupDialog ps = new PageSetupDialog();

ps.Document = pd;   // Assign reference to PrinterDocument

ps.ShowDialog();

This dialog box lets the user set all the margins, choose landscape or portrait orientation, select a paper type, and set printer resolution. These values are exposed through the DefaultPageSettings properties listed in Figure 9-5. As we will see, they are also made available to the PrintPage event handler through the PrintPageEventArgs parameter and to the QueryPageSettingsEvent tHRough its QueryPageSettingsEventArgs parameter. The latter can update the values, whereas PrintPage has read-only access.

Figure 9-6 illustrates the layout of a page that has the following DefaultPageSettings values:

Bounds.X = 0;

Bounds.Y = 0;

Bounds.Width = 850;

Bounds.Height = 1100;

PaperSize.PaperName = "Letter";

PaperSize.Height = 1100;

PaperSize.Width = 850;

Margins.Left = 100;

Margins.Right = 100;

Margins.Top = 100;

Margins.Bottom = 100;

Figure 9-6. Page settings layout

All measurements are in hundredths of an inch. The MarginBounds rectangle shown in the figure represents the area inside the margins. It is not a PrinterSettings property and is made available only to the PrintPage event handler.

Core Note

Many printers preserve an edge around a form where printing cannot occur. On many laser printers, for example, this is one-quarter of an inch. In practical terms, this means that all horizontal coordinates used for printing are shifted; thus, if DrawString is passed an x coordinate of 100, it actually prints at 125. It is particularly important to be aware of this when printing on preprinted forms where measurements must be exact.

PrintDocument Events

Four PrintDocument events are triggered during the printing process: BeginPrint, QueryPageSettingsEvent, PrintPage, and EndPrint. As we've already seen, PrintPage is the most important of these from the standpoint of code development because it contains the logic and statements used to generate the printed output. It is not necessary to handle the other three events, but they do provide a handy way to deal with the overhead of initialization and disposing of resources when the printing is complete.

BeginPrint Event

This event occurs when the PrintDocument.Print method is called and is a useful place to create font objects and open data connections. The PrintEventHandler delegate is used to register the event handler routine for the event:

pd.BeginPrint += new PrintEventHandler(Rpt_BeginPrint);

This simple event handler creates the font to be used in the report. The font must be declared to have scope throughout the class.

private void Rpt_BeginPrint(object sender, PrintEventArgs e)

{

   rptFont = new Font("Arial",10);

   lineHeight= (int)rptFont.GetHeight(); // Line height

}

EndPrint Event

This event occurs after all printing is completed and can be used to destroy resources no longer needed. Associate an event handler with the event using

pd.EndPrint += new PrintEventHandler(Rpt_EndPrint);

This simple event handler disposes of the font created in the BeginPrint handler:

private void Rpt_EndPrint(object sender, PrintEventArgs e)

{

   rptFont.Dispose();

}

QueryPageSettingsEvent Event

This event occurs before each page is printed and provides an opportunity to adjust the page settings on a page-by-page basis. Its event handler is associated with the event using the following code:

pd.QueryPageSettings += new

      QueryPageSettingsEventHandler(Rpt_Query);

The second argument to this event handler exposes a PageSettings object and a Cancel property that can be set to true to cancel printing. This is the last opportunity before printing to set any PageSettings properties, because they are read-only in the PrintPage event. This code sets special margins for the first page of the report:

private void Rpt_Query(object sender,

                       QueryPageSettingsEventArgs e)

{

   // This is the last chance to change page settings

   // If first page, change margins for title

   if (currPg ==1) e.PageSettings.Margins =

                       new Margins(200,200,200,200);

   else e.PageSettings.Margins = new Margins(100,100,100,100);

}

This event handler should be implemented only if there is a need to change page settings for specific pages in a report. Otherwise, the DefaultPageSettings properties will prevail throughout.

PrintPage Event

The steps required to create and print a report fall into two categories: setting up the print environment and actually printing the report. The PrinterSettings and PageSettings classes that have been discussed are central to defining how the report will look. After their values are set, it's the responsibility of the PrintPage event handler to print the report to the selected printer, while being cognizant of the paper type, margins, and page orientation.

Figure 9-7 lists some of the generic tasks that an event handler must deal with in generating a report. Although the specific implementation of each task varies by application, it's a useful outline to follow in designing the event handler code. We will see an example shortly that uses this outline to implement a simple report application.

Figure 9-7. Tasks required to print a report

Defining the PrintPage Event Handler

The event handler method matches the signature of the PrintPageEventHandler delegate (refer to Listing 9-2):

public delegate void PrintPageEventHandler(

      object sender, PrintPageEventArgs e);

The PrintPageEventArgs argument provides the system data necessary for printing. As shown in Table 9-3, its properties include the Graphics object, PageSettings object, and a MarginBounds rectangle—mentioned earlier—that defines the area within the margins. These properties, along with variables defined at a class level, provide all the information used for printing.

Table 9-3. PrintPageEventArgs Members

Property

Description

Cancel

Boolean value that can be set to true to cancel the printing.

Graphics

The Graphics object used to write to printer.

HasMorePages

Boolean value indicating whether more pages are to be printed. Default is false.

MarginBounds

Rectangular area representing the area within the margins.

PageBounds

Rectangular area representing the entire page.

PageSettings

Page settings for the page to be printed.

Previewing a Printed Report

The capability to preview a report onscreen prior to printing—or as a substitute for printing—is a powerful feature. It is particularly useful during the development process where a screen preview can reduce debugging time, as well as your investment in print cartridges.

To preview the printer output, you must set up a PrintPreviewDialog object and set its Document property to an instance of the PrintDocument:

PrintPreviewDialog prevDialog = new PrintPreviewDialog();

      prevDialog.Document = pd;

The preview process is invoked by calling the ShowDialog method:

prevDialog.ShowDialog();

After this method is called, the same steps are followed as in actually printing the document. The difference is that the output is displayed in a special preview window (see Figure 9-8). This provides the obvious advantage of using the same code for both previewing and printing.

Figure 9-8. Report can be previewed before printing

A Report Example

This example is intended to illustrate the basic elements of printing a multi-page report. It includes a data source that provides an unknown number of records, a title and column header for each page, and detailed rows of data consisting of left-justified text and right-justified numeric data.

Data Source for the Report

The data in a report can come from a variety of sources, although it most often comes from a database. Because database access is not discussed until Chapter 11, "ADO.NET," let's use a text file containing comma-delimited inventory records as the data source. Each record consists of a product ID, vendor ID, description, and price:

1000761,1050,2PC/DRESSER/MIRROR,185.50

A StreamReader object is used to load the data and is declared to have class-wide scope so it is available to the PrintPage event handler:

// Using System.IO namespace is required

// StreamReader sr; is set up in class declaration

sr = new StreamReader("c:\\inventory.txt");

The PrintPage event handler uses the StreamReader to input each inventory record from the text file as a string. The string is split into separate fields that are stored in the prtLine array. The event handler also contains logic to recognize page breaks and perform any column totaling desired.

Code for the Application

Listing 9-3 contains the code for the PrintDocument event handlers implemented in the application. Because you cannot pass your own arguments to an event handler, the code must rely on variables declared at the class level to maintain state information. These include the following:

StreamReader sr;   // StreamReader to read inventor from file

string[]prtLine;   // Array containing fields for one record

Font rptFont;      // Font for report body

Font hdrFont;      // Font for header

string title= "Acme Furniture: Inventory Report";

float lineHeight;  // Height of a line (100ths inches)

The fonts and StreamReader are initialized in the BeginPrint event handler. The corresponding EndPrint event handler then disposes of the two fonts and closes the StreamReader.

Listing 9-3. Creating a Report

// pd.PrintPage  += new PrintPageEventHandler(Inven_Report);

// pd.BeginPrint += new PrintEventHandler(Rpt_BeginPrint);

// pd.EndPrint   += new PrintEventHandler(Rpt_EndPrint);

//

// BeginPrint event handler

private void Rpt_BeginPrint(object sender, PrintEventArgs e)

{

   // Create fonts to be used and get line spacing.

   rptFont = new Font("Arial",10);

   hdrFont = new Font(rptFont,FontStyle.Bold);

   // insert code here to set up Data Source...

}

// EndPrint event Handler

private void Rpt_EndPrint(object sender, PrintEventArgs e)

{

   // Remove Font resources

   rptFont.Dispose();

   hdrFont.Dispose();

   sr.Close();       // Close StreamReader

}

// PrintPage event Handler

private void Inven_Report(object sender, PrintPageEventArgs e)

{

   Graphics g = e.Graphics;

   int xpos= e.MarginBounds.Left;

   int lineCt = 0;

   // Next line returns 15.97 for Arial-10

   lineHeight = hdrFont.GetHeight(g);

   // Calculate maximum number of lines per page

   int linesPerPage = int((e.MarginBounds.Bottom –

             e.MarginBounds.Top)/lineHeight –2);

   float yPos = 2* lineHeight+ e.MarginBounds.Top;

   int hdrPos = e.MarginBounds.Top;

   // Call method to print title and column headers

   PrintHdr(g,hdrPos, e.MarginBounds.Left);

   string prod;

   char[]delim=  {','};

   while(( prod =sr.ReadLine())!=null)

   {

      prtLine= prod.Split(delim,4);

      yPos += lineHeight;   // Get y coordinate of line

      PrintDetail(g,yPos);  // Print inventory record

      if(lineCt > linesPerPage)

      {

         e.HasMorePages= true;

         break;

      }

   }

}

private void PrintHdr( Graphics g, int yPos, int xPos)

{

   // Draw Report Title

   g.DrawString(title,hdrFont,Brushes.Black,xPos,yPos);

   // Draw Column Header

   float[] ts = {80, 80,200};

   StringFormat strFmt = new StringFormat();

   strFmt.SetTabStops(0,ts);

   g.DrawString("Code\tVendor\tDescription\tCost",

         hdrFont,Brushes.Black,xPos,yPos+2*lineHeight,strFmt);

}

private void PrintDetail(Graphics g, float yPos)

{

   int xPos = 100;

   StringFormat strFmt = new StringFormat();

   strFmt.Trimming = StringTrimming.EllipsisCharacter;

   strFmt.FormatFlags = StringFormatFlags.NoWrap;

   RectangleF r = new RectangleF(xPos+160,yPos,

                                 200,lineHeight);

   // Get data fields from array

   string invenid = prtLine[0];

   string vendor  = prtLine[1];

   string desc    = prtLine[2];

   decimal price  = decimal.Parse(prtLine[3]);

   g.DrawString(invenid, rptFont,Brushes.Black,xPos, yPos);

   g.DrawString(vendor, rptFont,Brushes.Black,xPos+80, yPos);

   // Print description within a rectangle

   g.DrawString(desc, rptFont,Brushes.Black,r,strFmt);

   // Print cost right justified

   strFmt.Alignment = StringAlignment.Far;  // Right justify

   strFmt.Trimming= StringTrimming.None;

   g.DrawString(price.ToString("#,###.00"),

        rptFont,Brushes.Black, xPos+400,yPos,strFmt);

}

The PrintPage event handler Inven_Report directs the printing process by calling PrintHdr to print the title and column header on each page and PrintDetail to print each line of inventory data. Its responsibilities include the following:

  • Using the MarginBounds rectangle to set the x and y coordinates of the title at the upper-left corner of the page within the margins.

  • Calculating the maximum number of lines to be printed on a page. This is derived by dividing the distance between the top and bottom margin by the height of a line. It then subtracts 2 from this to take the header into account.

  • Setting the HasMorePages property to indicate whether more pages remain to be printed.

The PrintHdr routine is straightforward. It prints the title at the coordinates passed to it, and then uses tabs to print the column headers. The PrintDetail method is a bit more interesting, as it demonstrates some of the classes discussed earlier in the chapter. It prints the inventory description in a rectangle and uses the StringFormat class to prevent wrapping and specify truncation on a character. StringFormat is also used to right justify the price of an item in the last column.

Figure 9-9 shows an example of output from this application. Measured from the left margin, the first three columns have an x coordinate of 0, 80, and 160, respectively. Note that the fourth column is right justified, which means that its x coordinate of 400 specifies where the right edge of the string is positioned. Vertical spacing is determined by the lineHeight variable that is calculated as

float lineHeight = hdrFont.GetHeight(g);

Figure 9-9. Output from the report example

This form of the GetHeight method returns a value based on the GraphicsUnit of the Graphics object passed to it. By default, the Graphics object passed to the BeginPrint event handler has a GraphicsUnit of 100 dpi. The margin values and all coordinates in the example are in hundredths of an inch. .NET takes care of automatically scaling these units to match the printer's resolution.

Creating a Custom PrintDocument Class

The generic PrintDocument class is easy to use, but has shortcomings with regard to data encapsulation. In the preceding example, it is necessary to declare variables that have class-wide scope—such as the StreamReader—to make them available to the various methods that handle PrintDocument events. A better solution is to derive a custom PrintDocument class that accepts parameters and uses properties and private fields to encapsulate information about line height, fonts, and the data source. Listing 9-4 shows the code from the preceding example rewritten to support a derived PrintDocument class.

Creating a custom PrintDocument class turns out to be a simple and straightforward procedure. The first step is to create a class that inherits from PrintDocument. Then, private variables are defined that support the fonts and title that are now exposed as properties. Finally, the derived class overrides the OnBeginPrint, OnEndPrint, and OnPrintPage methods of the base PrintDocument class.

The overhead required before printing the report is reduced to creating the new ReportPrintDocument object and assigning property values.

string myTitle = "Acme Furniture: Inventory Report";

ReportPrintDocument rpd = new ReportPrintDocument(myTitle);

rpd.TitleFont = new Font("Arial",10, FontStyle.Bold);

rpd.ReportFont = new Font("Arial",10);

PrintPreviewDialog prevDialog = new PrintPreviewDialog();

prevDialog.Document = rpd;

prevDialog.ShowDialog();   // Preview Report

// Show Print Dialog and print report

PrintDialog pDialog = new PrintDialog();

pDialog.Document = rpd;

if (pDialog.ShowDialog() == DialogResult.OK)

{

   rpd.Print();

}

The preceding code takes advantage of the new constructor to pass in the title when the object is created. It also sets the two fonts used in the report.

Listing 9-4. Creating a Custom PrintDocument Class

// Derived Print Document Class

public class ReportPrintDocument: PrintDocument

{

   private Font hdrFont;

   private Font rptFont;

   private string title;

   private StreamReader sr;

   private float lineHeight;

   // Constructors

   public ReportPrintDocument()

   {}

   public ReportPrintDocument(string myTitle)

   {

      title = myTitle;

   }

   // Property to contain report title

   public string ReportTitle

   {

      get {return title;}

      set {title = value;}

   }

   // Fonts are exposed as properties

   public Font TitleFont

   {

      get {return hdrFont;}

      set {hdrFont = value;}

   }

   public Font ReportFont

   {

      get {return rptFont;}

      set {rptFont = value;}

   }

   // BeginPrint event handler

   protected override void OnBeginPrint(PrintEventArgs e)

   {

      base.OnBeginPrint(e);

      // Assign Default Fonts if none selected

      if (TitleFont == null)

      {

         TitleFont =

              new Font("Arial",10,FontStyle.Bold);

         ReportFont = new Font("Arial",10);

      }

      / Code to create StreamReader or other data source

      // goes here ...

      sr = new StreamReader(inputFile);

   }

   protected override void OnEndPrint(PrintEventArgs e)

   {

      base.OnEndPrint(e);

      TitleFont.Dispose();

      ReportFont.Dispose();

      sr.Close();

   }

   // Print Page event handler

   protected override void OnPrintPage(PrintPageEventArgs e)

   {

      base.OnPrintPage(e);

      // Remainder of code for this class is same as in

      // Listing 9-3 for Inven_Report, PrintDetail, and

      // PrintHdr

   }

}

This example is easily extended to include page numbering and footers. For frequent reporting needs, it may be worth the effort to create a generic report generator that includes user selectable data source, column headers, and column totaling options.


9.4. Summary

This chapter has focused on using the GDI+ library to display and print text. The first section explained how to create and use font families and font classes. The emphasis was on how to construct fonts and understand the elements that comprise a font by looking at font metrics.

After a font has been created, the Graphics.DrawString method is used to draw a text string to a display or printer. Its many overloads permit text to be drawn at specific coordinates or within a rectangular area. By default, text printed in a rectangle is left justified and wrapped to the next line when it hits the bounds of the rectangle. This default formatting can be overridden by passing a StringFormat object to the DrawString method. The StringFormat class is the key to .NET text formatting. It is used to justify text, specify how text is truncated, and set tab stops for creating columnar output.

GDI+ provides several classes designed to support printing to a printer. These include the following:

  • PrintDocument. Sends output to the printer. Its Print method initiates the printing process and triggers the BeginPrint, QueryPageSettingsEvent, PrintPage, and EndPrint events. The event handler for the PrintPage event contains the logic for performing the actual printing.

  • PrintPreviewDialog. Enables output to be previewed before printing.

  • PrinterSettings. Has properties that specify the page range to be printed, the list of available printers, and the name of the target printer. These values correspond to those a user can select on the PrintDialog dialog box.

  • DefaultPageSettings. Has properties that set the bounds of a page, the orientation (landscape or portrait), the margins, and the paper size. These values correspond to those selected on the PageSetupDialog dialog box.

An example for printing a columnar report demonstrated how these classes can be combined to create an application that provides basic report writing. As a final example, we illustrated how the shortcomings of the PrintDocument class can be overcome by creating a custom PrintDocument class that preserves data encapsulation.


9.5. Test Your Understanding

1:

Does Arial Bold refer to a font family, typeface, or font?

2:

What is the default unit of measurement for a font? What size is it (in inches)?

3:

Which method and enumeration are used to right justify an output string?

4:

When the following code is executed, what is the x coordinate where the third column begins?

float[] tStops = {50f, 60f, 200f, 40f};

StringFormat sf = new StringFormat();

sf.SetTabStops(0,tStops);

string hdr =  "Col1\tCol2\tCol3\tCol4";

g.DrawString(hdr, myFont, Brushes.Black, 10,10,sf);

5:

Which PrintDocument event is called after PrintDocument.Print is executed?

6:

Which class available to the PrintPage event handler defines the margins for a page?

7:

What three steps must be included to permit a document to be previewed before printing?


Chapter 10. Working with XML in .NET

Topics in This Chapter

  • Introduction to Using XML: Introduces some of the basic concepts of working with XML. These include the XML validation and the use of an XML style sheet.

  • Reading XML Data: Explains how to use the .NET XML stack to access XML data. The XmlReader, XmlNodeReader, XmlTextReader are examined.

  • Writing XML Data: The easiest way to create XML data is to use the .NET XmlSerializer to serialize data into the XML format. When the data is not in a format that can be serialized, an alternative is the XmlWriter class.

  • Searching and Updating XML Documents: XPath is a query language to search XML documents. Examples illustrate how to use it to search an XmlDocument, XmlDataDocument, and XPathDocument.

Extensible Markup Language (XML) plays a key role in the .NET universe. Configuration files that govern an application or Web page's behavior are deployed in XML; objects are stored or streamed across the Internet by serializing them into an XML representation; Web Services intercommunication is based on XML; and as we see in Chapter 11, "ADO.NET," .NET methods support the interchange of data between an XML and relational data table format.

XML describes data as a combination of markup language and content that is analogous to the way HTML describes a Web page. Its flexibility permits it to easily represent flat, relational, or hierarchical data. To support one of its design goals—that it "should be human-legible and reasonably clear"[1]—it is represented in a text-only format. This gives it the significant advantage of being platform independent, which has made it the de facto standard for transmitting data over the Internet.

[1] W3C Extensible Markup Language (XML), 1.0 (Third Edition), http://www.w3.org/TR/REC-xml/

This chapter focuses on pure XML and the classes that reside in the System.Xml namespace hierarchy. It begins with basic background information on XML: how schemas are used to validate XML data and how style sheets are used to alter the way XML is displayed. The remaining sections present the .NET classes that are used to read, write, update, and search XML documents. If you are unfamiliar with .NET XML, you may surprised how quickly you become comfortable with reading and searching XML data. Extracting information from even a complex XML structure is refreshingly easy with the XPath query language—and far less tedious than the original search techniques that required traversing each node of an XML tree. In many ways, it is now as easy to work with XML as it is to work with relational data.


10.1. Working with XML

Being literate in one's spoken language is defined as having the basic ability to read and write that language. In XML, functional literacy embraces more than reading and writing XML data. In addition to the XML data document, there is an XML Schema document (.xsd) that is used to validate the content and structure of an XML document. If the XML data is to be displayed or transformed, one or more XML style sheets (.xsl) can be used to define the transformation. Thus, we can define our own form of XML literacy as the ability to do five things:

  1. Create an XML file.

  2. Read and query an XML file.

  3. Create an XML Schema document.

  4. Use an XML Schema document to validate XML data.

  5. Create and use an XML style sheet to transform XML data.

The purpose of this section is to introduce XML concepts and terminology, as well as some .NET techniques for performing the preceding tasks. Of the five tasks, all are covered in this section, with the exception of reading and querying XML data, which is presented in later sections.

Using XML Serialization to Create XML Data

As discussed in Chapter 4, "Working with Objects in C#," serialization is a convenient way to store objects so they can later be deserialized into the original objects. If the natural state of your data allows it to be represented as objects, or if your application already has it represented as objects, XML serialization often offers a good choice for converting it into an XML format. However, there are some restrictions to keep in mind when applying XML serialization to a class:

  • The class must contain a public default (parameterless) constructor.

  • Only a public property or field can be serialized.

  • A read-only property cannot be serialized.

  • To serialize the objects in a custom collection class, the class must derive from the System.Collections.CollectionBase class and include an indexer. The easiest way to serialize multiple objects is usually to place them in a strongly typed array.

An Example Using the XmlSerializer Class

Listing 10-1 shows the XML file that we're going to use for further examples in this section. It was created by serializing instances of the class shown in Listing 10-2.

Listing 10-1. Sample XML File

<?xml version="1.0" standalone="yes"?>

   <films>

      <movies>

         <movie_ID>5</movie_ID>

         <movie_Title>Citizen Kane </movie_Title>

         <movie_Year>1941</movie_Year>

         <movie_DirectorID>Orson Welles</movie_DirectorID>

         <bestPicture>Y</bestPicture>

         <AFIRank>1</AFIRank>

      </movies>

      <movies>

         <movie_ID>6</movie_ID>

         <movie_Title>Casablanca </movie_Title>

         <movie_Year>1942</movie_Year>

         <movie_Director>Michael Curtiz</movie_Director>

         <bestPicture>Y</bestPicture>

         <AFIRank>1</AFIRank>

      </movies>

   </films>

In comparing Listings 10-1 and 10-2, it should be obvious that the XML elements are a direct rendering of the public properties defined for the movies class. The only exceptional feature in the code is the XmlElement attribute, which will be discussed shortly.

Listing 10-2. Using XmlSerializer to Create an XML File

using System.Xml;

using System.Xml.Serialization;

// other code here ...

public class movies

{

   public movies()  // Parameterless constructor is required

   {   }

   public movies(int ID, string title, string dir,string pic,

                 int yr, int movierank)

   {

      movieID = ID;

      movie_Director = dir;

      bestPicture = pic;

      rank = movierank;

      movie_Title = title;

      movie_Year = yr;

   }

   // Public properties that are serialized

   public int movieID

   {

      get { return mID; }

      set { mID = value; }

   }

   public string movie_Title

   {

      get { return mTitle; }

      set { mTitle = value; }

   }

   public int movie_Year

   {

      get { return mYear; }

      set { mYear = value; }

   }

   public string movie_Director

   {

      get { return mDirector; }

      set { mDirector = value; }

   }

   public string bestPicture

   {

      get { return mbestPicture; }

      set { mbestPicture = value; }

   }

   [XmlElement("AFIRank")]

   public int rank

   {

      get { return mAFIRank; }

      set { mAFIRank = value; }

   }

   private int mID;

   private string mTitle;

   private int mYear;

   private string mDirector;

   private string mbestPicture;

   private int mAFIRank;

}

To transform the class in Listing 10-2 to the XML in Listing 10-1, we follow the three steps shown in the code that follows. First, the objects to be serialized are created and stored in an array. Second, an XmlSerializer object is created. Its constructor (one of many constructor overloads) takes the object type it is serializing as the first parameter and an attribute as the second. The attribute enables us to assign "films" as the name of the root element in the XML output. The final step is to execute the XmlSerializer.Serialize method to send the serialized output to a selected stream—a file in this case.

// (1) Create array of objects to be serialized

movies[] films = {new movies(5,"Citizen Kane","Orson Welles",

                             "Y", 1941,1 ),

                  new movies(6,"Casablanca","Michael Curtiz",

                             "Y", 1942,2)};

// (2) Create serializer

//     This attribute is used to assign name to XML root element

XmlRootAttribute xRoot = new XmlRootAttribute();

   xRoot.ElementName = "films";

   xRoot.Namespace = "http://www.corecsharp.net";

   xRoot.IsNullable = true;

// Specify that an array of movies types is to be serialized

XmlSerializer xSerial = new XmlSerializer(typeof(movies[]),

                                          xRoot);

string filename=@"c:\oscarwinners.xml";

// (3) Stream to write XML into

TextWriter writer = new StreamWriter(filename);

xSerial.Serialize(writer,films);

Serialization Attributes

By default, the elements created from a class take the name of the property they represent. For example, the movie_Title property is serialized as a <movie_Title> element. However, there is a set of serialization attributes that can be used to override the default serialization results. Listing 10-2 includes an XmlElement attribute whose purpose is to assign a name to the XML element that is different than that of the corresponding property or field. In this case, the rank property name is replaced with AFIRank in the XML.

There are more than a dozen serialization attributes. Here are some other commonly used ones:

XmlAttribute | Is attached to a property or field and causes it to be rendered as an attribute within an element. Example: XmlAttribute("movieID")] Result: <movies movie>
XmlIgnore | Causes the field or property to be excluded from the XML.
XmlText | Causes the value of the field or property to be rendered as text. No elements are created for the member name. Example: [XmlText] public string movie_Title{ Result: <movies movie>Citizen Kane

XML Schema Definition (XSD)

The XML Schema Definition document is an XML file that is used to validate the contents of another XML document. The schema is essentially a template that defines in detail what is permitted in an associated XML document. Its role is similar to that of the BNF (Backus-Naur Form) notation that defines a language's syntax for a compiler.

.NET provides several ways (others are included in Chapter 11, "ADO.NET") to create a schema from an XML data document. One of the easiest ways is to use the XML Schema Definition tool (Xsd.exe). Simply run it from a command line and specify the XML file for which it is to produce a schema:

C:/ xsd.exe  oscarwinners.xml

The output, oscarwinners.xsd, is shown in Listing 10-3.

Listing 10-3. XML Schema to Apply Against XML in Listing 10-1

<xs:schema  xmlns=""

        xmlns:xs=http://www.w3.org/2001/XMLSchema

        xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">

  <xs:element name="films" msdata:IsDataSet="true">

    <xs:complexType>

      <xs:choice minOccurs="0" maxOccurs="unbounded">

        <xs:element name="movies">

          <xs:complexType>

            <xs:sequence>

              <xs:element name="movie_ID" type="xs:int"

                     minOccurs="0" />

              <xs:element name="movie_Title" type="xs:string"

                     minOccurs="0" />

              <xs:element name="movie_Year" type="xs:int"

                     minOccurs="0" />

              <xs:element name="movie_Director" type="xs:string"

                     minOccurs="0" />

              <xs:element name="bestPicture" type="xs:string"

                     minOccurs="0" />

              <xs:element name="AFIRank" type="xs:int"

                     minOccurs="0"

              />

            </xs:sequence>

          </xs:complexType>

        </xs:element>

      </xs:choice>

    </xs:complexType>

  </xs:element>

</xs:schema>

As should be evident from this small sample, the XML Schema language has a rather complex syntax. Those interested in all of its details can find them at the URL shown in the first line of the schema. For those with a more casual interest, the most important thing to note is that the heart of the document is a description of the valid types that may be contained in the XML data that the schema describes. In addition to the string and int types shown here, other supported types include boolean, double, float, dateTime, and hexBinary.

The types specified in the schema are designated as simple or complex. The complextype element defines any node that has children or an attribute; the simpletype has no attribute or child. You'll encounter many schemas where the simple types are defined at the beginning of the schema, and complex types are later defined as a combination of simple types.

XML Schema Validation

A schema is used by a validator to check an XML document for conformance to the layout and content defined by the schema. .NET implements validation as a read and check process. As a class iterates through each node in an XML tree, the node is validated. Listing 10-4 illustrates how the XmlValidatingReader class performs this operation.

First, an XmlTextReader is created to stream through the nodes in the data document. It is passed as an argument to the constructor for the XmlValidatingReader. Then, the ValidationType property is set to indicate a schema will be used for validation. This property can also be set to XDR or DTD to support older validation schemas.

The next step is to add the schema that will be used for validating to the reader's schema collection. Finally, the XmlValidatingReader is used to read the stream of XML nodes. Exception handling is used to display any validation error that occurs.

Listing 10-4. XML Schema Validation

private static bool ValidateSchema(string xml, string xsd)

{

   // Parameters: XML document and schemas

   // (1) Create a validating reader

   XmlTextReader tr = new XmlTextReader(xml");

   XmlValidatingReader xvr = new XmlValidatingReader(tr);

   // (2) Indicate schema validation

   xvr.ValidationType= ValidationType.Schema;

   // (3) Add schema to be used for validation

   xvr.Schemas.Add(null, xsd);

   try

   {

      Console.WriteLine("Validating: ");

      // Loop through all elements in XML document

      while(xvr.Read())

      {

         Console.Write(".");

      }

   }catch (Exception ex)

   { Console.WriteLine( "\n{0}",ex.Message); return false;}

   return true;

}

Note that the XmlValidatingReader class implements the XmlReader class underneath. We'll demonstrate using XmlReader to perform validation in the next section. In fact, in most cases, XmlReader (.NET 2.0 implmentation) now makes XmlValidatingReader obsolete.

Using an XML Style Sheet

A style sheet is a document that describes how to transform raw XML data into a different format. The mechanism that performs the transformation is referred to as an XSLT (Extensible Style Language Transformation) processor. Figure 10-1 illustrates the process: The XSLT processor takes as input the XML document to be transformed and the XSL document that defines the transformation to be applied. This approach permits output to be generated dynamically in a variety of formats. These include XML, HTML or ASPX for a Web page, and a PDF document.

Figure 10-1. Publishing documents with XSLT

The XslTransform Class

The .NET version of the XSLT processor is the XslTransform class found in the System.Xml.Xsl namespace. To demonstrate its use, we'll transform our XML movie data into an HTML file for display by a browser (see Figure 10-2).

Figure 10-2. XML data is transformed into this HTML output

Before the XslTransform class can be applied, an XSLT style sheet that describes the transformation must be created. Listing 10-5 contains the style sheet that will be used. As you can see, it is a mixture of HTML markup, XSL elements, and XSL commands that displays rows of movie information with three columns. The XSL elements and functions are the key to the transformation. When the XSL style sheet is processed, the XSL elements are replaced with the data from the original XML document.

Listing 10-5. XML Style Sheet to Create HTML Output

<?xml version="1.0"?>

<xsl:stylesheet version="1.0"

    xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

  <xsl:template match="/">

   <HTML>

     <TITLE>Movies</TITLE>

     <Table border="0" padding="0" cellspacing="1">

     <THEAD>

       <TH>Movie Title</TH>

       <TH>Movie Year </TH>

       <TH>AFI Rank   </TH>

       <TH>Director   </TH>

     </THEAD>

     <xsl:for-each select="//movies">

         <xsl:sort select="movie_Title" />

       <tr>

         <td><xsl:value-of select="movie_Title"/> </td>

         <td align="center"><xsl:value-of select=

               "movie_Year"/></td>

         <td align="center"><xsl:value-of select=

               "AFIRank" /></td>

      <td><xsl:value-of select="movie_Director" /></td>

        </tr>

      </xsl:for-each>

      </Table>

    </HTML>

  </xsl:template>

</xsl:stylesheet>

Some points of interest:

  • The URL in the namespace of the <xsl:stylesheet> element must be exactly as shown here.

  • The match attribute is set to an XPath query that indicates which elements in the XML file are to be converted. Setting match="/" selects all elements.

  • The for-each construct loops through a group of selected nodes specified by an XPath expression following the select attribute. XPath is discussed in Section 10.4, "Using XPath to Search XML."

  • The value-of function extracts a selected value from the XML document and inserts it into the output.

  • The <xsl:sort> element is used to sort the incoming data and is used in conjunction with the for-each construct. Here is its syntax:

select = XPath expression

order = {"ascending" | "descending"}

data-type = {"text" | "number"}

case-order = {"upper-first" | "lower-first"}

After a style sheet is created, using it to transform a document is a breeze. As shown by the following code, applying the XslTransform class is straightforward. After creating an instance of it, you use its Load method to specify the file containing the style sheet. The XslTransform.Transform method performs the transformation. This method has several overloads. The version used here requires an XpathDocument object that represents the XML document, as a parameter, and an XmlWriter parameter that designates where the output is written—an HTML file in this case.

// Transform XML into HTML and store in movies.htm

XmlWriter writer = new

      XmlTextWriter("c:\\movies.htm",Encoding.UTF8);

XslTransform xslt = new XslTransform();

XPathDocument xpd = new

      XPathDocument("c:\\oscarwinners.xml");

xslt.Load("movies.xsl");

xslt.Transform(xpd, null, writer,null);

Core Note

You can link a style sheet to an XML document by placing an HRef statement in the XML document on the line preceding the root element definition:

<?xml:stylesheet type="text/xsl" href="movies.xsl" ?>

If a document is linked to a style sheet that converts XML to HTML, most browsers automatically perform the transformation and display the HTML. This can be a quick way to perform trial-and-error testing when developing a style sheet.

It takes only a small leap from this simple XSLT example to appreciate the potential of being able to transform XML documents dynamically. It is a natural area of growth for Web Services and Web pages that now on demand accept input in one format, transform it, and serve the output up in a different format.


10.2. Techniques for Reading XML Data

XML can be represented in two basic ways: as the familiar external document containing embedded data, or as an in-memory tree structure know as a Document Object Model (DOM). In the former case, XML can be read in a forward-only manner as a stream of tokens representing the file's content. The object that performs the reading stays connected to the data source during the read operations. The XmlReader and XmlTextReader shown in Figure 10-3 operate in this manner.

Figure 10-3. Classes to read XML data

More options are available for processing the DOM because it is stored in memory and can be traversed randomly. For simply reading a tree, the XmlNodeReader class offers an efficient way to traverse a tree in a forward, read-only manner. Other more sophisticated approaches that also permit tree modification are covered later in this section.

XmlReader Class

XmlReader is an abstract class possessing methods and properties that enable an application to pull data from an XML file one node at a time in a forward-only, read-only manner. A depth-first search is performed, beginning with the root node in the document. Nodes are inspected using the Name, NodeType, and Value properties.

XmlReader serves as a base class for the concrete classes XmlTextReader and XmlNodeReader. As an abstract class, XmlReader cannot be directly instantiated; however, it has a static Create method that can return an instance of the XmlReader class. This feature became available with the release of .NET Framework 2.0 and is recommended over the XmlTextReader class for reading XML streams.

Listing 10-6 illustrates how to create an XmlReader object and use it to read the contents of a short XML document file. The code is also useful for illustrating how .NET converts the content of the file into a stream of node objects. It's important to understand the concept of nodes because an XML or HTML document is defined (by the official W3C Document Object Model (DOM) specification[2] ) as a hierarchy of node objects.

[2] W3C Document Object Model (DOM) Level 3 Core Specification, April, 2004, http://www.w3.org/TR/2004/REC-DOM-Level-3-Core-20040407/core.html

Listing 10-6. Using XmlReader to Read an XML Document

// Include these namespaces:

// using System.Xml;

// using System.Xml.XPath;

public void ShowNodes()

{

   //(1) Settings object enables/disables features on XmlReader

   XmlReaderSettings settings = new XmlReaderSettings();

   settings.ConformanceLevel = ConformanceLevel.Fragment;

   settings.IgnoreWhitespace = true;

   try

   {

      //(2) Create XmlReader object

      XmlReader rdr = XmlReader.Create("c:\\oscarsshort.xml",

                                       settings);

      while (rdr.Read())

      {

         Format(rdr);

      }

      rdr.Close();

   }

   catch (Exception e)

   {

       Console.WriteLine ("Exception: {0}", e.ToString());

   }

}

private static void Format(XmlTextReader reader)

{

   //(3) Print Current node properties

   Console.Write( reader.NodeType+ "<" + reader.Name + ">" +

                  reader.Value);

   Console.WriteLine();

}

Before creating the XmlReader, the code first creates an XmlReaderSettings object. This object sets features that define how the XmlReader object processes the input stream. For example, the ConformanceLevel property specifies how the input is checked. The statement

settings.ConformanceLevel = ConformanceLevel.Fragment;

specifies that the input must conform to the standards that define an XML 1.0 document fragment—an XML document that does not necessarily have a root node.

This object and the name of the XML document file are then passed to the Create method that returns an XmlReader instance:

XmlReader rdr = XmlReader.Create("c:\\oscarsshort.xml, settings);

The file's content is read in a node at a time by the XmlReader.Read method, which prints the NodeType, Name, and Value of each node. Listing 10-7 shows the input file and a portion of the generated output. Line numbers have been added so that an input line and its corresponding node information can be compared.

Listing 10-7. XML Input and Corresponding Nodes

Input File: oscarsshort.xml

(1) <?xml version="1.0" standalone="yes"?>

(2) <films>

(3)  <movies>

(4)    <!-- Selected by AFI as best movie -->

(5)    <movie_ID>5</movie_ID>

(6)    <![CDATA[<a href="http://www.imdb.com/tt0467/">Kane</a>]]>

(7)    <movie_Title>Citizen Kane </movie_Title>

(8)    <movie_Year>1941</movie_Year>

(9)    <movie_Director>Orson Welles</movie_Director>

(10)   <bestPicture>Y</bestPicture>

(11) </movies>

(12)</films>

Program Output (NodeType, <Name>, Value):

(1) XmlDeclaration<xml>version="1.0" standalone="yes"

(2) Element<films>

(3) Element<movies>

(4) Comment<> Selected by AFI as best movie

(5) Element<movie_ID>

      Text<>5

    EndElement<movie_ID>

(6) CDATA<><a href="http://www.imdb.com/tt0467/">Kane</a>

(7) Element<movie_Title>

      Text<>Citizen Kane

    EndElement<movie_Title>

       ...

(12)EndElement<films>

Programs that use XmlReader typically implement a logic pattern that consists of an outer loop that reads nodes and an inner switch statement that identifies the node using an XMLNodeType enumeration. The logic to process the node information is handled in the case blocks:

while (reader.Read())

{

   switch (reader.NodeType)

   {

      case XmlNodeType.Element:

      // Attributes are contained in elements

         while(reader.MoveToNextAttribute())

         {

            Console.WriteLine(reader.Name+reader.Value);

         }

      break;

      case XmlNodeType.Text:

      // Process ..

      break;

      case XmlNodeType.EndElement

      // Process ..

      break;

   }

}

The Element, Text, and Attribute nodes mark most of the data content in an XML document. Note that the Attribute node is regarded as metadata attached to an element and is the only one not exposed directly by the XmlReader.Read method. As shown in the preceding code segment, the attributes in an Element can be accessed using the MoveToNextAttribute method.

Table 10-1 summarizes the node types. It is worth noting that these types are not an arbitrary .NET implementation. With the exception of Whitespace and XmlDeclaration, they conform to the DOM Structure Model recommendation.

Table 10-1. XmlNodeType Enumeration

Option

Description and Use

Attribute

An attribute or value contained within an element. Example:

<movie_title genre="comedy">The Lady Eve

</movie_title>

Attribute is genre="comedy". Attributes must be located within an element

if(reader.NodeType==XmlNodeType.Element){

  while(reader.MoveToNextAttribute())

   {

    Console.WriteLine(reader.Name+reader.Value);

   }

CData

Designates that the element is not to be parsed. Markup characters are treated as text:

![CDATA[<ELEMENT>

<a href="http://www.imdb.com">movies</a>

</ELEMENT>]]>

Comment

To make a comment:

<!-- comment -->

To have comments ignored:

XmlReaderSettings.IgnoreComment = true;

Document

A document root object that provides access to the entire XML document.

DocumentFragment

A document fragment. This is a node or subtree with a document. It provides a way to work with part of a document.

DocumentType

Document type declaration indicated by <!DOCTYPE … >. Can refer to an external Document Type Definition (DTD) file or be an inline block containing Entity and Notation declarations.

Element

An XML element. Designated by the < > brackets: <movie_Title>

EndElement

An XML end element tag. Marks the end of an element: </movie_Title>

EndEntity

End of an Entity declaration.

Entity

Defines text or a resource to replace the entity name in the XML. An entity is defined as a child of a document type node:

<!DOCTYPE movies[

   <!ENTITY leadingactress "stanwyck">

]>

XML would then reference this as: <actress>&leadingactress;</actress>

EntityReference

A reference to the entity. In the preceding example, &leadingactress; is an EntityReference.

Notation

A notation that is declared within a DocumentType declaration. Primary use is to pass information to the XML processor. Example:

<!NOTATION homepage="www.sci.com" !>

ProcessingInstruction

Useful for providing information about how the data was generated or how to process it. Example:

<?pi1 Requires IE 5.0 and above ?>

Text

The text content of a node.

Whitespace

Whitespace refers to formatting characters such as tabs, line feeds, returns, and spaces that exist between the markup and affect the layout of a document.

XmlDeclaration

The first node in the document. It provides version information.

<?xml version="1.0" standalone="yes"?>

XmlNodeReader Class

The XmlNodeReader is another forward-only reader that processes XML as a stream of nodes. It differs from the XmlReader class in two significant ways:

  • It processes nodes from an in-memory DOM tree structure rather than a text file.

  • It can begin reading at any subtree node in the structure—not just at the root node (beginning of the document).

In Listing 10-8, an XmlNodeReader object is used to list the movie title and year from the XML-formatted movies database. The code contains an interesting twist: The XmlNodeReader object is not used directly, but instead is passed as a parameter to the constructor of an XmlReader object. The object serves as a wrapper that performs the actual reading. This approach has the advantage of allowing the XmlSettings values to be assigned to the reader.

Listing 10-8. Using XmlNodeReader to Read an XML Document

private void ListMovies()

{

   // (1) Specify XML file to be loaded as a DOM

   XmlDocument doc = new XmlDocument();

   doc.Load("c:\\oscarwinners.xml");

   // (2) Settings for use with XmlNodeReader object

   XmlReaderSettings settings = new XmlReaderSettings();

   settings.ConformanceLevel = ConformanceLevel.Fragment;

   settings.IgnoreWhitespace = true;

   settings.IgnoreComments = true;

   // (3) Create a nodereader object

   XmlNodeReader noderdr = new XmlNodeReader(doc);

   // (4) Create an XmlReader as a wrapper around node reader

   XmlReader reader = XmlReader.Create(noderdr, settings);

   while (reader.Read())

   {

      if(reader.NodeType==XmlNodeType.Element){

         if (reader.Name == "movie_Title")

         {

            reader.Read();  // next node is text for title

            Console.Write(reader.Value);    // Movie Title

         }

         if (reader.Name == "movie_Year")

         {

            reader.Read();  // next node is text for year

            Console.WriteLine(reader.Value); // year

         }

      }

   }

}

The parameter passed to the XmlNodeReader constructor determines the first node in the tree to be read. When the entire document is passed—as in this example—reading begins with the top node in the tree. To select a specific node, use the XmlDocument.SelectSingleNode method as illustrated in this segment:

XmlDocument doc = new XmlDocument();

doc.Load("c:\\oscarwinners.xml");  // Build tree in memory

XmlNodeReader noderdr = new

      XmlNodeReader(doc.SelectSingleNode("films/movies[2]"));

Refer to Listing 10-1 and you can see that this selects the second movies element group, which contains information on Casablanca.

If your application requires read-only access to XML data and the capability to read selected subtrees, the XmlNodeReader is an efficient solution. When updating, writing, and searching become requirements, a more sophisticated approach is required; we'll look at those techniques later in this section.

The XmlReaderSettings Class

A significant advantage of using an XmlReader object—directly or as a wrapper—is the presence of the XmlReaderSettings class as a way to define the behavior of the XmlReader object. Its most useful properties specify which node types in the input stream are ignored and whether XML validation is performed. Table 10-2 lists the XmlReaderSettings properties.

Table 10-2. Properties of the XmlReaderSettings Class

Property

Default Value

Description

CheckCharacters

true

Indicates whether characters and XML names are checked for illegal XML characters. An exception is thrown if one is encountered.

CloseInput

false

An XmlReader object may be created by passing a stream to it. This property indicates whether the stream is closed when the reader object is closed.

ConformanceLevel

Document

Indicates whether the XML should conform to the standards for a Document or DocumentFragment.

DtdValidate

false

Indicates whether to perform DTD validation.

IgnoreComments

IgnoreInlineSchema

IgnoreProcessingInstructions

IgnoreSchemaLocation

IgnoreValidationWarnings

IgnoreWhitespace

false

true

false

true

true

false

Specify whether a particular node type is processed or ignored by the XmlReader.Read method.

LineNumberOffset

LinePositionOffset

0

0

XmlReader numbers lines in the XML document beginning with 0. Set this property to change the beginning line number and line position values.

Schemas

is empty

Contains the XmlSchemaSet to be used for XML Schema Definition Language (XSD) validation.

XsdValidate

false

Indicates whether XSD validation is performed.

Using an XML Schema to Validate XML Data

The final two properties listed in Table 10-2—Schemas and XsdValidate—are used to validate XML data against a schema. Recall that a schema is a template that describes the permissible content in an XML file or stream. Validation can be (should be) used to ensure that data being read conforms to the rules of the schema. To request validation, you must add the validating schema to the XmlSchemaSet collection of the Schemas property; next, set XsdValidate to true; and finally, define an event handler to be called if a validation error occurs. The following code fragment shows the code used with the schema and XML data in Listings 10-1 and 10-3:

XmlReaderSettings settings = new XmlReaderSettings();

// (1) Specify schema to be used for validation

settings.Schemas.Add(null,"c:\\oscarwinners.xsd");

// (2) Must set this to true

settings.XsdValidate = true;

// (3) Delegate to handle validation error event

settings.ValidationEventHandler += new

      System.Xml.Schema.ValidationEventHandler(SchemaValidation);

// (4) Create reader and pass settings to it

XmlReader rdr = XmlReader.Create("c:\\oscarwinners.xml",

      settings);

// process XML data ...

...

// Method to handle errors detected during schema validation

private void SchemaValidation(object sender, System.Xml.Schema.ValidationEventArgs e)

{

   MessageBox.Show(e.Message);

}

Note that a detected error does not stop processing. This means that all the XML data can be checked in one pass without restarting the program.

Options for Reading XML Data

All the preceding examples that read XML data share two characteristics: data is read a node at a time, and a node's value is extracted as a string using the XmlReader.Value property. This keeps things simple, but ignores the underlying XML data. For example, XML often contains numeric data or data that is the product of serializing a class. Both cases can be handled more efficiently using other XmlReader methods.

XmlReader has a suite of ReadValueAs xxx methods that can read the contents of a node in its native form. These include ReadValueAsBoolean, ReadValueAsDateTime, ReadValueAsDecimal, ReadValueAsDouble, ReadValueAsInt32, ReadValueAsInt64, and ReadValueAsSingle. Here's an example:

int age;

if(reader.Name == "Age") age= reader.ReadValueAsInt32();

XML that corresponds to the public properties or fields of a class can be read directly into an instance of the class with the ReadAsObject method. This fragment reads the XML data shown in Listing 10-1 into an instance of the movies class. Note that the name of the field or property must match an element name in the XML data.

// Deserialize XML into a movies object

if (rdr.NodeType == XmlNodeType.Element && rdr.Name == "movies")

{

   movies m = (movies)rdr.ReadAsObject(typeof(movies));

   // Do something with object

}

// XML data is read directly into this class

public class movies

{

   public int movie_ID;

   public string movie_Title;

   public string movie_Year;

   private string director;

   public string bestPicture;

   public string movie_Director

   {

      set { director = value; }

      get { return (director); }

   }

}


10.3. Techniques for Writing XML Data

In many cases, the easiest way to present data in an XML format is to use .NET serialization. As demonstrated in Section 10.1, if the data is in a collection class, it can be serialized using the XmlSerializer class; as we see in the next chapter, if it's in a DataSet, the DataSet.WriteXml method can be applied. The advantages of serialization are that it is easy to use, generates well-formed XML, and is symmetrical—the XML that is written can be read back to create the original data objects.

For cases where serialization is not an option—a comma delimited file, for instance—or where more control over the XML layout is needed, the XmlWriter class is the best .NET solution.

Writing XML with the XmlWriter Class

The XmlWriter class offers precise control over each character written to an XML stream or file. However, this flexibility does require a general knowledge of XML and can be tedious to code, because a distinct Writexxx method is used to generate each node type. On the positive side, it offers several compliance checking features, and the ability to write CLR typed data directly to the XML stream:

  • XmlWriterSettings. CheckCharacters property configures the XmlWriter to check for illegal characters in text nodes and XML names, as well as check the validity of XML names. An exception is thrown if an invalid character is detected.

  • XmlWriterSettings. ConformanceLevel property configures the XmlWriter to guarantee that the stream complies with the conformance level that is specified. For example, the XML may be set to conform to a document or document fragment.

  • XmlWriter. WriteValue method is used to write data to the XML stream as a CLR type (int, double, and so on) without having to first convert it to a string.

Listing 10-9 illustrates the basic principles involved in using the XmlWriter class. Not surprisingly, there are a lot of similarities to the closely related XmlReader class. Both use the Create method to create an object instance, and both have constructor overloads that accept a settings object—XmlWriterSettings, in this case—to define the behavior of the reader or writer. The most important of these setting properties is the conformance level that specifies either document or fragment (a subtree) conformance.

A series of self-describing methods, which support all the node types listed in Table 10-1, generate the XML. Note that exception handling should always be enabled to trap any attempt to write an invalid name or character.

Listing 10-9. Write XML Using XmlWriter Class

private void WriteMovie()

{

   string[,] movieList = { { "Annie Hall", "Woody Allen" },

                       { "Lawrence of Arabia", "David Lean" } };

   // (1) Define settings to govern writer actions

   XmlWriterSettings settings = new XmlWriterSettings();

   settings.Indent = true;

   settings.IndentChars = ("    ");

   settings.ConformanceLevel = ConformanceLevel.Document;

   settings.CloseOutput = false;

   settings.OmitXmlDeclaration = false;

   // (2) Create XmlWriter object

   XmlWriter writer = XmlWriter.Create("c:\\mymovies.xml",

                                       settings);

   writer.WriteStartDocument();

   writer.WriteComment("Output from xmlwriter class");

   writer.WriteStartElement("films");

   for (int i = 0; i <= movieList.GetUpperBound(0) ; i++)

   {

      try

      {

         writer.WriteStartElement("movie");

         writer.WriteElementString("Title", movieList[i, 0]);

         writer.WriteElementString("Director", movieList[i, 1]);

         writer.WriteStartElement("Movie_ID");

         writer.WriteValue(i); // No need to convert to string

         writer.WriteEndElement();

         writer.WriteEndElement();

      }

      catch (Exception ex)

      {

         MessageBox.Show(ex.Message);

      }

   }

   writer.WriteEndElement();

   writer.Flush();  // Flush any remaining content to XML stream

   writer.Close();

   /*

      Output:

      <?xml version="1.0" encoding="utf-8"?>

      <!--Output from xmlwriter class-->

      <films>

         <movie>

            <Title>Annie Hall</Title>

            <Director>Woody Allen</Director>

            <Movie_ID>0</Movie_ID>

         </movie>

         <movie>

            <Title>Lawrence of Arabia</Title>

            <Director>David Lean</Director>

            <Movie_ID>1</Movie_ID>

         </movie>

      </films>

   */

}

Before leaving the topic of XML writing, note that .NET also provides XmlTextWriter and XmlNodeWriter classes as concrete implementations of the abstract XmlWriter class. The former does not offer any significant advantages over the XmlWriter. The node writer is a bit more useful. It creates a DOM tree in memory that can be processed using the many classes and methods designed for that task. Refer to .NET documentation for XmlNodeWriter details.


10.4. Using XPath to Search XML

A significant benefit of representing XML in a tree model—as opposed to a data stream—is the capability to query and locate the tree's content using XML Path Language (XPath). This technique is similar to using a SQL command on relational data. An XPath expression (query) is created and passed to an engine that evaluates it. The expression is parsed and executed against a data store. The returned value(s) may be a set of nodes or a scalar value.

XPath is a formal query language defined by the XML Path Language 2.0 specification (www.w3.org/TR/xpath). Syntactically, its most commonly used expressions resemble a file system path and may represent either the absolute or relative position of nodes in the tree.

In the .NET Framework, XPath evaluation is exposed through the XPathNavigator abstract class. The navigator is an XPath processor that works on top of any XML data source that exposes the IXPathNavigable interface. The most important member of this interface is the CreateNavigator method, which returns an XPathNavigator object. Figure 10-4 shows three classes that implement this interface. Of these, XmlDocument and XmlDataDocument are members of the System.Xml namespace; XPathDocument (as well as the XmlNavigator class) resides in the System.Xml.XPath namespace.

  • XmlDocument. Implements the W3C Document Object Model (DOM) and supports XPath queries, navigation, and editing.

  • XmlDataDocument. In addition to the features it inherits from XmlDocument, it provides the capability to map XML data to a DataSet. Any changes to the DataSet are reflected in the XML tree and vice versa.

  • XPathDocument. This class is optimized to perform XPath queries and represents XML in a tree of read-only nodes that is more streamlined than the DOM.

Figure 10-4. XML classes that support XPath navigation

Constructing XPath Queries

Queries can be executed against each of these classes using either an XPathNavigator object or the SelectNodes method implemented by each class. Generic code looks like this:

// XPATHEXPRESSION is the XPath query applied to the data

// (1) Return a list of nodes

XmlDocument doc = new XmlDocument();

doc.Load("movies.xml");

XmlNodeList selection = doc.SelectNodes(XPATHEXPRESSION);

// (2) Create a navigator and execute the query

XPathNavigator nav = doc.CreateNavigator();

XPathNodeIterator iterator = nav.Select(XPATHEXPRESSION);

The XpathNodeIterator class encapsulates a list of nodes and provides a way to iterate over the list.

As with regular expressions (refer to Chapter 5, "C# Text Manipulation and File I/O"), an XPath query has its own syntax and operators that must be mastered in order to efficiently query an XML document. To demonstrate some of the fundamental XPath operators, we'll create queries against the data in Listing 10-10.

Listing 10-10. XML Representation of Directors/Movies Relationship

<films>

  <directors>

    <director_id>54</director_id>

    <first_name>Martin</first_name>

    <last_name>Scorsese</last_name>

    <movies>

      <movie_ID>30</movie_ID>

      <movie_Title>Taxi Driver</movie_Title>

      <movie_DirectorID>54</movie_DirectorID>

      <movie_Year>1976</movie_Year>

    </movies>

    <movies>

      <movie_ID>28</movie_ID>

      <movie_Title>Raging Bull </movie_Title>

      <movie_DirectorID>54</movie_DirectorID>

      <movie_Year>1980</movie_Year>

    </movies>

  </directors>

</films>

Table 10-3 summarizes commonly used XPath operators and provides an example of using each.

Table 10-3. XPath Operators

Operator

Description

Child operator (/)

References the root of the XML document, where the expression begins searching. The following expression returns the last_name node for each director in the table:

/films/directors/last_name

Recursive descendant operator (//)

This operator indicates that the search should include descendants along the specified path. The following all return the same set of last_name nodes. The difference is that the first begins searching at the root, and second at each directors node:

//last_name

//directors//last_name

Wildcard operator (*)

Returns all nodes below the specified path location. The following returns all nodes that are descendants of the movies node:

//movies/*

Current operator (.)

Refers to the currently selected node in the tree, when navigating through a tree node-by-node. It effectively becomes the root node when the operator is applied. In this example, if the current node is a directors node, this will find any last_name child nodes:

.//last_name

Parent operator (..)

Used to represent the node that is the parent of the current node. If the current node were a movies node, this would use the directors node as the start of the path:

../last_name

Attribute operator (@)

Returns any attributes specified. The following example would return the movie's runtime assuming there were attributes such as <movie_ID time="98"> included in the XML.

//movies//@time

Filter operator ([ ])

Allows nodes to be filtered based on a matching criteria. The following example is used to retrieve all movie titles directed by Martin Scorsese:

//directors[last_name='Scorsese']

   /movies/movie_Title

Collection operator ([ ])

Uses brackets just as the filter, but specifies a node based on an ordinal value. Is used to distinguish among nodes with the same name. This example returns the node for the second movie, Raging Bull:

//movies[2] (Index is not 0 based.)

Union operator (|)

Returns the union of nodes found on specified paths. This example returns the first and last name of each director:

//last_name | //first_name

Note that the filter operator permits nodes to be selected by their content. There are a number of functions and operators that can be used to specify the matching criteria. Table 10-4 lists some of these.

Table 10-4. Functions and Operators used to Create an XPath Filter

Function/Operator

Description

and, or

Logical operators.

Example: "directors[last_name= 'Scorsese' and first_name= 'Martin']"

position( )

Selects node(s) at specified position.

Example: "//movies[position()=2]"

contains(node,string)

Matches if node value contains specified string.

Example: "//movies[contains(movie_Title,'Tax')]"

starts-with(node,string)

Matches if node value begins with specified string.

Example: "//movies[starts-with(movie_Title,'A')]"

substring-after(string,string)

Extracts substring from the first string that follows occurrence of second string.

Example: "//movies[substring-after('The Graduate','The ')='Graduate']"

substring(string, pos,length)

Extracts substring from node value.

Example: "//movies[substring(movie_Title,2,1)='a']"

Refer to the XPath standard (http://www.w3.org/TR/xpath) for a comprehensive list of operators and functions.

Let's now look at examples of using XPath queries to search, delete, and add data to an XML tree. Our source XML file is shown in Listing 10-10. For demonstration purposes, examples are included that represent the XML data as an XmlDocument, XPathDocument, and XmlDataDocument.

XmlDocument and XPath

The expression in this example extracts the set of last_name nodes. It then prints the associated text. Note that underneath, SelectNodes uses a navigator to evaluate the expression.

string exp = "/films/directors/last_name";

XmlDocument doc = new XmlDocument();

doc.Load("directormovies.xml");  // Build DOM tree

XmlNodeList directors = doc.SelectNodes(exp);

foreach(XmlNode n in directors)

   Console.WriteLine(n.InnerText);  // Last name or director

The XmlNode.InnerText property concatenates the values of child nodes and displays them as a text string. This is a convenient way to display tree contents during application testing.

XPathDocument and XPath

For applications that only need to query an XML document, the XPathDocument is the recommended class. It is free of the overhead required for updating a tree and runs 20 to 30 percent faster than XmlDocument. In addition, it can be created using an XmlReader to load all or part of a document into it. This is done by creating the reader, positioning it to a desired subtree, and then passing it to the XPathDocument constructor. In this example, the XmlReader is positioned at the root node, so the entire tree is read in:

string exp = "/films/directors/last_name";

// Create method was added with .NET 2.0

XmlReader rdr = XmlReader.Create("c:\\directormovies.xml");

// Pass XmlReader to the constructor

xDoc = new XPathDocument(rdr);

XPathNavigator nav= xDoc.CreateNavigator();

XPathNodeIterator iterator;

iterator = nav.Select(exp);

// List last name of each director

while (iterator.MoveNext())

   Console.WriteLine(iterator.Current.Value);

// Now, list only movies for Martin Scorsese

string exp2 =

   "//directors[last_name='Scorsese']/movies/movie_Title";

iterator = nav.Select(exp2);

while (iterator.MoveNext())

   Console.WriteLine(iterator.Current.Value);

Core Note

Unlike the SelectNodes method, the navigator's Select method accepts XPath expressions as both plain text and precompiled objects. The following statements demonstrate how a compiled expression could be used in the preceding example:

string exp = "/films/directors/last_name";

// use XmlNavigator to create XPathExpression object

XPathExpression compExp = nav.Compile(exp);

iterator = nav.Select(compExp);

Compiling an expression improves performance when the expression (query) is used more than once.

XmlDataDocument and XPath

The XmlDataDocument class allows you to take a DataSet (an object containing rows of data) and create a replica of it as a tree structure. The tree not only represents the DatSet, but is synchronized with it. This means that changes made to the DOM or DataSet are automatically reflected in the other.

Because XmlDataDocument is derived from XmlDocument, it supports the basic methods and properties used to manipulate XML data. To these, it adds methods specifically related to working with a DataSet. The most interesting of these is the GeTRowFromElement method that takes an XmlElement and converts it to a corresponding DataRow.

A short example illustrates how XPath is used to retrieve the set of nodes representing the movies associated with a selected director. The nodes are then converted to a DataRow, which is used to print data from a column in the row.

// Create document by passing in associated DataSet

XmlDataDocument xmlDoc = new XmlDataDocument(ds);

string exp = "//directors[last_name='Scorsese']/movies";

XmlNodeList nodeList =

      xmlDoc.DocumentElement.SelectNodes(exp);

DataRow myRow;

foreach (XmlNode myNode in nodeList)

{

   myRow = xmlDoc.GetRowFromElement((XmlElement)myNode);

   if (myRow != null){

      // Print Movie Title from a DataRow

      Console.WriteLine(myRow["movie_Title"].ToString());

   }

}

This class should be used only when its hybrid features add value to an application. Otherwise, use XmlDocument if updates are required or XPathDocument if the data is read-only.

Adding and Removing Nodes on a Tree

Besides locating and reading data, many applications need to add, edit, and delete information in an XML document tree. This is done using methods that edit the content of a node and add or delete nodes. After the changes have been made to the tree, the updated DOM is saved to a file.

To demonstrate how to add and remove nodes, we'll operate on the subtree presented as text in Listing 10-10 and as a graphical tree in Figure 10-5.

Figure 10-5. Subtree used to delete and remove nodes

This example uses the XmlDocument class to represent the tree for which we will remove one movies element and add another one. XPath is used to locate the movies node for Raging Bull along the path containing Scorsese as the director:

"//directors[last_name='Scorsese']/movies[movie_Title=

      'Raging Bull']"

This node is deleted by locating its parent node, which is on the level directly above it, and executing its RemoveChild method.

Listing 10-11. Using XmlDocument and XPath to Add and Remove Nodes

Public void UseXPath()

{

   XmlDocument doc = new XmlDocument();

   doc.Load("c:\\directormovies.xml");

   // (1) Locate movie to remove

   string exp = "//directors[last_name='Scorsese']/

         movies[movie_Title='Raging Bull']";

   XmlNode movieNode = doc.SelectSingleNode(exp);

   // (2) Delete node and child nodes for movie

   XmlNode directorNode = movieNode.ParentNode;

   directorNode.RemoveChild(movieNode);

   // (3) Add new movie for this director

   //     First, get and save director's ID

   string directorID =

         directorNode.SelectSingleNode("director_id").InnerText;

   // XmlElement is dervied from XmlNode and adds members

   XmlElement movieEl = doc.CreateElement("movies");

   directorNode.AppendChild(movieEl);

   // (4) Add Movie Description

   AppendChildElement(movieEl, "movie_ID", "94");

   AppendChildElement(movieEl, "movie_Title", "Goodfellas");

   AppendChildElement(movieEl, "movie_Year", "1990");

   AppendChildElement(movieEl, "movie_DirectorID",

                               directorID);

   // (5) Save updated XML Document

   doc.Save("c:\\directormovies2.xml");

}

// Create node and append to parent

public void AppendChildElement(XmlNode parent, string elName,

                               string elValue)

{

   XmlElement newEl =

         parent.OwnerDocument.CreateElement(elName);

   newEl.InnerText = elValue;

   parent.AppendChild(newEl);

}

Adding a node requires first locating the node that will be used to attach the new node. Then, the document's Create xxx method is used to generate an XmlNode or XmlNode-derived object that will be added to the tree. The node is attached using the current node's AppendChild, InsertAfter, or InsertBefore method to position the new node in the tree. In this example, we add a movies element that contains information for the movie Goodfellas.


10.5. Summary

To work with XML, a basic understanding of the XML document, schema, and style sheet is required. An XML document, which is a representation of information based on XML guidelines, can be created in numerous ways. The XmlSerializer class can be used when the data takes the form of an object or objects within a program. After the XML document is created, a schema can be derived from it using the XML Schema Definition (XSD) tool. Several classes use the schema to provide automatic document validation. The usefulness of XML data is extended by the capability to transform it into virtually any other format using an XML style sheet. The style sheet defines a set of rules that are applied during XML Style Sheet Transformation (XSLT).

XML data can be processed as a stream of nodes or an in-memory tree known as a Document Object Model (DOM). The XmlReader and XmlNodeReader classes provide an efficient way to process XML as a read-only, forward-only stream. The Xml Reader, XPathDocument, and XmlDataReader classes offer methods for processing nodes in the tree structure.

In many cases, data extraction from an XML tree can be best achieved using a query, rather than traversing the tree nodes. The XPath expression presents a rich, standardized syntax that is easily used to specify criteria for extracting a node, or multiple nodes, from an XML tree.


10.6. Test Your Understanding

1:

XmlReader is an abstract class. How do you create an instance of it to read an XML document?

2:

What is the purpose of the XmlReaderSettings class?

3:

Which of these classes cannot be used to update an XML document?

  1.
XmlDocument

  2.
XmlDataDocument

  3.
XmlPathDocument

4:

Using the XML data from Listing 10-10, show the node values returned by the following XPath expressions:

  1.
//movies[substring( movie_Title,2,1)='a']

  2.
//movies[2]

  3.
//movies[movie_Year >= 1978]

  4.
//directors[last_name='Scorsese']

      /movies/movie_Title

5:

Describe two ways to perform schema validation on an XML document.


Chapter 11. ADO.NET

Topics in This Chapter

  • ADO.NET Architecture: ADO.NET provides access to data using custom or generic data providers. This section looks at the classes a provider must supply that enable an application to connect to a data source and interact with it using SQL commands.

  • Introduction to Using ADO.NET: ADO.NET supports data access using a connected or disconnected connectivity model. An introduction and comparison of the two architectures is provided.

  • The Connected Model: Use of the connected model requires an understanding of the role that Connection and Command classes have in retrieving data. Examples illustrate how to create a connection and use the Command class to issue SQL commands, invoke stored procedures, and manage multi-command transactions.

  • The Disconnected Model: Disconnected data is stored in an in-memory DataTable or DataSet. The latter is usually made up of multiple DataTables that serve as a local relational data store for an application. A DataAdapter is typically used to load this data and then apply updates to the original data source. Techniques are introduced for handling synchronization issues that arise when updating disconnected data.

  • XML Data Access: Although it does not provide intrinsic XML classes, ADO.NET supports XML integration through properties and methods of the DataSet class. Examples show how to use WriteXml and ReadXml to create an XML file from a DataSet and populate a DataSet from an XML file, respectively.

ADO.NET is based on a flexible set of classes that allow data to be accessed from within the managed environment of .NET. These classes are used to access a variety of data sources including relational databases, XML files, spreadsheets, and text files. Data access is through an API, known as a managed data provider. This provider may be written specifically for a database, or may be a more generic provider such as OLE DB or ODBC (Open DataBase Connectivity). Provider classes expose a connection object that binds an application to a data source, and a command object that supports the use of standard SQL commands to fetch, add, update, or delete data.

ADO.NET supports two broad models for accessing data: disconnected and connected. The disconnected model downloads data to a client's machine where it is encapsulated as an in-memory DataSet that can be accessed like a local relational database. The connected model relies on record-by-record access that requires an open and sustained connection to the data source. Recognizing the most appropriate model to use in an application is at the heart of understanding ADO.NET. This chapter examines both models—offering code examples that demonstrate the classes used to implement each.


11.1. Overview of the ADO.NET Architecture

The ADO.NET architecture is designed to make life easier for both the application developer and the database provider. To the developer, it presents a set of abstract classes that define a common set of methods and properties that can be used to access any data source. The data source is treated as an abstract entity, much like a drawing surface is to the GDI+ classes. Figure 11-1 depicts this concept.

Figure 11-1. ADO.NET data access options

For database providers, ADO.NET serves as a blueprint that describes the base API classes and interface specifications providers must supply with their product. Beneath the surface, the vendor implements the custom code for connecting to their database, processing SQL commands, and returning the results. Many database products, such as MySQL and Oracle, have custom .NET data provider implementations available. In addition, they have generic OLE DB versions. The .NET data provider should always be the first choice because it offers better performance and often supports added custom features. Let's look at both the OLE DB and native .NET data providers

OLE DB Data Provider in .NET

An OLE DB provider is the code that sits between the data consumer and the native API of a data source. It maps the generic OLE DB API to the data source's native APIs. It is a COM-based solution in which the data consumer and provider are COM objects that communicate through COM interfaces. Database vendors and third parties have written OLE DB providers for just about every significant data source. In contrast, far fewer .NET data providers exist. To provide a bridge to these preexisting OLE DB interfaces, .NET includes an OleDB data provider that functions as a thin wrapper to route calls into the native OLE DB. Because interoperability with COM requires switching between managed and unmanaged code, performance can be severely degraded.[1]

[1] Test results for .NET 1.1 have shown the SQL Client provider to be up to 10 times faster than OLE DB.

As we see in the next section, writing code to use OLE DB is essentially the same as working with a .NET data provider. In fact, new .NET classes provide a "factory" that can dynamically produce code for a selected provider. Consequently, responding to a vendor's upgrade from OLE DB to a custom provider should have no appreciable effect on program logic.

.NET Data Provider

The .NET data provider provides the same basic service to the client as the OLE DB provider: exposing a data source's API to a client. Its advantage is that it can directly access the native API of the data source, rather than relying on an intermediate data access bridge. Native providers may also include additional features to manipulate data types native to the data source and improve performance. For example, the Oracle provider, ODP.NET, includes adjustable settings to control connection pooling, the number of rows to be pre-fetched, and the size of non-scalar data being fetched.

Data Provider Objects for Accessing Data

A managed data provider exposes four classes that enable a data consumer to access the provider's data source. Although these classes are specific to the provider, they derive from abstract ADO.NET classes:

  • DbConnection. Establishes a connection to the data source.

  • DbCommand. Used to query or send a command to the data source.

  • DbDataReader. Provides read-only and forward-only access to the data source.

  • DBDataAdapter. Serves as a channel through which a DataSet connects to a provider.

Because these are abstract classes, the developer is responsible for specifying the vendor's specific implementation within the code. As we see next, the object names can be hard coded or provided generically by a provider factory class.

Provider Factories

Each data provider registers a ProviderFactory class and a provider string in the machine.config file. The available providers can be listed using the static GetFactoryClasses method of the DbProviderFactories class. As this code shows, the method returns a DataTable containing four columns of information about the provider.

DataTable tb = DbProviderFactories.GetFactoryClasses();

foreach (DataRow drow in tb.Rows )

{

   StringBuilder sb = new StringBuilder("");

   for (int i=0; i<tb.Columns.Count; i++)

   {

      sb.Append((i+1).ToString()).Append(drow[i].ToString());

      sb.Append("\n");

   }

   Console.WriteLine(sb.ToString());

}

Running this code for ADO.NET 2.0 lists four Microsoft written providers: Odbc, OleDb, OracleClient, and SqlClient. Figure 11-2 shows output for the SqlClient provider.

Figure 11-2. Data provider information returned by GetFactoryClasses()

To use these providers, your code must create objects specific to the provider. For example, the connection object for each would be an OdbcConnection, OleDbConnection, OracleConnection, or SqlConnection type. You can create the objects supplied by the providers directly:

SqlConnection conn = new SqlConnection();

SqlCommand cmd     = new SqlCommand();

SqlDataReader dr   = cmd.ExecuteReader();

However, suppose your application has to support multiple data sources. A switch/case construct could be used, but a better—and more flexible—approach is to use a class factory to create these objects dynamically based on the provider selected. ADO.NET provides just that—a DbProviderFactory class that is used to return objects required by a specific data provider. It works quite simply. A string containing the provider name is passed to the GetFactory method of the DbProviderFactories class. The method returns a factory object that is used to create the specific objects required by the provider. Listing 11-1 demonstrates using a factory.

Listing 11-1. Using the DbProviderFactory Class

// System.Data.Common namespace is required

DbProviderFactory factory ;

string provider = "System.Data.SqlClient";  // data provider

string connstr = "Data Source=MYSERVER;Initial Catalog=films;

           User Id=filmsadmin;Password=bogart;";

// Get factory object for SQL Server

factory = DbProviderFactories.GetFactory(provider);

// Get connection object. using ensures connection is closed.

using (DbConnection  conn = factory.CreateConnection())

{

   conn.ConnectionString = connstr;

   try

   {

      conn.Open();

      DbCommand cmd = factory.CreateCommand(); // Command object

      cmd.CommandText = "SELECT * FROM movies WHERE movie_ID=8" ;

      cmd.Connection = conn;

      DbDataReader dr;

      dr = cmd.ExecuteReader();

      dr.Read();

      MessageBox.Show((string)dr["movie_Title"]);

      conn.Close();

   }

   catch (DbException ex)

   { MessageBox.Show(ex.ToString()); }

   catch (Exception ex)

   { MessageBox.Show(ex.ToString()); }

   finally { conn.Close(); }

}

This approach requires only that a provider and connection string be provided. For example, we can easily switch this to an ODBC provider by changing two statements:

string provider= "System.Data.Odbc";

// The DSN (Data Source Name) is defined using an ODBC utility

string connstr = "DSN=movies;Database=films";

Note that the factory class provides a series of Create methods that returns the objects specific to the data provider. These methods include CreateCommand, CreateConnection, and CreateDataAdapter.


11.2. Data Access Models: Connected and Disconnected

This section offers an overview of using ADO.NET to access data stored in relational tables. Through simple examples, it presents the classes and concepts that distinguish the connected and disconnected access models.

All examples in this section—as well as the entire chapter—use data from the Films database defined in Figure 11-3. It consists of a movies table containing the top 100 movies as selected by the American Film Institute (AFI) in 1996, an actors table that lists the principal actors who performed in the movies, and an actor-movie helper table that links the two. The data is downloadable as a Microsoft Access (.mdb) file and an XML text (.xml) file.

Figure 11-3. Films database tables

[View full size image]

Connected Model

In the ADO.NET connected mode, an active connection is maintained between an application's DataReader object and a data source. A row of data is returned from the data source each time the object's Read method is executed. The most important characteristic of the connected model is that it reads data from a resultset (records returned by a SQL command) one record at a time in a forward-only, read-only manner. It provides no direct way to update or add data. Figure 11-4 depicts the relation ship between the DataReader, Command, and Connection classes that comprise the connected model.

Figure 11-4. DataReader is used in ADO.NET connected mode

Working with the DataReader typically involves four steps:

1. | The connection object is created by passing a connection string to its constructor.
2. | A string variable is assigned the SQL command that specifies the data to fetch.
3. | A command object is created. Its overloads accept a connection object, a query string, and a transaction object (for executing a group of commands).
4. | The DataReader object is created by executing the Command.ExecuteReader() method. This object is then used to read the query results one line at a time over the active data connection.

The following code segment illustrates these steps with a SqlClient data provider. The code reads movie titles from the database and displays them in a ListBox control. Note that the DataReader, Command, and Connection objects are described in detail later in this chapter.

//System.Data.SqlClient namespace is required

// (1) Create Connection

SqlConnection conn = new SqlConnection(connstr);

conn.Open();

// (2) Query string

string sql = "SELECT movie_Title FROM movies ORDER BY

movie_Year";

// (3) Create Command object

SqlCommand cmd = new SqlCommand(sql, conn);

DbDataReader rdr;

// (4) Create DataReader

rdr = cmd.ExecuteReader(CommandBehavior.CloseConnection);

while (rdr.Read())

{

   listBox1.Items.Add(rdr["movie_Title"]);  // Fill ListBox

}

rdr.Close();  // Always close datareader

The parameter to ExecuteReader specifies that the connection is closed when the data reader object is closed.

Disconnected Model

The concept behind the disconnected model is quite simple: Data is loaded—using a SQL command—from an external source into a memory cache on the client's machine; the resultset is manipulated on the local machine; and any updates are passed from data in-memory back to the data source.

The model is "disconnected" because the connection is only open long enough to read data from the source and make updates. By placing data on the client's machine, server resources—data connections, memory, processing time—are freed that would otherwise be required to manipulate the data. The drawback is the time required to load the resultset, and the memory used to store it.

As Figure 11-5 illustrates, the key components of the disconnected model are the DataApdapter and DataSet. The DataAdapter serves as a bridge between the data source and the DataSet, retrieving data into the tables that comprise the DataSet and pushing changes back to the data source. A DataSet object functions as an in-memory relational database that contains one or more DataTables, along with optional relationships that bind the tables. A DataTable contains rows and columns of data that usually derive from a table in the source database.

Figure 11-5. DataAdapter is used in ADO.NET disconnected mode

Among the numerous methods and properties exposed by the DataAdapter class, the Fill and Update methods are the two most important. Fill passes a query to a database and stores the returned set of data in a selected DataTable; Update performs a deletion, insertion, or update operation based on changes within the DataSet. The actual update commands are exposed as DataAdapter properties. The DataAdapter is presented in much more detail in Section 11.4, "DataSets, DataTables, and the Disconnected Model."

Core Note

Each data provider supplies its own data adapter. Thus, if you look through the System.Data child namespaces (SqlClient, OracleClient, Oledb), you'll find a SqlDataAdapter, OracleDataAdapter, and OleDbDataAdapter, among others. An easy way to acquire the desired adapter in your application is to call the DbProviderFactory.CreateDataAdapter method to return an instance of it.

As a simple introduction to how a DataAdapter and DataSet work together, Listing 11-2 shows how to create a DataTable, fill it with data from a database, and add it to a DataSet.

Listing 11-2. Using a DataAdapter to Load Data from a Database

string sql = "SELECT movie_Title, movie_Year FROM movies";

string connStr = " Data Source=MYSERVER;Initial Catalog=films;

                   User Id=filmsadmin;Password=bogart;";

// (1) Create data adapter object

SqlDataAdapter da = new SqlDataAdapter(sql,connStr);

// (2) Create dataset

DataSet ds = new DataSet();

// (3) Create table in dataset and fill with data

da.Fill(ds, "movies");  // Fill table with query results

DataTable dt = ds.Tables["movies"];

// (4) Add movie titles to list box

for (int i=0; i< dt.Rows.Count;i++)

{

   DataRow row = dt.Rows[i];

   listBox1.Items.Add(row["movie_Title"]);

}

The first step is to create an instance of a SqlDataAdapter by passing the select command and the connection string to its constructor. The data adapter takes care of creating the Connection object and opening and closing the connection as needed. After an empty DataSet is created, the DataAdapter's Fill method creates a table movies in the DataSet and fills it with rows of data returned by the SQL command. Each column of the table corresponds to a column in the source data table. Behind the scenes, the data transfer is performed by creating a SqlDataReader that is closed after the transfer is complete.

The data in the table is then used to populate a list box by looping through the rows of the table. As we see in the next chapter, we could achieve the same effect by binding the list control to the table—a mechanism for automatically filling a control with data from a bound source.


11.3. ADO.NET Connected Model

As described earlier, the connected model is based on establishing a connection to a database and then using commands to update, delete, or read data on the connected source. The distinguishing characteristic of this model is that commands are issued directly to the data source over a live connection—which remains open until the operations are complete. Whether working with a connected or disconnected model, the first step in accessing a data source is to create a connection object to serve as a communications pathway between the application and database.

Connection Classes

There are multiple connection classes in ADO.NET—each specific to a data provider. These include SqlConnection, OracleConnection, OleDBConnection, and OdbcConnection. Although each may include custom features, ADO.NET compatibility requires that a connector class implement the IDbConnection interface. Table 11-1 summarizes the members defined by this interface.

Table 11-1. Members of the IDbConnection Interface

Category

Name

Description

Property

ConnectionString

Gets or sets the string used to connect to a data source.

Property

ConnectionTimeout

The number of seconds to wait while trying to establish a connection to a data source before timing out.

Property

Database

Name of the database associated with the current connection.

Property

State

Current state of the connection. Returns a ConnectionState enumeration name: Broken, Closed, Connecting, Executing, Fetching, or Open.

Method

Open Close

Opens a connection. Rolls back any pending operations and closes the connection—returning it to a connection pool, if one is used.

Method

BeginTransaction

Initiates a database transaction.

Method

ChangeDatabase

Changes the current database for an open connection. The new database name is passed as string to the method.

Method

CreateCommand

Creates a command object to be associated with connection.

Core Note

Even though connection classes implement the IDbConnection interface, they do not necessarily have to provide meaningful functionality. For example, the OracleConnection class does not support the ConnectionTimeOut, Database, or ChangeDatabase members.

Connection String

The connection string specifies the data source and necessary information required to access the data source, such as password and ID. In addition to this basic information, the string can include values for fields specific to a data provider. For example, a SQL Server connection string can include values for Connection Timeout and Packet Size (size of network packet).

Table 11-2 offers a representative list of commonly used connection strings.

Table 11-2. Connection String Examples

Connection Type

Description and Use

SqlConnection

Using SQL Server authentication.

"server=MYSERVER;

uid=filmsadmin;

pwd=bogart;

database=films;"

Or

"Data Source=MYSERVER;User ID=filmsadmin;

password=bogart;Initial Catalog=films;"

SqlConnection

Using Windows authentication.

"server=MYSERVER;

database=films;

Trusted_Connection=yes"

OleDbConnection

Connects to a Microsoft Access database.

"Provider=Microsoft.Jet.OLEDB.4.0;

Data Source=c:\\movies.mdb;"

For Internet applications, you may not be able to specify a physical path. Use MapPath to convert a virtual path into a physical path:

string path=

   Server.MapPath("/data/movies.mdb");

Data Source="+path+";"

ODBC (DSN)

"DSN=movies;".

The connection string is used to create the connection object. This is typically done by passing the string to the constructor of the connection object.

string cn= "Data Source=MYSERVER;Initial Catalog=films;

            User Id=filmsadmin;Password=bogart;";

SqlConnection conn = new SqlConnection(cn);

conn.Open();   // Open connection

A connection string can also be built using a safer, object-oriented manner using one of the ConnectionStringBuilder classes supplied by a managed data provider.[2] As this code demonstrates, the values comprising the connection string are assigned to properties of this class. Internally, the object constructs a string from these properties and exposes it as a ConnectionString property.

[2] SqlClient, Oracle, OleDB, and ODBC implementations are available with ADO.NET 2.0.

SqlConnectionStringBuilder sqlBldr = new

      SqlConnectionStringBuilder();

scb.DataSource    = "MYSERVER";

// Or scp["Data Source"] = "MYSERVER";

sqlBldr.Password  = "bogart";

sqlBldr.UserID    = "filmsadmin";

sqlBldr.InitialCatalog = "films";

SqlConnection conn = new

SqlConnection(sqlBldr.ConnectionString);

conn.Open();

The ConnectionStringBuilder object is also useful for applications that input the connection string from a configuration file or other source. Setting the ConnectionString property to the connection string value exposes members that control the behavior of the connection. Table 11-3 lists selected properties of the SqlConnectionStringBuilder class.

Table 11-3. Selected Properties of the SqlConnectionStringBuilder Class

Method

Description

AsynchronousProcessing

Boolean value that indicates whether asynchronous process is permitted on the connection. The command object is responsible for making asynchronous requests.

ConnectionTimeout

Corresponds to the ConnectionTimeout property of the Connection object.

DataSource

Name or address of the SQL Server to connect to.

MaxPoolSize MinPoolSize

Sets or returns the maximum and minimum number of connections in the connection pool for a specific connection string.

Password

Password for accessing SQL Server account.

Pooling

A boolean value that indicates whether connection pooling is used.

UserID

User ID required to access a SQL Server account.

Core Note

For demonstration purposes, the connection strings in these examples are shown as cleartext within the application's code. In reality, a connection string should be stored outside of an application's assembly. For Web applications, the Web.Config file is often a reasonable choice. As described in Chapter 17, "The ASP.NET Application Environment," .NET includes a special configuration section to hold connection strings and supports techniques to encrypt the configuration information.

Desktop applications that access a central database can store the information on the client's machine (in the registry or a configuration file) or have it downloaded as part of an application's startup. The latter approach provides better scalability and security, particularly if the server returns a connection object rather than the string.

Connection Pooling

Creating a connection is a time-consuming process—in some cases taking longer than the subsequent commands take to execute. To eliminate this overhead, ADO.NET creates a pool of identical connections for each unique connection string request it receives. This enables future requests with that connection string to be satisfied from the pool, rather than by reconnecting to the server and performing the overhead to validate the connection.

There are several rules governing connection pooling that you should be aware of:

  • Connection pooling is turned on by default. It can be disabled for a SqlConnection by including "Pooling=false" in the connection string; an OleDbConnection requires "OLE DB Services=-4".

  • Each connection pool is associated with a distinct connection string. When a connection is requested, the pool handler compares the connection string with those of existing pools. If it matches, a connection is allocated from the pool.

  • If all connections in a pool are in use when a request is made, the request is queued until a connection becomes free. Connections are freed when the Close or Dispose method on a connection is called.

  • The connection pool is closed when all connections in it are released by their owners and have timed out.

Under SQL Server, you control the behavior of connection pooling by including key-value pairs in the connection string. These keywords can be used to set minimum and maximum numbers of connections in the pool, and to specify whether a connection is reset when it is taken from the pool. Of particular note is the Lifetime keyword that specifies how long a connection may live until it is destroyed. This value is checked when a connection is returned to the pool. If the connection has been open longer than its Lifetime value, it is destroyed.

This code fragment demonstrates the use of these keywords for SqlClient:

cnString = "Server=MYSERVER;Trusted_Connection=yes;

      database=films;"          +

      "connection reset=false;" +

      "connection Lifetime=60;" +  // Seconds

      "min pool size=1;"        +

      "max pool size=50";           // Default=100

SqlConnection conn = new SqlConnection(cnString);

The Command Object

After a connection object is created, the next step in accessing a database—for the connected model—is to create a command object that submits a query or action command to a data source. Command classes are made available by data providers and must implement the IDbCommand interface.

Creating a Command Object

You can use one of its several constructors to create a command object directly, or use the ProviderFactory approach mentioned in Section 11.1.

This segment demonstrates how to create a command object and explicitly set its properties:

SqlConnection conn = new SqlConnection(connstr);

Conn.open();

string sql = "insert into movies(movie_Title,movie_Year,

        movie_Director) values(@title,@yr,@bestpicture)";

SqlCommand cmd = new SqlCommand();

// Assign connection object and sql query to command object

cmd.Connection = conn;

cmd.commandText = sql;

// Fill in parameter values in query

// This is recommended over concatenation in a query string

cmd.Parameters.AddWithValue ("@title", "Schindler's List");

cmd.Parameters.AddWithValue ("@yr", "1993");

cmd.Parameters.AddWithValue ("@bestpic", "Y");

In situations where multiple data providers may be used, a provider factory provides a more flexible approach. The factory is created by passing its constructor a string containing the data provider. The factory's CreateCommand method is used to return a command object.

string provider = "System.Data.SqlClient";

DBProviderFactory factory =

   DbProviderFactories.GetFactory(provider);

DbCommand cmd   = factory.CreateCommand();

cmd.CommandText = sql;     // Query or command

cmd.Connection  = conn;    // Connection object

Note that DbCommand is an abstract class that implements the IDbCommand interface. It assumes the role of a generic command object. This can eliminate the need to cast the returned command object to a specific provider's command object such as SqlCommand. However, casting is required if you need to access custom features of a provider's command class—for example, only SqlCommand has an ExecuteXmlReader method.

Executing a Command

The SQL command assigned to the CommandText property is executed using one of the four command methods in Table 11-4.

Table 11-4. Command Execute xxx Methods

Method

Description

ExecuteNonQuery

Executes an action query and returns the number of rows affected:

cmd.CommandText = "DELETE movies WHERE movie_ID=220";

int ct = cmd.ExecuteNonQuery();

ExecuteReader

Executes a query and returns a DataReader object that provides access to the query's resultset. This method accepts an optional CommandBehavior object that can improve execution efficiency.

cmd.CommandText="SELECT * FROM movies

   WHERE movie_year > '1945';

SqlDataReader rdr= cmd.ExecuteReader();

ExecuteScalar

Executes a query and returns the value of the first column in the first row of the resultset as a scalar value.

cmd.CommandText="SELECT COUNT(movie_title)

   FROM movies";

int movieCt = (int) cmd.ExecuteScalar();

ExecuteXmlReader

Available for SQL Server data provider only. Returns an XmlReader object that is used to access the resultset. XmlReader is discussed in Chapter 10, "Working with XML in .NET."

The ExecuteReader method is the most important of these methods. It returns a DataReader object that exposes the rows returned by the query. The behavior of this method can be modified by using its overload that accepts a CommandBehavior type parameter. As an example, the following statement specifies that a single row of data is to be returned:

rdr = cmd.ExecuteReader(sql, CommandBehavior.SingleResult);

Some data providers take advantage of this parameter to optimize query execution. The list of values for the CommandBehavior enumeration includes the following:

  • SingleRow. Indicates that the query should return one row. Default behavior is to return multiple resultsets.

  • SingleResult. The query is expected to return a single scalar value.

  • KeyInfo. Returns column and primary key information. It is used with a data reader's GetSchema method to fetch column schema information.

  • SchemaOnly. Used to retrieve column names for the resultset. Example:

dr=cmd.ExecuteReader(CommandBehavior.SchemaOnly);

string col1= dr.GetName(0);  // First column name

  • SequentialAccess. Permits data in the returned row to be accessed sequentially by column. This is used with large binary (BLOB) or text fields.

  • CloseConnection. Close connection when reader is closed.

Executing Stored Procedures with the Command Object

A stored procedure is a set of SQL code stored in a database that can be executed as a script. It's a powerful feature that enables logic to be encapsulated, shared, and reused among applications. ADO.NET supports the execution of stored procedures for OleDb , SqlClient, ODBC, and OracleClient data providers.

Executing a stored procedure is quite simple: set the SqlCommand.CommandText property to the name of the procedure; set the CommandType property to the enumeration CommandType.StoredProcedure; and then call the ExecuteNonQuery method.

cmd.CommandText = "SP_AddMovie";  // Stored procedure name

cmd.CommandType = CommandType.StoredProcedure;

cmd.ExecuteNonQuery();

When a stored procedure contains input or output parameters, they must be added to the command object's Parameters collection before the procedure is executed. To demonstrate, let's execute the stored procedure shown in Listing 11-3. This procedure allows records to be fetched from the movies table as pages containing 10 rows of data. Input to the procedure is the desired page; the output parameter is the total number of pages available. This code fragment illustrates how parameters are set and how the procedure is invoked to return the first page:

SqlCommand cmd  = new SqlCommand();

cmd.CommandText = "SPMOVIES_LIST";  // Stored procedure name

cmd.CommandType = CommandType.StoredProcedure;

cmd.Parameters.Add(@PageRequest", SqlDbType.Int);

cmd.Parameters.Add(@TotalPages", SqlDbType.Int);

cmd.Parameters[0].Direction= ParameterDirection.Input;

cmd.Parameters[0].Value= 1;        // Retrieve first page

cmd.Parameters[1].Direction=ParameterDirection.Output;

cmd.CommandTimeout=10;  // Give command 10 seconds to execute

SqlDataReader rdr = cmd.ExecuteReader();

while (rdr.Read()){

//   do something with results

}

rdr.Close();  // Must close before reading parameters

int totpages=  cmd.Parameters[1].Value;

This example uses the SqlClient data provider. With a couple of changes, OleDb can be used just as easily. The primary difference is in the way they handle parameters. SqlClient requires that the parameter names match the names in the stored procedure; OleDb passes parameters based on position, so the name is irrelevant. If the procedure sends back a return code, OleDB must designate the first parameter in the list to handle it. SqlClient simply adds a parameter—the name is unimportant—that has its direction set to ReturnValue.

Listing 11-3. Stored SQL Server Procedure to Return a Page of Records

CREATE PROCEDURE SPMOVIES_LIST

   @PageRequest int,

   @TotalPages int output

AS

   /*

      Procedure to return a resultset of movies ordered

      by title.

      Resultset contains 10 movies for the specified page.

   */

   SET NOCOUNT ON

   select @TotalPages = CEILING(COUNT(*)/10) from movies

   if @PageRequest = 1 or @PageRequest <1

   begin

      select top 10 * from movies order by movie_Title

      set @PageRequest = 1

      return 0

   end

   begin

      if @PageRequest > @TotalPages

         set @PageRequest = @TotalPages

      declare @RowCount int

      set @RowCount = (@PageRequest * 10)

      exec ('SELECT * FROM

      (SELECT TOP 10 a.* FROM

      (SELECT TOP ' + @RowCount + ' * FROM movies ORDER BY

          movie_Title) a

       ORDER BY movie_Title desc) b

       ORDER BY Movie_Title')

       return 0

   end

Using Parameterized Commands Without Stored Procedures

An earlier example (see "Creating a Command Object" on page 511) used this statement to create a SQL command to store a movie in the Films database:

string sql = "insert into movies(movie_Title,movie_Year,

      bestpicture) values(@title,@yr,@bestpic)";

// Parameters set values to be stored

cmd.Parameters.AddWithValue ("@title", "Schindler's List");

cmd.Parameters.AddWithValue ("@yr", "1993");

cmd.Parameters.AddWithValue ("@bestpic", "Y");

The alternative, which uses concatenation, looks like this:

string title = "Schindler''s List";  // Two single quotes needed

string yr = "1993";

string pic = "Y";

sql = "insert into movies(movie_Title,movie_Year,

      bestpicture) values";

sql += "('"+title+"',"+yr+",'"+pic+"') ";

Not only is the parameterized version more readable and less prone to syntactical error, but it also provides a significant benefit: It automatically handles the problem of placing double single quotes ('') in a SQL command. This problem occurs when attempting to store a value such as O'Quinn, which has an embedded quote that conflicts with SQL syntax. Parameters eliminate the usual approach to search each string and replace an embedded single quote with a pair of single quotes.

DataReader Object

As we have seen in several examples, a DataReader exposes the rows and columns of data returned as the result of executing a query. Row access is defined by the IDataReader interface that each DataReader must implement; column access is defined by the IDataRecord interface. We'll look at the most important members defined by these interfaces as well as some custom features added by data providers.

Accessing Rows with DataReader

A DataReader returns a single row from a resultset each time its Read method is executed. If no rows remain, the method returns false. The reader should be closed after row processing is completed in order to free system resources. You can check the DataReader.IsClosed property to determine if a reader is closed.

Although a DataReader is associated with a single command, the command may contain multiple queries that return multiple resultsets. This code fragment demonstrates how a DataReader processes the rows returned by two queries.

string q1 = "SELECT * FROM movies WHERE movie_Year < 1940";

string q2 = "SELECT * FROM movies WHERE movie_Year > 1980";

cmd.CommandText = q1 + ";" + q2;

DbDataReader rdr = cmd.ExecuteReader();

bool readNext = true;

while (readNext)

{

   while (rdr.Read())

   {

      MessageBox.Show(rdr.GetString(1));

   }

   readNext = rdr.NextResult(); // Another resultset?

}

rdr.Close();

conn.Close();

The two things to note are the construction of the CommandString with multiple queries and the use of the Nextresult method to determine if results from another query are present.

Core Note

The DataReader has no property or method that provides the number of rows returned in its resultset. Because data is received one row at a time, the resultset could be altered by additions and deletions to the database as records are read. However, there is a HasRows property that returns TRue if the data reader contains one or more rows.

Accessing Column Values with DataReader

There are numerous ways to access data contained in the columns of the current DataReader row: as an array with column number (zero-based) or name used as an index; using the GetValue method by passing it a column number; and using one of the strongly typed Get xxx methods that include GetString, GetInt32, GetdateTime, and Getdouble. The following code segment contains an example of each technique:

cmd.CommandText="SELECT movie_ID, movie_Title FROM movies";

rdr = cmd.ExecuteReader();

rdr.Read();

string title;

// Multiple ways to access data in a column

title = rdr.GetString(1);

title = (string)rdr.GetSqlString(1); // SqlClient provider

title = (string)rdr.GetValue(1);

title = (string)rdr["movie_Title"];  // Implicit item

title = (string)rdr[1];              // Implicit item

The GetString method has the advantage of mapping the database contents to a native .NET data type. The other approaches return object types that require casting. For this reason, use of the Get methods is recommended. Note that although GetString does not require casting, it does not perform any conversion; thus, if the data is not of the type expected, an exception is thrown.

Many applications rely on a separate data access layer to provide a DataReader. In such cases, the application may require metadata to identify column names, data types, and other columnar information. Column names, which are useful for generating report headings, are readily available through the GetName method:

// List column names for a DataReader

DbDataReader rdr = GetReader();        // Get a DataReader

for (int k = 0; k < rdr.FieldCount; k++)

{

   Console.WriteLine(rdr.GetName(k));  // Column name

}

rdr.Close();

Complete column schema information is available through the GetSchemaTable method. It returns a DataTable in which there is one row for each field (column) in the resultset. The columns in the table represent schema information. This code segment demonstrates how to access all the column information for a resultset. For brevity, only three of the 24 columns of information are shown:

DataTable schemaTable = rdr.GetSchemaTable();

int ict = 0;

foreach (DataRow r in schemaTable.Rows)

{

   foreach (DataColumn c in schemaTable.Columns){

      Console.WriteLine(ict.ToString()+"

                        "+c.ColumnName + ": "+r[c]);

      ict++;

   }

}

// Selected Output:

//       0 ColumnName: movie_ID

//       1 ColumnOrdinal: 0

//       12 DataType: System.Int32


11.4. DataSets, DataTables, and the Disconnected Model

The ADO.NET disconnected model is based on using a DataSet object as an in-memory cache. A DataAdapter serves as the intermediary between the DataSet and the data source that loads the cache with data. After it has completed its task, the DataAdapter returns the connection object to the pool, thus disconnecting the data from the data source. Interestingly, the DataAdapter is actually a wrapper around a data provider's DataReader, which performs the actual data loading.

The DataSet Class

In many ways, a DataSet object plays the role of an in-memory database. Its Tables property exposes a collection of DataTables that contain data and a data schema describing the data. The Relations property returns a collection of DataRelation objects that define how tables are interrelated. In addition, DataSet methods are available to Copy, Merge, and Clear the contents of the DataSet.

Keep in mind that the DataSet and DataTable are core parts of ADO.NET and—unlike the Connection, DataReader, and DataAdapter—they are not tied to a specific data provider. An application can create, define, and populate a DataSet with data from any source.

Besides tables and their relations, a DataSet can also contain custom information defined by the application. A look at Figure 11-6 shows the major collection classes in the DataSet hierarchy. Among these is PropertyCollection, which is a set of custom properties stored in a hash table and exposed through the DataSet.ExtendedProperties property. It is often used to hold a time stamp or descriptive information such as column validation requirements for tables in the data set.

Figure 11-6. DataSet class hierarchy

The discussion of the DataSet class begins with its most important member—the DataTable collection.

DataTables

One step below the DataSet in the disconnected model hierarchy is the DataTable collection. This collection—accessed through the DataSet.Tables property—stores data in a row-column format that mimics tables in a relational database. The DataTable class has a rich set of properties and methods that make it useful as a stand-alone data source or as part of a table collection in a DataSet. The most important of these are the Columns and Rows properties, which define the layout and content of a table.

DataColumns

The DataTable.Columns property exposes a collection of DataColumn objects that represent each data field in the DataTable. Taken together, the column properties produce the data schema for the table. Table 11-5 summarizes the most important properties.

Table 11-5. Properties of the DataColumn Class

Method

Description

ColumnName

Name of the column.

DataType

Type of data contained in this column.

Example: col1.DataType = System.Type.GetType("System.String")

MaxLength

Maximum length of a text column. -1 if there is no maximum length.

ReadOnly

Indicates whether the values in the column can be modified.

AllowDBNull

Boolean value that indicates whether the column may contain null values.

Unique

Boolean value that indicates whether the column may contain duplicate values.

Expression

An expression defining how the value of a column is calculated.

Example: colTax.Expression = "colSales * .085";

Caption

The caption displayed in the user interface.

DataTable

The name of the DataTable containing this column.

DataTable columns are created automatically when the table is filled with the results of a database query or from reading an XML file. However, for applications that fill a table dynamically—such as from user input or real-time data acquisition—it may be necessary to write the code that defines the table structure. It's a worthwhile exercise in its own right that enhances a developer's understanding of the DataSet hierarchy.

The following segment creates a DataTable object, creates DataColumn objects, assigns property values to the columns, and adds them to the DataTable. To make things interesting, a calculated column is included.

DataTable tb = new DataTable("Order");

DataColumn dCol = new DataColumn("ID",

      Type.GetType("System.Int16"));

dCol.Unique = true;  // ID must be unique for each data row

dCol.AllowDBNull = false;

tb.Columns.Add(dCol);

dCol= new DataColumn("Price", Type.GetType("System.Decimal"));

tb.Columns.Add(dCol);

dCol=new DataColumn("Quan",Type.GetType("System.Int16"));

tb.Columns.Add(dCol);

dCol= new DataColumn("Total",Type.GetType("System.Decimal"));

dCol.Expression= "Price * Quan";

tb.Columns.Add(dCol);

// List column names and data type

foreach (DataColumn dc in tb.Columns)

{

   Console.WriteLine(dc.ColumnName);

   Console.WriteLine(dc.DataType.ToString());

}

Note that the ID column is defined to contain unique values. This constraint qualifies the column to be used as a key field in establishing a parent-child relationship with another table in a DataSet. To qualify, the key must be unique—as in this case—or defined as a primary key for the table. You assign a primary key to a table by setting its PrimaryKey field to the value of an array containing the column(s) to be used as the key. Here is an example that specifies the ID field a primary key:

DataColumn[] col = {tb.Columns["ID"]};

tb.PrimaryKey = col;

We'll see how to use a primary key to create table relationships and merge data later in this section.

Core Note

If a primary key consists of more than one column—such as a first and last name—you can enforce a unique constraint on these columns in three steps: by creating an array to hold the columns, creating a UniqueConstraint object by passing the array to its constructor; and adding the constraint to the data table's Constraints collection:

DataColumn[] cols = {tb.Columns["fname"]

                     tb.Columns["lname"]};

tb.Constraints.Add(new UniqueConstraint("nameKey", cols));

DataRows

Data is added to a table by creating a new DataRow object, filling it with column data, and adding the row to the table's DataRow collection. Here is an example that places data in the table created in the preceding example.

DataRow row;

row = tb.NewRow();      // Create DataRow

row["Title"] = "Casablanca";

row["Price"] = 22.95;

row["Quan"] = 2;

row["ID"] = 12001;

tb.Rows.Add(row);       // Add row to Rows collection

Console.WriteLine(tb.Rows[0]["Total"].ToString()); // 45.90

A DataTable has methods that allow it to commit and roll back changes made to the table. In order to do this, it keeps the status of each row in the DataRow.RowState property. This property is set to one of five DataRowState enumeration values: Added, Deleted, Detached, Modifed, or Unchanged. Let's extend the preceding example to demonstrate how these values are set:

tb.Rows.Add(row);                    // Added

tb.AcceptChanges();                  // ...Commit changes

Console.Write(row.RowState);         // Unchanged

tb.Rows[0].Delete();                 // Deleted

// Undo deletion

tb.RejectChanges();                  // ...Roll back

Console.Write(tb.Rows[0].RowState);  // Unchanged

DataRow myRow;

MyRow = tb.NewRow();                 // Detached

The two DataTable methods AcceptChanges and RejectChanges are equivalent to the commit and rollback operations in a database. These apply to all changes made from the time the table was loaded or since AcceptChanges was previously invoked. In this example, we are able to restore a deleted row because the deletion is not committed before RejectChanges is called. Note that the changes are to the data table—not the original data source.

For each column value in a row, ADO.NET maintains a current and original value. When RejectChanges is called, the current values are set to the original values. The opposite occurs if AcceptChanges is called. The two sets of values can be accessed concurrently through the DataRowVersion enumerations Current and Original:

DataRow r = tb.Rows[0];

r["Price"]= 14.95;

r.AcceptChanges();

r["Price"]= 16.95;

Console.WriteLine("Current: {0} Original: {1} ",

      r["Price",DataRowVersion.Current],

      r["Price",DataRowVersion.Original]);

// output:  Current: 16.95  Original: 14.95

Keeping track of table row changes takes on added importance when the purpose is to update an underlying data source. We'll see later in this section how the DataAdapter updates database tables with changes made to DataTable rows.

Loading Data into a DataSet

Now that we have seen how to construct a DataTable and punch data into it row-by-row, let's look at how data and a data schema can be automatically loaded from a relational database into tables in a DataSet. For details on loading XML data, refer to Section 11.5, "XML and ADO.NET," on page 533.

Using the DataReader to Load Data into a DataSet

A DataReader object can be used in conjunction with a DataSet or DataTable to fill a table with the rows generated by a query. This requires creating a DataReader object and passing it as a parameter to the DataTable.Load method:

cmd.CommandText = "SELECT * FROM movies WHERE movie_Year < 1945";

DBDataReader rdr =

      cmd.ExecuteReader(CommandBehavior.CloseConnection);

DataTable dt = new DataTable("movies");

dt.Load(rdr);   // Load data and schema into table

Console.WriteLine(rdr.IsClosed);   // True

The DataReader is closed automatically after all of the rows have been loaded. The CloseConnection parameter ensures that the connection is also closed.

If the table already contains data, the Load method merges the new data with the existing rows of data. Merging occurs only if rows share a primary key. If no primary key is defined, rows are appended. An overloaded version of Load takes a second parameter that defines how rows are combined. This parameter is a LoadOption enumeration type having one of three values: OverwriteRow, PreserveCurrentValues, or UpdateCurrentValues. These options specify whether the merge operation overwrites the entire row, original values only, or current values only. This code segment illustrates how data is merged with existing rows to overwrite the current column values:

cmd.CommandText = "SELECT * FROM movies WHERE movie_Year < 1945";

DBDataReader rdr = cmd.ExecuteReader( );

DataTable dt = new DataTable("movies");

dt.Load(rdr);                    // Load rows into table

Console.Write(dt.Rows[0]["movie_Title"]);  // Casablanca

// Assign primary key so rows can be merged

DataColumn[] col = new DataColumn[1];

col[0] = dt.Columns["movie_ID"];

dt.PrimaryKey = col;

DataRow r = dt.Rows[0];           // Get first row of data

r["movie_Title"] = "new title";   // Change current column value

// Since reader is closed, must fill reader again

rdr = cmd.ExecuteReader(CommandBehavior.CloseConnection);

// Merge data with current rows. Overwrites current values

dt.Load(rdr, LoadOption.UpdateCurrentValues );

// Updated value has been overwritten

Console.Write(dt.Rows[0]["movie_Title"]);  // Casablanca

Using the DataAdapter to Load Data into a DataSet

A DataAdapter object can be used to fill an existing table, or create and fill a new table, with the results from a query. The first step in this process is to create an instance of the DataAdapter for a specific data provider. As the following code shows, several constructor overloads are available:

// (1) The easiest: a query and connection string as arguments

String sql = "SELECT * FROM movies";

SqlDataAdapter da = new SqlDataAdapter(sql, connStr);

// (2) Assign a command object to the SelectCommand property

SqlDataAdapter da = new SqlDataAdapter();

SqlConnection conn = new SqlConnection(connStr);

da.SelectCommand = new SqlCommand(sql,conn);

// (3) Pass in a query string and connection object

SqlConnection conn = new SqlConnection(connStr);

SqlDataAdapter da = new SqlDataAdapter(sql, conn);

Of these, the first version is the simplest. It accepts two strings containing the query and connection. From these, it constructs a SqlCommand object that is assigned internally to its SelectCommand property. Unlike the other constructors, there is no need to write code that explicitly creates a SqlCommand or SqlConnection object.

In the overloads that accept a connection object as a parameter, the opening and closing of the connection is left to the DataAdapter. If you add a statement to explicitly open the connection, you must also include code to close it. Otherwise, the DataAdapter leaves it open, which locks the data in the database.

After the DataAdapter object is created, its Fill method is executed to load data into a new or existing table. In this example, a new table is created and assigned the default name Table:

DataSet ds = new DataSet();

// Create DataTable, load data, and add to DataSet

// Could use da.Fill(ds,"movies") to specify table name.

int numrecs = da.Fill(ds);  // Returns number of records loaded

For an existing table, the behavior of the Fill command depends on whether the table has a primary key. If it does, those rows having a key that matches the key of the incoming data are replaced. Incoming rows that do not match an existing row are appended to the DataTable.

Using the DataAdapter to Update a Database

After a DataAdapter has loaded data into a table, the underlying connection is closed, and subsequent changes made to the data are reflected only in the DataSet—not the underlying data source. To apply changes to the data source, a DataAdapter is used to restore the connection and send the changed rows to the database. The same DataAdapter used to fill the DataSet can be used to perform this task.

The DataAdapter has three properties—InsertCommand, DeleteCommand, and UpdateCommand—that are assigned the actual SQL commands to perform the tasks that correspond to the property name. These commands are executed when the Upate method of the DataAdapter is invoked. The challenge lies in creating the SQL commands that post the changes and assigning them to the appropriate DataAdapter properties. Fortunately, each data provider implements a CommandBuilder class that can be used to handle this task automatically.

The CommandBuilder Object

A CommandBuilder object generates the commands necessary to update a data source with changes made to a DataSet. It's amazingly self-sufficient. You create an instance of it by passing the related DataAdapter object to its constructor; then, when the DataAdapter.Update method is called, the SQL commands are generated and executed. The following segment shows how changes to a DataTable are flushed to the database associated with the DataAdapter:

DataTable dt= ds.Tables["movies"]; // Shortcut to reference table

// (1) Use command builder to generate update commands

SqlCommandBuilder sb = new SqlCommandBuilder(da);

// (2) Add movie to table

DataRow drow = dt.NewRow();

drow["movie_Title"] = "Taxi Driver";

drow["movie_Year"] = "1976";

dt.Rows.Add(drow);

// (3) Delete row from table

dt.Rows[4].Delete();

// (4) Edit Column value

dt.Rows[5]["movie_Year"] = "1944";

// (5) Update underlying Sql Server table

int updates = da.Update(ds, "movies");

MessageBox.Show("Rows Changed: " +updates.ToString());  // 3

There are a couple of restrictions to be aware of when using the CommandBuilder: The Select command associated with the DataAdapter must refer to a single table, and the source table in the database must include a primary key or a column that contains unique values. This column (or columns) must be included in the original Select command.

Core Note

You can create your own update commands without using a CommandBuilder. Although it can be a lengthy process, it can also yield more efficient commands. For applications that require considerable database updates, you may want to consult an ADO.NET book for details on coding update logic directly.

Synchronizing the DataSet and the DataBase

As demonstrated in this example, the use of a DataAdapter simplifies and automates the process of updating a database—or any data store. However, there is a rock in this snowball: the problem of multi-user updates. The disconnected model is based on optimistic concurrency, an approach in which the rows of the underlying data source are not locked between the time they are read and the time updates are applied to the data source. During this interval, another user may update the data source. Fortunately, the Update method recognizes if changes have occurred since the previous read and fails to apply changes to a row that has been altered.

There are two basic strategies for dealing with a concurrency error when multiple updates are being applied: roll back all changes if a violation occurs, or apply the updates that do not cause an error and identify the ones that do so they can be reprocessed.

Using Transactions to Roll Back Multiple Updates

When the DataAdapter.ContinueUpdateonErrors property is set to false, an exception is thrown when a row update cannot be completed. This prevents subsequent updates from being attempted, but does not affect updates that occurred prior to the exception. Because updates may be interdependent, applications often require an all-or-none strategy. The easiest way to implement this strategy is to create a .NET transaction in which all of the update commands execute. To do so, create a SqlTransaction object and associate it with the SqlDataAdapater.SelectCommand by passing it to its constructor. If an exception occurs, the transaction's Rollback method is used to undo any changes; if no exceptions occur, the Commit method is executed to apply all the update commands. Listing 11-4 is an example that wraps the updates inside a transaction.

Listing 11-4. Using Transaction to Roll Back Database Updates

SqlDataAdapter da = new SqlDataAdapter();

SqlCommandBuilder sb = new SqlCommandBuilder(da);

SqlTransaction tran;

SqlConnection conn = new SqlConnection(connStr);

conn.Open();      // Must open to use with transaction

// (1) Create a transaction

SqlTransaction tran = conn.BeginTransaction();

// (2) Associate the SelectCommand with the transaction

da.SelectCommand = new SqlCommand(sql, conn, tran);

DataSet ds = new DataSet();

da.Fill(ds, "movies");

//

// Code in this section makes updates to DataSet rows

try

{

   int updates = da.Update(ds, "movies");

   MessageBox.Show("Updates: "+updates.ToString());

}

// (3) If exception occurs, roll back all updates in transaction

catch (Exception ex)

{

   MessageBox.Show(ex.Message);   // Error updating

   if (tran != null)

   {

      tran.Rollback();      // Roll back any updates

      tran = null;

      MessageBox.Show("All updates rolled back.");

   }

}

finally

{

// (4) If no errors, commit all updates

   if (tran != null)

   {

       tran.Commit();

       MessageBox.Show("All updates successful. ");

       tran = null;

   }

}

conn.Close();

Identifying Rows That Cause Update Errors

When DataAdapter.ContinueUpdateonErrors is set to TRue, processing does not halt if a row cannot be updated. Instead, the DataAdapter updates all rows that do not cause an error. It is then up to the programmer to identify the rows that failed and determine how to reprocess them.

Rows that fail to update are easily identified by their DataRowState property (discussed earlier in the description of DataRows). Rows whose update succeeds have a value of Unchanged; rows that fail have their original Added, Deleted, or Modified value. A simple code segment demonstrates how to loop through the rows and identify those that are not updated (see Listing 11-5).

Listing 11-5. Identify Attempts to Update a Database That Fails

// SqlDataAdapter da loads movies table

da.ContinueUpdateOnError = true;

DataSet ds = new DataSet();

try

{

   da.Fill(ds, "movies");

   DataTable dt = ds.Tables["movies"];

   SqlCommandBuilder sb = new SqlCommandBuilder(da);

   // ... Sample Update operations

   dt.Rows[29].Delete();                      // Delete

   dt.Rows[30]["movie_Year"] = "1933";        // Update

   dt.Rows[30]["movie_Title"] = "King Kong";  // Update

   dt.Rows[31]["movie_Title"] = "Fantasia";   // Update

   DataRow drow = dt.NewRow();

   drow["movie_Title"] = "M*A*S*H";

   drow["movie_Year"] = "1970";

   dt.Rows.Add(drow);                         // insert

   // Submit updates

   int updates = da.Update(ds, "movies");

   // Following is true if any update failed

   if (ds.HasChanges())

   {

      // Load rows that failed into a DataSet

      DataSet failures = ds.GetChanges();

      int rowsFailed = failures.Rows.Count;

      Console.WriteLine("Update Failures: "+rowsFailed);

      foreach (DataRow r in failures.Tables[0].Rows )

      {

         string state = r.RowState.ToString());

         // Have to reject changes to show deleted row

         if (r.RowState == DataRowState.Deleted)

               r.RejectChanges();

         string ID= ((int)r["movie_ID"]).ToString();

         string msg= state + " Movie ID: "+ID;

         Console.WriteLine(msg);

      }

   }

Note that even though the delete occurs first, it does not affect the other operations. The SQL statement that deletes or updates a row is based on a row's primary key value—not relative position. Also, be aware that updates on the same row are combined and counted as a single row update by the Update method. In this example, updates to row 30 count as one update.

Handling concurrency issues is not a simple task. After you identify the failures, the next step—how to respond to the failures—is less clear, and depends on the application. Often times, it is necessary to re-read the rows from the database and compare them with the rows that failed in order to determine how to respond. The ability to recognize RowState and the current and original values of rows is the key to developing code that resolves update conflicts.

Defining Relationships Between Tables in a DataSet

A DataRelation is a parent/child relationship between two DataTables. It is defined on matching columns in the two tables. The columns must be the same DataType, and the column in the parent table must have unique values. The syntax for its constructor is

public DataRelation(

   string relationName,

   DataColumn parentColumn,

   DataColumn childColumn)

A DataSet has a Relations property that provides access to the collection of DataRelations defined for tables contained in the DataSet. Use the Relations.Add method to place relations in the collection. Listing 11-6 illustrates these ideas. It contains code to set up a parent/child relationship between the directors and movies tables in order to list movies by each director.

Listing 11-6. Create a Relationship Between the Directors and Movies Tables

DataSet ds = new DataSet();

// (1) Fill table with movies

string sql = "SELECT movie_ID,movie_Title,movie_DirectorID,

      movie_Year FROM movies";

SqlConnection conn = new SqlConnection(connStr);

SqlCommand cmd = new SqlCommand();

SqlDataAdapter da = new SqlDataAdapter(sql, conn);

da.Fill(ds, "movies");

// (2) Fill table with directors

sql = "SELECT director_id,(first_name + ' '+ last_name) AS

      fullname FROM directors";

da.SelectCommand.CommandText = sql;

da.Fill(ds, "directors");

// (3) Define relationship between directors and movies

DataTable parent = ds.Tables["directors"];

DataTable child = ds.Tables["movies"];

DataRelation relation = new DataRelation("directormovies",

      parent.Columns["director_ID"],

      child.Columns["movie_DirectorID"]);

// (4) Add relation to DataSet

ds.Relations.Add(relation);

// (5) List each director and his or her movies

foreach (DataRow r in parent.Rows)

{

   Console.WriteLine(r["fullname"];      // Director name

   foreach (DataRow rc in

      r.GetChildRows("directormovies"))

   {

      Console.WriteLine("  "+rc["movie_title"]);

   }

}

/*

   Sample Output:

      David Lean

         Lawrence of Arabia

         Bridge on the River Kwai, The

      Victor Fleming

         Gone with the Wind

         Wizard of Oz, The

*/

Relations and Constraints

When a relationship is defined between two tables, it has the effect of adding a ForeignKeyConstraint to the Constraints collections of the child DataTable. This constraint determines how the child table is affected when rows in a parent table are changed or deleted. In practical terms, this means that if you delete a row in the parent table, you can have the related child row(s) deleted—or optionally, have their key value set to null. Similarly, if a key value is changed in the parent table, the related rows in the child can have their key value changed or set to null.

The rule in effect is determined by the value of the DeleteRule and UpdateRule properties of the constraint. These can take one of four Rule enumeration values:

  • Cascade. Deletes or updates related rows in child table. This is the default.

  • None. Takes no action.

  • SetDefault. Sets key values in child rows to column's default value.

  • SetNull. Sets key values in child rows to null.

This code segment illustrates how constraints affect the capability to add a row to a child table and delete or change a row in the parent table. The tables from the preceding example are used.

// (1) Try to add row with new key to child table

DataRow row = child.NewRow();

row["movie_directorID"] = 999;

child.Rows.Add(row);   // Fails – 999 does not exist in parent

// (2) Delete row in parent table

row = parent.Rows[0];

row.Delete();      // Deletes rows in child having this key

// (3) Relax constraints and retry adding row

ds.EnforceConstraints = false;

row["movie_directorID"] = 999;

child.Rows.Add(row);   // Succeeds

ds.EnforceConstraints = true;    // Turn back on

// (4) Change constraint to set rows to null if parent changed

((ForeignKeyConstraint)child.Constraints[0]).DeleteRule =

       Rule.SetNull ;

Note that setting the EnforceConstraints property to false turns off all constraints—which in database terms eliminates the check for referential integrity. [3] This allows a movie to be added even though its movie_DirectorID column (foreign key) does not have a corresponding row in the directors table. It also permits a director to be deleted even though a movie by that director exists in the movies table. This clearly compromises the integrity of the database and should be used only when testing or populating individual tables in a database.

[3] The foreign key in any referencing table must always refer to a valid row in the referenced table.

Choosing Between the Connected and Disconnected Model

The DataReader and DataSet offer different approaches to processing data—each with its advantages and disadvantages. The DataReader provides forward-only, read-only access to data. By processing a row at a time, it minimizes memory requirements. A DataSet, on the other hand, offers read/write access to data, but requires enough memory to hold a copy of the data fetched from a data source. From this, you can derive a couple of general rules: If the application does not require the capability to update the data source and is used merely for display and selection purposes, a DataReader should be the first consideration; if the application requires updating data, a DataSet should be considered.

Of course, the general rules have to be weighed against other factors. If the data source contains a large number of records, a DataSet may require too many resources; or if the data requires only a few updates, the combination of DataReader and Command object to execute updates may make more sense. Despite the gray areas, there are many situations where one is clearly preferable to the other.

A DataSet is a good choice when the following apply:

  • Data need to be serialized and/or sent over the wire using HTTP.

  • Multiple read-only controls on a Windows Form are bound to the data source.

  • A Windows Form control such as a GridView or DataView is bound to an updatable data source.

  • A desktop application must edit, add, and delete rows of data.

A DataReader is a good choice when the following apply:

  • A large number of records must be handled so that the memory requirements and time to load make a DataSet impracticable.

  • The data is read-only and bound to a Windows or Web Form list control.

  • The database is highly volatile, and the contents of a DataSet might be updated often.


11.5. XML and ADO.NET

Just as relational data has a schema that defines its tables, columns, and relationships, XML uses a Schema Definition language (XSD) to define the layout of an XML document. Its main use is to validate the content of XML data. See Section 10.1, "Working with XML," for a discussion of XML Schema.

The XML classes reside in the System.Xml namespace hierarchy and are not part of ADO.NET. However, the ADO.NET DataSet class provides a bridge between the two with a set of methods that interacts with XML data and schemas:

  • ReadXML. Loads XML data into a DatSet.

  • WriteXml and GetXml. Writes the DataSet's contents to an XML formatted stream.

  • WriteXmlSchema and GetXmlSchema. Generates an XML Schema from the DataSet schema.

  • ReadXmlSchema. Reads an XML Schema file and creates a database schema.

  • InferXmlSchema. Creates a DataSet schema from XML data.

  • GetXml and GetXmlSchema, Returns a string containing the XML representation of the data or the XSD schema for XML representation.

We'll first look at examples that show how to write XML from a DataSet. This XML output is then used as input in subsequent examples that create a DataSet from XML.

Using a DataSet to Create XML Data and Schema Files

When working with XML, the DataSet is used as an intermediary to convert between XML and relational data. For this to work, the XML data should be structured so that it can be represented by the relationships and row-column layout that characterizes relational data.

The following code segment illustrates how easy it is to create an XML data file and schema from a DataSet's contents. A DataAdapter is used to populate a DataSet with a subset of the movies table. The WriteXml and WriteXmlSchema methods are then used to translate this to XML output.

DataSet ds = new DataSet("films");

DataTable dt = ds.Tables.Add("movies");

string sql = "SELECT * FROM movies WHERE bestPicture='Y'";

SqlDataAdapter da = new SqlDataAdapter(sql, conn);

da.Fill(dt);

// Write Schema representing DataTable to a file

ds.WriteXmlSchema("c:\\oscars.xsd");  // create schema

// Write Table data to an XML file

ds.WriteXml("c:\\oscarwinners.xml");  // data in xml format

/* To place schema inline with XML data in same file:

   ds.WriteXml(("c:\\oscarwinners.xml",

              XmlWriteMode.WriteSchema);

*/

The schema output shown in Listing 11-7 defines the permissible content of an XML document (file). If you compare this with Figure 11-3 on page 502, you can get a general feel for how it works. For example, each field in the movies table is represented by an element containing the permissible field name and type.

Listing 11-7. XML Schema from Movies Table—oscars.xsd

<?xml version="1.0" encoding="utf-16"?>

<xs:schema  xmlns=""

xmlns:xs="http://www.w3.org/2001/XMLSchema"

xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">

  <xs:element name="films" msdata:IsDataSet="true">

    <xs:complexType>

      <xs:choice minOccurs="0" maxOccurs="unbounded">

        <xs:element name="movies">

          <xs:complexType>

            <xs:sequence>

              <xs:element name="movie_ID" type="xs:int"

                        minOccurs="0" />

              <xs:element name="movie_Title" type="xs:string"

                        minOccurs="0" />

              <xs:element name="movie_Year" type="xs:int"

                        minOccurs="0" />

              <xs:element name="movie_DirectorID" type="xs:int"

                        minOccurs="0" />

              <xs:element name="AFIRank" type="xs:int"

                        minOccurs="0" />

              <xs:element name="bestPicture" type="xs:string"

                        minOccurs="0" />

            </xs:sequence>

          </xs:complexType>

        </xs:element>

      </xs:choice>

    </xs:complexType>

  </xs:element>

</xs:schema>

Listing 11-8 displays an abridged listing of the XML version of the relational data. The name of the DataSet is the root element. Each row in the table is represented by a child element (movies) containing elements that correspond to the columns in the data table.

Listing 11-8. Movies Data as an XML Document—oscarwinners.xml

<?xml version="1.0" encoding="utf-16"?>

<?xml version="1.0" standalone="yes"?>

<films>

  <movies>

    <movie_ID>5</movie_ID>

    <movie_Title>Citizen Kane </movie_Title>

    <movie_Year>1941</movie_Year>

    <movie_DirectorID>1</movie_Director>

    <AFIRank>1</AFIRank>

    <bestPicture>Y</bestPicture>

  </movies>

  <movies>

    <movie_ID>6</movie_ID>

    <movie_Title>Casablanca </movie_Title>

    <movie_Year>1942</movie_Year>

    <movie_DirectorID>2</movie_Director>

    <AFIRank>2</AFIRank>

    <bestPicture>Y</bestPicture>

  </movies>

  ...

</films>

Creating a DataSet Schema from XML

Each ADO.NET DataSet has a schema that defines the tables, table columns, and table relationships that comprise the DataSet. As we saw in the preceding example, this schema can be translated into an XML schema using the WriteXmlSchema method. ReadXmlSchema mirrors the process—adding tables and relationships to a DataSet. In this example, the XML schema for the movies table (refer to Listing 11-7) is used to create a DataSet schema:

DataSet ds = new DataSet();

ds.ReadXmlSchema("c:\\oscars.xsd");

DataTable tb = ds.Tables[0];

// List Columns for table

string colList = tb.TableName +": ";

for (int i = 0; i < tb.Columns.Count; i++)

   { colList += tb.Columns[i].Caption + "  "; }

Console.WriteLine(colList);

/* output is:

movies: movie_ID  movie_Title  movie_Year movie_DirectorID

        bestpicture AFIRank

*/

It is also possible to create a schema by inferring its structure from the XML data or using a DataAdapter to configure the schema:

// (1) Create schema by inferring it from XML data

ds.Tables.Clear();   // Remove tables from DataSet

ds.InferXmlSchema("c:\\oscarwinners.xml",null);

// (2) Create schema using Data Adapter

ds.Tables.Clear();

string sql = "SELECT * FROM movies";

SqlDataAdapter da = new SqlDataAdapter(sql, connStr);

// Creates DataTable named "movies"

da.FillSchema(ds, SchemaType.Source, "movies");

Core Note

By creating the DataSet schema(s) in a separate step from reading in XML data, you can control the data that is read from the source XML file. Only data for the columns defined by the schema are read in. Conversely, if the schema defines more columns than are in the XML file, these columns are empty in the DataSet.

Reading XML Data into a DataSet

The DataSet.ReadXml method provides a way to read either data only or both the data and schema into a DataSet. The method has several overloads that determine whether a schema is also created. The two overloads used with files are

XmlReadMode ReadXml(string XMLfilename);

XmlReadMode ReadXml(string XMLfilename, XmlReadMode mode);

Parameters:

XMLfilename | Name of file (.xml) containing XML data.
mode | One of the XmlReadMode enumeration values.

The XmlReadMode parameter merits special attention. Its value specifies how a schema is derived for the table(s) in a DataSet. It can specify three sources for the schema: from a schema contained (inline) in the XML file, from the schema already associated with the DataSet, or by inferring a schema from the contents of the XML file. Table 11-6 summarizes how selected enumeration members specify the schema source. The numbers in the table indicate the order in which a schema is selected. For example, ReadSchema specifies that the inline schema is the first choice; if it does not exist, the schema associated with the DataSet is used; if neither exists, a data table is not built.

Table 11-6. XmlReadMode Values Determine How a Schema Is Derived for a DataSet

Schema Source

XmlReadMode

Inline

DataSet

Infer

Comment

Auto

1

2

3

The default when no XmlReadMode is provided.

IgnoreSchema

1

Uses only the DataSet's schema. Data in the file that is not in the schema is ignored.

InferSchema

1

Ignores inline schema, and builds tables by inference from XML file. Error occurs if DataSet already contains conflicting schema.

ReadSchema

1

2

If tables created from inline schema already exist in DataSet, an exception is thrown.

The code segment in Listing 11-9 loads an XML file into a DataSet and then calls a method to display the contents of each row in the table created. Because the DataSet does not have a predefined schema, and the file does not include an inline schema, ReadXml infers it from the contents of the file.

Listing 11-9. Using ReadXml to Load XML Data into a DataSet

//  Load XML data into dataset and create schema if one does

//  not exist

DataSet ds = new DataSet();

ds.ReadXml("c:\\oscarwinners.xml");

// Save source of data in dataset

ds.ExtendedProperties.Add("source", "c:\\oscarwinners.xml");

ShowTable(ds.Tables[0]);

// Display each row in table

private void ShowTable(DataTable t)

{

   foreach(DataRow dr in t.Rows)

   {

      StringBuilder sb = new StringBuilder("Table: ");

      sb.Append(t.TableName).Append("\n");

      foreach(DataColumn c in t.Columns)

      {

          sb.Append(c.Caption).Append(": ");

          sb.Append(dr[c.ColumnName].ToString()).Append("\n");

      }

      Console.WriteLine(sb.ToString());

}

Note the use of ExtendedProperties to store the name of the data source in the data set. Because this collection of custom properties is implemented as a Hashtable, it is accessed using that syntax:

string src = (string)ds.ExtendedProperties["source"];

ds.ExtendedProperties.Clear(); // clear hash table

Using ReadXml with Nested XML Data

The XML file used in the preceding example has a simple structure that is easily transformed into a single table: The <movies> tag (refer to Listing 11-8) represents a row in a table, and the elements contained within it become column values. Most XML is more complex than this example and requires multiple tables to represent it. Although ReadXml has limitations (it cannot handle attributes), it can recognize nested XML and render multiple tables from it. As an example, let's alter the oscarwinners.xml file to include a <director> tag within each <movies> block.

<films>

  <movies>

    <movie_ID>5</movie_ID>

    <movie_Title>Citizen Kane </movie_Title>

    <movie_Year>1941</movie_Year>

    <director>

      <first_name>Orson</first_name>

      <last_name>Welles</last_name>

    </director>

    <bestPicture>Y</bestPicture>

    <AFIRank>1</AFIRank>

  </movies>

  ... more movies here

</films>

Next, run this code to display the contents of the table(s) created:

DataSet ds = new DataSet();

ds.ReadXml("c:\\oscarwinnersv2.xml");

foreach (DataTable dt in ds.Tables)

   ShowTable(dt);

Figure 11-7 depicts the DataSet tables created from reading the XML file. It creates two tables, automatically generates a movies_ID key for each table, and assigns values to this key, which link a row in the movies table to an associated row in the director table.

Figure 11-7. DataSet tables and relationship created from XML

    Previous Section < Day Day Up > Next Section


11.6. Summary

ADO.NET supports two database connectivity models: connected and disconnected. The connected model remains connected to the underlying database while traversing a resultset in a forward-only read-only manner; the disconnected model can retrieve a resultset into an in-memory cache and disconnect itself from the source data. Two distinctly separate data storage objects are available for implementing these models: the DataReader and the DataSet. The DataReader serves up data a row at a time; the DataSet functions as an in-memory relational database. Changes to the contents of the DataSet can be posted back to the original data source using the DataAdapter object. This object includes properties and methods designed to address synchronization issues that arise when disconnected data is used to update a database.

Although XML classes are not part of the ADO.NET namespaces, a level of interaction between relational and XML data is provided through the ADO.NET DataSet class. This class includes WriteXmlSchema and WriteXml methods that are used to create an XML schema and document. The versatile DataSet.ReadXml method has several overloads that are used to construct a DataSet from an XML data file or schema.


11.7. Test Your Understanding

1:

What four classes must a .NET data provider supply?

2:

Given the following code:

SqlCommand cmd = new SqlCommand();

cmd.Connection = conn;

string s = "insert into movies(movie_title, movie_year,

            bestpicture)values(&p1,&p2,&p3); "

cmd.CommandText= s;

which of the following code segments completes the SQL query string to insert the movie row?

  1.
string p1="Star Wars";

int p2 = 1977;

string p3= "N";

  2.
cmd.Parameters.AddWithValue ("@p1","Star Wars");

cmd.Parameters.AddWithValue ("@p2", "1977");

cmd.Parameters.AddWithValue ("@p3", "N");

  3.
cmd.Parameters.Add("@p1", SqlDbType.NVarChar, 100);

cmd.Parameters.Add("@p2", SqlDbType.NVarChar, 4);

cmd.Parameters.Add("@p3", SqlDbType.NVarChar, 1);

p1.Value = "Star Wars";

p2.Value = "1977";

p3.Value = "N";

3:

Describe the purpose of these three command object methods:

ExecuteNonQuery

ExecuteReader

ExecuteScalar

4:

Compare the role of the DataReader and the DataAdapter.

5:

What is the difference between a DataSet and a DataTable?

6:

The DataRowState property maintains the status of a row. Which of these is not a valid DataRowState value?

  1.
Added

  2.
Deleted

  3.
Rejected

  4.
Changed

7:

You have an XML file and want to create a DataSet that has rows and columns that match the layout of the XML file. The XML file does not have a schema (.xsd) file associated with it. What DataSet method is used to create the DataSet schema?


Chapter 12. Data Binding with Windows Forms Controls

Topics in This Chapter

  • DataBinding Overview: Associating data with a control is easy on the surface; however, it is important to understand what's going on underneath. This section provides an overview of simple and complex data binding, one-way and two-way data binding, and the role of the Binding and BindingManagerBase classes.

  • A Data Binding Application: A Windows Forms application illustrates how to bind data in a DataSet and DataTable to simple and complex controls.

  • The DataGridView: The DataGridView introduced with .NET 2.0 has a rich set of features that enable it to display and manipulate relational data. Examples illustrate how to create a master-detail grid and a virtual mode grid.

Chapter 11, "ADO.NET." discussed how to access data using ADO.NET. This chapter extends the discussion to describe the techniques by which data is "bound" to the Windows Forms controls that display data. Because all controls derive their data binding capabilities from the base Control class, knowledge of its properties and methods can be applied to all controls. Although many of the same concepts apply to Web controls, data binding for them is discussed separately in Chapter 16, "ASP.NET Web Forms and Controls."

Data binding comes in two flavors: simple and complex. Controls that contain one value, such as a label or Textbox, rely on simple binding. Controls populated with rows of data, such as a ListBox, DataGrid, or DataGridView, require complex binding. We'll look at how both are implemented.

Of the Windows Forms controls that bind to data, the DataGridView is the most complex and useful. Its layout maps directly to the rows and columns of a relational database or similarly structured XML document. This chapter takes a detailed look at the properties and methods of this control, and provides examples of how this control can be used to implement common database applications.


12.1. Overview of Data Binding

Data binding provides a way to link the contents of a control with an underlying data source. The advantage to this linkage or "binding" is that changes to the immediate data source can be reflected automatically in data controls bound to it, and changes in the data control are posted automatically to the intermediate data source. The term intermediate data source is used to distinguish it from the original data source, which may be an external database. The controls cannot be bound directly to a data source over an active connection. Binding is restricted to the in-memory representation of the data. Figure 12-1 shows the basic components of the binding model: the original data source, the intermediate storage, and the Form controls that are bound to values in the local storage through a binding object. Let's examine the model in more detail.

Figure 12-1. Multiple controls bound to a single data source

Simple Data Binding

Simple data binding, which is available to all controls, links a data source to one or more properties of a control. A good example is the Textbox control that exposes easily recognizable properties such as Text, Width, and BackColor. An application can set these dynamically by binding them to a data source. Here is a code segment that creates an object whose public properties are mapped to the properties on the TextBox.

// Create object (width, text, color)

TextParms tp = new TextParms(200, "Casablanca", Color.Beige);

// Bind text and BackColor properties of control

txtMovie.DataBindings.Add("Text", tp, "Tb_Text");

txtMovie.DataBindings.Add("BackColor", tp, "Tb_Background");

// Or create binding and then add in two steps

Binding binding = new Binding("Width", tp, "Tb_Width");

txtMovie.DataBindings.Add(binding);

The DataBindings.Add method creates a collection of bindings that links the data source to the control's properties. The method's syntax is

DataBindings.Add( control property, data source, data member)

control property | Property on the control that is being bound.
data source | Object that contains data being bound to control.
data member | Data member on the data source that is being used. Set this to null if the data source's ToString() method provides the value.

A control may have multiple bindings associated with it, but only one per property. This means that the code used to create a binding can be executed only once; a second attempt would generate an exception. To avoid this, each call to add a binding should be preceded with code that checks to see if a binding already exists; if there is a binding, it should be removed.

if (txtMovie.DataBindings["Text"] != null)

    txtMovie.DataBindings.Remove(txtMovie.DataBindings["Text"]);

txtMovie.DataBindings.Add("Text", tp, "Tb_Text");

Binding to a List

The true value of data binding becomes obvious when the data source contains multiple items to be displayed. In the preceding example, the control was bound to a single object. Let's now create an array of these objects—each representing a different movie. Instead of binding to a single object, the control is bound to the array (see Figure 12-2). The control can still only display a single movie title at a time, but we can scroll through the array and display a different title that corresponds to the current array item selected. This scrolling is accomplished using a binding manager, which is discussed shortly.

Figure 12-2. Binding TextBox properties to objects in a list

This example creates an ArrayList of objects that are used to set the TextBox properties on the fly.

ArrayList tbList = new ArrayList();

// Beige color indicated movie won oscar as best picture

tbList.Add(new TextParms(200,"Casablanca",Color.Beige));

tbList.Add(new TextParms(200, "Citizen Kane", Color.White));

tbList.Add(new TextParms(200, "King Kong", Color.White));

// Bind to properties on the Textbox

txtMovie.DataBindings.Add("Text", tbList, "Tb_Text");

txtMovie.DataBindings.Add("BackColor", tbList,

                          "Tb_Background");

txtMovie.DataBindings.Add("Width", tbList, "Tb_Width");

The one difference in the bindings from the preceding example is that the data source now refers to the ArrayList. By default, the TextBox takes the values associated with the first item in the array. When the index of the array points to the second row, the displayed value changes to "Citizen Kane".

Simple Binding with ADO.NET

Binding to a table in a DataSet is basically the same as binding to a list. In this example, the Text property of the control is bound to the movie_Year column in a DataTable.

ds = new DataSet("films");

string sql = "select * from movies order by movie_Year";

da = new SqlDataAdapter(sql, conn);

da.Fill(ds,"movies");      // create datatable "movies"

// Bind text property to movie_Year column in movies table

txtYr.DataBindings.Add("Text", ds,"movies.movie_Year");

Although the control could be bound directly to a DataTable, the recommended approach is to bind the property to a DataSet and use the DataTable name as a qualifier to specify the column that provides the data. This makes it clear which table the value is coming from.

Complex Data Binding with List Controls

Complex binding is only available on controls that include properties to specify a data source and data members on the data source. This select group of controls is limited to the ListBox, CheckedListBox, ComboBox, DataGrid, and DataGridView. Complex binding allows each control to bind to a collection of data—the data source must support the IList interface—and display multiple items at once. Because the DataGridView is discussed at length in the last half of this chapter, let's look at how complex binding is implemented on the ListBox control. The details also apply to other List controls.

Binding a list control to a data source requires setting a minimum of two properties: DataSource, which specifies the source, and DisplayMember, which describes the member—usually a data column or property—in the data source that is displayed in the control. This code segment illustrates how a ListBox bound to a DataSet displays movie titles:

da.Fill(ds,"movies");

DataTable dt = ds.Tables[0];

// Minimum properties to bind listbox to a DataTable

listBox1.DataSource = ds;

listBox1.DisplayMember = "movies.movie_Title";

// Optional property that assigns a value to each item row

listBox1.ValueMember = "movies.movie_ID";

After these values are set, the list box is automatically filled. The DataSource property can be changed programmatically to fill the control with a different set of data, or it can be set to null to clear the control's content. Note also that although no Binding object is explicitly created, a DataBindings collection is created underneath and is accessible through code.

The bound list box control is often grouped with other controls, such as a text box or label, in order to display multiple values from a row of data. When the controls are bound to the same data source, scrolling through the list box causes each control to display a value from the same data row. To illustrate, let's add the following simple bindings to the preceding code:

txtStudio.DataBindings.Add("Text", ds,"movies.studio");

txtYear.DataBindings.Add("Text", ds,"movies.movie_Year");

These text boxes display the studio name and year of the movie currently selected in the list box (see Figure 12-3).

Figure 12-3. Using data binding to populate controls on a form

One-Way and Two-Way Data Binding

The data bound to a control can be changed in two ways: by updating the underlying data source, such as adding a row to a table, or by modifying the visible contents of the control. In both cases, the changes should be reflected in the associated control or data source—a process referred to as two-way data binding. In general, that is what happens. However, a control may be bound to a data source in read-only mode when its only purpose is to present data. To understand how these techniques are implemented, let's look at how updating occurs—from the perspective of the control and the data source.

Effects on the Data Source of Updating a Control Value

By default, changes made to data in a control are also made to the underlying in-memory data source. If the year value in Figure 12-3 is changed, the value in the corresponding row and column of the DataTable is also changed. Note that if the year is represented as an integer in the table, the value entered in the control must be an integer value. Data binding automatically checks types and rejects values (keeps the same value in the control) that do not match the type of the underlying data.

In the case where a control is bound to a property on an object, the property must provide write support in order for its value to be updated. For example, if the year and studio list boxes in the preceding example were bound to the following properties, respectively, only year could be updated; changes made to the studio control would be ignored and it would revert to its original value.

public int Movie_Year { set { myYear = value;  }

                        get { return myYear;   } }

// Read only property. Control cannot update this.

public string Studio  { get { return myStudio; } }

Note that changes in a control are not propagated to the data source until the user moves to another item in the GUI control. Underneath, this changes the current position within the binding manager—firing an event that causes the data to be updated.

Effects on a Control of Updating the Data Source

When a DataSet is used as the data source for controls, any additions, deletions, or changes made to the data are automatically reflected in the associated bound control(s). Custom data sources require some programming assistance to accomplish this.

If a control is bound to an object property, a change to the value of that property is not automatically sent to the control. Instead, the binding manager looks for an event named property Changed on the data source. If found, it provides a handler for this event to receive notice when that property's value changes. To enable the binding manager to handle a changed value, you must define a property Changed event on the data source class, and fire the event when a change occurs. To illustrate, let's extend the previous example to add the event to the class containing the Movie_Year property, and add code to fire the event when the property changes.

// Event to notify bound control that value has changed

public event EventHandler Movie_YearChanged;

// Property control is bound to year value

public int Movie_Year {

   set {

          myYear = value;

          // Notify bound control(s) of change

          if (Movie_YearChanged != null)

             Movie_YearChanged(this, EventArgs.Empty);

   }

   get { return myYear; }

}

The other situation to handle is when a data item is deleted from or added to the data source. Controls that are bound to the source using simple binding are updated automatically; controls using complex binding are not. In the latter case, the update can be forced by executing the Refresh method of a CurrencyManager object. As we see next, the CurrencyManager is a binding manager especially designed for list data sources.

Using Binding Managers

As illustrated in Figure 12-4, each data source has a binding manager that keeps track of all connections to it. When the data source is updated, the binding manager is responsible for synchronizing the values in all controls bound to the data. Conversely, if a value is changed on one of the bound controls, the manager updates the source data accordingly. A binding manager is associated with only one data source. Thus, if an application has controls bound to multiple data sources, each will have its own manager.

Figure 12-4. Binding managers synchronize the data source and controls

Binding requires the interaction of several objects to coordinate the two-way flow of data between a data source and control. Let's look at the four most important objects, which are denoted by numbers in Figure 12-4.

  1. Binding. Maintains a simple binding between a property on a control and a property on a single object. The following statements demonstrate how to create a binding and gain access to it:

txtYr.DataBindings.Add("Text", ds,

                       "movies.movie_Year");

Binding binding = txtYr.DataBindings["Text"];

// "txtYr"

MessageBox.Show(binding.Control.ToString());

// Create a binding manager object

BindingManagerBase mgr= binding.BindingManagerBase;

  2. CurrencyManager. This class derives from the abstract BindingManagerBase class and serves as a binding manager for list data sources such as a DataTable or Array. This object provides five members that an application can use to manage the relationship between a data source and control:

    • Bindings. Returns the collection of bindings being managed.

    • Count. The number of rows in the list that is being managed.

    • Current. Returns the current item (such as a row) in the data source as an object.

    • Position. Gets/sets the position in the data source currently indexed by the control.

    • PositionChanged. Fires when the Position in the list changes.

    • CurrentChanged. Is triggered when the bound value changes.

  3. PropertyManager. This class, which also derives from BindingManagerBase, maps the properties on an object to properties on a bound control.

  4. BindingContext. Observe in Figure 12-4 that the BindingContext is linked to a form and a collection of BindingManagerBase objects. Its job is to manage a collection of binding managers for a specific control—in this case, a Form. The control could just as well be a Panel or GroupBox on a form. A program's main interest in the BindingContext is to use it to gain access to the binding manager for a data source. These statements, for example, return the manager for the table movies.

BindingManagerBase mgr = this.BindingContext[ds,"movies"];

// Or use casting to get specific manager.

CurrencyManager mgr= (CurrencyManager)

                      this.BindingContext[ds,"movies"];

Using the BindingManagerBase to Navigate a List

Let's now look at how the members of the BindingManagerBase class are used to move through the items in a source data list and simultaneously update the contents of controls bound to the list. The example binds a list box and text box to the familiar movies data table.

// Bind listbox to a dataset.datatable

listBox1.DataSource = ds;

listBox1.DisplayMember = "movies.movie_Title";

// Bind to TextBox

txtStudio.DataBindings.Add("text", ds, "movies.studio");

// BindingManagerBase bmb has class-wide scope

bmb = this.BindingContext[ds, "movies"];

// Create delegate pointing to event handler

bmb.PositionChanged += new

      EventHandler(bmb_PositionChanged);

The following method moves to the next item in the data source list when a button is clicked. It would typically be paired with a method to move backward in the list.

// This method moves to the next row in the table.

// If at the end of the table it moves to the beginning.

private void Forward_Click (object sender, EventArgs e)

{

   if (listBox1.Items.Count > 0)

   {

      bmb.Position = bmb.Position >= bmb.Count - 1 ? 0 :

             ++bmb.Position;

   }

}

The PositionChanged event is fired each time the binding manager moves to a new position in the list. This could be triggered programmatically or by the user clicking a row in the list box control.

private void bmb_PositionChanged(object sender,

                                 EventArgs e)

{

   BindingManagerBase bmb = (BindingManagerBase)sender;

   // Item should be a DataRowView if from a table

   object ob = bmb.Current.GetType();

   if (ob == typeof(System.Data.DataRowView))

   {

      DataRowView view = (DataRowView)bmb.Current;

      // Could access: ((string)view["movie_Title"]);

   }

}

Note that the Current property is used to return an object representing the current item. The data source in this example is a data table, but the object returned is not the expected DataRow—it is a DataRowView object. It is up to the code to provide the proper casting to access properties in the selected item.


12.2. Using Simple and Complex Data Binding in an Application

Several concepts were introduced in the first section. Let's bring them together in an application that relies on data binding to display and update information from the Films database. Figure 12-5 shows a screen shot of the application's user interface.

Figure 12-5. Application combining complex and simple binding

[View full size image]

Each control on the Windows Form—except buttons—is bound to a data source. A ListBox and ComboBox illustrate complex binding; two text boxes, a CheckBox, and a PictureBox, are bound using simple binding. The controls can be bound dynamically to either a data table or an array that contains custom objects. The Scroll button moves down the list box by internally using a binding manager to advance to the next item in the data source list. Let's dissect the program by looking at code associated with the buttons. Much of the code should be familiar from code segments in the previous section and does not require further explanation.

Binding to a DataTable

The code in Listing 12-1 is executed when the Bind to Table button is clicked. It loads the necessary data from the Films database into a table and binds the controls on the form to it. This populates the ListBox and ComboBox with a list of movie titles. The value in the other controls is derived from the content of the current row (highlighted in the list box). The most interesting of these is the PictureBox, which has its BackgroundImage property bound to a column in the table containing images. Because the database does not contain images, the program adds this column to the data table and fills it with images for the movie in each row.

Listing 12-1. Binding Controls to a DataSet

// Bind control to data from a database

private void btnTableBind_Click(object sender, EventArgs e)

{

   SqlConnection conn = new SqlConnection(GetString());

   conn.Open();

   ds = new DataSet("films");

   string sql = "SELECT movie_ID, movie_title, movie_year,

         studio, afi_rank, CASE WHEN bestpicture ='Y'

         THEN 1 ELSE 0 END as BestPicture FROM movies ORDER BY

         movie_Year";

   da = new SqlDataAdapter(sql, conn);

   // Command builder keeps track of changes to data

   SqlCommandBuilder sb = new SqlCommandBuilder(da);

   da.Fill(ds,"movies");

   DataTable dt = ds.Tables[0];

   Data Column dCol = new DataColumn("movie_Image",

         Type.GetType("System.Object"));

   dt.Columns.Add(dCol);

   // Place image in new column. Name is based on movie ranking.

   Image defaultImage = Image.FromFile(@"c:\defaultimg.jpg");

   foreach (DataRow dRow in dt.Rows)

   {

      string rank = ((int)dRow["afi_rank"]).ToString();

      string imgFile = "c:\\afi" + rank + ".gif";

      try

      {

         Image imgObject = Image.FromFile(imgFile);

         dRow["movie_Image"] = imgObject;

      }

      catch (Exception ex)

      {

         dRow["movie_Image"] = defaultImage;

      }

   }

   // Nothing to this point should be considered a change

   dt.AcceptChanges();

   // Bind listbox and combobox to datasource

   listBox1.DataSource = ds;

   listBox1.DisplayMember = "movies.movie_Title";

   listBox1.ValueMember = "movies.movie_ID";

   comboBox1.DataSource = ds;

   comboBox1.DisplayMember = "movies.movie_Title";

   // Binding manager has global scope

   bmb = this.BindingContext[ds, "movies"];

   bmb.PositionChanged += new

         EventHandler(bmb_PositionChanged);

   try

   {

      // TextBox.Text  – binds to studio name

      if(txtStudio.DataBindings["text"] != null)

            txtStudio.DataBindings.Remove(

               txtStudio.DataBindings["Text"]);

      txtStudio.DataBindings.Add("text", ds, "movies.studio");

      // TextBox.Text – binds to year movie released

      if(txtYear.DataBindings["text"] != null)

            txtYear.DataBindings.Remove(

                  txtYear.DataBindings["Text"]);

      txtYear.DataBindings.Add("text", ds,

                               "movies.movie_year");

      // CheckBox.Checked - binds to best picture value (0 or 1)

      if (checkBox1.DataBindings["Checked"] != null)

            checkBox1.DataBindings.Remove(

               checkBox1.DataBindings["Checked"]);

      checkBox1.DataBindings.Add("Checked", ds,

            "movies.BestPicture");

      // PictureBox.BackgroundImage – Binds to image

      if (pictureBox1.DataBindings["BackgroundImage"] != null)

            pictureBox1.DataBindings.Remove(

               pictureBox1.DataBindings["BackgroundImage"]);

      pictureBox1.DataBindings.Add("BackgroundImage", ds,

            "movies.movie_Image");

   }

   catch (Exception ex)

   {

      MessageBox.Show(ex.Message);

   }

}

Binding Controls to an ArrayList

Clicking the Bind to Array button, binds the controls to an ArrayList that is filled with instances of the custom class MyMovie (see Listing 12-2). After the data source is created, the binding process is identical to that followed with the data set.

Listing 12-2. Binding Controls to an Array of Objects

// Bind control to array populated with instances of custom class

private void BindToArray()

{

   movieList = new ArrayList();

   Image movieImg = Image.FromFile(@"c:\defaultimg.jpg");

   // Create objects and add to array

   movieList.Add(new MyMovie("2","Casablanca",1942,

         "Warner Bros.",true, Image.FromFile("c:\afi2.gif")));

   movieList.Add(new MyMovie("1","Citizen Kane", 1941,

         "RKO", false,

         Image.FromFile("c:\afi1.gif")));

   movieList.Add(new MyMovie("4","Gone with the Wind", 1941,

         "Selznick International", true,

         Image.FromFile("c:\afi4.gif")));

   //

   listBox1.DataSource =  movieList;

   listBox1.DisplayMember = "Movie_Title";

   //

   comboBox1.DataSource = movieList;

   comboBox1.DisplayMember = "Movie_Title";

   bmb = this.BindingContext[movieList]; ;

   bmb.PositionChanged += new

         EventHandler(bmb_PositionChanged);

   if (txtStudio.DataBindings["Text"] != null)

         txtStudio.DataBindings.Remove(

               txtStudio.DataBindings["Text"]);

   txtStudio.DataBindings.Add("Text", movieList, "Studio");

   //

   if (txtYear.DataBindings["Text"] != null)

         txtYear.DataBindings.Remove(

               txtYear.DataBindings["Text"]);

   txtYear.DataBindings.Add("Text", movieList, "Movie_Year");

   //

   if (checkBox1.DataBindings["Checked"] != null)

         checkBox1.DataBindings.Remove(

            checkBox1.DataBindings["Checked"]);

   checkBox1.DataBindings.Add("Checked", movieList,

                              "BestPicture");

   //

   if (pictureBox1.DataBindings["BackgroundImage"] != null)

         pictureBox1.DataBindings.Remove(

            pictureBox1.DataBindings["BackgroundImage"]);

   pictureBox1.DataBindings.Add("BackgroundImage", movieList,

         "Movie_Image");

}

When designing a custom class to be used as a data source, the primary consideration is whether the bindable properties provide read-only or read/write access. If they are read-only, the only requirement is that they be public. For properties that can be updated, the class must expose and fire an event to which the binding can subscribe. Recall that the name of this event is propertyname Changed. This event is fired in the Set block of the property (see Listing 12-3).

Listing 12-3. Custom Data Source Class

// Bind control to array populated with instances of

// custom class

public class MyMovie

{

   private string myID;

   private string myTitle;

   private int myYear;

   private string myStudio;

   private bool myBestPicture;

   private Image myImage;

   //

   public event EventHandler Movie_YearChanged;

   public event EventHandler StudioChanged;

   public MyMovie(string id, string title, int year,

                  string studio,

         bool bp, Image img)

   {

      myTitle = title;

      myYear = year;

      myStudio = studio;

      myBestPicture = bp;

      myImage = img;

      myID = id;

   }

   // Only public properties can be bound to control

   public string Movie_Title  { get { return myTitle; } }

   // Make read/write so update can occur

   public int Movie_Year {

      get { return myYear; }

      set {

         myYear = value;

         if (Movie_YearChanged != null)

             Movie_YearChanged(this, EventArgs.Empty);

      }

   }

   public string Studio {

      get { return myStudio; }

      set {

         myStudio = value;

         if (StudioChanged != null) StudioChanged(this,

             EventArgs.Empty);

      }

   }

   public Image  Movie_Image  { get { return myImage; } }

   public bool   BestPicture  { get { return myBestPicture; } }

}

Adding an Item to the Data Source

Clicking the Add Movie button causes information about a single movie to be added to the data source (see Listing 12-4). If the source is a table, a row is added; if an array, an object is created and inserted. An addition to a data table is automatically pushed to the control and made visible. When a custom object is added, the Refresh method of the CurrencyManager must be executed to synchronize the control. Note that Refresh is specific to the CurrencyManager class and not available on BindingManagerBase.

Listing 12-4. Add an Item to a Data Source

// Test effects of adding a new item to the data source

private void button2_Click(object sender, EventArgs e)

{

   if (ds != null)

   {

      // Add a row to the table

      DataTable dt = ds.Tables[0];

      DataRow dRow = dt.NewRow();

      dRow["movie_ID"] = 99;

      dRow["movie_Title"] = "Rear Window";

      dRow["movie_Year"] = "1954";

      dRow["studio"] = "Paramount";

      dRow["BestPicture"] = 0;

      dRow["afi_rank"] = 42;

      Image defaultImage = Image.FromFile(@"c:\afi42.gif");

      dRow["movie_Image"] = defaultImage;

      dt.Rows.Add(dRow);

   }

   else

   {

      Image movieImg = Image.FromFile(@"c:\afi42.gif");

      movieList.Add(new MyMovie("42", "Rear Window", 1954,

            "Paramount", false, movieImg));

      // Refresh() is needed to display item in ListBox/ComboBox

      CurrencyManager cm =

             (CurrencyManager)this.BindingContext[movieList];

      cm.Refresh();

   }

}

Identifying Updates

The rows in a table have a RowState property that can be used to determine if a value in the row has been changed (discussed in Chapter 11). This method checks the value of that property for each row in the data source table. If the value is DataRowState.Modified, each column in the row is checked to determine which values have changed (see Listing 12-5). This routine can be used to determine whether an update to the original database is necessary. Observe that the method checks only for data changes. You can easily extend it to check for deletions and additions.

Listing 12-5. Check Data Source for Any Updates

// Checks status of each row in data table to identify any

// changes. This works only when data source is a Data Table.

private bool DataIsDirty(DataTable dt){

   bool result = false;

   foreach(DataRow drw in dt.Rows){

      // Check all rows in the table for a modified state

      if(drw.RowState == DataRowState.Modified)

      {

         string msg = (string)drw["movie_Title"]+":";

         string curr;

         string orig;

         // Examine each column in the row for a change

         foreach(DataColumn col in dt.Columns)

         {

            curr= drw[col,

               DataRowVersion.Current].ToString().Trim();

            orig= drw[col,

               DataRowVersion.Original].ToString().Trim();

            if(!curr.Equals(orig) || curr != orig ||

               string.CompareOrdinal(curr,orig) !=0)

            {

               msg += "\r\n" + orig + " " + curr;

               result=true;

            }

         }

         MessageBox.Show(msg);   // Display changes in a row

      }

   }

   return result;

}

Update Original Database with Changes

When the modifiable data source is a data table, the Update method of its associated DataAdapter can be used to flush changes to the database. This topic is discussed in detail in Section 11.4, "DataSets, DataTables, and the Disconnected Model."

try

{

   int updates = da.Update(ds, "movies");

   MessageBox.Show("Updates: "+updates.ToString());

}

catch (Exception ex)

{

   MessageBox.Show(ex.Message);

}


12.3. The DataGridView Class

The DataGridView control, introduced with .NET 2.0, supersedes the DataGrid—which now exists primarily for legacy purposes. With more than a hundred properties and methods, the DataGridView is by far the most complex Windows Forms control for displaying data. Accordingly, it is also the most flexible. Styles that govern appearance can be applied on a cell-by-cell basis, by rows, by columns, or across all cells in the grid. Cells are not limited to text. They may contain a TextBox, Image, CheckBox, Link, or Button control.

Data binding is supported by the DataSource property, just as with the controls defined in the previous section. In addition, the DataGridView provides a unique virtual mode that permits it to handle more than 100,000 rows of data. DataGridView methods, events, and properties allow an application to easily manage the mapping between virtual and physical storage.

All of these features are discussed in this section. We'll look at selected properties and events along with code examples that illustrate their use.

Properties

Despite its myriad features, the DataGridView has an elegantly simple structure. As shown in Figure 12-6, in its most elemental form, it consists of column headers, row headers, and cells. To these, we can add the Columns and Rows collections that allow an application to access the grid by indexing a row or column. That is the foundation. Each property and event discussed in this section relates to one of these five classes.

Figure 12-6. Basic DataGridView elements

The DataGridView class inherits many of its properties from the Control class; to these, it adds properties required to support its own special characteristics and behavior. The properties listed in Table 12-1 are primarily in this latter category. The list is not meant to be exhaustive; instead, it presents those properties you'll refer to most frequently when implementing a grid.

Table 12-1. Selected Properties of the DataGridView Class

Category

Property Name

Description

User functionality

AllowUserToAddRows

AllowUserToDeleteRows

Indicates whether a user may add/delete rows. Default: true.

AllowUserToOrderColumns

Indicates whether user can rearrange columns.

ColumnHeadersHeightResizable

Indicates whether the user can change the height of the column headers. Default: TRue.

MultiSelect

Indicates whether user may select one or more rows at a time.

SelectionMode

Indicates the cells selected when clicking any individual or header cell.

enum DataGridViewSelectionMode values:

ColumnHeaderSelect

RowHeaderSelect

FullColumnSelect

FullRowSelect

CellSelect

ReadOnly

Indicates whether user can modify data in cells. Values: TRue or false.

Appearance

AlternatingRowsDefaultCellStyle

Gets or sets the default cell style applied to the odd numbered rows in the grid.

BackColor

The background color of the grid.

BackgroundColor

Gets or sets the background color for the area of the grid not containing data cells or column/row headers.

BorderStyle

Gets or sets the border style for the DataGridView.

enum BorderStyle values:

BorderStyle.Fixed3D

BorderStyle.FixedSingle

BorderStyle.None

CellBorderStyle

Gets or sets the border style used for cells in the grid.

enum DataGridViewCellBorderStyle values:

(to draw between rows)

SingleHorizontal

SunkenHorizontal

RaisedHorizontal

(to draw between columns)

SingleVertical

SunkenVertical

RaisedVertical

(to place a border between rows and columns)

SingleSunken

Raised

ColumnCount

Gets or sets the number of columns in the DataGridView.

ColumnHeadersBorderStyle

RowHeadersBorderStye

Border style applied to to column/row headers.

enum DataGridViewHeaderBorderStyle values:

Custom     Raised     Sunk

None       Single

ColumnHeadersVisible

RowHeadersVisible

Displays or suppresses headers. Values: true or false.

ColumnHeaderDefaultCellStyle

Defines cell style properties for column header cells.

DefaultCellStyle

DataGridViewCellStyle object that defines the default cell style properties for cells. Note that this includes column header cells.

FirstDisplayedCell

The first cell displayed in the grid, usually upper-left corner.

GridColor

The color of the lines separating the cells.

Collections

Columns

Collection of all grid columns. Individual columns are accessed by an index: Columns[iI].

Rows

Collection of all grid rows. Individual rows are accessed by an index: Rows[i].

SelectedColumns

Collection of columns selected.

SelectedRows

Collection of rows selected.

SelectedCells

Collection of cells selected.

Constructing a DataGridView

Listing 12-6 shows how to define columns for a DataGridView, set properties to define its appearance and behavior, and add rows of data. (We'll see in the succeeding example how to use the more common approach of loading data from a database.)

Note that the column header cells and data cells have different styles. If a style is not set for the header, it uses the same DefaultCellStyle as the data cells.

Listing 12-6. Setting DataGridView Properties and Adding Rows of Data

// Set properties of a DataGridView and fill with data

private void CreateGrid()

{

   // (1) Define column headers

   dataGridView1.ColumnCount = 3;

   dataGridView1.Columns[0].HeaderText = "Movie Title";

   dataGridView1.Columns[1].HeaderText = "Year";

   dataGridView1.Columns[2].HeaderText = "Director";

   dataGridView1.Columns[1].Name  = "Year";

   dataGridView1.Columns[0].Width = 150;

   dataGridView1.Columns[1].Width = 40;

   dataGridView1.Columns[2].Width = 110;

   // (2) Define style for data cells

   DataGridViewCellStyle style = new DataGridViewCellStyle();

   style.BackColor = Color.Bisque;

   style.Font = new Font("Arial", 8, FontStyle.Bold);

   style.ForeColor = Color.Navy;

   //                         (left,top,right,bottom)

   style.Padding = new Padding(5, 2, 5, 5);

   style.SelectionBackColor = Color.LightBlue;

   dataGridView1.DefaultCellStyle = style;

   // (3) Define style for column headers

   DataGridViewCellStyle styleHdr = new

         DataGridViewCellStyle();

   styleHdr.Padding = new Padding(1, 1, 1, 1);

   styleHdr.BackColor = Color.OldLace;

   styleHdr.ForeColor = Color.Black;

   dataGridView1.ColumnHeadersDefaultCellStyle = styleHdr;

   // (4) Define user capabilities

   dataGridView1.AllowUserToAddRows      = false;

   dataGridView1.AllowUserToOrderColumns = false;

   dataGridView1.AllowUserToDeleteRows   = false;

   // (5) Place data in grid manually (datasource is better)

   object[] row1 = {"Casablanca", "1942","Michael Curtiz"};

   dataGridView1.Rows.Add(row1);

   object[] row2 = {"Raging Bull","1980","Martin Scorsese"};

   dataGridView1.Rows.Add(row2);

   object[] row3 = {"On the Waterfront","1954","Elia Kazan"};

   dataGridView1.Rows.Add(row3);

   object[] row4 = {"Some Like it Hot","1959","Billy Wilder"};

   dataGridView1.Rows.Add(row4);

}

Figure 12-7 shows the DataGridView created by this code.

Figure 12-7. DataGridView built from code in Listing 12-6

DataBinding with a DataGridView

A DataGridView is bound to a data source using complex binding. As in our list box example, the DataSource property specifies the data source. The similarity ends there, however, because a DataGridView must display multiple data values. To do so, the DataMember property is set to the name of a table within the data source. The data to be displayed in each column is specified by setting the column's DataPropertyName property to the name of the underlying data table column.

// Turn this off so column names do not come from data source

dataGridView1.AutoGenerateColumns = false;

// Specify table as data source

dataGridView1.DataSource = ds;        // Dataset

dataGridView1.DataMember = "movies";  // Table in dataset

// Tie the columns in the grid to column names in the data table

dataGridView1.Columns[0].DataPropertyName = "Title";

dataGridView1.Columns[1].DataPropertyName = "Year";

dataGridView1.Columns[2].DataPropertyName = "director";

The DataGridView supports two-way data binding for ADO.NET data sources: Changes made to the grid are reflected in the underlying table, and changes made to the table are reflected in the grid. For example, this code responds to a button click by adding a new row to the grid's data source. The addition is immediately reflected in the control. However, if we try to add a row directly to the DataGridView, an exception occurs because adding directly to a bound control is not permitted.

private void buttonAdd_Click(object sender, EventArgs e)

{

   // Adding to underlying table is okay

   r[0] = "TAXI";

   r[1] = "1976";

   r[2] = "Martin Scorsese";

   dt.Rows.Add(r);

   // Adding directly to DataGridView does not work

   object[] row = {"The Third Man", "1949", "Orson Welles"};

   DataRow r = dt.NewRow();

   DataGridView1.Rows.Add(row4);  // Fails!

}

Updating the original database from which a grid is loaded can be done by issuing individual SQL commands or using a DataAdapter. The discussion in the previous section applies.

Core Note

A DataGridView may have a mixture of bound and non-bound columns. Thus, columns can be added to a bound control, but rows cannot.

Setting the Row Height

The default height of rows in a DataGridView is based on accommodating a single line of text. If the row contains large sized fonts or images, they are truncated. It is usually better to force the grid to take the size of each cell in the row into account and base the overall height on the tallest cell. That's the role of the grid's AutoSizeRows method. Its simplest overloaded version takes a single parameter—a DataGridViewAutoSizeRowsMode enumeration value—that indicates the criterion used for setting row height. The two most useful enumeration members are ColumnAllRows, which bases the row height on all columns in the row, and ColumnsDisplayedRows, which applies the same criterion, but to visible rows only.

dataGridView1.AutoSizeRows(

      DataGridViewAutoSizeRowsMode.ColumnsAllRows);

The AutoSizeRows method sets the row size when it is executed. If subsequent updates cause the height of cells in a row to change, the row height does not adjust to the changes. Also, if a row is sortable, clicking a column header to sort the grid causes all rows to revert to the default row height. Fortunately, the DataGridView has an AutoSizeRowsMode property that causes row heights to automatically adjust to changes in grid content.

dataGridView1.AutoSizeRowsMode =

      DataGridViewAutoSizeRowsMode.HeaderAndColumnsAllRows;

Note that this statement does not take effect until the AutoSizeRows method is executed, and that it prevents users from manually resizing rows.

Working with Columns and Column Types

The DataGridView is not a full-blown spreadsheet, but it does offer some features a user expects from a spreadsheet. These include the following:

  • Frozen Column(s). For a grid that requires horizontal scrolling, it is often useful to "freeze" columns so that they always remain on the screen. Setting a column's Frozen property to true has the effect of freezing it and all columns to its left.

 dataGridView1.Columns[0].Frozen = true;

  • ReadOnly Columns. Selected column can be made read-only.

 dataGridView1.Columns[2].ReadOnly = true;

  • Minimum Width. By default, a user can widen and narrow columns in a grid. The minimum size permitted for a column can be controlled by setting the MinimumWidth property to a value in pixels:

 dataGridView1.Columns[0].MinimumWidth=100;

  • Sorting. By default, clicking a column header sorts the rows based on values in that column—if the column contains sortable values. It's SortMode property can be used to disable sorting:

 dataGridView1.Columns[0].SortMode =

              DataGridViewColumnSortMode.NotSortable;

  • Multiple Column Types. Six predefined column classes are available that can be used to represent information in a grid, using the familiar formats of the TextBox, CheckBox, Image, Button, ComboBox, and Link. The name for each of these controls follows the format DataGridView Controlname Column.

    This code segment adds a column of buttons to a grid. The first step is to create an instance of the column class. Its characteristics and data values—if any—are then set. Finally, the Columns.Add method is used to add the column to the grid's column collection.

// (1) Create instance of column type

DataGridViewButtonColumn buttons = new

      DataGridViewButtonColumn();

// Text to place in column header

buttons.HeaderText = "Delete";

// (2) Set characteristics of control

buttons.Text = "Delete";   // Default text for button

buttons.FlatStyle = FlatStyle.Standard;

// Create a datagridview cell to use as a template to set

// all buttons in the column to the same style.

buttons.CellTemplate = new DataGridViewButtonCell();

buttons.CellTemplate.Style.BackColor = Color.Yellow ;

buttons.CellTemplate.Style.Font = new Font("Arial", 8);

// Specify column position on grid

buttons.DisplayIndex = 1;

// (3) Add column to grid

dataGridView.Columns.Add(buttons);

Any of the column types may be bound to a data source. Although a button is usually set manually, it can be bound to a property in the grid's data source in two ways:

// Use the DataGridviewButtonColumn class

buttons.DataPropertyName = "Title";

// Use the Columns class (button is in column 1 of the grid)

dataGridView3.Columns[1].DataPropertyName = "Title";

Buttons provide a convenient way for a user to select a grid row and trigger an action such as a pop-up form that displays further information related to the row. Buttons located in grid cells, however, have no direct event, such as a Click, associated with them. Instead, events are associated with an action on the overall grid or specific cells on the grid. By identifying a cell for instance, an event handler can determine which button is clicked.

Events

Just about every mouse and cursor movement that can occur over a DataGridView can be detected by one of its events. In addition, events signify when data is changed, added, or deleted. Table 12-2 provides a summary of the most useful events. Accompanying the table is a list of the delegate used to implement these events. (See Appendix B for a complete list of events.)

Table 12-2. Selected DataGridView Events

Category

Event (Delegate)

Description

Cell actions

CellValueChanged (1)

Occurs when the value of a cell changes.

CurrentCellChanged (3)

Occurs when the value of the current cell changes

CellClick (1)

Occurs when any part of the cell is clicked. This includes cell borders and padding.

CellContentClick (1)

Occurs only if the cell content is clicked.

CellEnter (1)

CellLeave (1)

Occurs when cell receives/loses input focus.

CellFormatting (5)

Occurs prior to formatting a cell for display.

CellMouseClick (2)

CellMouseDoubleClick (2)

Occurs whenever a mouse clicks/double clicks anywhere on a cell.

CellMouseDown (2)

CellMouseUp (2)

Occurs when a mouse button is pressed/raised while it is over a cell

CellMouseEnter (1)

CellMouseLeave (1)

Occurs when the mouse pointer enters or leaves a cell's area.

CellPainting (6)

Raised when a cell is to be painted.

Column actions

ColumnHeaderMouseClick (2)

ColumnHeaderMouseDouble-Click (2)

Occurs when a column header is clicked/double clicked.

Row actions

RowEnter (1)

RowLeave (1)

Occurs when a row receives/loses the input focus.

RowHeaderMouseClick (2)

RowHeaderDoubleMouse-Click (2)

Occurs when a user clicks/double clicks a row header

UserAddedRow (4)

UserDeletedRow (4)

Occurs when a user adds/deletes a row in the grid.

Data error

DataError (7)

Occurs when an external data parsing or validation operations fails. Typically occurs due to an attempt to load invalid data into a data grid cell.

The following are delegates associated with events in Table 12-2:

(1) public sealed delegate void DataGridViewCellEventHandler(

      object sender, DataGridViewCellEventArgs e)

(2) public sealed delegate void DataGridViewCellM_useEventHandler(

      object sender, DataGridViewCellMouseEventArgs e)

(3) public sealed delegate void EventHandler(

      object sender, EventHandlerArgs e)

(4) public sealed delegate void DataGridViewRowEventHandler (

      object sender, DataGridViewRowEventArgs e)

(5) public sealed delegate void

      DataGridViewCellFormattingEventHandler(

         object sender, DataGridViewCellFormattingEventArgs e)

(6) public sealed delegate void

      DataGridViewCellPaintingEventHandler(

         object sender, DataGridViewCellPaintingEventArgs e)

(7) public sealed delegate void

      DataGridViewDataErrorEventHandler(

         object sender, DataGridViewDataErrorEventArgs e)

Let's look at some common uses for these events.

Cell Formatting

The CellFormatting event gives you the opportunity to format a cell before it is rendered. This comes in handy if you want to distinguish a subset of cells by some criteria. For example, the grid in Figure 12-7 contains a column indicating the year a movie was released. Let's change the background color of cells in that column to red if the year is less than 1950.

// Set cells in year column to red if year is less than 1950

private void Grid3_CellFormatting(object sender,

      DataGridViewCellFormattingEventArgs e)

{

   if (this.dataGridView3.Columns[e.ColumnIndex].Name == "Year")

   {

      string yr = (string)e.Value;

      if (Int32.Parse(yr) < 1950)

      {

          e.CellStyle.ForeColor = Color.Red;

          e.CellStyle.SelectionForeColor = Color.Red;

          // Indicate that event was handled

          e.FormattingApplied = true;

      }

   }

}

The ColumnIndex property of the EventArgs parameter is used to determine if the year column is being formatted. If so, the code checks the year and formats the cell accordingly. Note that the FormattingApplied property must be set if custom formatting is performed.

Recognizing Selected Rows, Columns, and Cells

As shown in Table 12-2, selecting a cell in a grid can trigger any number of events that can be used to indicate the current cell or the cell just left. Some of the events are almost over-engineered. For example, there seems little to distinguish CellContentClick and CellClick. Others exist to recognize grid navigation using both the mouse and keyboard: The CellClick is not triggered by arrow keys; however, the CellEnter event is fired no matter how a cell is selected. All of these cell-related events have a consistent event handler signature. The EventArgs parameter provides column and row index properties to identify the cell. Here is an example:

private void Grid1_CellEnter(object sender,

      DataGridViewCellEventArgs e)

{

   // Both of these display the column index of the selected cell

   MessageBox.Show("enter "+e.ColumnIndex.ToString());

   MessageBox.Show(

         DataGridView1.CurrentCell.ColumnIndex.ToString());

}

Core Note

Although row and column header cells cannot become "current cells," they are assigned a column and row index value. Row headers always have a column index of –1, and column headers have row index of –1.

The cell events can be used to recognize a single row and column selection. However, a grid may also permit multiple row, column, and cell selections. In these cases, it is necessary to use the SelectedRows, SelectedColumns, and SelectedCells collections to access the selected grid values.

Multiple row selection is made available on a DataGridView by setting its MultiSelect property to TRue—which is the default value. A row is selected by clicking its row header. It can also be selected by clicking any cell in the row if the grid's SelectionMode property is set to DataGridViewSelectionMode.FullRowSelect. The property can also be set to FullColumnSelect, which causes a cell's column to be selected. Note that column and row selection are mutually exclusive: only one can be in effect at a time.

This segment illustrates how to iterate through the collection of selected rows. The same approach is used for columns and cells.

// Display selected row numbers and content of its column 1

if (dataGridView1.SelectedRows.Count > 0)

{

   StringBuilder sb = new StringBuilder();

   for (int i = 0; i < dataGridView1.SelectedRows.Count; i++)

   {

      sb.Append("Row: ");

      sb.Append(

            dataGridView1.SelectedRows[i].Index.ToString() );

      sb.Append( dataGridView1.SelectedRows[i].Cells[1].Value);

      sb.Append(Environment.NewLine);

   }

   MessageBox.Show (sb.ToString(), "Selected Rows");

}

Data Error Handling

The DataError event fires when a problem occurs loading data into a grid or posting data from the grid to the underlying data store. The error is quite easy to detect: compare the value of the Context property of the ErrorEventArgs parameter with the DataGridViewDataErrorContext enumeration values. Here is an example:

// Define event handler

DataGridView1.DataError += new

      DataGridViewDataErrorEventHandler(DataGridView1_DataError);

// DataError Event Handler

private void dataGridView1_DataError(object sender,

      DataGridViewDataErrorEventArgs dgError)

{

   // Context provides information about the grid when the

   // error occurred.

   MessageBox.Show("Error: " + dgError.Context.ToString());

   // Problem committing grid data to underlying data source

   if (dgError.Context == DataGridViewDataErrorContext.Commit)

   {

      MessageBox.Show("Commit error");

   }

   // Occurs when selection cursor moves to another cell

   if (dgError.Context ==

         DataGridViewDataErrorContext.CurrentCellChange)

   {

      MessageBox.Show("Cell change");

   }

   if (dgError.Context ==

         DataGridViewDataErrorContext.Parsing)

   {

      MessageBox.Show("parsing error");

   }

   // Could not format data coming from/going to data source

   if (dgError.Context ==

         DataGridViewDataErrorContext.Formatting)

   {

      MessageBox.Show("formatting error");

   }

}

Setting Up Master-Detail DataGridViews

One of the more common relationships between tables in a database is that of the master-detail relationship, where the records in the master table have multiple associated records in the detail table. DataGridViews provide a natural way of displaying this relationship. To illustrate, let's create an application based on the Films database that displays a master grid containing a list of movies and a detail grid that display actors who played in the movie selected in the first grid. To make it interesting, we'll include an image column in the movie grid that contains a picture of the Oscar statuette for movies that won for best picture.

The master grid is bound to the movies table; the details grid is bound to the actors table. Both tables, as shown in Figure 12-8, contain the columns that are bound to their respective DataGridView columns. In addition, they contain a movieID column that links the two in the master-detail relationship.

Figure 12-8. Master-detail tables

The tables and their relationships are created using the techniques described in Chapter 11:

ds = new DataSet();

DataTable dt = new DataTable("movies");  // Master

DataTable da = new DataTable("actors");  // Detail

da.Columns.Add("movieID");

da.Columns.Add("firstname");

da.Columns.Add("lastname");

//

dt.Columns.Add("movieID");

dt.Columns.Add("Title");

dt.Columns.Add("Year");

dt.Columns.Add("picture", typeof(Bitmap));  // To hold image

ds.Tables.Add(dt);

ds.Tables.Add(da);

// Define master-detail relationship

DataRelation rel = new DataRelation("movieactor",

      dt.Columns["movieID"], da.Columns["movieID"]);

ds.Relations.Add(rel);

After defining the table schemas, they are populated from the database using a DataReader object. Because the database does not contain an image—although it could—the image is inserted based on the value of the bestPicture field.

Bitmap oscar   = new Bitmap(@"c:\oscar.gif");   // Oscar image

Bitmap nooscar = new Bitmap(@"c:\nooscar.gif"); // Blank image

// Populate movies table from datareader

while (dr.Read())

{

   DataRow drow = dt.NewRow();

   drow["Title"] = (string)(dr["movie_Title"]);

   drow["Year"]  = ((int)dr["movie_Year"]).ToString();

   drow["movieID"] = (int)dr["movie_ID"];

   if ((string)dr["bestPicture"] == "Y") drow["picture"] =

         oscar; else drow["picture"] = nooscar;

   dt.Rows.Add(drow);

}

The actors table is filled with the results of the query:

sql = "SELECT am.movie_ID, actor_first,actor_last FROM actors a

      JOIN actor_movie am ON a.actor_ID = am.actor_ID";

After the tables are created and populated, the final steps are to define the grids and bind their columns to the tables. This segment adds three columns to the master grid—one of which is an image type column.

DataGridViewImageColumn vic = new DataGridViewImageColumn();

dataGridView1.Columns.Add(vic);   // Add image type column

//

dataGridView1.ColumnCount = 3;

dataGridView1.Columns[0].Name = "Oscar";

dataGridView1.Columns[1].HeaderText = "Movie Title";

dataGridView1.Columns[2].HeaderText = "Year";

Then, the binding is performed:

// Bind grids to dataset

dataGridView1.DataSource = ds;

dataGridView1.DataMember = "movies";

dataGridView2.DataSource = ds;

// ***Set to DataRelation for detail

dataGridView2.DataMember = dt.TableName+".movieactor";

// Bind grid columns to table columns

dataGridView1.Columns[0].DataPropertyName = "picture";

dataGridView1.Columns[1].DataPropertyName = "Title";

dataGridView1.Columns[2].DataPropertyName = "Year";

dataGridView1.Columns[3].DataPropertyName = "director";

dataGridView2.Columns[0].DataPropertyName = "firstname";

dataGridView2.Columns[1].DataPropertyName = "lastname";

Pay close attention to the binding of dataGridView2. It is bound to the relationship defined between the tables, rather than directly to the actors table. This binding causes the names of the movie's cast to be displayed in the grid when a movie is selected.

Figure 12-9 shows a sample screen. Much of the excluded code in this example deals with setting grid styles and capabilities. A full code listing is available in the book's code download. (See the Preface for the download URL addresses and instructions.)

Figure 12-9. Master-detail relationship

Virtual Mode

When a DataGridView is bound to a data source, the entire data source must exist in memory. This enables quick refreshing of the control's cells as a user navigates from row to row. The downside is that a large data store may have prohibitive memory requirements. To handle excessive memory requirements, a DataGridView can be run in virtual mode by setting its VirtualMode property to TRue. In this mode, the application takes responsibility for maintaining an underlying data cache to handle the population, editing, and deletion of DataGridView cells based on actions of the user. The cache contains data for a selected portion of the grid. If a row in the grid cannot be satisfied from cache, the application must load the cache with the necessary data from the original data source. Figure 12-10 compares virtual storage with binding to a DataTable.

Figure 12-10. Data binding versus virtual mode

Virtual mode implementation requires that an application handle two special virtual mode events: CellValueNeeded, which occurs when a cell value must be displayed; and CellValuePushed, which occurs when a cell's value is edited. Other events are also required to manage the data cache. These are summarized in Table 12-3.

Table 12-3. DataGridView Events Used to Implement Virtual Mode

Event

Description

NewRowsNeeded

Virtual mode event. Occurs when a row is appended to the DataGridView.

CellValueNeeded

Virtual mode event. Occurs when cell in grid needs to be displayed.

CellValuePushed

Virtual mode event. Occurs when a cell value is edited by the user.

RowValidated

Occurs when another row is selected.

UserDeletingRow

Occurs when a row is selected and the Delete key is pressed.

To illustrate the fundamentals of implementing a DataGridView in virtual mode, let's look at the code used to create the DataGridView shown in Figure 12-11.

Figure 12-11. DataGridView using virtual mode

The variables having class scope are shown here. Note that the data cache is implemented as a generics List object that holds instances of the movie class. The movie class exposes three properties that are displayed on the grid: Title, Movie_Year, and Director.

DataGridView dgv;

List<movie> movieList = new List<movie>(20);  // cache

bool rowNeeded;   // True when new row appended to grid

int storeRow = 0;

int currRow = -1; // Set to row being added

movie currMovie;  // Holds movie object for current row

Listing 12-7 shows the overhead code to initialize the DataGridView, register the event handlers, and populate the data cache (this would usually come from a database).

Listing 12-7. Virtual DataGridView: Initialization

// Set properties of a DataGridView and fill with data

dgv = new DataGridView();

// Event handlers for virtual mode events

dgv.CellValueNeeded += new

      DataGridViewCellValueEventHandler(CellNeeded);

dgv.CellValuePushed += new

      DataGridViewCellValueEventHandler(CellPushed);

dgv.NewRowNeeded += new

      DataGridViewRowEventHandler(RowNeeded);

// Event handlers always available for DataGridView

dgv.UserDeletingRow += new

      DataGridViewRowCancelEventHandler (RowDeleting);

dgv.RowValidated += new

      DataGridViewCellEventHandler( RowValidated);

dgv.VirtualMode = true;

dgv.RowCount = 5;

dgv.ColumnCount = 3;

// Headers for columns

dgv.Columns[0].HeaderText = "title";

dgv.Columns[1].HeaderText = "year";

dgv.Columns[2].HeaderText = "director";

// Fill cache. In production, this would come from database.

movieList.Add(new movie("Citizen Kane",1941,"Orson Welles"));

movieList.Add(new movie("The Lady Eve",1941,"

                        "Preston Sturges"));

// ... Add other movies here

The heart of the application is represented by the event handler methods shown in Listing 12-8. To summarize them:

  • RowNeeded. Is triggered when the user begins to add a new row at the bottom of the grid. currRow is set to the row number of any row being added.

  • CellNeeded. Is triggered when a cell needs to be redrawn. This does not require that a row be selected, but occurs as you move the cursor over cells in the grid. This routine identifies the column the cell is in and displays the data from the cache or the object that is created for new rows. Note that the MapRow() is called to translate a row in the grid to its corresponding row in the cache. In this simple example, there is always a one-to-one relationship because the cache and grid contain the same number of rows. In a production application, row 5000 in a grid might map to row 1 in the cache.

  • CellPushed. Called when a cell value is edited. This routine updates a movie object that represents the selected row with the new value.

  • RowValidated. Signals that a different row has been selected and is used to update the previous row. If the row exists in the cache, it is updated; a new row is added to the cache.

  • RowDeleting. Called when user selects a row to delete. If the row exists in the cache, it is removed.

Listing 12-8. Virtual DataGridView: Event Handlers

// Called when a new row is appended to grid

private void RowNeeded(object sender,

                       DataGridViewRowEventArgs e)

{

   rowNeeded = true;

   currRow = dgv.Rows.Count - 1;

}

// Called when a cell must be displayed/refreshed

private void CellNeeded(object sender,

                        DataGridViewCellValueEventArgs e)

{

   if (rowNeeded)

   {

      rowNeeded = false;

      currMovie = new movie();

      return;

   }

   storeRow = MapRow(e.RowIndex);

   if(storeRow >=0 && currRow  ==-1)

         currMovie =  movieList[storeRow];

   string colName = dgv.Columns[e.ColumnIndex].HeaderText;

   if(storeRow>=0)  // Refresh cell from cache

   {

      if (colName == "title")e.Value =

            movieList[storeRow].Title;

      if (colName == "year") e.Value =

            movieList[storeRow].Movie_Year.ToString();

      if (colName == "director") e.Value =

            movieList[storeRow].Director;

   } else           // refresh cell from object for new row

   {

      if (colName == "title")e.Value = currMovie.Title;

      if (colName == "year")e.Value =

            currMovie.Movie_Year.ToString();

      if (colName == "director") e.Value = currMovie.Director;

   }

}

// Cell has been updated

private void CellPushed(object sender,

                        DataGridViewCellValueEventArgs e)

{

   // Update property on movie object for this row

   storeRow = MapRow(e.RowIndex);

   string colName = dgv.Columns[e.ColumnIndex].HeaderText;

   if (colName == "title") currMovie.Title = (string)e.Value;

   if (colName == "year")

   {

      int retval;

      if(int.TryParse((string)e.Value,out retval))

            currMovie.Movie_Year = retval;

   }

   if (colName == "director") currMovie.Director =

         (string)e.Value;

}

// Occurs when user changes current row

// Update previous row in cache when this occurs

private void RowValidated(object sender,

                          DataGridViewCellEventArgs e)

{

   storeRow = MapRow(e.RowIndex);

   if (storeRow < 0) storeRow = movieList.Count;

   currRow = -1;

   if (currMovie != null)

   {

      // Save the modified Customer object in the data store.

      storeRow = MapRow(e.RowIndex);

      if (storeRow >= 0)

            movieList[storeRow] = currMovie;

      else movieList.Add(currMovie);

         currMovie = null;

   }

}

// Row selected and Del key pushed

private void RowDeleting(object sender,

                         DataGridViewRowCancelEventArgs e)

{

   if (MapRow(e.Row.Index)>=0)

   { movieList.RemoveAt(e.Row.Index); }

   if (e.Row.Index == currRow)

   {

      currRow = -1;

      currMovie = null;

   }

}

// Maps grid row to row in cache. More logic would be added

// for application that refreshes cache from database.

private int MapRow(int dgvRow)

{

   if (dgvRow < movieList.Count)return dgvRow;

      else return -1;

}

This example provides only the basic details for implementing a virtual DataGridView. The next step is to extend it to include a virtual memory manager that reloads the cache when data must be fetched from disk to display a cell.


12.4. Summary

Data binding is used to link the data displayed in a control with an underlying data source. In many cases, it can eliminate the manual code required to populate controls. There are two basic types of binding: simple and complex. Simple is used with controls that display only one value; complex is used to display multiple data values in selected controls such as a list box or data grid.

Each data source has a binding manager that keeps track of all connections to it. This manager is responsible for synchronizing values in the data store and controls bound to it. For list data sources such as an array or data table, the binding manager is a CurrencyManager object; for a property on an object, the binding manager is a PropertyManger object. Both of these objects expose methods that allow them to be used to navigate through their data source.

Of the data bound controls, the DataGridView offers the richest interface. It permits data to be displayed and manipulated in a grid format. Style classes and appearance properties enable almost all of its features to be customized. Its event members allow virtually any action involving the grid to be detected—from a click on a cell to the addition of a row. It also permits control types such as a button, image, or ComboBox to be inserted into any of its cells. Although data binding is typically used to populate a DataGridView, the control also supports a virtual mode that allows an application to manage the grid's content using a custom data cache.


12.4. Test Your Understanding

1:

Indicate whether the following are true or false:

  1. A TextBox supports complex binding.

  2. The width of a control can be bound to a data source.

  3. All controls support simple binding.

  4. A data source can only be bound to one control at a time.

  5. A data source may have multiple binding managers.

  6. Changes made to the value of a control are always propagated to the data source.

  7. Controls can be bound to custom data objects.

  8. The PropertyManager class inherits from the CurrencyManager class.

  9. Only public properties may be bound to a control.

2:

What is the difference between simple and complex binding? One-way and two-way binding?

3:

Describe how to allow a custom data source to support two-way binding.

4:

Which property and enumeration cause the entire row in a DataGridView to be highlighted when a single cell in the row is selected?

5:

Which of these cannot be included in a DataGridView cell?

  1.
TextBox

  2.
ListBox

  3.
Button

  4.
ComboBox

6:

How do you ensure that a column in a DataGridView is always displayed on the screen?


    Part III: Advanced Use of C# and the .NET Framework

    Chapter 13 Asynchronous Programming and Multithreading

    Chapter 14 Creating Distributed Applications with Remoting

    Chapter 15 Code Refinement, Security, and Deployment


Chapter 13. Asynchronous Programming and Multithreading

Topics in This Chapter

  • Asynchronous Programming: Unlike synchronous programming, in which a task can begin only when the preceding one completes, asynchronous programming permits multiple tasks to be performed simultaneously.

  • Multithreading: Multiple threads can enhance the performance of an application that can separate its tasks into operations that can run on separate threads. This section describes how a program can implement multithreading, the factors that affect thread scheduling, and when it's useful to create multiple threads.

  • Thread Synchronization: The use of multiple threads in an application raises several synchronization issues regarding how to create thread-safe code. Several .NET manual synchronization techniques are presented, including the Monitor class, the Mutex class, and the use of semaphores.

An application or component can be designed to operate in a synchronous or asynchronous manner. In the synchronous model, tasks are performed in sequence—as in a relay race, one runner (task) must complete his segment before the next one can start. In contrast, asynchronous programming permits an application to be broken into subtasks that perform concurrently. This approach (sometimes referred to as send and forget) allows one method to call another method and then continue processing without waiting for the called method to finish.

The key to asynchronous programming is the use of threads. A thread is essentially a code sequence that runs independently. This permits a program to work on multiple tasks in a parallel manner. For example, an application may use one thread to accept user input to a form, while a second thread concurrently processes a print request. When used judiciously, threads can greatly improve a program's performance and responsiveness; when used incorrectly, they can cause programs to hang or terminate without properly completing a task.

A thread is created and run by the operating system—not by .NET. What .NET does is create a wrapper around a thread so that it obeys the rules of the .NET managed environment. An asynchronous application may work indirectly or directly with threads. In the former case, delegates are used to automatically allocate and handle threads; in the latter case, a program explicitly creates instances of the Thread class and takes responsibility for synchronizing thread behavior.

The chapter begins with an overview of threads and then looks at asynchronous programming using both delegates and explicit thread creation. The final section examines the synchronization issues that arise when multiple threads are running, and introduces several synchronization techniques that can be used to enable threads to share resources.


13.1. What Is a Thread?

When an assembly (.exe file) begins execution, a primary thread is created that serves as the entry point to the application—in C#, this is an application's Main() method. The thread is the unit or agent responsible for executing code.

.NET does not physically create threads—that is the responsibility of the operating system. Instead, it provides a THRead class that serves as a managed version of the unmanaged physical thread. The THRead class, located in the System.Threading namespace, exposes properties and methods that allow a program to perform thread-related operations. These class members allow an application to create a thread, set its priority, suspend, activate or kill it, and have it run in the background or foreground.

Figure 13-1 is a simplified representation of the relationship between a process, applications, and threads. Physically, a thread consists of CPU registers, a call stack (memory used for maintaining parameter data and method calls), and a container known as Thread Local Storage (TLS) that holds the state information for a thread.

Figure 13-1. Threads contained in a process

Multithreading

In a single CPU system, only one thread can execute at a time. The order in which threads run is based on their priority. When a thread reaches the top of the priority queue, its code stream is executed for a fixed amount of time known as a time slice. If the thread does not complete execution, its state information must be stored so that the thread can later resume execution at the point it is interrupted. The state information includes registers, stack pointers, and a program counter that tells the thread which instruction is executed next. All of this information is stored in the area of memory allocated to Thread Local Storage.

Core Note

.NET provides support for multiple processor systems by permitting a process to be assigned to a processor. This is set using the ProcessAffinity property of the System.Diagnostics.Process class.

Thread Priority

As mentioned, the order in which a thread runs is based strictly on its priority. If a thread is running and a thread with a higher priority becomes available to run, the running thread is preempted to allow the higher priority thread to run. If more than one thread has the same priority, the operating system executes them in a round-robin fashion.

In .NET, a thread's Priority property is used to get or set its priority level. It may have one of five values based on the THReadPriority enum: Lowest, BelowNormal, Normal, AboveNormal, and Highest. The default is THReadPriority.Normal.

You should override thread priorities only in situations where a task has a clearly defined need to execute with a low or high priority. Using thread priorities to fine-tune an algorithm can be self-defeating for several reasons:

  • Even threads with the highest priority are subject to blocking by other threads.

  • Raising the priority of a thread can place it into competition with the operating system's threads, which can affect overall system performance.

  • An operating system keeps track of when a thread runs. If a thread has not run for a while, its priority is increased to enable it to be executed.

Foreground and Background Threads

.NET classifies each thread as either a background or foreground thread. The difference in these two types is quite simple: An application ends when all foreground threads stop; and any background threads still running are stopped as part of the shutdown process.

By default, a new thread is set to run as a foreground thread. It can be changed to background by setting its IsBackground property to true. Clearly, you only want to set this for noncritical tasks that can logically and safely end when the program does. Note that even though .NET attempts to notify all background threads when the program shuts down, it's good practice to explicitly manage thread termination.

Thread State

During its lifetime, a thread may exist in several states: It begins life in an Unstarted state; after it is started and the CPU begins executing it, it is in Running mode; when its slice of execution time ends, the operating system may suspend it; or if it has completed running, it moves into Stopped mode. Running, Stopped, and Suspended are somewhat deterministic states that occur naturally as the operating system manages thread execution. Another state, known as WaitSleepJoin, occurs when a thread must wait for resources or for another thread to complete its execution. After this blocking ends, the thread is then eligible to move into Running mode.

Figure 13-2 illustrates the states that a thread may assume and the methods that invoke these states. It is not a complete state diagram, because it does not depict the events that can lead to a thread being placed in an inconsistent state. For example, you cannot start a running thread nor can you abort a suspended thread. Such attempts cause an interrupt to be thrown.

Figure 13-2. Thread states

A thread's current state is available through a read-only property named ThreadState. This property's value is based on the ThreadState enum that defines 10 states:

Aborted        = 256   StopRequested    = 1

AbortRequested = 128   Suspended        = 64

Background     = 4     SuspendRequested = 2

Running        = 0     Unstarted        = 8

Stopped        = 16    WaitSleepJoin    = 32

If a program is not interested in a specific state, but does need to know if a thread has been terminated, the Boolean Thread.IsAlive property should be used.


13.2. Asynchronous Programming

In a synchronous (single-threaded) application, program execution follows a single path; in an asynchronous (multithreaded) version, operations occur in parallel on multiple paths of execution. This advantage of this latter approach is that slow applications, such as file I/O, can be performed on a separate thread while the main thread continues execution.

Figure 13-3 provides an abstract representation of the two techniques. In the synchronous version, each method is executed in sequence; in the asynchronous version, method B runs at the same time as A and C. This prospect of two or more tasks running (nearly) simultaneously raises a set of questions not present in a single-threaded program:

  • What type of communication between the main thread and worker thread is required? The code on the worker thread can be invoked and forgotten, or it may be necessary for the main thread to know when the task is completed.

  • How does the main thread know when the worker thread is completed? Two approaches are available: the callback technique, in which the worker thread returns control to the main thread when it is finished; or a polling approach, in which the main thread calls a method that returns the results of the worker thread execution.

  • How to synchronize thread requests for the same resources? The issues here are similar to those faced when synchronizing access to a database. The integrity of the data must be maintained and deadlock situations must be avoided.

  • How to shutdown an application while worker threads are still executing? Several choices are available: They can be terminated; the main application can continue to run until all threads finish; or the main application can end and allow the threads to continue running.

Figure 13-3. Synchronous versus asynchronous execution

Before tackling these issues, let's first look at the basics of how to write code that provides asynchronous code execution. As we see in the next section, threads can be explicitly created and used for parallel code execution. An easier approach is to use a delegate to allocate a worker thread and call a method to execute on the thread—a process referred to as asynchronous delegate invocation. Delegates can also be used to specify the callback method that a worker thread calls when it finishes execution.

Although a discussion of creating threads is deferred until later in this chapter, it's worth noting now that the threads allocated for asynchronous methods come from a pre-allocated thread pool. This eliminates the overhead of dynamically creating threads and also means they can be reused. At the same time, indiscriminate use of asynchronous calls can exhaust the thread pool—causing operations to wait until new threads are available. We'll discuss remedies for this in the section on threads.

Asynchronous Delegates

Delegates—which were introduced in Chapter 4, "Working with Objects in C#"—provide a way to notify one or more subscribing methods when an event occurs. In the earlier examples, all calls were synchronous (to methods on the same thread). But delegates can also be used to make an asynchronous call that invokes a method on a separate worker thread. Before looking at the details of this, let's review what a delegate is and how it's used.

The following code segment illustrates the basic steps involved in declaring a delegate and using it to invoke a subscribing method. The key points to note are that the callback method(s) must have the same signature as the delegate's declaration, and that multiple methods can be placed on the delegate's invocation chain (list of methods to call). In this example, the delegate is defined to accept a string parameter and return no value. ShowUpper and ShowMessage have the same signature.

//(1) Declare delegate. Declare anywhere a class can be declared.

public delegate void myDelegate(string msg);

private void TestDelegate()

{

   // (2) Create instance of delegate and pass method to it

   myDelegate msgDelegate= new myDelegate(ShowMessage);

   //     Second method is placed on delegate invocation chain

   msgDelegate+= new myDelegate(ShowUpper);

   // (3) Invoke delegate

   msgDelegate("Delegate Called.");

}

// First method called by delegate

private void ShowMessage(string msg)

{

   MessageBox.Show(msg);

}

// Second method called by delegate

private void ShowUpper(string msg)

{

   msg = msg.ToUpper();   // Make uppercase before displaying

   MessageBox.Show(msg);

}

Understanding the Delegate Class

When a delegate is defined, .NET automatically creates a class to represent the delegate. Here is the code generated for the delegate in the preceding example:

// Class created from delegate declaration

public class myDelegate : MulticastDelegate

{

   // Constructor

   public myDelegate(Object target, Int32 methodPtr);

   public void virtual Invoke(string msg);

   // Used for asynchronous invocation

   public virtual IAsyncResult BeginInvoke(

           string msg, AsyncCallback callback,

           Object state);

   // Used to get results from called method

   public virtual void EndInvoke(IAsyncResult result);

   // Other members are not shown

}

A close look at the code reveals how delegates support both synchronous and asynchronous calls.

Constructor

Takes two parameters. The important thing to note here is that when your program creates an instance of the delegate, it passes a method name to the constructor—not two parameters. The compiler takes care of the details of generating the parameters from the method name.

Invoke

The compiler generates a call to this method by default when a delegate is invoked. This causes all methods in the invocation list to be called synchronously. Execution on the caller's thread is blocked until all of the methods in the list have executed.

BeginInvoke

This is the method that enables a delegate to support asynchronous calls. Invoking it causes the delegate to call its registered method on a separate worker thread. BeginInvoke has two required parameters: the first is an AsyncCallback delegate that specifies the method to be called when the asynchronous method has completed its work; the second contains a value that is passed to the delegate when the method finishes executing. Both of these values are set to null if no callback is required. Any parameters defined in the delegate's signature precede these required parameters.

Let's look at the simplest form of BeginInvoke first, where no callback delegate is provided. Here is the code to invoke the delegate defined in the preceding example asynchronously:

IAsyncResult IAsync =

      msgDelegate.BeginInvoke("Delegate Called.",null,null)

There is one small problem, however—this delegate has two methods registered with it and delegates invoked asynchronously can have only one. An attempt to compile this fails. The solution is to register only ShowMessage or ShowUpper with the delegate.

Note that BeginInvoke returns an object that implements the IAsyncResult interface. As we see later, this object has two important purposes: It is used to retrieve the output generated by the asynchronous method; and its IsCompleted property can be used to monitor the status of the asynchronous operation.

You can also pass an AsyncCallBack delegate as a parameter to BeginInvoke that specifies a callback method the asynchronous method invokes when its execution ends. This enables the calling thread to continue its tasks without continually polling the worker thread to determine if it has finished. In this code segment, myCallBack is called when ShowMessage finishes.

private delegate void myDelegate(string msg);

myDelegate d= new myDelegate(ShowMessage);

d.BeginInvoke("OK",new AsyncCallback(myCallBack),null);

It is important to be aware that myCallBack is run on a thread from the thread pool rather than the application's main thread. As we will see, this affects the design of UI (user interface) applications.

EndInvoke

Is called to retrieve the results returned by the asynchronous method. The method is called by passing it an object that implements the IAsyncResult interface—the same object returned when BeginInvoke is called. These two statements illustrate this approach:

// Save the interface returned

IAsyncResult IAsync = GetStatus.BeginInvoke(null,null);

// ... Do some work here; then get returned value

int status = GetStatus.EndInvoke(IAsync);

EndInvoke should be called even if the asynchronous method returns no value. It can be used to detect exceptions that may be thrown by the asynchronous method; and more importantly, it notifies the Common Language Runtime (CLR) to clean up resources that were used in creating the asynchronous call.

Examples of Implementing Asynchronous Calls

The challenge in using BeginInvoke is to determine when the called asynchronous method finishes executing. As touched on earlier, the .NET Framework offers several options:

  • EndInvoke. After BeginInvoke is called, the main thread can continue working and then call this method. The call to EndInvoke blocks process on the main thread until the asynchronous worker thread completes its execution. This should never be used on a thread that services a user interface because it will lock up the interface.

  • Use a WaitHandle Synchronization object. The IAsyncResult object returned by BeginInvoke has a WaitHandle property that contains a synchronization object. The calling thread can use this object (or objects) to wait until one or more asynchronous tasks complete execution.

  • CallBack Method. As mentioned earlier, one of the parameters to BeginInvoke can be a delegate that specifies a method to be called when the asynchronous method finishes. Because the callback method is run on a new thread from the thread pool, this technique is useful only when the original calling thread does not need to process the results of the asynchronous method.

  • Polling. The IAsyncResult object has an IsCompleted property that is set to true when the method called by BeginInvoke finishes executing. Polling is achieved by periodically checking this value.

Figure 13-4 illustrates the four options.

Figure 13-4. Options for detecting the completion of an asynchronous task

Using Polling and Synchronization Objects

Table 13-1 lists the IAsyncResult properties that are instrumental in implementing the various asynchronous models. The class is in the System.Runtime.Remoting.Messaging namespace.

Table 13-1. Selected IAsyncResult Properties

Property

Description

AsyncState

The object that is passed as the last parameter to the BeginInvoke method.

AsyncWaitHandle

Returns a WaitHandle type object that is used to wait for access to resources. Access is indicated by a "signal" that the asynchronous task has completed. Its methods allow for various synchronization schemes based on one or multiple active threads:

WaitOne. Blocks thread until WaitHandle receives signal.

WaitAny. Waits for any thread to send a signal (static).

WaitAll. Waits for all threads to send a signal (static).

AsyncDelegate

Returns the delegate used for the asynchronous call.

IsCompleted

Boolean value that returns the status of the asynchronous call.

The WaitHandle and IsCompleted properties are often used together to implement polling logic that checks whether a method has finished running. Listing 13-1 illustrates this cooperation. A polling loop is set up that runs until IsCompleted is true. Inside the loop, some work is performed and the WaitHandle.WaitOne method is called to detect if the asynchronous method is done. WaitOne blocks processing until it receives a signal or its specified wait time (20 milliseconds in this example) expires.

Listing 13-1. Asynchronous Invocation Using Polling to Check Status

// Code to return a Body Mass Index Value

private delegate decimal bmiDelegate(decimal ht, decimal wt);

decimal ht_in = 72;

decimal wt_lbs=168;

// (1) Invoke delegate asynchronously

bmiDelegate bd= new bmiDelegate(CalcBMI);

IAsyncResult asRes= bd.BeginInvoke(ht_in, wt_lbs,null,null);

int numPolls=0;

while(!asRes.IsCompleted)

{

   //     Do some work here

   // (2) Wait 20 milliseconds for method to signal completion

   asRes.AsyncWaitHandle.WaitOne(20,false);

   numPolls+=1;

}

// (3) Get result now that asynchronous method has finished

decimal myBMI = bd.EndInvoke(asRes);

Console.WriteLine("Polls: {0}  BMI: {1:##.00}",

      numPolls, myBMI);       // --> Polls: 3  BMI: 22.78

// Calculate BMI

private decimal CalcBMI(decimal ht, decimal wt)

{

   Thread.Sleep(200);         // Simulate a delay of 200 ms

   Console.WriteLine("Thread:{0}",

         Thread.CurrentThread.GetHash());

   return((wt * 703 *10/(ht*ht))/10);

}

For demonstration purposes, this example includes a 200-millisecond delay in the asynchronous method CalcBMI. This causes WaitOne, which blocks for up to 20 milliseconds, to execute seven times (occasionally eight) before the loop ends. Because EndInvoke is not reached until the asynchronous calculation has ended, it causes no blocking.

A more interesting use of the WaitHandle methods is to manage multiple asynchronous tasks running concurrently. In this example, the static WaitAll method is used to ensure that three asynchronous tasks have completed before the results are retrieved. The method is executed by passing it an array that contains the wait handle created by each call to BeginInvoke. As a side note, this point where threads must rendezvous before execution can proceed is referred to as a barrier.

int istart= Environment.TickCount;  // Start Time

bmiDelegate bd1     = new bmiDelegate(Form1.CalcBMI);

IAsyncResult asRes1 = bd1.BeginInvoke(72, 168,null,null);

//

bmiDelegate bd2     = new bmiDelegate(CalcBMI);

IAsyncResult asRes2 = bd2.BeginInvoke(62, 124,null,null);

//

bmiDelegate bd3     = new bmiDelegate(CalcBMI);

IAsyncResult asRes3 = bd3.BeginInvoke(67, 132,null,null);

// Set up array of wait handles as required by WaitAll method

WaitHandle[] bmiHandles = {asRes1.AsyncWaitHandle,

                           asRes2.AsyncWaitHandle,

                           asRes3.AsyncWaitHandle);

// Block execution until all threads finish at this barrier point

WaitHandle.WaitAll(bmiHandles);

int iend = Environment.TickCount;

// Print time required to execute all asynchronous tasks

Console.WriteLine("Elapsed Time: {0}", iend – istart);

// Get results

decimal myBMI1 = bd1.EndInvoke(asRes1);

decimal myBMI2 = bd2.EndInvoke(asRes2);

decimal myBMI3 = bd3.EndInvoke(asRes3);

To test performance, the method containing this code was executed multiple times during a single session. The results showed that execution time was more than 700 milliseconds for the first execution and declined to 203 for the fourth and subsequent ones when three different threads were allocated.

Execution:   1     2     3     4     5

Thread:     75    75    80    75    75

Thread:     75    80    12    80    80

Thread:     80    75    80    12    12

Time(ms):  750   578   406   203   203

For comparison, the code was then run to execute the three tasks with each BeginInvoke followed by an EndInvoke. It ran at a consistent 610 ms, which is what would be expected given the 200 ms block by each EndInvoke—and is equivalent to using synchronous code. The lesson to a developer is that asynchronous code should be used when a method will be executed frequently; otherwise the overhead to set up multithreading negates the benefits.

Core Note

Applications that need to host ActiveX controls or interact with the clipboard must apply the STAThread (single-threaded apartment) attribute to their Main() method. Unfortunately, you cannot use WaitAll() in applications that have this attribute due to conflicts between COM and the Win32 method that WaitAll wraps. Visual Studio users should be aware of this because C# under VS.NET adds the attribute by default.

Using Callbacks

Callbacks provide a way for a calling method to launch an asynchronous task and have it call a specified method when it is done. This is not only an intuitively appealing model, but is usually the most efficient asynchronous model—permitting the calling thread to focus on its own processing rather than waiting for an activity to end. As a rule, the callback approach is preferred when the program is event driven; polling and waiting are better suited for applications that operate in a more algorithmic, deterministic manner.

The next-to-last parameter passed to BeginInvoke is an optional delegate of type AsyncCallback. The method name passed to this delegate is the callback method that an asynchronous task calls when it finishes executing a method. The example in Listing 13-2 should clarify these details.

Listing 13-2. Using a Callback Method with Asynchronous Calls

using System.Runtime.Remoting.Messaging ;

// Delegate is defined globally for class

public delegate decimal bmiDelegate(decimal wt, decimal ht);

public class BMIExample

{

   public void BMICaller(decimal ht, decimal wt, string name)

   {

      bmiDelegate bd= new bmiDelegate(CalcBMI);

      // Pass callback method and state value

      bd.BeginInvoke(ht,wt,new AsyncCallback(OnCallBack),name);

   }

   // This method is invoked when CalcBMI ends

   private void OnCallBack(IAsyncResult asResult)

   {

      // Need AsyncResult so we can get original delegate

      AsyncResult asyncObj = (AsyncResult)asResult;

      // Get state value

      string name= (string)asyncObj.AsyncState ;

      // Get original delegate so EndInvoke can be called

      bmiDelegate bd= (bmiDelegate)asyncObj.AsyncDelegate;

      // Always include exception handling

      try {

         decimal bmi = bd.EndInvoke(asResult);

         Console.WriteLine("BMI for {0}: {1:##.00}",name,bmi);

      } catch (Exception ex)

      {

         Console.WriteLine(ex.Message);

      }

   }

   private decimal CalcBMI(decimal ht, decimal wt)

   {

      Console.WriteLine("Thread:{0}",

            Thread.CurrentThread.GetHashCode());

      return((wt * 703 *10/(ht*ht))/10);

   }

}

Things to note:

  • The BeginInvoke signature includes optional data parameters as well as a delegate containing the callback method and a state object:

bd.BeginInvoke(ht,wt,new AsyncCallback(OnCallBack),name);

  • The final parameter can be information of any type that is useful to the code that receives control after the asynchronous method completes. In this example, we pass the name of the person whose BMI is calculated.

  • The callback method must have the signature defined by the AsyncCallback delegate.

public delegate void AsyncCallback(IAsyncResult

   asyncResult);

  • The callback method must cast its parameter to an AsyncResult type in order to access the original delegate and call EndInvoke.

AsyncResult asyncObj = (AsyncResult)asResult;

// Get the original delegate

bmiDelegate bd= (bmiDelegate)asyncObj.AsyncDelegate;

decimal bmi = bd.EndInvoke(asResult);

  • The call to EndInvoke should always be inside an exception handling block. When an exception occurs on an asynchronous method, .NET catches the exception and later rethrows it when EndInvoke is called.

  • The BMICaller method is invoked from an instance of BMIExample using the following code. Note that the main thread is put to sleep so it does not end before the result is calculated.

BMIExample bmi = new BMIExample();

bmi.BMICaller(68,122, "Diana");

Thread.Sleep(500);  // Give it time to complete

Multiple Threads and User Interface Controls

When working with Windows Forms and user interfaces in general, it is important to understand that all controls on a form belong to the same thread and should be accessed only by code running on that thread. If multiple threads are running, a control should not be accessed—even though it's technically accessible—by any code not running on the same thread as the control. This is a .NET commandment; and as is the nature of commandments, it can be broken—but with unpredictable results. Suppose our application wants to use the callback method in the preceding example to display the calculated BMI value on a label control. One's instinct might be to assign the value directly to the control:

private void OnCallBack(IAsyncResult asResult)

{

   // ... Initialization code goes here

   decimal bmi = bd.EndInvoke(asResult);

   Label.Text= bmi.ToText();  // Set label on UI to BMI value

}

This may work temporarily, but should be avoided. As an alternative, .NET permits a limited number of methods on the Control class to be called from other threads: Invoke, BeginInvoke, EndInvoke, and CreateGraphics. Calling a control's Invoke or BeginInvoke method causes the method specified in the delegate parameter to be executed on the UI thread of that control. The method can then work directly with the control.

To illustrate, let's replace the assignment to Label.Text with a call to a method DisplayBMI that sets the label value:

DisplayBMI(bmi);

We also add a new delegate, which is passed to Invoke, that has a parameter to hold the calculated value.

// Delegate to pass BMI value to method

private delegate void labelDelegate(decimal bmi);

private void DisplayBMI(decimal bmi)

{

   // Determines if the current thread is the same thread

   // the Form was created on.

   if(this.InvokeRequired == false)

   {

      labelthread.Text= bmi.ToString("##.00");

   }

   else

   {

      // The Form's Invoke method is executed, which

      // causes DisplayBMI to run on the UI thread.

      // bmiObj is array of arguments to pass to method.

      object[] bmiObj= {bmi};

      this.Invoke(new labelDelegate(DisplayBMI),bmiObj);

   }

}

This code segment illustrates an important point about threads and code: The same code can be run on multiple threads. The first time this method is called, it runs on the same thread as OnCallBack. The InvokeRequired property is used to determine if the current thread can access the form. If not, the Invoke method is executed with a delegate that calls back DisplayBMI on the UI thread—permitting it to now interact with the UI controls. To make this an asynchronous call, you only need replace Invoke with BeginInvoke.

Using MethodInvoker to Create a Thread

In situations where your code needs to create a new thread but does not require passing arguments or receiving a return value, the system-defined MethodInvoker delegate should be considered. It is the simplest possible delegate—it takes no parameters and returns no value. It is created by passing the name of a method to be called to its constructor. It may then be invoked synchronously (Invoke) or asynchronously (BeginInvoke):

// NewThread is method called by delegate

MethodInvoker mi = new MethodInvoker(NewThread);

// Note that parameters do not have to be null

mi.BeginInvoke(null,null); // Asynchronous call

mi();                      // Synchronous call

The advantage of using the built-in delegate is that you do not have to design your own, and it runs more efficiently than an equivalent custom delegate.

Using Asynchronous Calls to Perform I/O

Asynchronous operations are not new; they were originally implemented in operating systems via hardware and software as a way to balance the slow I/O (Input/Output) process against the much faster CPU operations. To encourage asynchronous I/O, the .NET Framework includes methods on its major I/O classes that can be used to implement the asynchronous model without explicitly creating delegates or threads. These classes include FileStream, HttpWebRequest, Socket, and NetworkStream. Let's look at an example using the FileStream class that was introduced in Chapter 5, "C# Text Manipulation and File I/O."

FileStream inherits from the System.IO.Stream class an abstract class that supports asynchronous operations with its BeginRead, BeginWrite, EndRead, and EndWrite methods. The Begin xxx methods are analogous to BeginInvoke and include callback and status parameters; the End xxx methods provide blocking until a corresponding Begin xxx method finishes.

The code in Listing 13-3 uses BeginRead to create a thread that reads a file and passes control to a callback method that compresses the file content and writes it as a .gz file. The basic callback method operations are similar to those in Listing 13-2. Note how the file name is retrieved from the AsyncState property. The compression technique—based on the GZipStream class—is available only in .NET 2.0 and above.

Listing 13-3. Using Aysnchronous I/O to Compress a File

// Special namespaces required:

using System.IO.Compression;

using System.Runtime.Remoting.Messaging;

//

// Variables with class scope

Byte[] buffer;

FileStream infile;

// Compress a specified file using GZip compression

private void Compress_File(string fileName)

{

   bool useAsync = true;  // Specifies asynchronous I/O

   infile = new FileStream(fileName, FileMode.Open,

         FileAccess.Read, FileShare.Read, 2000, useAsync);

   buffer = new byte[infile.Length];

   int ln = buffer.Length;

   // Read file and let callback method handle compression

   IAsyncResult ar = infile.BeginRead(buffer, 0, ln,

         new AsyncCallback(Zip_Completed), fileName);

   //

}

// Callback method that compresses raw data and stores in file

private void Zip_Completed(IAsyncResult asResult)

{

   // Retrieve file name from state object

   string filename = (string)asResult.AsyncState;

   infile.EndRead(asResult);   // Wrap up asynchronous read

   infile.Close();

   //

   MemoryStream ms = new MemoryStream();

   // Memory stream will hold compressed data

   GZipStream zipStream = new GZipStream(ms,

         CompressionMode.Compress, true);

   // Write raw data in compressed form to memory stream

   zipStream.Write(buffer, 0, buffer.Length);

   zipStream.Close();

   // Store compressed data in a file

   FileStream fs = new FileStream(filename+".gz",

         FileMode.OpenOrCreate,FileAccess.Write,FileShare.Read);

   byte[] compressedData = ms.ToArray();

   fs.Write(compressedData, 0, compressedData.Length);

   fs.Close();

}

As a rule, asynchronous techniques are not required for file I/O. In fact, for read and write operations of less than 64KB, .NET uses synchronous I/O even if asynchronous is specified. Also, note that if you specify asynchronous operation in the FileStream constructor (by setting the useAsync parameter to true), and then use synchronous methods, performance may slow dramatically. As we demonstrate in later chapters, asynchronous techniques provide a greater performance boost to networking and Web Services applications than to file I/O.


13.3. Working Directly with Threads

The asynchronous techniques discussed in the previous section work best when an application or component's operations can be run on independent threads that contain all the data and methods they need for execution—and when the threads have no interest in the state of other concurrently running threads. The asynchronous techniques do not work as well for applications running concurrent threads that do have to share resources and be aware of the activities of other threads.

The challenge is no longer to determine when a thread finishes executing, but how to synchronize the activities of multiple threads so they do not corrupt each other's work. It's not an easy thing to do, but it can greatly improve a program's performance and a component's usability. In this section, we'll look at how to create and manage threads running concurrently. This serves as a background for the final section that focuses on synchronization techniques used to ensure thread safety.

Creating and Working with Threads

An application can create a thread, identify it, set its priority, set it to run in the background or foreground, coordinate its activities with other threads, and abort it. Let's look at the details.

The Current Thread

All code runs on either the primary thread or a worker thread that is accessible through the CurrentThread property of the Thread class. We can use this thread to illustrate some of the selected Thread properties and methods that provide information about a thread:

Thread currThread = Thread.CurrentThread;

Console.WriteLine(currThread.GetHashCode());

Console.WriteLine(currThread.CurrentCulture);      // en-US

Console.WriteLine(currThread.Priority);            // normal

Console.WriteLine(currThread.IsBackground);        // false

Console.WriteLine(AppDomain.GetCurrentThreadId()); // 3008

Thread.GetHashCode overrides the Object.GetHashCode method to return a thread ID. The thread ID is not the same as the physical thread ID assigned by the operating system. That ID, which .NET uses internally to recognize threads, is obtained by calling the AppDomain.GetCurrentThreadID method.

Creating Threads

To create a thread, pass its constructor a delegate that references the method to be called when the thread is started. The delegate parameter may be an instance of the ThreadStart or ParameterizedTheadStart delegate. The difference in the two is their signature: ThreadStart accepts no parameters and returns no value; ParameterizedThreadStart accepts an object as a parameter, which provides a convenient way to pass data to thread.

After the thread is created, its Start method is invoked to launch the thread. This segment illustrates how the two delegates are used to create a thread:

Thread newThread  = new Thread(new ThreadStart(GetBMI));

newThread.Start();      // Launch thread asynchronously

Thread newThread  = new Thread(new

      ParameterizedThreadStart(GetBMI));

newThread.Start(40);    // Pass data to the thread

To demonstrate thread usage, let's modify the method to calculate a BMI value (see Listing 13-2) to execute on a worker thread (Listing 13-4). The weight and height values are passed in an array object and extracted using casting. The calculated value is exposed as a property of the BMI class.

Listing 13-4. Passing Parameters to a Thread's Method

// Create instance of class and set properties

BMI b = new BMI();

decimal[] bmiParms = { 168M, 73M };  // Weight and height

// Thread will execute method in class instance

Thread newThread  = new Thread(

      new ParameterizedThreadStart(b.GetBMI));

newThread.Start(bmiParms);        // Pass parameter to thread

Console.WriteLine(newThread.ThreadState);  // Unstarted

Console.WriteLine(b.Bmi); // Use property to display result

// Rest of main class ...

}

public class BMI

{

   private decimal bmival;

   public void GetBMI(object obj)

   {

      decimal[] parms= (decimal[])obj;

      decimal weight = parms[0];

      decimal height = parms[1] ;

      // Simulate delay to do some work

      Thread.Sleep(1000);  // Build in a delay of one second

      bmival = (weight * 703 * 10/(height*height))/10 ;

   }

   // Property to return BMI value

   public decimal Bmi

   { get {return bmival; }}

}

In reality, the method GetBMI does not do enough work to justify running on a separate thread; to simulate work, the Sleep method is called to block the thread for a second before it performs the calculation. At the same time, the main thread continues executing. It displays the worker thread state and then displays the calculated value. However, this logic creates a race condition in which the calling thread needs the worker thread to complete the calculation before the result is displayed. Because of the delay we've included in GetBMI, that is unlikely—and at best unpredictable.

One solution is to use the THRead.Join method, which allows one thread to wait for another to finish. In the code shown here, the Join method blocks processing on the main thread until the thread running the GetBMI code ends execution:

newThread.Start();

Console.WriteLine(newThread.ThreadState);

newThread.Join();        // Block until thread finishes

Console.WriteLine(b.bmi);

Note that the most common use of Join is as a safeguard to ensure that worker threads have terminated before an application is shut down.

Aborting a Thread

Any started thread that is not in a suspended state can be requested to terminate using the THRead.Abort method. Invoking this method causes a THReadAbortException to be raised on its associated thread; thus, the code running the thread must implement the proper exception handling code. Listing 13-5 shows the code to implement both the call and the exception handling.

The calling method creates a thread, sleeps for a second, and then issues an Abort on the worker thread. The parameter to this command is a string that can be displayed when the subsequent exception occurs. The Join command is then used to wait for the return after the thread has terminated.

The method running on the worker thread loops until it is aborted. It is structured to catch the THReadAbortException raised by the Abort command and print the message exposed by the exception's ExceptionState property.

Listing 13-5. How to Abort a Thread

using System;

using System.Threading;

class TestAbort

{

   public static void Main()

   {

      Thread newThread = new Thread(new ThreadStart(TestMethod));

      newThread.Start();

      Thread.Sleep(1000);

      if(newThread.IsAlive)

      {

         Console.WriteLine("Aborting thread.");

         // (1) Call abort and send message to Exception handler

         newThread.Abort("Need to close all threads.");

         // (2) Wait for the thread to terminate

         newThread.Join();

         Console.WriteLine("Shutting down.");

      }

   }

   static void TestMethod()

   {

      try

      {

         bool iloop=true;

         while(iloop)

         {

            Console.WriteLine("Worker thread running.");

            Thread.Sleep(500);

            // Include next statement to prevent abort

            // iloop=false;

         }

      }

      catch(ThreadAbortException abortException)

      {

        // (3) Display message sent with abort command

        Console.WriteLine((string)abortException.ExceptionState);

      }

   }

}

The Abort command should not be regarded as a standard way to terminate threads, any more than emergency brakes should be regarded as a normal way to stop a car. If the thread does not have adequate exception handling, it will fail to perform any necessary cleanup actions—leading to unpredictable results. Alternate approaches to terminating a thread are presented in the section on thread synchronization.

Multithreading in Action

To gain insight into thread scheduling and performance issues, let's set up an application to create multiple threads that request the same resources. Figure 13-5 illustrates our test model. The server is a class that loads images from its disk storage on request and returns them as a stream of bytes to a client. The client spins seven threads with each thread requesting five images. To make things interesting, the threads are given one of two different priorities. Parenthetically, this client can be used for stress testing because the number of threads and images requested can be set to any value.

Figure 13-5. Multithreading used to return images as a byte array

The ImageServer class shown in Listing 13-6 uses the Stream class to input the requested image file, write it into a memory stream, and convert this stream to an array of bytes that is returned to the client. Note that any exceptions thrown in the server are handled by the client code.

Listing 13-6. Class to Return Images

public class ImageServer

{

   public static byte[] GetMovieImage(string imageName,

                                      int threadNum )

   {

     // Returns requested image to client as a series of bytes,

      // and displays thread number of calling thread.

      int imgByte;

      imageName= "c:\\images\\"+imageName;

      // If file not available exception is thrown and caught by

      // client.

      FileStream s = File.OpenRead(imageName);

      MemoryStream ms = new MemoryStream();

      while((imgByte =s.ReadByte())!=-1)

      {

         ms.WriteByte(((byte)imgByte));

      }

      // Display order in which threads are processed

      Console.WriteLine("Processing on Thread: {0}",threadNum);

      return ms.ToArray();

   }

}

The code shown in Listing 13-7 uses the techniques described earlier to create seven threads that call the static FetchImage method on the ImageServer class. The threads are alternately assigned a priority of Lowest or AboveNormal, so that we can observe how their scheduling is affected by priority. Each thread makes five requests for an image from the server by calling its GetMovieImage method. These calls are inside an exception handling block that displays any exception message originating at the server.

Listing 13-7. Using Multithreading to Retrieve Images

using System;

using System.Collections;

using System.Threading;

namespace ThreadExample

{

   class SimpleClient

   {

      static void Main(string[] args)

      {

         Threader t=new Threader();

      }

   }

   class Threader

   {

      ImageServer server;

      public Threader(){

         server = new ImageServer(); Object used to fetch images

         StartThreader();

      }

      public void StartThreader()

      {

         // Create seven threads to retrieve images

         for (int i=0; i<7; i++)

         {

            // (1) Create delegate

           ThreadStart threadStart = new ThreadStart(FetchImage);

            // (2) Create thread

            Thread workerThread = new Thread(threadStart);

            // (3) Set two priorities for comparison testing

            if( i % 2 == 1)

              workerThread.Priority = ThreadPriority.Lowest;

            else

               workerThread.Priority = ThreadPriority.AboveNormal;

            // (4) Launch Thread

            workerThread.Start();

         }

      }

      public void FetchImage()

      {

         // Display Thread ID

         Console.WriteLine(

              "Spinning: "+Thread.CurrentThread.GetHashCode());

         string[] posters = {"afi1.gif","afi2.gif",

                            "afi4.gif", "afi7.gif","afi89gif"};

         // Retrieve five images on each thread

         try

         {

            for (int i=0;i<5;i++)

            {

               byte[] imgArray = server.GetMovieImage(

                     posters[i],

                     Thread.CurrentThread.GetHashCode());

               MemoryStream ms = new MemoryStream(imgArray);

               Bitmap bmp = new Bitmap(ms);

            }

         }

         catch (Exception ex)

         {

            Console.WriteLine(ex.Message);

         }

      }  // FetchImage

   }     // Threader

         // ImageServer class goes here...

}        // ThreadExample

Because GetMovieImage prints the hash code associated with each image it returns, we can determine the order in which thread requests are fulfilled. Figure 13-6 shows the results of running this application. The even-numbered threads have the higher priority and are processed first in round-robin sequence. The lower priority threads are then processed with no interleaved execution among the threads.

Figure 13-6. Effect of thread priority on thread execution

[View full size image]

The program was run several times to test the effects of varying the number of images requested. In general, the same scheduling pattern shown here prevails, although as more images are requested the lower priority threads tend to run in an interleaved fashion.

Using the Thread Pool

Creating threads can be a relatively expensive process, and for this reason, .NET maintains a collection of predefined threads known as a thread pool. Threads in this pool can be acquired by an application and then returned for reuse when they have finished running. Recall from Section 13.2 that when a program uses asynchronous delegate invocation to create a thread, the thread actually comes from the thread pool. An application can also access this pool directly by following two simple steps.

The first step is to create a WaitCallback delegate that points to the method to be executed by the thread. This method must, of course, match the signature of the delegate, which takes one object parameter and returns no value. Next, the QueueUserWorkItem static method of the ThreadPool class is called. The first parameter to this method is the delegate; it also takes an optional second parameter that can be used to pass information to the method called by the delegate.

To illustrate, let's alter the previous example to acquire threads from a pool rather than creating them explicitly. An object parameter must be added to FetchImage so that it matches the delegate signature. Then, replace the code to create threads with these two statements:

WaitCallback callBack = new WaitCallback(FetchImage);

ThreadPool.QueueUserWorkItem(callBack, "image returned");

This places a request on the thread pool queue for the next available thread. The first time this runs, the pool must create a thread, which points out an important fact about the thread pool. It contains no threads when it is created, and handles all thread requests by either creating a thread or activating one already in the pool. The pool has a limit (25) on the number of threads it can hold, and if these are all used, a request must wait for a thread to be returned. You can get some information about the status of the thread pool using the GetAvailableThreads method:

int workerThreads;

int asyncThreads;

ThreadPool.GetAvailableThreads(out workerThreads, out

asyncThreads);

This method returns two values: the difference between the maximum number of worker and asynchronous threads the pool supports, and the number of each currently active. Thus, if three worker threads are being used, the workerThreads argument has a value of 22.

The thread pool is most useful for applications that repeatedly require threads for a short duration. For an application that requires only a few threads that run simultaneously, the thread pool offers little advantage. In fact, the time required to create a thread and place it in the thread pool exceeds that of explicitly creating a new thread.

Core Note

Threads exist in the thread pool in a suspended state. If a thread is not used in a given time interval, it destroys itself—freeing its resources.

Timers

Many applications have a need to perform polling periodically to collect information or check the status of devices attached to a port. Conceptually, this could be implemented by coupling a timer with a delegate: The delegate handles the call to a specified method, while the timer invokes the delegate to place the calls at a specified interval. In .NET, it is not necessary to write your own code to do this; instead, you can use its prepackaged Timer classes. Let's look at a couple of the most useful ones: System.Timers.Timer and Windows.Forms.Timer. The former is for general use, whereas the latter is designed for Windows Forms applications.

System.Timers.Timer Class

To use the Timer class, simply register an event handling method or methods with the class's Elapsed event. The signature of the method(s) must match that of the ElapsedEventHandler delegate associated with the event:

public delegate void ElapsedEventHandler(object sender,

                                         ElapsedEventArgs e);

The Elapsed event occurs at an interval specified by the Timer.Interval property. A thread from the thread pool is used to make the call into the event handler(s). This code segment demonstrates how the Timer causes a method to be called every second:

using System;

using System.Timers;

public class TimerTest

{

   public static void Main()

   {

      SetTimer t = new SetTimer();

      t.StartTimer();

   }

}

class SetTimer

{

   int istart;

   public void StartTimer()

   {

      istart= Environment.TickCount; //Time when execution begins

         Timer myTimer = new myTimer();

         myTimer.Elapsed+=new ElapsedEventHandler(OnTimedEvent);

      myTimer.Interval=1000;           // 1000 milliseconds

      myTimer.Enabled=true;

      Console.WriteLine("Press any key to end program.");

      Console.Read();

      myTimer.Stop();

   }

   // Timer event handler

   private void OnTimedEvent(object source, ElapsedEventArgs e)

   {

      Console.WriteLine("Elapsed Time: {0}",

                        Environment.TickCount-istart);

   }

}

System.Windows.Forms.Timer Class

We can dispense with a code example of this class, because its implementation parallels that of the Timers.Timer class, with two differences: It uses a Tick exception rather than Elapsed, and it uses the familiar EventHandler as its delegate. However, the feature that distinguishes it from the other Timer class is that it does not use a thread from the thread pool to call a method. Instead, it places calls on a queue to be handled by the main UI thread. Except for situations where the time required by the invoked method may make the form unresponsive, a timer is preferable to using threading. It eliminates the need to deal with concurrent threads and also enables the event handler to directly update the form's controls—something that cannot be done by code on another thread.


13.4. Thread Synchronization

Thread synchronization refers to the techniques employed to share resources among concurrent threads in an efficient and orderly manner. The specific objective of these techniques is to ensure thread safety. A class (or its members) is thread-safe when it can be accessed by multiple threads without having its state corrupted. The potential corruption arises from the nature of thread scheduling. Recall from the previous section that a thread executes in time slices. If it does not finish its task, its state is preserved and later restored when the thread resumes execution. However, while suspended, another thread may have executed the same method and altered some global variables or database values that invalidate the results of the original thread. As an example, consider the pseudo-code in Figure 13-7 that describes how concurrent threads execute the same code segment.

Figure 13-7. Execution path that requires synchronization

Because the first thread is suspended before it updates the log file, both threads update the file with the same value. Because server applications may have hundreds of active threads, there is clear need for a mechanism to control access to shared resources.

The implementation of the pseudo-code is presented in Listing 13-8. Executing this code multiple times produces inconsistent results, which is the pitfall of using code that is not thread-safe. About half the time, the counter is incremented correctly by 2; other times, the first thread is preempted and the second thread gets in before the first finishes updating. In this case, the counter is incorrectly incremented by 1.

Listing 13-8. Example of Class That Requires Synchronization

using System;

using System.Threading;

using System.IO;

public class MyApp

{

   public static void Main()

   {

      CallerClass cc = new CallerClass();

      Thread worker1 =

            new Thread(new ThreadStart(cc.CallUpdate));

      Thread worker2 =

            new Thread(new ThreadStart(cc.CallUpdate));

      worker1.Start();

      worker2.Start();

   }

}

public class CallerClass

{

   WorkClass wc;

   public CallerClass()

   {

      wc= new WorkClass();  // create object to update log

   }

   public void CallUpdate()

   {

      wc.UpdateLog();

   }

}

public class WorkClass

{

   public void UpdateLog()

   {

      // Open stream for reading and writing

      try

      {

         FileStream fs = new

         FileStream(@"c:\log.txt",FileMode.OpenOrCreate,

               FileAccess.ReadWrite, FileShare.ReadWrite);

         StreamReader sr = new StreamReader(fs);

         // Read current counter

         string ctr = sr.ReadLine();

         if(ctr==null) ctr="0";

         int oldCt = int.Parse(ctr) + 1;

         // If the thread's time slice ends here, the counter

         // is not updated.

         fs.Seek(0,SeekOrigin.Begin);

         StreamWriter sw= new StreamWriter(fs);

         sw.WriteLine(oldCt.ToString());

         Console.WriteLine(oldCt);

         sw.Close();

         sr.Close();

      }  catch(Exception ex)

         {

            Console.WriteLine(ex.Message);

         }

   }

}     // WorkClass

A solution is to ensure that after a thread invokes UpdateLog, no other thread can access it until the method completes execution. That is essentially how synchronization works: permitting only one thread to have ownership of a resource at a given time. Only when the owner voluntarily relinquishes ownership of the code or resource is it made available to another thread. Let's examine the different synchronization techniques available to implement this strategy.

The Synchronization Attribute

The developers of .NET recognized that the overhead required to make all classes thread-safe by default would result in unacceptable performance. Their solution was to create a .NET architecture that naturally supports the ability to lock code segments, but leaves the choice and technique up to the developer. An example of this is the optional Synchronization attribute. When attached to a class, it instructs .NET to give a thread exclusive access to an object's code until the thread completes execution. Here is the code that implements this type of synchronization in the log update example:

[Synchronization]

public class WorkClass: ContextBoundObject

The class to which the [Synchronization] attribute is applied should derive from the ContextBoundObject class. When .NET sees this is a base class, it places the object in a context and applies the synchronization to the context. This is referred to as context-bound synchronization. For this to make sense, let's look at the .NET architecture to understand what a context is.

When an application starts, the operating system runs it inside a process. The .NET runtime is then loaded and creates one or more application domains (AppDomains) inside the process. As we will see in the next chapter, these are essentially logical processes that provide the managed environment demanded by .NET applications. Just as a process may contain multiple AppDomains, an AppDomain may contain multiple contexts.

A context can be defined as a logical grouping of components (objects) that share the same .NET component services. Think of a context as a layer that .NET wraps around an object so that it can apply a service to it. When a call is made to this object, it is intercepted by .NET and the requested service is applied before the call is routed to the object. Synchronization is one type of component service. In our example, .NET intercepts the call to UpdateLog and blocks the calling thread if another thread has ownership of the context containing this method. Another component service of interest—call authorization—enables .NET to check the calling thread to ensure it has the proper credentials to access the object.

The [Synchronization] attribute is the easiest way to control thread access to a class—only two statements are changed in our preceding example. The drawback to this approach is that it must be applied to the entire class—even if only a small section of the class contains critical code that requires thread synchronization. The manual synchronization approaches we look at next permit a more granular implementation.

The Monitor Class

The Monitor class allows a single thread to place a lock on an object. Its methods are used to control thread access to an entire object or selected sections of code in an object. Enter and Exit are its most commonly used methods. Enter assigns ownership of the lock to the calling thread and prevents any other thread from acquiring it as long as the thread owns it. Exit releases the lock. Let's look at these methods in action.

Using Monitor to Lock an Object

Monitor.Enter takes an object as a parameter and attempts to grant the current thread exclusive access to the object. If another thread owns the object, the requesting thread is blocked until the object is free. The object is freed by executing the complementary Monitor.Exit.

To illustrate the use of a monitor, let's return to the example in Listing 13-8 in which two threads compete to read and update a log file. The read and write operations are performed by calling the UpdateLog method on a WorkClass object. To ensure these operations are not interrupted, we can use a monitor to lock the object until the method completes executing. As shown here, it requires adding only two statements:

public void CallUpdate()

{

   Monitor.Enter(wc);   // wc is WorkClass object

   wc.UpdateLog();

   Monitor.Exit(wc);

In addition to Monitor.Enter, there is a Monitor.TryEnter method that attempts to acquire an exclusive lock and return a TRue or false value indicating whether it succeeds. Its overloads include one that accepts a parameter specifying the number of millseconds to wait for the lock:

if (!Monitor.TryEnter(obj) return; // Return if lock unavailable

if (!Monitor.TryEnter(obj, 500) return; // Wait 500 ms for lock

Encapsulating a Monitor

A problem with the preceding approach is that it relies on clients to use the monitor for locking; however, there is nothing to prevent them from executing UpdateLog without first applying the lock. To avoid this, a better design approach is to encapsulate the lock(s) in the code that accesses the shared resource(s). As shown here, by placing Monitor.Enter inside UpdateLog, the thread that gains access to this lock has exclusive control of the code within the scope of the monitor (to the point where Monitor.Exit is executed).

public void UpdateLog()

{

   Monitor.Enter(this);   // Acquire a lock

   try

   {

      // Code to be synchronized

   }

   finally   // Always executed

   {

      Monitor.Exit(this);   // Relinquish lock

   }

Note the use of finally to ensure that Monitor.Exit executes. This is critical, because if it does not execute, other threads calling this code are indefinitely blocked. To make it easier to construct the monitor code, C# includes the lock statement as a shortcut to the TRy/finally block. For example, the previous statements can be replaced with the following:

lock(this)

{

   // Code to be synchronized

}

Monitor and lock can also be used with static methods and properties. To do so, pass the type of object as a command parameter rather than the object itself:

Monitor.Enter(typeof(WorkClass));

// Synchronized code ...

Monitor.Exit(typeof(WorkClass));

Core Recommendation

Be wary of using synchronization in static methods. Deadlocks can result when a static method in class A calls static methods in class B, and vice versa. Even if a deadlock does not occur, performance is likely to suffer.

The Mutex

To understand the Mutex class, it is first necessary to have some familiarity with the WaitHandle class from which it is derived. This abstract class defines "wait" methods that are used by a thread to gain ownership of a WaitHandle object, such as a mutex. We saw earlier in the chapter (refer to Table 13-1) how asynchronous calls use the WaitOne method to block a thread until the asynchronous operation is completed. There is also a WaitAll method that can be used to block a thread until a set of WaitHandle objects—or the resources they protect—are available.

An application can create an instance of the Mutex class using one of several constructors. The most useful are

public Mutex();

public Mutex(bool initiallyOwned);

public Mutex(bool initiallyOwned, string name);

The two optional parameters are important. The initallyOwned parameter indicates whether the thread creating the object wants to have immediate ownership of it. This is usually set to false when the mutex is created within a class whose resources it is protecting. The name parameter permits a name or identifier to be assigned to the mutex. This permits a specific mutex to be referenced across AppDomains and even processes. Because thread safety usually relies on encapsulating the locking techniques within an object, exposing them by name to outside methods is not recommended.

Using a mutex to provide thread-safe code is a straightforward process. A mutex object is created, and calls to its wait methods are placed strategically in the code where single thread access is necessary. The wait method serves as a request for ownership of the mutex. If another thread owns it, the requesting thread is blocked and placed on a wait queue. The thread remains blocked until the mutex receives a signal from its owner that it has been released. An owner thread releases a mutex in two ways: by calling the object's ReleaseMutex method or when the thread is terminated. Here is an example of how the log update application is altered to use a mutex to provide thread safety:

public class WorkClass

{

   Mutex logMutex;

   public WorkClass()

   {

      logMutex = new Mutex(false);

   }

   public void UpdateLog()

  {

      logMutex.WaitOne();   // Wait for mutex to become available

         // Code to be synchronized

      logMutex.ReleaseMutex();

   }

}

As part of creating an instance of WorkClass, the constructor creates an instance of the Mutex class. The Boolean false parameter passed to its constructor indicates that it is not owned (the parameterless constructor also sets ownership to false). The first thread that executes UpdateLog then gains access to the mutex through the WaitOne call; when the second thread executes this statement, it is blocked until the first thread releases the mutex.

The Semaphore

The Semaphore class is another WaitHandle derived class. It functions as a shared counter and—like a mutex—uses a wait call to control thread access to a code section or resource. Unlike a mutex, it permits multiple threads to concurrently access a resource. The number of threads is limited only by the specified maximum value of the semaphore.

When a thread issues a semaphore wait call, the thread is not blocked if the semaphore value is greater than 0. It is given access to the code and the semaphore value is decremented by 1. The semaphore value is incremented when the thread calls the semaphore's Release method. These characteristics make the semaphore a useful tool for managing a limited number of resources such as connections or windows that can be opened in an application.

The Semaphore class has several overloaded constructor formats, but all require the two parameters shown in this version:

public Semaphore(int initialCount, int maximumCount );

The maximumCount parameter specifies the maximum number of concurrent thread requests the semaphore can handle; initialCount is the initial number of requests the semaphore can handle. Here is an example:

Semaphore s = new Semaphore(5,10);

This semaphore permits a maximum of 10 concurrent threads to access a resource. When it is first created, only 5 are permitted. To increase this number, execute the Semaphore.Release(n) command—where n is the number used to increment the count permitted. The intended purpose of this command is to free resources when a thread completes executing and wants to exit a semaphore. However, the command can be issued even if the thread has never requested the semaphore.

Now let's see how the Semaphore class can be used to provide synchronization for the log update example. As a WaitHandle derived class, its implementation is almost identical to the mutex. In this example, the semaphore is created with its initial and maximum values set to 1—thus restricting access to one thread at a time.

public class WorkClass

{

   private Semaphore s;

   public WorkClass()

   {

      // Permit one thread to have access to the semaphore

      s  = new Semaphore(1, 1);

   }

   public void UpdateLog(object obj)

   {

      try {

         s.WaitOne();   // Blocks current thread

      // code to update log ...

      } finally

      {

         s.Release();

      }

   }

}

Avoiding Deadlock

When concurrent threads compete for resources, there is always the possibility that a thread may be blocked from accessing a resource (starvation) or that a set of threads may be blocked while waiting for a condition that cannot be resolved. This deadlock situation most often arises when thread A, which owns a resource, also needs a resource owned by thread B; meanwhile, thread B needs the resource owned by thread A. When thread A makes its request, it is put in suspended mode until the resource owned by B is available. This, of course, prevents thread B from accessing A's resource. Figure 13-8 depicts this situation.

Figure 13-8. Deadlock situation

Most deadlocks can be traced to code that allows resources to be locked in an inconsistent manner. As an example, consider an application that transfers money from one bank account to another using the method shown here:

public void Transfer(Account acctFrom,

                     Account acctTo, decimal amt)

{

   Monitor.Enter(acctFrom);   // Acquire lock on from account

   Monitor.Enter(acctTo);     // Acquire lock on to account

   // Perform transfer ...

   Monitor.Exit(acctFrom);    // Release lock

   Monitor.Exit(acctTo);      // Release lock

}

As you would expect, the method locks both account objects so that it has exclusive control before performing the transaction. Now, suppose two threads are running and simultaneously call this method to perform a funds transfer:

Thread A:   Transfer(Acct1000, Acct1500, 500.00);

Thread B:   Transfer(Acct1500, Acct1000, 300.00);

The problem is that the two threads are attempting to acquire the same resources (accounts) in a different order and run the risk of creating a deadlock if one is preempted before acquiring both locks. There are a couple of solutions. First, we could lock the code segment being executed to prevent a thread from being preempted until both resources are acquired:

lock(this)

{

   ... Monitor statements

}

Unfortunately, this can produce a performance bottleneck. Suppose another method is working with one of the account objects required for the current transaction. The thread executing the method is blocked as well as all other threads waiting to perform a funds transfer.

A second solution—recommended for multithreading in general—is to impose some order on the condition variables that determine how locking can occur. In this example, we can impose a lock sequence based on the objects' account numbers. Specifically, a lock must be acquired on the account with the lower account number before the second lock can be obtained.

If(acctFrom < acctTo)

{

   Monitor.Enter(acctFrom);

   Monitor.Enter(acctTo);

}else

{

   Monitor.Enter(acctTo);

   Monitor.Enter(acctFrom);

}

As this example should demonstrate, a deadlock is not caused by thread synchronization per se, but by poorly designed thread synchronization. To avoid this, code should be designed to guarantee that threads acquire resource locks in a consistent order.

Summary of Synchronization Techniques

Table 13-2 provides an overview of the synchronization techniques discussed in this chapter and provides general advice on selecting the one to best suit your application's needs.

Table 13-2. Overview of Selected Thread Synchronization Techniques

Technique

Description

When to Use

Synchronization attribute

An attribute that can be used with classes that inherit from the ContextBoundObject class.

To limit thread access to an entire object. If you need to protect only a small section of code while permitting access to other class members, choose another technique.

Monitor/lock

Locks selected code segments that are encased between a Monitor.Enter and Monitor.Exit statement. Lock provides equivalent code with built-in event handling.

To provide single thread access to selected code segments in an object. To synchronize access to value types, use a mutex.

Mutex

Uses wait methods inherited from the WaitHandle class to manage thread access to resources.

To permit a thread to request exclusive access to one or more resources. Requests can be made across AppDomains and processes.

Semaphore

Uses wait methods inherited from the WaitHandle class to manage multiple concurrent thread access to resources.

To make a limited number of resources available concurrently to more than one thread.

In addition to these, .NET offers specialized synchronization classes that are designed for narrowly defined tasks. These include Interlocked, which is used to increment and exchange values, and ReaderWriterLock, which locks the writing operation on a file but leaves reading open to all threads. Refer to online documentation (such as MSDN) for details on using these.


13.5. Summary

Designing an application to perform tasks concurrently can result in an application that provides better responsiveness to a user and manages system resources more efficiently. This requires replacing the traditional synchronous approach to code execution with an asynchronous approach that uses threads. A thread is a path of execution. Each program begins running on a main thread that may create worker threads to perform tasks concurrent to its own processing.

One way to create a thread that executes a specified method is to make an asynchronous delegate invocation. This is done by creating a delegate and passing it the name of the method to be called. The delegate is then invoked with its BeginInvoke method. This causes the delegate's method to be executed on a thread that is fetched from a thread pool managed by .NET. An optional parameter to BeginInvoke is a callback method that is called when the worker thread ends. Unlike synchronous processing, in which a call to a method blocks processing in the calling method, asynchronous invocation returns control to the calling method so that it can continue processing.

Applications that require more control over a thread can create their own by passing a THReadStart or ParameterizedThreadStart delegate to the THRead constructor. A thread is executed by calling its Start method.

After the decision is made to use threads, the problem of thread-safe code must be considered. An operating system executes threads in time slices. When a thread's time expires, it is swapped out and another thread begins executing. The effects of a thread being interrupted in the middle of a task can produce unpredictable results. Thread synchronization is used to ensure that one thread has exclusive access to a code path until it completes processing. .NET provides several approaches to synchronization: an automatic approach that uses the Synchronization attribute to lock an object until a thread has finished using it; and the Mutex, Monitor, and Semaphore classes that provide a manual—but more granular—approach to implementing thread safety.


13.5. Test Your Understanding

1:

An asynchronous delegate must have a void return value.

  1. True

  2. False

2:

Given this delegate

private delegate void myDelegate(string msg);

myDelegate d = new myDelegate(PrintMessage);

identify the role of ia, p1, p2, and p3 in this BeginInvoke call:

ia = d.BeginInvoke(p1, p2, p3);

3:

What is thread local storage used for?

4:

What is the default maximum number of threads that a thread pool can hold?

5:

What two delegates are used to create a thread directly, and how do they differ?

6:

Describe a syntactically simpler way to generate the following code:

Monitor.Enter(obj);

{

   // Code to synchronize

} finally {

   Monitor.Exit(obj);

}

7:

How many times does the following code print the console message?

private static s;

public static void Main()

{

   s = new Semaphore(0, 3);

   // Create and start five numbered threads

   for(int i = 1; i <= 5; i++)

   {

      Thread t = new Thread(new ThreadStart(Worker));

      t.Start();

   }

}

private static void Worker(object num)

{

   s.WaitOne();

   Console.WriteLine("Thread enters semaphore  ");

   Thread.Sleep(100);

   s.Release();

}

  1. 0

  2. 1

  3. 3

  4. 5

8:

What happens when you attempt to run this code?

class UseMutex

{

   public void ThreadStart()

   {

      Mutex mutex = new Mutex(false, "MyMutex");

      mutex.WaitOne();

      Console.WriteLine("Worker Thread");

   }

   static void Main()

   {

      UseMutex obj = new UseMutex();

      Thread thread = new Thread(

            new ThreadStart(obj.ThreadStart));

      Mutex mutex = new Mutex(true, "MyMutex");

      thread.Start();

      Thread.Sleep(1000);

      Console.WriteLine("Primary Thread");

      mutex.ReleaseMutex();

   }

}

  1. It prints:

Worker Thread

Primary Thread

  2. It prints:

Primary Thread

Worker Thread

  3. The program deadlocks and there is no output.

9:

To illustrate deadlocking, Edsger Dijkstra introduced a "Dining Philosopher" metaphor that has become a classical way of introducing resource allocation and deadlocking. The metaphor (with variations) goes like this:

Five philosophers, who spend their lives alternately thinking and eating, are sitting around a table. In the center of the round table is an infinite supply of food. Before each philosopher is a plate, and between each pair of plates is a single chopstick. Once a philosopher quits thinking, he or she attempts to eat. In order to eat, a philosopher must have possession of the chopstick to the left and right of the plate.

Your challenge is to design a program that creates five threads—one to represent each philosopher—that continually perform the tasks of thinking and eating. The program should implement a synchronization scheme that allows each thread to periodically acquire two chopsticks so that it can eat for a fixed or random (your choice) amount of time. After eating, a philosopher should release its chopsticks and think for a while before trying to eat again.

Hint: Use Monitor.Wait() to try to acquire chopsticks, and Monitor.PulseAll() to notify all threads that chopsticks are being released.


Chapter 14. Creating Distributed Applications with Remoting

Topics in This Chapter

  • Application Domains: The logical partition within which a .NET application runs is called an application domain (or AppDomain). In this chapter, we examine how applications communicate and access resources across AppDomain boundaries.

  • Remoting: An application may call a method that resides in another AppDomain, another process, or on another machine. This process, known as remoting, requires creating a client and server that agree to communicate across these boundaries. This chapter looks at the options available to implement remoting.

  • Leasing and Sponsorship: Garbage Collection in .NET does not recognize remote references when removing objects. To prevent a remote object from being destroyed, .NET provides a Lease object that is used to reference a remote object so that it avoids Garbage Collection.

This chapter introduces the .NET way of developing distributed applications. The emphasis is on how a technique known as remoting permits client computers to access resources on other local or remote computers. Remoting is typically designed for intranet applications, although it does support the HTTP protocol for communicating over the Internet. In some cases, it may be regarded as an alternative to Web Services and a Web browser. We'll see in this chapter that remoting offers more flexibility and a richer set of features than these standard Web-based solutions.

To fully appreciate remoting technology, it is necessary to understand the relationship among processes, application domains, and assemblies. Toward that end, the first section explains the role of the application domain in the .NET architecture. We'll see that the security and code isolation it offers requires that objects in separate AppDomains agree upon the port number, protocol, and type of message formatting before they can communicate.

The second section forms the heart of the chapter. It provides both a conceptual and hands-on approach to remoting. Code examples illustrate how to select and implement the remoting options that best fit a distributed application. The section describes how to create client-and server-activated objects, select formatting and protocol options, deploy assemblies for a distributed application, and use leases to manage the lifetime of an object.

You may want to follow up this chapter by reading the chapter on Web Services, which presents a second .NET technique for implementing a distributed application. Although there are conceptual similarities between remoting and Web Services, there are also distinct differences in performance, interoperability, and implementation complexity that an architect must understand. A reading of these chapters should provide the know-how to select the approach that best meets the requirements of your distributed application.


14.1. Application Domains

When you install .NET on a machine, you create a virtual, managed environment in which applications can run. This environment is designed to insulate applications from the demands and vagaries of the host operating system. The AppDomain is one of the key architectural features supporting the managed environment.

Most operating systems see the world in terms of processes that provide the resources, such as memory and tables required by applications. Because a .NET application cannot run directly in the unmanaged process, .NET partitions a process into one or more logical areas in which assemblies execute. These logical areas are AppDomains. As shown in Figure 14-1, a process may contain more than one AppDomain and an AppDomain may contain one or more assemblies. A default AppDomain is created when the Common Language Runtime (CLR) initializes, and additional ones are created by the CLR as needed. An application may also instruct the CLR to create a new AppDomain.

Figure 14-1. Multiple application domains may reside in a single process

Advantages of AppDomains

Aside from the need for a managed environment, the use of AppDomains provides several advantages over the traditional process-based architecture:

  • Code Isolation. AppDomains institute a level of fault isolation that prevents a code failure in one AppDomain from causing another AppDomain to crash. .NET achieves this code separation in two ways: by preventing an AppDomain from directly referencing objects in another AppDomain, and by having each AppDomain load and maintain its own copy of key assemblies that allow it to run independently. As a by-product of this, an AppDomain can be selectively debugged and unloaded without directly affecting other AppDomains in the process.

  • Performance. Implementing an application to run in multiple AppDomains can produce better performance than a comparable design that relies on multiple processes. This efficiency derives from several factors: A physical process requires more memory and resources than AppDomains, which share the resources of a single process; creating and disposing of processes is much more time consuming than comparable operations on AppDomains; and making a call between processes is slower and requires more overhead than making a call between AppDomains residing in the same process.

  • Security. By its very nature, an AppDomain presents a security boundary between its contained resources and assemblies attempting to access them. To cross this boundary, an outside assembly must rely on remoting, which requires cooperation between the AppDomains. In addition, an AppDomain has its own security policy that it can impose upon assemblies to restrict their permissible operations. This "sandbox" security model allows AppDomains to ensure that assemblies are well behaved. In Chapter 15, "Code Refinement, Security, and Deployment," we'll look at examples of using AppDomains to enforce code security.

Application Domains and Assemblies

When an application runs, it may reference code in several assemblies. By default, the CLR loads these referenced assemblies into the AppDomain of the calling assembly. This means that if a process contains multiple AppDomains that reference the same assembly, a copy of that assembly is placed in each domain. However, it is possible to override the default and share a single assembly among multiple domains. Assemblies used in this manner are referred to as domain-neutral assemblies.

Some crucial assemblies required by all applications are automatically loaded as domain-neutral. The most prominent of these is mscorlib.dll, an assembly that contains the native types integral to the .NET Framework. However, a developer can also specify that a custom assembly be made domain-neutral so that domains can share it.

The use of a domain-neutral assembly saves memory. However, there is a trade-off in terms of performance and flexibility in assigning permissions to assemblies. Even shared assemblies require that their static data and methods be copied into the AppDomains that reference them. The overhead to manage this can slow performance. Only one set of permissions, defining what operations an assembly can perform, can be assigned to domain-neutral assemblies. Thus, if an AppDomain requires a different set of permissions, it must have its own copy of the assembly.

Working with the AppDomain Class

The System.AppDomain class exposes methods and properties that enable an application to create an AppDomain, remove an AppDomain from a process, enumerate the assemblies it contains, and identify itself by name or internal ID. The majority of applications do not require programming at the AppDomain level; however, applications with a need to isolate a custom type library, or include special security features, will find direct interaction with the class a necessity.

In this section, we'll look at an example that illustrates some of the basic techniques used to manipulate an AppDomain. Working though this code should not only make you familiar with the basic class members, but also further clarify the relationship that exists among a process, an AppDomain, and an assembly.

The example defines two classes: RemoteClass, which is compiled into a DLL file, and AppDomainClient, which is compiled into AppdTestClient.exe. RemoteClass is packaged as an assembly that does nothing more than display the name of the domain in which it is running. It uses the static AppDomain.CurrentDomain property to retrieve the current domain, and displays its name using the FriendlyName property.

Note that RemoteClass derives from MarshalByRefObject. As we see in the next section on remoting, this is required if an object is to be accessed from an AppDomain other than its own.

// AppdTestClass.dll assembly

using System;

using System.Diagnostics;

namespace AppDomainTest

{

   public class RemoteClass: MarshalByRefObject

   {

      public void ShowDomain()

      {

         AppDomain currentAppDomain = AppDomain.CurrentDomain;

         Console.WriteLine("Domain:{0}",

                    currentAppDomain.FriendlyName);

      }

   }

}

AppDomainClient, shown in Listing 14-1, illustrates how to use several AppDomain class members. It begins by creating an instance of RemoteClass using the following statement:

(RemoteClass)currentAppDomain.CreateInstanceAndUnwrap(

                   "appdtestclass", "AppDomainTest.RemoteClass");

The first parameter is the name of the assembly, and the second is the name of the type being instantiated. This statement returns an object that is cast to an instance of RemoteClass. The object's ShowDomain method is then executed to display the domain it is running in. In this case, the console output is appdtestclient.exe, the default name given to the AppDomain when the assembly is executed. The remainder of the code displays the assemblies in the AppDomain, creates a new AppDomain, and repeats these operations for it.

Listing 14-1. Working with Application Domains

// AppdTestClient.exe assembly

using System;

using System.Reflection;

using AppDomainTest;

public class AppDomainClient

{

   static void Main()

   {

      AppDomain currentAppDomain;

      currentAppDomain = AppDomain.CurrentDomain;

      RemoteClass rc;

      // (1) Create instance of RemoteClass

      rc=(RemoteClass)currentAppDomain.CreateInstanceAndUnwrap(

              "appdtestclass", "AppDomainTest.RemoteClass");

      // (2) Execute method on RemoteClass

      rc.ShowDomain();

      // (3) Show assemblies in this appdomain

      Assembly[] currAssemblies =

            currentAppDomain.GetAssemblies();

      ShowAssemblies (currAssemblies);

      // (4) Create a new AppDomain

      AppDomain myAppDomain =

            AppDomain.CreateDomain("New Domain");

      rc=(RemoteClass)myAppDomain.CreateInstanceAndUnwrap(

              "appdtestclass", "AppDomainTest.RemoteClass");

      rc.ShowDomain();

      // (5) Show assemblies in new appdomain

      currAssemblies = myAppDomain.GetAssemblies();

      ShowAssemblies(currAssemblies);

      // (6) The domain a thread runs in can be displayed

      Console.Write(

            System.Threading.Thread.GetDomain().FriendlyName);

      // (7) Unload the new appdomain from the process

      AppDomain.Unload(myAppDomain);

   }

      private static void ShowAssemblies(

            Assembly[] currAssemblies)

      {

         foreach(Assembly a in currAssemblies)

         {

             Console.WriteLine(

                   " Assembly Name: {0}",a.GetName().Name);

         }

      }

   }

Figure 14-2 shows the three assemblies that exist in the default AppDomain: mscorlib, appdtestclient, and appdtestclass. Mscorlib is automatically loaded by .NET because it contains the System namespace required by the application.

Figure 14-2. AppDomains and assemblies created from code in Listing 14-1

Note that the process also includes a second domain that holds two assemblies. This AppDomain is created using the static CreateDomain method:

AppDomain myAppDomain = AppDomain.CreateDomain("New Domain");

The two assemblies are loaded when AppDomainClient creates an instance of RemoteClass. As you would expect, executing ShowDomain causes the console to display "New Domain" as the name of the current domain.

The final step in the example is to unload the new domain from the process. Note that .NET does not permit an individual assembly to be unloaded from an AppDomain.


14.2. Remoting

At its core, remoting is as a way to permit applications in separate AppDomains to communicate and exchange data. This is usually characterized as a client-server relationship in which the client accesses resources or objects on a remote server that agrees to provide access. The way in which this agreement between client and server is implemented is what remoting is all about. The physical proximity of the AppDomains does not matter: They may be in the same process, in different processes, or on different machines on different continents.

Remoting is often portrayed as a concept that is difficult to understand and implement—particularly when compared to Web Services. This sentiment is misleading and simply not true for many applications.

Consider the steps required to enable a client to access an object on a remote server:

  • Create a TCP or HTTP connection between the client and server.

  • Select how the messages sent between server and client are formatted.

  • Register the type that is to be accessed remotely.

  • Create the remote object and activate it from the server or the client.

.NET takes care of all the details. You don't have to understand the underlying details of TCP, HTTP, or ports—just specify that you want a connection and what port to use. If HTTP is selected, communications use a Simple Object Access Protocol (SOAP) format; for TCP, binary is used. The registration process occurs on both the server and client. The client selects a registration method and passes it a couple of parameters that specify the address of the server and the type (class) to be accessed. The server registers the types and ports that it wants to make available to clients, and how it will make them available. For example, it may implement the object as a singleton that is created once and handles calls from all clients; or it may choose to create a new object to handle each call.

Figure 14-3 depicts the learning curve that developers new to remoting can expect to encounter. By hiding much of the underlying communications details, .NET enables a developer to quickly develop functioning applications that can access remote objects. The complexity often associated with remoting comes into play as you move further up the learning curve to take advantage of advanced remoting techniques such as creating sink providers and custom transport channels. Knowledge of these advanced techniques enables one to customize the way distributed applications communicate. For information on these topics, refer to a book on advanced .NET remoting, such as Advanced .NET Remoting by Ingo Rammer.[1]

[1] Advanced .NET Remoting, Second Edition, by Ingo Rammer and Mario Szpuszta; Apress, 2005.

Figure 14-3. The learning curve for developing remoting applications

This chapter focuses on topics to the left of the advanced remoting line. You'll learn how to design applications that permit remote objects to be created by either the server or the client, how to control the lifetime of these objects, and how to design and deploy assemblies that best take advantage of the remoting architecture. There are quite a few code examples whose purpose is to present prototypes that you can use to implement a wide range of remoting applications. Included are examples of a server that streams requested images to clients, and a message server that both receives messages and sends them to the targeted recipient upon request.

Remoting Architecture

When a client attempts to invoke a method on a remote object, its call passes through several layers on the client side. The first of these is a proxy—an abstract class that has the same interface as the remote object it represents. It verifies that the number and type of arguments in the call are correct, packages the request into a message, and passes it to the client channel. The channel is responsible for transporting the request to the remote object. At a minimum, the channel consists of a formatter sink that serializes the request into a stream and a client transport sink that actually transmits the request to a port on the server. The sinks within a channel are referred to as a sink chain. Aside from the two standard sinks, the channel may also contain custom sinks that operate on the request stream.

On the server side, the process is reversed, as the server transport sink receives the message and sends it up the chain. After the formatter rebuilds the request from the stream, .NET creates the object on the server and executes the requested method.

Figure 14-4 illustrates the client-server roles in a remoting architecture. Let's examine its three key components: proxies, formatter classes, and channel classes.

Figure 14-4. High-level view of .NET remoting architecture

Proxies

When a client attempts to communicate with a remote object, its reference to the object is actually handled by an intermediary known as a proxy. For .NET remoting, there are two types of proxies: a transparent proxy that the client communicates with directly, and a real proxy that takes the client request and forwards it to the remote object.

The transparent proxy is created by the CLR to present an interface to the client that is identical to the remote class. This enables the CLR to verify that all client calls match the signature of the target method—that is, the type and number of parameters match. Although the CLR takes care of constructing the transparent proxy, the developer is responsible for ensuring that the CLR has the metadata that defines the remote class available at compile time and runtime. The easiest way is to provide the client with a copy of the server assembly that contains the class. But, as we discuss later, there are better alternatives.

After the transparent proxy verifies the call, it packages the request into a message object—a class that implements the IMessage interface. The message object is passed as a parameter to the real proxy's Invoke method, which passes it into a channel. There, a formatter object serializes the message and passes it to a channel object that physically sends the message to the remote object.

Core Note

The real proxy that is responsible for sending a message to a remote object is an implementation of the RealProxy class that is generated automatically by the CLR. Although this meets the needs of most remoting applications, a developer has the option of creating a custom implementation.

Formatters

Two formatters are included as part of the .NET Remoting classes: a binary formatter and a SOAP formatter. SOAP, which is discussed in detail in the Web Services chapter, serializes messages into an XML format. Binary produces a much smaller message stream than SOAP, because it sends the message as a raw byte stream.

By default, SOAP is used when the HTTP protocol is selected and binary is used with the TCP protocol. However, you can also choose to send SOAP over TCP and binary over HTTP. Although not as efficient as the binary format, the combination of SOAP and HTTP has become a de facto standard for transmitting data through firewalls whether using remoting or Web Services. The binary format is recommended for cases where firewalls are not an issue.

Channels

Channel objects are created from classes that implement the IChannel interface. .NET comes with two that handle most needs: HttpChannel and TcpChannel. It is the responsibility of the remote host to register the channel over which it is willing to provide access; similarly, the client registers the channel it wants to issue its calls on.

In its simplest form, registration on the host consists of creating an instance of the channel object and registering it by passing it as a parameter to the static RegisterChannel method of the ChannelServices class. This example registers an HTTP channel on port 3200:

// Channel Registration

HttpChannel c = new HttpChannel(3200);  // Port 3200

ChannelServices.RegisterChannel(c);

The only difference in registering on the client side is that the port number does not have to be specified:

HttpChannel c = new HttpChannel();

ChannelServices.RegisterChannel(c);

There are some rules to keep in mind when registering channels on the client and server:

  • Both the host and client can register multiple channels; however, the client must register a channel that matches one the host has registered.

  • Multiple channels cannot use the same port.

  • By default, HTTP and TCP channels are given the names http and tcp, respectively. If you attempt to register multiple HTTP or TCP channels using the default name, you will receive an exception. The way around this is to create the channels using a constructor (described shortly) that accepts a channel name parameter.

As an alternative to embedding the channel and protocol information within code, .NET permits it to be specified in configuration files associated with the client and host assemblies. For example, if the host assembly is named MessageHost.exe, we could have a configuration file name MessageHost.exe.config containing the following port and formatting specification.

<application>

   <channels>

      <channel ref="http" port="3200"/>

   </channels>

</application>

A program uses this file by passing the file name to the static Configure method of the RemotingConfiguration class:

RemotingConfiguration.Configure("MessageHost.exe.config");

The configuration file must be in the same directory as the assembly referencing it.

Assigning a Name to a Channel

To open multiple channels using the same protocol, a host must assign a name to each channel. To do so, it must use this form of the HttpChannel constructor:

HttpChannel(IDictionary properties,

            IClientChannelSinkProvider csp,

            IserverChannelSinkProvicer ssp )

Only the first parameter is of interest for naming purposes. The second or third can be used to specify the formatter used on the client or server side:

IDictionary chProps = new Hashtable();

chProps["name"] = "Httpchannel01";

chProps["port"] = "3202";

ChannelServices.RegisterChannel(new HttpChannel(chProps,

                                null, null));

Types of Remoting

Recall that the parameters in a C# method may be passed by value or by reference. Remoting uses the same concept to permit a client to access objects—although the terminology is a bit different. When a client gets an actual copy of the object, it is referred to as marshaling by value (MBV); when the client gets only a reference to the remote object, it is referred to as marshaling by reference (MBR). The term marshaling simply refers to the transfer of the object or request between the client and server.

Marshaling by Value

When an object is marshaled by value, the client receives a copy of the object in its own application domain. It can then work with the object locally and has no need for a proxy. This approach is much less popular than marshaling by reference where all calls are made on a remote object. However, for objects that are designed to run on a client as easily as on a server, and are called frequently, this can reduce the overhead of calls to the server.

As an example, consider an object that calculates body mass index (BMI). Instead of having the server implement the class and return BMI values, it can be designed to return the BMI object itself. The client can then use the object locally and avoid further calls to the server. Let's see how to implement this.

For an object to be marshaled by value, it must be serializable. This means that the class must either implement the ISerializable interface or—the easier approach—have the [Serializable] attribute. Here is the code for the class on the server:

[Serializable]

public class BMICalculator

{

   // Calculate body mass index

   public decimal inches;

   public decimal pounds;

   public decimal GetBMI()

   {

      return ((pounds*703* 10/(inches*inches))/10);

   }

}

The HealthTools class that is marshaled by reference returns an instance of BMICalculator:

public class HealthTools: MarshalByRefObject

{

   // Return objects to calculate BMI

   public BMICalculator GetBMIObj(){

      return new BMICalculator();

   }

The client creates an instance of HealthTools and calls the GetBMIObj method to return the calculator object:

HealthMonitor remoteObj = new HealthMonitor();

BMICalculator calc= remoteObj.GetBMIObj();

calc.pounds= 168M;

calc.inches= 73M;

Console.WriteLine(calc.GetBMI());

It is important to understand that this example uses both marshaling by value and marshaling by reference: an MBR type (HealthTools) implements a method (GetBMIObj) that returns an MBV type (BMICalculator). You should recognize this as a form of the factory design pattern discussed in Chapter 4, "Working with Objects in C#."

Marshaling by Reference

Marshaling by reference (MBR) occurs when a client makes a call on an object running on a remote server. The call is marshaled to the server by the proxy, and the results of the call are then marshaled back to the client.

Objects accessed using MBR must inherit from the MarshalbyRefObject class. Its most important members, InitializeLiIfetimeServices and GetLifeTimeServices, create and retrieve objects that are used to control how long a remoting object is kept alive on the server. Managing the lifetime of an object is a key feature of remoting and is discussed later in this section.

MarshalByRefObjects come in two flavors: client-activated objects (CAO) and server-activated objects (SAO)—also commonly referred to as well-known objects (WKO). Server-activated objects are further separated into single call and singleton types. A server may implement both client-activated and server-activated objects. It's up to the client to choose which one to use. If the client selects SAO, the server makes the determination as to whether to use server-activated single call or server-activated singleton objects.

The choice of activation mode profoundly affects the overall design, performance, and scalability of a remoting application. It determines when objects are created, how many objects are created, how their lifecycle is managed, and whether objects maintain state information. Let's look at the details.

Client-Activated Objects

The use and behavior of a client-activated object (CAO) resembles that of a locally created object. Both can be created using the new operator; both may have parameterized constructors in addition to their default constructor; and both maintain state information in properties or fields. As shown in Figure 14-5, they differ in that the CAO runs on a host in a separate application domain and is called by a proxy.

Figure 14-5. Client-activated objects: client retains control of object

The fact that the object resides in another AppDomain means that it is subject to Garbage Collection there and can be destroyed even though the remote client is still using it. .NET handles this potential problem by assigning a lease to each object that can be used to keep it alive. Leases are discussed in detail later in this chapter.

Server-Activated Objects

A server-activated object (SAO) may be implemented as a singleton or single call object. The former is best suited for sharing a single resource or collaborative operation among multiple users. Examples include a chat server and class factory. Single call mode is used when clients need to execute a relatively short operation on the server that does not require maintaining state information from one call to the next. This approach is the most scalable solution and has the added advantage of working well in an environment that uses load balancing to direct calls to multiple servers.

Server-Activated Singleton

Figure 14-6 illustrates how a single object is used to handle all calls in singleton-based design. The server creates the object when the first client attempts to access it—not when it tries to create it. Because the object is created only once, efforts by other clients to create an instance of it are ignored; instead, they are all given a reference to the same singleton object. Each time a client invokes the object, the CLR allocates a new thread from the thread pool. For this reason, it is the responsibility of the developer to ensure the server code is thread-safe. This also limits scalability because there is usually only a finite number of threads available.

Figure 14-6. Server-activated singleton object: one object handles all calls

Server-Activated Single Call

In single call activation mode, the server creates a new object each time a call is made on an object. After the call has been handled, the object is deactivated. Figure 14-7 illustrates how multiple calls are handled: The first call has been completed and the object created for it destroyed; at this point, the proxy of the client that made this call references a null value. The second call comes from another client and results in the creation of the second server object. Finally, the first client makes its second call and a third object is created.

Figure 14-7. Server-activated single call: one object is created for each request

The advantage of single call activation is that resources are made available as soon as a call is completed. This is in contrast to the client-activated call where the client holds on to the resources until it has finished using the object. The disadvantage of the single call is that it does not inherently maintain state information between calls. If you do want to take advantage of single call scalability, but require that information about previous calls be maintained, you can design the server to maintain its own state information in a file or database.

Type Registration

An application may support multiple activation modes and multiple objects. A client indicates the object(s) it wants to access on a server and whether to use client or server-activation mode. The server, on the other hand, indicates which objects it wants to make available to remote clients and the activation mode that is required to access them. This is done using a mechanism known as type registration. As a complement to channel registration, which tells .NET how to transport messages, type registration specifies the objects that can be remotely accessed and the activation mode to use. It's the final part of the agreement that permits a client in one AppDomain to access objects on a host in another AppDomain.

Registering Server-Activated Objects

A host assembly uses the RegisterWellKnowServiceType method of the RemotingConfiguration class to register a type. The method has three parameters: the type of the object, a string representing the object's URI (universal resource identifier), and a WellKnownObjectMode enum that indicates whether the object is implemented as a singleton or single call object. This code segment registers a MessageManager object to execute as a singleton.

// Server Registration: Server-Activated Objects

Type ServerType = typeof(SimpleServer.MessageManager);

RemotingConfiguration.RegisterWellKnownServiceType(

      ServerType,                    // Type of Object

      "MyObject",                    // Arbitrary name

      WellKnownObjectMode.Singleton );

Replace Singleton with SingleCall to register the object to run in single call mode.

To access an SAO, a client uses the RegisterWellKnownClientType method. It takes two parameters: the object type and a string containing the URL where the object can be located. Note that the client does not have any say in whether it uses a singleton or single call object—it uses whichever the server provides.

// Client Registration: Server-Activated Objects

Type ServerType = typeof(SimpleServer.MessageManager);

string url= "http://localhost:3200/MyObject";

// Register type for Server Activation Mode

RemotingConfiguration.RegisterWellKnownClientType(

      ServerType,

      url);

MessageManager mm = new MessageManager();

When the client uses new to create an instance of the object, .NET recognizes that the object is registered and uses its URL to locate it.

Registering Client-Activated Objects

A host uses the RegisterActivatedServiceType method to register a CAO. It requires only one parameter—the object type:

// Server Registration: Client-ativated Objects

Type ServerType = typeof(ImageServer);  // ImageServer class

RemotingConfiguration.RegisterActivatedServiceType(

      ServerType);

The client registration is almost as easy. It invokes the RegisterActivatedClientType method and passes it the object type and URL where the object can be located:

// Client Registration: Client-ativated Objects

Type ServerType = typeof(ImageServer);

RemotingConfiguration.RegisterActivatedClientType(

      ServerType,

      "tcp://localhost:3201");

Type Registration Using a Configuration File

As with channels, the type registration instructions can be placed in an assembly's configuration file. These two code segments illustrate how to register the SAO from the preceding example on the server and client:

//Server: Register MessageManager object as a singleton

<application >

   <service>

      <wellknown

       mode="Singleton"

       type="SimpleServer.MessageManager, msgserver"

       objectUri="MyObject"/>

   </service>

</application>

//Client: Register MessageManager object on port 3200

<application >

   <client >

      <wellknown

       type="SimpleServer.MessageManager, msgserver"

       url="http://localhost:3200/MyObject" />

   </client>

</application>

Observe that the registration information is represented as attributes in the <wellknown> tag, and that the type attribute denotes the object by its namespace and name as well as the assembly containing it.

Client-activated registration uses an <activated> tag to specify the remote object in both the server and client configuration file.

//Server: Register ImageServer to be client-activated

<application >

   <service >

      <activated type="ImageServer,caoimageserver"/>

   </service>

</application>

The client also includes a url attribute to provide the address of the remote object:

//Client: Register ImageServer to be client-activated

<application >

   <client url="tcp://localhost:3201" >

      <activated type="ImageServer,caoimageserver"/>

   </client>

</application>

Remoting with a Server-Activated Object

With an understanding of how to register channels and types, you're ready to implement a remoting application. Our first example builds an application that permits users to post and retrieve messages from other users. It's based on SAO, and we'll look at two ways to design it. Our second example uses CAO to retrieve images from an image server.

A Message Server Example

The minimum requirements for a remoting application are an assembly containing the client code, and an assembly that runs as a server and provides the implementation code for the remoting objects. As Figure 14-8 shows, the more common model uses three assemblies: a client, a host that performs channel and type registration, and a server that contains code for the objects. This is the model used for our initial message server project.

Figure 14-8. Three assemblies are used in message server remoting example

Before examining the code, we need to clarify the terminology used to describe the assemblies. In this chapter, server refers to an assembly that declares and implements the remote classes; host or listener refers to an assembly that contains the code to perform type and channel registration. If the host and server functions are combined, the assembly is referred to as a server or host/server. In the world of remoting literature, you'll find that some authors reverse this meaning of host and server, whereas others refer to the host as general assembly.

Our message server application consists of three source files that are compiled into the msgserver.dll, msghost.exe, and msgclient.exe assemblies:

csc /t:library       msgserver.cs

csc /r:msgserver.dll msghost.cs

csc /r:msgserver.dll msgclient.cs

Note that the server code is packaged as a library (DLL) and must be referenced by both the host and client during compilation.

Server Assembly

Listing 14-2 contains the code for a MessageManager class that is made available to clients as a server-activated singleton. Aside from the required MarshalByRefObject inheritance, the class is indistinguishable from a non-remoting class. It exposes two methods, SetMessage and FetchMessages, which are used to post and retrieve messages, respectively. A call to SetMessage contains the ID of the sender and recipient along with the message. This information is packaged into an instance of the Envelope class and stored in an array. Clients retrieve messages by invoking FetchMessages with their client ID. The method searches the array of messages and returns a string containing all messages for that ID.

Listing 14-2. Remoting Server

// msgserver.cs  (DLL)

using System;

using System.Collections;

namespace SimpleServer{

   public class MessageManager: MarshalByRefObject

   {

      ArrayList Messages = new ArrayList();

      public MessageManager()

      {

         Console.WriteLine("Message Object Created.");

      }

      // Concatenate all messages and return string to client

      public string FetchMessages(string clientID)

      {

         string msgList= "";

         for (int i=Messages.Count-1;i>=0;i--)

         {

            Envelope env= (Envelope)Messages[i];

            if(env.RecipientID== clientID)

            {

               msgList+= env.SenderID+": "+env.Message+"\n";

               Messages.RemoveAt(i);  // Remove message

            }

         }

         Console.WriteLine("Sending:\n {0}",msgList);

         return(msgList);

      }

      // Accept message from client and store in memory

      public void SetMessage(string msg, string sender,

                             string recipient)

      {

         // Save Message received from client as object

         // in an array

         Envelope env= new Envelope();

         env.Message= msg;

         env.SenderID= sender;

         env.RecipientID= recipient;

         Messages.Add(env);           // add message to array

         Console.WriteLine("Received:\n{0}", msg);

      }

   }

   // Messages are stored as instances of Envelope

   public class Envelope

   {

      public string Message;

      public string SenderID;

      public string RecipientID;

   }

}

Host Assembly

The host assembly, shown in Listing 14-3, performs channel and type registration. The channel is configured to use HTTP over port 3200; and the MessageManager object is designated to run as a singleton. Keep in mind that the port number, which is essentially an address associated with the application, should be greater than 1024 so as not to conflict with reserved port IDs.

Listing 14-3. Remoting Host/Listener

// msghost.cs  (exe)

using System;

using System.Runtime.Remoting;

using System.Runtime.Remoting.Channels;

using System.Runtime.Remoting.Channels.Http;

using SimpleServer;                       // Namespace of server

namespace SimpleHost

{

   public class MessageHost

   {

      static void Main()

      {

         Console.WriteLine("Host Started.");

         // Channel Registration

         HttpChannel c = new HttpChannel(3200);

         ChannelServices.RegisterChannel(c);

         // Type Registration–Use server-activated object (SAO)

         // Type is specified as (namespace.class)

         Type ServerType = typeof(SimpleServer.MessageManager);

         RemotingConfiguration.RegisterWellKnownServiceType(

            ServerType,                    // Type of Object

            "MyObject",                    // Arbitrary name

            WellKnownObjectMode.Singleton );

         Console.Read();  // Keep host running

      }

   }

}

After registration is completed, this assembly continues running and monitors port 3200 for calls to MessageManager. Any messages received are passed on to the object.

Client Assembly

The code for the client class is shown in Listing 14-4. It is run from the command line and takes an optional parameter that is used as the client ID:

> msgclient 005

The client first registers the type and channel. The latter must specify the same port (3200) as registered by the host assembly. Following registration, an instance of MessageManager is created using the new operator. When the user types an R, the object retrieves messages; to send a message, an S is entered at one prompt and the recipient ID and message at the next prompt.

Listing 14-4. Remoting Client

// msgclient.cs  (exe)

using System;

using System.Runtime.Remoting;

using System.Runtime.Remoting.Channels;

using System.Runtime.Remoting.Channels.Http;

using System.Collections;

using SimpleServer;   // Namespace of server

namespace SimpleClient

{

   public class MessageClient

   {

      static void Main(string[] args)

      {

         string myID;

         // Client ID is passed as command line argument

         if(args.Length>0) myID= args[0]; else my;

         Console.WriteLine("Client Started.");

         // (1) Channel Registration

         HttpChannel c = new HttpChannel();

         ChannelServices.RegisterChannel(c);

         // (2) Type Registration: SAO using port 3200

         Type ServerType = typeof(SimpleServer.MessageManager);

         string url= "http://localhost:3200/MyObject";

         // Register type for Server Activation Mode

         RemotingConfiguration.RegisterWellKnownClientType(

               ServerType,url);

         // (3) Create instance of Remote Object

         MessageManager mm = new MessageManager();

         string msg;

         string oper="";

         // Allow user to send or receive a message

         while(oper !="Q")

         {

            Console.WriteLine("(S)end, (R)eceive, (Q)uit");

            oper= Console.ReadLine();

            oper = oper.ToUpper();

            if(oper=="S"){

              Console.WriteLine("enter Recipient ID: messsage");

               msg= Console.ReadLine();

               // : Separates ID and message

               int ndx= msg.IndexOf(":");

               if(ndx>0) {

                 string recipientID=msg.Substring(0,ndx).Trim();

                  msg= msg.Substring(ndx+1);

                  mm.SetMessage(msg, myID, recipientID);

               }

            } else

            {

               if (oper=="R"){

                  Console.WriteLine(mm.FetchMessages(myID));

               }

            }

         }   // while

      }   // method

   }  // class

}  // namespace

Figure 14-9 shows the interactive dialog on the client screen and the corresponding output on the server/host screen. Observe that Message Object Created, which is inside the constructor, occurs when the first call is made to the remote object—not when the host begins executing. Also, the constructor is only executed once because this is a singleton object.

Figure 14-9. Client interacting with remote MessageServer configured as singleton

[View full size image]

MessageServer Configuration Files

Both msghost and msgclient contain code to perform explicit channel and type registration. As mentioned previously in this section, an alternative is to place channel and/or type registration information in an assembly's configuration file. The in-line registration code is then replaced with a call to a static Configure method that reads the configuration file and performs the registration:

RemotingConfiguration.Configure("MsgHost.exe.config");

Here is how the configuration for msghost.exe is expressed in XML:

//msghost.exe.config

<configuration>

   <system.runtime.remoting>

       <application name = "SimpleHost">

          <service>

            <wellknown

             mode="Singleton"

             type="SimpleServer.MessageManager, msgserver"

             objectUri="MyObject"/>

          </service>

          <channels>

                   <channel ref="http" port="3200"/>

          </channels>

      </application>

   </system.runtime.remoting>

</configuration>

If you compare this with the source code, it's obvious that information in the wellknown tag corresponds to the parameters in the RegisterWellKnownServiceType method; and the channel tag provides information encapsulated in the HttpChannel object. The client configuration file shows a similar correspondence between these tags and source code:

//msgclient.exe.config

<configuration>

   <system.runtime.remoting>

       <application name = "SimpleClient">

          <client >

            <wellknown

             type="SimpleServer.MessageManager, msgserver"

             url="http://localhost:3200/MyObject" />

          </client>

          <channels>

               <channel ref="http"/>

          </channels>

      </application>

   </system.runtime.remoting>

</configuration>

Note that the file contains no tag to specify the type of formatting to be used—so the default applies. You can specify a different formatter by extending the channel block:

<channel ref="http">

   <clientProviders>

      <formatter ref="Binary" />

   </clientProviders>

</channel>

Using an Interface Assembly with Server-Activated Objects

In the preceding example, the msgserver assembly is packaged as a DLL and must be deployed on both the host and client machine. On the server, it provides the actual code that is executed when a call to the remote object occurs. The client requires the assembly's metadata to compile and build a proxy at runtime, but has no use for the Intermediate Language (IL). Rather than placing this code on each client's machine—exposing it to anyone with a disassembler—the application can be redesigned to use an interface assembly to provide the metadata required by the client.

To do this, we'll combine the code in the msghost and msgserver file to create an assembly that both implements the object and performs registration. The third assembly, now called msggeneral, contains an interface that defines the methods supported by the MessageServer class (see Figure 14-10). The client then gets its metadata from this lightweight assembly rather than the full server assembly.

Figure 14-10. MessageServer redesigned to use an interface to provide remote object metadata

The simple interface shown here defines the signature of the two methods exposed by the remote class.

// msggeneral.cs  (DLL)

namespace SimpleServer

{

   // Define an interface to provide method descriptions

   // used by client and implemented by server.

   public interface IMessageManager

   {

      string FetchMessages(string clientID);

      void   SetMessage(string message, string sender,

                        string recipient);

   }

}

The server assembly contains the same object implementation code as in the first example. However, it now inherits from the newly defined IMessageManager interface and includes the registration code from the host/listener assembly. This code is contained in a new class StartServer that provides an entry point to the assembly so that it can be compiled into an .exe file.

// msgserverv2.cs (exe)

public class MessageManager: MarshalByRefObject, IMessageManager

// Code for MessageManager and Envelope class goes here ...

// Class to provide entry point to assembly and perform

// registration

class StartServer

{

   static void Main()

   {

      Console.WriteLine("Host Started.");

      // Channel Registration

      HttpChannel c = new HttpChannel(3200);

      ChannelServices.RegisterChannel(c);

      // Type Registration

      Type ServerType = typeof(SimpleServer.MessageManager);

      RemotingConfiguration.RegisterWellKnownServiceType(

         ServerType,    // Type of Object

         "MyObject",    // Arbitrary name

         WellKnownObjectMode.Singleton);

      Console.Read();

   }

}

Changes are required in the client code to account for the fact that the description of the remote object now comes from an interface; and because it's not possible to directly instantiate an interface, another way must be found to gain a reference to the remote object. The solution is to use Activator.GetObject to return an instance of the interface. This is an important point: .NET returns interface information to the client but creates the actual object on the server where it runs as a server-activated object. As this code segment illustrates, Activator.GetObject performs registration and returns an object in one step:

Type ServerType = typeof(IMessageManager);

// Activate Remote Object and perform registration

object remoteObj = Activator.GetObject(

      ServerType,

      "http://localhost:3200/MyObject");

IMessageManager mm = (IMessageManager) remoteObj;

No other changes are required in the msgclient.cs code.

The three source files are compiled into the msgserver.exe, msggeneral.dll, and msgclient.exe assemblies:

csc /t:library         msggeneral.cs

csc /r:msggeneral.dll  /out:msgserver.exe msgserverv2.cs

csc /r:msggeneral.dll  msgclient.cs

Remoting with a Client-Activated Object (CAO)

In the preceding server-activated singleton example, each call to the remote object is handled by the same object. In the client-activated model, each client creates its own object and can make multiple calls to that object (refer to Figure 14-5 on page 650). The model does not scale particularly well because a large number of concurrent client-created objects can deplete system resources. However, when the expected number of users is small and there is a need to maintain state information between calls, this model should be considered.

An Image Server Example

This project implements a remote server that serves up images to clients upon request. It loads the requested file from local storage and delivers it as a byte stream to the client. After being received by the client, the bytes are then reassembled into the original image.[2]

[2] For comparison, this image server is implemented in Chapter 18, "XML Web Services," as a Web Service.

Prior to examining the code, let's consider the rationale behind implementing the server using client activation, rather than one of the server-activation modes. A singleton can be ruled out; because each client's request is independent of any other client's request, there is no need to maintain state information. The choice between single call activation and client activation is less clear-cut and depends on expected client behavior. If a large number of clients requesting a single image are expected, single call is preferable because it manages resources better by discarding the server object as soon as the request is completed. If a client is expected to make several image requests during a session, client activation is preferred. It allows a client to create and reuse one object for several calls—obviating the need to build and tear down objects with each request.

Host Assembly

The host/listener assembly has the familiar task of registering the channel(s) and type. Because this application revolves around streaming raw bytes of data, binary formatting is selected over SOAP. This does not have to be specified directly, because the choice of the TCP protocol assigns binary formatting by default.

The other noteworthy change from the server-activated example is that RegisterActivatedServiceTypeCode is used for type registration instead of RegisterWellKnownServiceType.

using System.Runtime.Remoting.Channels.Tcp;

// Channel Registration for TCP

TcpChannel c = new TcpChannel(3201);

ChannelServices.RegisterChannel(c);

// Type Registration for CAO

Type ServerType = typeof(ImageServer);

RemotingConfiguration.RegisterActivatedServiceType(

      ServerType);

Server Assembly

Listing 14-5 provides the implementation of the ImageServer class, which exposes two methods: GetFiles and GetMovieImage. The former returns an array containing the name of all available image files. GetMovieImage is the heart of the system. It receives a string containing the name of a requested image, opens the corresponding file as a memory stream, and converts it to an array of bytes that is returned to the client. (See Chapter 5, "C# Text Manipulation and File I/O," for a refresher on memory streams.)

Listing 14-5. Using Remoting to Implement an Image Server

// caoimageserver.cs  (DLL)

using System;

using System.Runtime.Remoting;

using System.Collections;

using System.Drawing;

using System.IO;

public class ImageServer: MarshalByRefObject

{

   // Return list of available images as a string array

   public ArrayList GetFiles()

   {

      ArrayList a = new ArrayList();

      string dir=@"c:\images\";

      foreach(string fileName in Directory.GetFiles(dir))

      {

         // Strip path from file name

         int ndx= fileName.LastIndexOf("\\");

         string imgName = fileName.Substring(ndx+1);

         a.Add(imgName);

      }

      return a;

   }

   // Return requested image as byte stream

   public byte[] GetMovieImage(string imageName)

   {

      int imgByte;

      imageName= "c:\\images\\"+imageName;

      FileStream s = File.OpenRead(imageName);

      MemoryStream ms = new MemoryStream();

      while((imgByte =s.ReadByte())!=-1)

      {

         ms.WriteByte(((byte)imgByte));

      }

      return ms.ToArray();

   }

}

Client Assembly

The registration steps in Listing 14-6 enable the client to communicate with the host using TCP on port 3201. The call to RegisterActivatedClientType, which registers the ImageServer type, corresponds to the RegisterActivatedServiceType call on the host. After registration, the new operator is used to give the client a reference to the remote object (via a proxy).

Listing 14-6. Using a Client-Activated Object to Access ImageServer

// caoimageclient.cs  (.exe)

using System;

using System.Runtime.Remoting;

using System.Runtime.Remoting.Channels;

using System.Runtime.Remoting.Channels.Tcp;

using System.Collections;

using System.Drawing;

using System.IO;

namespace SimpleImageClient

{

   class ImageClient

   {

      static void Main(string[] args)

      {

         // (1) Register channel for TCP/Binary

         TcpChannel c = new TcpChannel();

         ChannelServices.RegisterChannel(c);

         // (2) Register remote type for CAO

         Type ServerType = typeof(ImageServer);

         RemotingConfiguration.RegisterActivatedClientType(

               ServerType,

              "tcp://localhost:3201");

         ImageServer imgMgr=null;

        bool serverOK=true;    // Indicates whether server is up

         // (3) Create instance of remote object

         try{

            imgMgr = new ImageServer();

         } catch (Exception ex) {

            Console.WriteLine(ex.Message);

            serverOK=false;

         }

         if(serverOK)

         {

            string oper="";

            while(oper !="Q")

            {

               Console.WriteLine(

                     "(L)ist files,(R)etrieve,(Q)uit");

               oper= Console.ReadLine();

               oper = oper.ToUpper();

               if(oper=="R"){

                  Console.WriteLine(

                        "Enter image name to retrieve:");

                  string fname= Console.ReadLine();

                // Exception is handled if image cannot be found

                  try

                  {

                     // Request image from server

                     byte[] image = imgMgr.GetMovieImage(fname);

                     MemoryStream memStream = new

                           MemoryStream(image);

                     Console.WriteLine("Image Size: {0}",

                                       memStream.Length);

                     // Convert memory stream to a Bitmap object

                     Bitmap bm = new Bitmap(memStream);

                     // Save image on local system

                     bm.Save("c:\\cs\\"+fname,

                       System.Drawing.Imaging.ImageFormat.Jpeg);

                  } catch (Exception ex) {

                     Console.WriteLine(ex.Message);

                  }

               }

               else

               {

                  if (oper=="L")   // List image file names

                  {

                     try

                     {

                        ArrayList images = imgMgr.GetFiles();

                        for (int i=0;i<images.Count;i++)

                        {

                           Console.WriteLine(images[i]);

                        }

                     } catch (Exception ex) {

                        Console.WriteLine(ex.Message);

                     }

                  }

               }

            }   // while

         }   // serverok

      }   // Main

   }    // class

}    // namespace

The code implementation is based on a simple command-line menu. When L is entered, a list of available images is displayed on the console; when R is entered, the program prompts for the name of a file to be downloaded. This image name is sent to the server, which returns the image—if it exists—as an array of bytes. A Bitmap object is created and its Save method is used to store the image on the client's disk.

Deploying the CAO Application

Use the C# compiler to create caoimagelistener.exe, caoimageserver.dll, and caoimageclient.exe:

csc /t:library             caoimageserver.cs

csc /r:caoimageserver.dll  caoimagelistener.cs

csc /r:caoimageserver.dll  caoimageclient.cs

Note that this implementation requires packaging caoimageserver.dll with caoimageclient.exe on the client's machine. As discussed earlier, this exposes the server code on the client machine. For the SAO application described earlier, using an interface implementation solved this problem. A similar approach can be used with client-activated objects, but requires creating a "factory" that returns an interface for the server object. Another solution is to use the .NET SoapSuds utility to extract metadata from the server assembly into a DLL that can be deployed instead of the server assembly. This utility is executed at the command-line prompt:

soapsuds –ia:caoimageserver –oa:serverstub.dll –nowp

-ia:  specifies the input assembly; do not include .dll or .exe

-oa:  specifies the output assembly that will contain the metadata.

-nowp specifies that a nonwrapped proxy is to be created. A wrapped

       proxy can be used only with SOAP channels and is designed

       for working with Web Services.

The output assembly, serverstub.dll, is now referenced as the client is compiled and is deployed on the client's machine along with the client assembly.

csc /r:serverstub.dll caoimageclient.cs

You now have two approaches that can be used to avoid deploying an entire server implementation assembly on the client's machine: an interface assembly or an assembly created from SoapSuds generated metadata. Which is better? In general, the interface approach is recommended. SoapSuds works well, but doesn't work for all cases. For example, if an assembly contains a class that implements ISerializable or has the [Serializable] attribute, it does not generate metadata for any of the class's properties. However, if you have a relatively simple server assembly as in the preceding example, SoapSuds can be used effectively.

Design Considerations in Creating a Distributed Application

One of the first decisions required in designing a remoting application is whether to use server- or client-activated objects. Here are some general guidelines:

  • If the application requires that the server maintain state information—as was the case in our message server example—an SAO singleton is the obvious choice. If the application does not require a singleton, do not use it. Scalability and synchronization can be a problem when accommodating a large number of users.

  • If each call to a remote object is independent of other calls, an SAO single call model is the best choice. This is also the most scalable solution because it does not maintain state information and is destroyed as soon as it's no longer needed.

  • The third choice—the client-activated object—allows the client to create the object and maintain state information in instance variables between calls. One drawback is that the model does not support the use of a shared interface assembly to provide the metadata required to create a proxy. Instead, a class-factory approach or SoapSuds must be used.

Configuring Assemblies

After the remoting model has been chosen, there remains the choice of how to design the assemblies required on the client and server side of the application. As discussed earlier, the client side of the application requires the client application, plus an assembly, to provide the necessary information for .NET to construct a proxy. The server implementation assembly is not a good choice because it includes code. Alternatives are a metadata assembly provided by the SoapSuds utility or an assembly that includes only an interface for the server class. Figure 14-11 summarizes the most common design choices.

Figure 14-11. Assembly combinations that can be used to deploy a remoting application

Assembly Definitions:

SoapSuds Metadata | An assembly created by running SoapSuds against a server assembly.
Interface | An assembly containing an interface that defines the server class.
Server Code | A DLL containing the implementation of the remote class.
Server/Host | An .exe file containing code to implement the remote class and perform type and channel registration.
Host | An .exe file that performs type and channel registration.

Each row shown in Figure 14-11 indicates the type of assemblies to be used in an application. For example, the second row shows a possible configuration for a server-activated object design. A DLL containing an interface inherited by the remote class is deployed on the client along with the client's executable assembly; the server side shares the same interface assembly and contains the interface implementation, as well as the registration code in a single server/host assembly.

This figure represents only a starting point in the design process, because many remoting applications are hybrids that may combine both SAO and CAO. However, an understanding of these core design techniques provides the foundation needed to implement and deploy more complex applications.


14.3. Leasing and Sponsorship

One of the signature features of .NET is its use of Garbage Collection to remove unused objects. As was discussed in Chapter 4, .NET maintains a graph to keep track of all references to objects in the managed heap. When Garbage Collection occurs, those objects for which references cannot be found are destroyed. Because .NET only recognizes references within a process, objects referenced by remote clients would be subject to Garbage Collection leaving the remote client(s) with no host object to call.

The rather clever .NET solution is to assign a lease object to each client-activated object or server-activated singleton when they are created. (Server-activated single call objects are not affected because they only exist for the duration of a call.) The lease provides a reference to the object that keeps the object from being made available for Garbage Collection as long as the lease exists—which is based on the lease time assigned to the lease. The time associated with the lease is referred to as the object's time-to-live (TTL).

As we see next, a lease may be renewed programatically by calling methods on the lease object to increase its lease time. If the lease does expire, .NET looks for an object associated with the lease, known as a sponsor, and gives it a chance to renew the lease. Let's look at the details.

Leasing

A lease is an object created internally by .NET that implements the ILease interface. This interface, located in the System.Runtime.Remoting.Lifetime namespace, defines the members that are used to govern a lease's behavior. These include properties to get or set the initial time of the lease (the default is 5 minutes), obtain the time remaining on the lease, and specify the time a lease is renewed for when its associated object is invoked. Table 14-1 summarizes the ILease members.

Table 14-1. ILease Interface Members

Member

Description

CurrentLeaseTime

Amount of time remaining before the object is made available for Garbage Collection. If it is invoked, the lease time is reset.

CurrentState

Returns the current state of the lease, defined by a LeaseState enumeration: Active, Expired, Initial, Null, or Renewing.

InitialLeaseTime

Gets or sets the initial amount of time-to-live that a lease assigns to an object. Default is 5 minutes.

RenewOnCallTime

Gets or sets the amount of time—specified using TimeSpan—that a call to an object increases the CurrentLeaseTime. Default is 2 minutes.

SponsorshipTimeout

Gets or sets the amount of time available for a sponsor to provide a new lease renewal time. Default is 2 minutes.

Register()

Registers a sponsor for the lease.

Renew()

Renews a lease for a specified amount of time.

UnRegister()

Removes a sponsor from an object's list of sponsors.

Setting Initial Lease Values for Objects in an Application

You can override the default values assigned to lease properties in an application's configuration file. For example, to set the lease values for all objects in our image server example, insert the following into the caoimagehost.exe.config file:

<application >

   <lifetime

      leaseTime = "8M"

      renewOnCallTime = "6M"

   />

   ...

</application>

In this example, the time is specified in minutes (M), but it may also be specified as days (D), hours (H), seconds (S), or milliseconds (MS).

The same effect can be achieved inside a program by setting static properties on the LifetimeServices class:

LifetimeServices.LeaseTime = System.TimeSpan.FromMinutes(8);

LifetimeServices.RenewOnCallTime =

      System.TimeSpan.FromMinutes(6);

Setting the Lease Values for an Individual Object

Both of the aforementioned techniques for setting initial lease values apply to all objects hosted by the server. To set lease values on an object-by-object basis, a different approach is required. Recall that for a class to implement remoting it must inherit the MarshalByRefObject class. This class includes an InitializeLifeTimeService method that is responsible for returning lease objects. It is a simple matter to override this method in a program to assign your own initial values to the lease object.

using System.Runtime.Remoting.Lifetime;  // Contains ILease

public class MessageManager: MarshalByRefObject, IMessageManager

{

// Override this method to set lease initialization values

public override object InitializeLifetimeService()

{

   ILease msgLease = (ILease)base.InitializeLifetimeService();

   msgLease.InitialLeaseTime = TimeSpan.FromMinutes(8);

   msgLease.RenewOnCallTime  = TimeSpan.FromMinutes(6);

   return msgLease;

}

Note that implementation of this method overrides any values in the configuration file. This means that a configuration file can be used to set default values for all objects, and this method override approach can be used to set non-default values for selected objects.

After a lease is in an active state, the only property on the lease that can be changed is its CurrentLeaseTime. This value can be renewed by a sponsor (described later) or by having the client or object explicitly invoke the lease's Renew method.

// Client

(ILease) lease=(ILease)RemotingServices.GetLifetimeServices(ob)

// Object on server

(ILease) lease=(ILease)RemotingServices.GetLifetimeSer-

vices(this)

if(lease.CurrentLeaseTime.TotalMinutes < 1.0)

      lease.Renew(TimeSpan.FromMinutes(5));

If the current lease time is greater than this renewal time, the lease is not reset.

The Lease Manager

Running in the background of each AppDomain is a lease manager that keeps track of each server object and its associated lease. The primary function of the manager is to periodically examine the leases for time expiration and invoke a sponsor if the lease has expired.

The lease manager uses a timer to control how frequently it checks for lease expirations. By default, this is set to 10 seconds. To override the default, add this setting in the configuration file:

<application >

   <lifetime

      LeaseManagerPollTime"5S"

   />

The value can also be set programmatically using the LifetimeServices class:

LifetimeServices.LeaseManagerPollTime =

      System.TimeSpan.FromSeconds(5);

The Lifetime of a Lease

It should be clear from the discussion thus far that a lease has a nondeterministic lifetime whose actual length is determined by calls on the associated object, direct invocation of the lease's Renew method, and any renewal coming from a lease's sponsor(s). The state diagram in Figure 14-12 illustrates the relationship between these actions and the current state of a lease. Note that a lease may be in one of four states—Initial, Active, Renewing, or Expired.

Figure 14-12. Different states in which a lease may exist

When a remote object is created, .NET creates a corresponding lease and sets its InitialLeaseTime—in descending order of priority—to either a value set programmatically, a value in the configuration file, or a default value of 5 minutes. The lease moves into an active state, and its time begins to tick away. When a call is made on the object, the remaining lease time is compared with the renewOnCallTime value. The lease time is set to the greater of the two values. For example, if 4 minutes remain, and the renew time is 6 minutes, the value is set 6 minutes; if 7 minutes remain, the value is not altered. When the TTL value goes to 0, the lease manager checks to see if the lease has a sponsor that will assign a new lease time; if not, the object is deactivated and made available for Garbage Collection. Calls made on an object whose lease has expired throw an exception indicating that "no receiver could be found" or the "object has been disconnected." For this reason, it is important to surround all calls on a remote object with exception handling code.

Sponsorship

A program's final opportunity to renew a lease is when the lease's CurrentLeaseTime value winds down to 0. At this point, the lease enters a LeaseState.Renewing state (see Figure 14-12), and the lease manager checks to see if any sponsor has been registered with the object (a lease may have sponsors created by multiple clients). If a sponsor is found, it is called and given the opportunity to renew the lease by returning a TimeSpan value that becomes the new TTL for the object.

This sponsorship model, based on registration and callbacks, closely resembles the .NET event handling model. In fact, a sponsor can be viewed as an event handler that is called when the lease time approaches 0. Table 14-2 spells out the similarities.

Table 14-2. Similarities Between an Event Handler and a Sponsor

Event Handler

Sponsor

Registers with a delegate to handle an object's event.

Registers with an object's lease when an object is created.

An event handler may be registered for multiple events.

A sponsor may register with multiple leases.

Implements a method that is called to take some action when the event occurs.

Implements a class that contains a method, Renewal, that is called when the TTL=0 event occurs. The method then takes action to extend or end the lease.

Signature of event handling method must match that of the delegate.

Renewal must always have this signature:

Timespan Renewal(ILease lease)

Although an event handler is a method, a sponsor is a class that meets two requirements: It inherits from MarshalByRefObject, and it implements the ISponsor interface:

public class ImageSponsor: MarshalByRefObject, ISponsor

The ISponsor interface defines only one member—a method, Renewal—that returns the new TTL value for the object as a TimeSpan type:

public TimeSpan Renewal(ILease lease)

A sponsor can be implemented on the server, client, or in a third-party assembly. The only requirement is that the lease manager can locate it. In reality, a sponsor should be defined where the application can best assess the factors that determine how or if an object's lease is extended. For an SAO singleton, this is on the server/host; for a CAO, the code should be on the client, because only the client knows how it's going to be using the object.

To demonstrate the details of implementing a sponsor, let's add one to the client code (see Listing 14-6) used in the image server example. It can be done in four steps:

1.
Add namespaces that define an HTTP channel to be used to receive the callback from the lease manager, and the ILease and ISponsor interfaces:

using System.Runtime.Remoting.Channels.Http;

// Contains ILease and ISponsor

using System.Runtime.Remoting.Lifetime;

2.
Register the channel to be used for the callback from the lease manager. Setting the port to 0 instructs .NET to automatically use any available port:

// Register channel for callback from lease manager

HttpChannel c2 = new HttpChannel(0);

ChannelServices.RegisterChannel(c2);

This could also be done in a client configuration file:

<channels>

   <channel ref="http" port="0">

</channels>

3.
Register a sponsor for the lease. First, use GetLifetimeService to get a reference to the lease. Then, pass an instance of the sponsor to the ILease.Register method:

// Register sponsor

ISponsor currSponsor = new ImageSponsor();

ILease lease =

      (ILease)RemotingServices.GetLifetimeService(imgMgr);

lease.Register(currSponsor);

4.
Define the sponsor class. It must have the proper inheritance and must provide an implementation of the ISponsor method Renewal.

// Implement Sponsor

public class ImageSponsor: MarshalByRefObject, ISponsor

{

   public TimeSpan Renewal(ILease lease)

   {

      Console.WriteLine(lease.CurrentLeaseTime);

      // Set object's time to live to 10 minutes

      return TimeSpan.FromMinutes(10);

   }

}

In this example, the sponsor renews the lease for 10 more minutes. Because this sponsor is called each time the TTL approaches 0, the object associated with the lease exists as long as the client is running. A more sophisticated implementation would include logic that decides whether to renew the lease. To indicate no renewal, the sponsor returns TimeSpan.Zero.

Core Note

A client-based sponsor works only if the server can access the client. This technique cannot be used for a client situated behind a firewall that blocks access.


14.4. Summary

To run code in a managed environment, .NET creates partitions called application domains for the assemblies to execute in. The AppDomain, which runs inside of a physical process, has the advantage of providing more secure code through code isolation and security boundaries. The boundaries prevent an object in one AppDomain from directly accessing an object in another AppDomain. For them to communicate, .NET provides a set of classes that support remoting—a technique that enables objects to communicate across AppDomain boundaries.

Remoting provides a way to implement client-server or peer-to-peer distributed applications. It's designed to conceal the underlying details of how messages are transported and permit the developer to focus on higher level tasks such as selecting a protocol or the way the transported message is formatted. A key component of the remoting architecture is the use of a proxy on the client side that serves as a surrogate for the remote object. It interacts with the client by presenting the same interface as on the remote object and encapsulates the information required to translate client calls into actual calls on the remote object.

The remote objects can be implemented in several ways: as a singleton that services all requests, as a server-activated single call object that is created each time the object is invoked, or as a client-activated object that persists as long as the client keeps it alive. The three offer a variety of different characteristics that enable a developer to select one most suited for an application.


14.5. Test Your Understanding

1:

True or False?

  1. A process may contain multiple AppDomains.

  2. An AppDomain may contain multiple assemblies.

  3. An AppDomain can contain both EXE and DLL assemblies.

  4. An AppDomain can be unloaded from memory.

2:

What are the three types of object activation modes, and which protocols can be used with each?

3:

Which activation mode creates a new object each time a client invokes a method on the object? Which mode creates a new object only the first time a client invokes a method?

4:

Explain the difference between CurrentLeaseTime, InitialLeaseTime, and RenewalOncallTime.

5:

What determines how often the lease manager checks for lease expirations?

6:

Which activation modes support the use of leases?

7:

When is a server-activated singleton object created?

  1. When the host begins running.

  2. When the client uses the new operator to create an instance of the object.

  3. When the client first calls a method on the object.

  4. When the object type is registered by the host.

8:

Compare channel registration and type registration.

9:

What is the reason to use SoapSuds or an interface when designing a remoting application? How is SoapSuds used?


Chapter 15. Code Refinement, Security, and Deployment

Topics in This Chapter

  • Code Refinement: .NET provides a tool, FxCop, which analyzes code by checking it against a set of best practice rules and recommendations. This tool is designed for building components, but most applications can benefit from it as way to amend and refine how code is implemented. An example demonstrates how to analyze code using this tool's command-line interface.

  • Strongly Named Assemblies: One aspect of code security is being able to verify an application's origin and version. .NET provides a way to mark an assembly with a key that identifies it, and supports an assembly versioning scheme that distinguishes between code versions—allowing multiple versions of a component to coexist.

  • Application Security: The .NET Code Access Security model is based on a simple principle: allow code to access system resources and perform operations only when it has permission to do so. Before an assembly can access resources such as files, sockets, or the registry, it is checked for evidence to determine the permissions that it can be given. This chapter explains the overall security model and looks at how it is applied administratively and within code.

  • Application Deployment: One of the touted benefits of .NET is the ability to install an application using XCOPY deployment—simple file copying. However, many applications require a more sophisticated approach that takes into account security policies and resource management. This chapter presents a checklist of issues to be considered.

In the earliest days of programming, computers were used primarily to perform calculations and tedious tabulations. The measure of a program's correctness was whether it produced accurate results for a given set of input values. Modern software development now relies more on component-based solutions. The components often come from multiple sources, and it's not always possible to know the origin or trustworthiness of the components. As a result, code security and the ease of deploying and updating an application are now important metrics against which an application's success is judged.

This chapter looks at the issues and steps involved in producing a deliverable .NET software product. It breaks the process down into the three categories shown in Figure 15-1: code refinement, which looks at how code is tested against best practice rules; code security, which ensures that code is accessed only by other code that has permission to do so; and code deployment, which looks at how an application or component is packaged and made available for deployment.

Figure 15-1. Deliverable software should meet coding standards, be secure, and be easily deployed

The first section shows how to use FxCop as a tool to analyze an assembly and generate code change recommendations based on a predefined set of coding standards. The second section looks at the details of how to create a strongly named assembly and the security benefits that accrue from doing so.

The next section—which forms the heart of the chapter—explores the topic of Code Access Security (CAS). It explains how an administrator uses .NET tools to define a multi-level security policy for a computing environment and how security features are embedded in code. It also stresses understanding the interrelated security roles of evidence, policy, and permissions.

The chapter concludes with a look at the issues to be considered in deploying an application to users or customers. The advantages and disadvantages of using XCOPY or an installer to physically distribute an application are discussed.


15.1. Following .NET Code Design Guidelines

The developers of .NET included a set of rules [1] that are intended to serve as a guide for writing code that runs on the .NET platform. Although the rules are written with component developers in mind (Microsoft uses them for its managed code libraries), any application can benefit from them.

[1] http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpgenref/html/cpconnetframeworkdesignguidelines.asp

To help a developer incorporate these rules and best practices in their code, .NET includes the FxCop tool that analyzes code against these rules. Some of the rules may be stricter than your development environment requires; or they may simply conflict with your own standards—you may prefer non-Microsoft naming conventions, for instance. To accommodate your own coding standards, the tool permits you to disable rules and add custom ones. Figure 15-2 shows how rules are displayed with check boxes that make it easy to enable or disable them.

Figure 15-2. FxCop allows rules to be enabled or disabled by clicking a check box

FxCop is available as both a GUI (fxcop.exe) and command line (fxcopcmd.exe) application. Downloads of the latest version and documentation are free and available at several Web locations. The download also includes an SDK that can be used to create custom rules—a topic beyond the scope of this section.

Using FxCop

The purpose of FxCop is to analyze an assembly and produce output that pinpoints code features that violate the set of recommended best practices. To illustrate how it works, let's create an assembly from the code in Listing 15-1 and run it through FxCop. The program is a simple console application that accepts an input string, reverses it, and prints it. A simple menu permits the user to specify whether she wants to reverse a string or quit.

To test with FxCop, compile the program and pass the assembly to FxCop:

C:/> csc fxtest.cs

C:/> fxcopcmd  /file:fxtest.exe  /out:fxoutput.txt

Due to the length of the output, it's better to direct it to a file rather than display it.

Listing 15-1. Code to Be Analyzed by FxCop

//fxtest.cs

using System;

using System.Text;

namespace FxTesting

{

   public class TestApp

   {

      static void Main()

      {

         string msg;

         string oper="";

         while(oper !="Q")

         {

            Console.WriteLine("(R)everse, (Q)uit");

            oper= Console.ReadLine();

            oper = oper.ToUpper();

            if(oper=="R"){

               Console.WriteLine("Enter phrase to reverse:");

               msg= Console.ReadLine();

               if(msg.Length>1) msg= Reverse(msg);

               Console.WriteLine(msg);

         }

      }

   }

   // Function to reverse a string

   public static String Reverse(String stringParameter)

   {

      if(stringParameter.Length==1)

      {

         return stringParameter;

      }

         else

         {

            return Reverse(stringParameter.Substring(1)) +

                           stringParameter.Substring(0,1);

         }

      }

   }

}

The output is serialized as XML that contains Message tags, describing each occurrence where the code does not conform to the recommended practices. Here is an example of the raw code comprising one message:

<Message TypeName="AssembliesShouldHaveValidStrongNames"

   Category="Microsoft.Design" Check Status="Active"

   Created="2005-01-12 02:41:07Z" FixCategory="NonBreaking">

   <Issue Name="NoStrongName" Certainty="95"

      Level="CriticalError">Sign 'fxtest' with a strong name key.

   </Issue>

</Message>

Let's look at the analysis FxCop produces for the fxtest assembly. For brevity, only the TypeName values from the XML are listed. Beneath each is a description of the code changes that will eliminate the message:

(1) | "AssembliesShouldDeclareMinimumSecurity" Requires adding a permission attribute that specifies the permissions required by this assembly. This is explained in Section 15.3 of this chapter.
(2) | "AssembliesShouldHaveValidStrongNames" Assembly should be assigned a strong name, which is a key that identifies the assembly. Assigning strong names is described in Section 15.2 of this chapter.
(3) | "MarkAssembliesWithAssemblyVersion" Add version attribute: [assembly: AssemblyVersion("1.0.0.0")].
(4) | "MarkAssembliesWithClsCompliant" Add compliant attribute: [System.CLSCompliant(true)]
(5) | "MarkAssembliesWithComVisible" Add ComVisible attribute: [ComVisible(true)]. This exposes public managed assemblies and types to COM. By default, they are not visible to COM.
(6) | "StaticHolderTypesShouldNotHaveConstructors" Because the class TestApp has only static members, it should not have a public constructor. In this case, the public constructor is the default parameterless constructor. To override this, add: private TestApp() {}.
(7) | "AvoidUnnecessaryStringCreation" To avoid allocating memory for strings, string operations should be avoided. The solution is to eliminate oper = oper.ToUpper(); and to use case- insensitive comparisons in the code, such as if(string.Compare(oper, "R", true)==0).
(8) | "AvoidTypeNamesInParameters" Naming rules recommend that type names not be included as part of a parameter name. In this case, it objects to the parameter name stringParameter.

A review of the output shows that the assembly recommendations (1 through 5) are oriented toward components that will be stored in libraries and used by other code. As one would expect, the emphasis is on how component code interacts with other code. In general, security and type visibility is of more importance here than in general application development. Recommendations 6 and 7 promote more efficient code, whereas rule 8 is a subjective naming convention.

Many of the rules are overly restrictive for most programming environments, and you'll want to disable them. However, it is beneficial to be aware of them. They represent a set of best practices that have evolved with .NET. Even if you do not incorporate them in your code, understanding the reasoning behind them will deepen your knowledge of .NET.


15.2. Strongly Named Assemblies

The software development process relies increasingly on integrating code with components from multiple vendors. The vendor may be well known and trusted, or the component may be Internet freeware from an unknown developer. In both cases, security concerns demand a way to identify and authenticate the software. The most reliable solution is to sign the software with some unique digital signature that guarantees its origin and trustworthiness. The certificates that identify the publisher of downloadable software on the Internet are an example of this.

One of the integral parts of .NET security is the use of signing to create a uniquely identifiable assembly. .NET refers to such an assembly as strongly named. A strongly named assembly uses four attributes to uniquely identify itself: its file or simple name, a version number, a culture identity, and a public key token (discussed later). Together, these four are referred to as the "name" of the assembly. In addition, the assembly has the digital signature created by signing it. All of this is stored in the assembly's manifest. [2]

[2] The manifest is a set of tables containing metadata that describes the files in the assembly.

Although an assembly does need to be strongly named, doing so offers several advantages:

  • It enables version control. Although you can add a version number to an assembly, the Common Language Runtime (CLR) ignores the version information unless the assembly is strongly named. As we will see later, having versioning in effect permits multiple versions of a component to run and ensures compatibility between assemblies sharing a version number.

  • It permits an assembly to be placed in the Global Assembly Cache (GAC) where it can be shared among calling assemblies.

  • The .NET Code Access Policy can be used to grant or restrict permissions to assemblies based on their strong name. In addition, the strong name can also be used programmatically to control access to resources.

  • It allows assemblies to have the same name, because the assemblies are identified by their unique information—not their name.

Creating a Strongly Named Assembly

The first step in creating a strong name is to create a pair of public and private encryption keys. During compilation, the private key is used to encrypt the hashed contents of the files contained in the assembly. The encrypted string that is produced is the digital signature that "signs" the assembly. It is stored in the manifest, along with the public key. Here are the steps to create and use a strongly named assembly:

1.
A strong name is generated using asymmetric public key cryptography. This scheme relies on a private key that is used for encryption and a public key that decrypts. To create a file containing this key pair, use the Strong Name command-line utility as shown here:

SN –k <keyfilename>

SN –k  KeyLib.snk

2.
Because the public key that is created is so large, .NET creates a more manageable public key token that is a 64-bit hash of the public key. When a client assembly is built that references a strongly named assembly, the public key token of the referenced assembly is stored in the manifest of the client assembly as a way to reference the target assembly.

3.
After you have the file containing the public/private key pair, creating the strongly named assembly is simple: just place the AssemblyKeyFile attribute (located in the System.Refection namespace) in your code at the assembly level:

[assembly: AssemblyKeyFile("KeyLib.snk")]

4.
This statement causes the compiler to extract the private key from the specified file and use it to sign the assembly. At the same time, the public key is placed in the assembly's manifest. Note that if you invoke the C# compiler from the command line, you can leave out the AssemblyKeyFile attribute and use the /keyfile flag to specify the path to the .snk file.

5.
When the runtime loads this assembly, it essentially reverses the signing process: It decrypts the signature using the public key found in the manifest. Then, it performs a hash of the assembly's contents and compares this with the decrypted hash. If they do not match, the assembly is not allowed to run.

Figure 15-3 summarizes the overall process. Note how decryption works. The decrypted signature yields a hash that should match the output when a new hash is performed on the assembly. If the two match, you can be sure that the private key associated with the public key was used for the original signing, and that the assembly has not been tampered with—changing even one bit will result in a different hash.

Figure 15-3. Using private and public keys to sign and verify a strong assembly

Core Note

A digital signature is not the same thing as a digital certificate. The digital signature in a strongly named assembly tells you nothing about who created the assembly, whereas a certificate contains the identity information that is used to authenticate the certificate publisher. Refer to documentation on Authenticode to learn how to obtain and use a certificate with an assembly.

Delayed Signing

It is imperative that the private key generated using Sn.exe (or some other process) not be compromised, because it is how an organization uniquely signs its software. If another party has access to the key, consumers cannot trust the ownership of the assembly.

One measure to secure the key is to limit its availability to developers, or withhold it altogether. During the development stage, developers are given only the public key. Use of the private key is delayed until it is necessary to sign the final software version. Delayed signing requires different steps than are used for creating a strongly named assembly:

1.
Sn.exe is used to create a file containing the public/private key pair. This should be the task of the security administrator.

2.
Sn.exe is run again to extract the public key into a separate file. The command uses the –p switch, and specifies the original file and the file to contain the public key:

SN –p  KeyLib.snk  PubKeyLib.snk

3.
The public key file is distributed to developers who must add two attributes to their assemblies to perform partial signing and have the public key stored in the assembly's manifest.

[assembly: AssemblyKeyFile("PubKeyLib.snk")]

[assembly: AssemblyDelaySign(true)]

4.
If you follow these preceding steps to create an assembly, you'll find that an exception occurs ("strong name validation failed") when you try to load it. This is because the assembly does not have a valid signature. To instruct the runtime to skip signature verification, run Sn.exe with the –Vr switch and specify the delay-signed assembly:

SN –Vr <delay-signed assembly>

5.
Prior to deployment, the assembly should be officially signed. Use SN with the –R switch to sign it:

SN –R <delay-signed assembly>

To reinstate signature verification for the assembly, run

SN –Vu <assembly>

Because an assembly references another strongly named assembly using its public key, there is no need to rebuild assemblies dependent on this one.

Global Assembly Cache (GAC)

The Global Assembly Cache is a special directory set aside where strongly named assemblies can be stored and located by the CLR. As part of resolving references at load time, the CLR automatically looks in the GAC for the requested assembly. The obvious advantage of storing assemblies in the GAC is that they are located in a central, well known location where they can be located and shared by multiple applications. A less obvious advantage is that the strong name signatures for assemblies in the GAC are verified only when installed in the GAC. This improves the performance of applications referencing these assemblies, because no verification is required when loading the assembly.

Physically, the GAC is a Microsoft Windows directory located on the following path:

C:\winnt\assembly

You can view its contents using a shell extension (ShFusion.dll) that is added to Windows Explorer as part of the .NET Framework installation. Each entry displays an assembly's name, type, version number, and public key token. By clicking an assembly entry, you can bring up a context menu that permits you to display the assembly's properties or delete it.

The easiest way to install a strongly named assembly into the GAC (or uninstall one) is to use GACUtil.exe, a command-line utility that ships with .NET SDK. Here is the syntax for performing selected operations:

>gacutil /i <assembly> | Installs assembly in GAC.
>gacutil /u <assembly> | Uninstalls an assembly from the GAC.
>gacutil /if <assembly> | Installs assembly in GAC; if an assembly with that name already exists, it is overwritten.
>gacutil /l | Lists contents of GAC.

There are a couple of drawbacks to storing an assembly in the GAC. First, it is difficult to reference during compilation due to the verbose GAC subdirectory naming conventions. An alternative is to compile referencing a local copy of the assembly. Then, remove the local assembly after compilation is completed.

Another possible drawback stems from the fact that an assembly cannot be copied into the GAC. If your application requires an assembly in the GAC, it eliminates deploying an application by simply copying files to a client's machine. Deployment issues are discussed in the last section of this chapter.

Versioning

A major benefit of using strongly named assemblies is that the CLR uses the assembly's version information to bind assemblies that are dependent on each other. When such an assembly is loaded, the CLR checks the version number of referenced assemblies to ensure they have the same version numbers as recorded in the calling assembly's manifest. If the version fails to match (usually because a new version has been created), an exception is thrown.

Assigning a Version Number to an Assembly

Every assembly has a version number. A default value is used if one is not explicitly defined. The version can be assigned using the Assembly Linker (Al.exe) tool, but is usually declared within the code using an AssemblyVersion attribute:

[assembly: AssemblyVersion("1.0.0.0")]

The version number has four parts:

<major version>.<minor version>.<build number>.<revision>

You can specify all the values or you can accept the default build number, revision number, or both by using an asterisk (*). For example:

[assembly:AssemblyVersion("2.3.")]     yields 2.3.0.0

[assembly:AssemblyVersion("2.3.*")]    yields 2.3.1830,4000

When an asterisk (*) is specified for the build number, a default build number is calculated by taking the number of days since January 1, 2000. The default revision number is the number of seconds past midnight divided by two.

You can use reflection to view an assembly's version along with the other parts of its identity. To illustrate, add the following attributes to the code in Listing 15-1 to create a custom version number and strong name:

[assembly: AssemblyKeyFile("Keylb.snk")]

[assembly: AssemblyVersion("1.0.*")]

Compile the code and use the Assembly.GetName method to display the assembly's identification.

Console.WriteLine(Assembly.GetExecutingAssembly().GetName());

This method returns an instance of the AssemblyName class that contains the assembly's simple name, culture, public key or public key token, and version.

fxtest, Version=1.0.1839.24546, Culture=neutral,

   PublicKeyToken=1f081c4ba0eeb6db


15.3. Security

The centerpiece of .NET security is the Code Access Security model. As the name implies, it is based on code access—not user access. Conceptually, the model is quite simple. Before an assembly or component within an assembly may access system resources (files, the registry, event log, and others), the CLR checks to ensure that it has permission to do so. It does this by collecting evidence about the assembly—where is it located and its content. Based on this evidence, it grants the assembly certain permissions to access resources and perform operations. Figure 15-4 illustrates the key elements in this process and introduces the terms that you must know in order to administer security.

Figure 15-4. An assembly is matched with code groups whose evidence it satisfies

When an assembly is loaded, the CLR gathers its evidence and attempts to match it with code groups whose evidence it satisfies. A code group is a binding between a set of permissions and a single type of evidence. For example, a code group may be defined so that only assemblies from a particular application directory are allowed to have Web access. If an assembly's site evidence indicates it is from that directory, it is part of the code group and has Web access. An assembly can be a member of multiple code groups, and, consequently, can have multiple permissions.

.NET provides predefined evidence, permissions, code groups, and security policies—a collection of code groups. Although code can be used to hook into and modify some aspects of security, an administrator performs the bulk of security configuration and management using .NET tools. In most cases, the predefined elements are all that an administrator needs. However, the security model is flexible, and permits an administrator to create security policies from custom evidence, permissions, and code groups.

This abstract representation of .NET security shown in Figure 15-4 is implemented in concrete types: The Evidence class is a collection that holds evidence objects; permission classes grant access to a resource or the right to perform some action; and a PermissionSet is a collection class that groups permissions and contains methods to manipulate them. The following sections take a close look at evidence, permissions, and how they are related. You'll then see how to implement a security policy, both as an administrator and by accessing the permission classes through code.

Permissions and Permission Sets

A permission is the right to access a resource or perform some action. An assembly is assigned a permission when its evidence matches the evidence requirements for a permission set. Figure 15-5 illustrates how permissions come into play when an assembly attempts to access a resource. In this case, assembly A calls assembly B, which creates a FileStream object and attempts to use its Write method. The code in the .NET Framework Class Library that implements this method demands that the calling assembly have permission to perform file I/O. Moreover, it requires that all assemblies further up the call stack also have this permission. This check—known as walking the call stack—ensures that an assembly that does not have a required permission cannot use one that does to illegally perform an operation.

Figure 15-5. An assembly must have permission to perform file I/O

Because both Assembly B and Assembly A possess the FileIO permission, the write operation is permitted. An exception of type System.Security.SecurityException is thrown if either assembly does not have the requisite permission.

Permissions are often interrelated: for example, the permission to write to a file requires an accompanying permission to access the directory containing the file. To avoid having to grant and deny all permissions on an individual basis, .NET includes a PermissionSet class that allows a collection of permissions to be treated as a single entity for the purpose of denying or granting permissions.

As we shall see, permission sets can be created and applied programmatically by creating and adding individual permission objects to a PermissionSet collection. An alternate approach is to use the .NET Configuration tool to create permission sets and assign them to code groups. To encourage this approach, .NET includes predefined permission sets.

Named Permission Sets

.NET provides seven built-in or named permission sets. They range from the most restrictive Nothing, which prevents an assembly from loading, to the least restrictive Full-Trust, which permits unrestricted access to all of the permissions listed in Table 15-1. Here they are in ascending order of trust:

  • Nothing. Prevents an assembly from being loaded. This is used primarily when the code is deemed untrustworthy because its origin cannot be determined.

  • Execution. Permits code to load and run, but little else. It cannot access external resources. Such code is useful only for making calculations.

  • Internet. Allows code to display and implement a user interface. By default, .NET grants this permission to all code coming from the Internet. Its permissions include the ability to open a file, perform safe printing, create safe top-level and subwindows, and connect to the originating site using HTTP or HTTPS.

  • LocalIntranet. Specifies the default permissions for code originating in the local intranet. In addition to the permissions granted the Internet set, its permissions include unrestricted access to FileDialogs, default printing privileges, and unrestricted user interface access.

  • Everything. Grants all permissions except permission to skip verification, because this is needed to verify the code is type-safe.

  • FullTrust. Grants access to all built-in permissions. All code running on the local machine is given this by default.

Table 15-1. Built-in Permission Classes

Namespace

Class Name

Controls Permission To:

System.Security.Permissions

Environment

Access user and OS environment variables

FileDialog

Access files or folders using a file dialog box.

FileIO

Access files or folders.

IsolatedStorage

Configure isolated storage and set quota on size of user's store.

KeyContainer

Access key containers. A key container is an area within a key database that contains key pairs used for cryptography.

Reflection

Access metadata using Reflection commands.

Registry

Access system registry.

Security

Access security permissions.

Store

Stores containing X.509 certificates. X.509 is a standard for public key certificates. Most commonly used for securing transmission of data over Internet.

UI

User interfaces and the clipboard.

System.Net

Dns

Domain name servers.

Socket

Connect or accept connects at a specified socket. A socket is an address designated by a port # and IP address.

Web

Connect to or from a Web host.

System.Drawing

Printing

Access printers.

System.Data.Common

DBData

Access data using a data provider.

System.Data.SqlClient

SqlClient

Use SQL Server data provider.

System

EventLog

Write to or browse a machine's event log.

System.ServiceProcess

ServiceController

Access to control or browse a machine's services.

System.Messaging

MessageQueue

Access the message queue.

Figure 15-6 lists the most interesting permission sets along with the individual permissions they contain. These sets cannot be modified; however, they can be copied and used as a base set for creating a custom permission set.

Figure 15-6. Permissions associated with selected named permission sets

[View full size image]

Permission Set Attributes

The built-in permission sets provide a convenient way to request permission for more than one permission type at a time. This code segment shows the syntax to request permission for the LocalIntranet permission set. It attaches a PermissionSetAttribute with a Name value set to the name of the desired permission set:

[PermissionSet(SecurityAction.Demand,Name="LocalIntranet")]

public string GetTitle()  // Requires LocalIntranet

{}

Note that all named permission sets except Everything can be applied as an attribute.

.NET Built-in Security Permissions

The individual permissions shown in Figure 15-5 are implemented in .NET as built-in permission classes. In addition to being accessed by the security configuration tools, they can also be accessed by code to implement a finer-grained security than can be configured using administrative tools.

Table 15-1 summarizes the more important built-in permission classes.

All permission classes inherit and implement the interfaces shown in Figure 15-7. Of these, IPermission and IStackWalk are the most useful. IPermission defines a Demand method that triggers the stack walk mentioned earlier; IStackWalk contains methods that permit a program to modify how the stack walk is performed. This proves to be a handy way to ensure that a called component does not perform an action outside of those that are requested. We'll look at these interfaces in more detail in the discussion of programmatic security.

Figure 15-7. Interfaces inherited by permission classes

Identity Permissions

Recall that when the CLR loads an assembly, it matches the assembly's evidence against that required by code groups and grants permissions from the code groups whose criteria it meets. These code group derived permissions are either custom permissions or built-in permissions as described in Table 15-1.

The CLR also grants another set of permissions that correspond directly to the identity evidence provided by the assembly. For example, there are ZoneIdentityPermission and StrongNamedIdentityPermission classes that demand an assembly originate from a specific zone or have a specific strong name identity. Table 15-2 lists the origin-based identity classes.

Table 15-2. Identity Permission Classes

Class

Identity Represented

PublisherIdentityPermission

The digital signature of the assembly's publisher.

SiteIdentityPermission

Web site where the code comes from.

StrongNamedIdentityPermission

Strong name of the assembly.

URLIdentityPermission

URL where the code comes from. This includes the protocols HTTP, HTTPS, and FTP.

ZoneIdentityPermission

Zone where the code originates: Internet, Intranet, MyComputer, NoZone, TRusted, Untrusted.

Unlike the built-in permissions described earlier, these classes cannot be administered using configuration tools. Instead, a program creates an instance of an identity permission class and uses its methods to demand that an assembly provide a specified identity to perform some action. This programmatic use of permission classes is referred to as imperative security.

Permission Attributes

All security permission classes have a corresponding attribute class that can be applied as an attribute to an assembly, class, and method to specify security equivalent to that provided by the permission class. This is referred to as declarative security, and serves two useful purposes: When applied at the assembly level, a permission attribute informs the runtime which permissions the assembly requires, and enables the runtime to throw an exception if it cannot grant these permissions; when applied to classes and methods within the code, the attribute specifies which permissions any calling assemblies must have to use this assembly. Examples using declarative security are provided later in the chapter.

Evidence

To qualify as a member of a code group and assume its privileges, an assembly must provide evidence that matches the evidence membership requirements of the code group. This evidence is based on either the assembly's origin or its signature. The origin identification includes Site, Url, and Zone evidence; the signature refers to an assembly's strong name, its digitally signed certificate (such as X.509), or a hash of the assembly's content. The Common Language Runtime provides seven predefined types of evidence. They are referred to by names used in the security administrative tools:

  • Strong Name. An assembly with a Strong Name has a public key that can be used to identify the assembly. A class or method can be configured to accept calls only from an assembly having a specified public key value. The most common use for this is to identify third-party components that share the same public key. A Strong Name has two other properties, Version and Name, that also can be required as evidence by a host assembly.

  • Publisher. This evidence indicates that an assembly has been digitally signed with a certificate such as X.509. Certificates are provided by a trusted certificate authority and are most commonly used for secure Internet transactions. When a signed assembly is loaded, the CLR recognizes the certificate and adds a Publisher object to the assembly.

  • Hash. By applying a computational algorithm to an assembly, a unique identifier known as a hash is created. This hash evidence is automatically added to each assembly and serves to identify particular builds of the assembly. Any change in the compiled code yields a different hash value—even if the version is unchanged.

  • Application Directory. This evidence is used to grant a permission set to all assemblies that are located in a specified directory or in a subdirectory of the running application.

  • Site. Site evidence is the top-level portion of a URL that excludes the format and any subdirectory identifiers. For example, www.corecsharp.net is extracted as site evidence from http://www.corecsharp.net/code.

  • URL. This evidence consists of the entire URL identifying where an assembly comes from. In the preceding example, http://www.corecsharp.net/code is provided as URL evidence.

  • Zone. The System.Security.SecurityZone enumeration defines five security zones: MyComputer, Intranet, Internet, trusted, and Untrusted. An assembly's zone evidence is the zone from which it comes.

    - MyComputer. Code coming from the local machine.

    - Intranet. Code coming from computers on the same local area network.

    - Internet. Code coming from the Internet that is identified by an HTTP or IP address. If the local machine is identified as http://localhost/, it is part of the Internet zone.

    - TRusted. Identifies Internet sites that are trusted. These sites are specified using Microsoft Internet Explorer (IE).

    - UnTRusted. Sites specified in IE as being malicious or untrustworthy.

In addition to these, there is also a blank evidence known as All Code evidence that is used by an administrator to create a code group that matches all assemblies.

The CLR maintains evidence in an instance of the Evidence collection class. This object contains two evidence collections: one for the built-in host evidence and another for user-defined evidence. This evidence is made available to security policy, which then determines the permissions available to the assembly. You can use reflection to view evidence programmatically. In this example, we view the evidence for an assembly, movieclient.exe, which is located on the local machine (the assembly's source code is presented later in this section):

using System;

using System.Reflection;

using System.Security.Policy;

class ClassEvidence

{

   public static void Main()

   {

      Assembly ClientAssembly;

      // (1)Load object to reference movieclient assembly

      ClientAssembly = Assembly.Load("movieclient");

      // (2) Evidence is available through Evidence property

      Evidence ev = ClientAssembly.Evidence;

      // (3) Display each evidence object

      foreach (object ob in ev)

      {

         Console.WriteLine(ob.ToString());

      }

   }

}

Output from the program reveals the Zone, Url, Strong Name, and Hash evidence associated with the assembly. No Site evidence is present because the assembly's Url origin is defined by a file:// rather than an http:// format. Application Directory evidence is also missing because it comes from the host application, not the assembly's metadata.

<System.Security.Policy.Zone version="1">

   <Zone>MyComputer</Zone>

</System.Security.Policy.Zone>

<System.Security.Policy.Url version="1">

   <Url>file://C:/movieclient.EXE</Url>

</System.Security.Policy.Url>

<StrongName version="1"

            Key="002400... 8D2"

            Name="movieclient"

            Version="0.0.0.0"/>

<System.Security.Policy.Hash version="1">

   <RawData>4D5A90000300000004000000FFFF0000B80

   </RawData>

</System.Security.Policy.Hash>

Security Policies

A .NET security policy defines how assembly evidence is evaluated to determine the permissions that are granted to the assembly. .NET recognizes four policy levels: Enterprise, Machine, User, and Application Domain. The policy-level names describe their recommended usage. Enterprise is intended to define security policy across all machines in the enterprise; Machine defines security for a single machine; User defines security policy for individual users; and Application Domain security is applied to code running in a specific AppDomain. Enterprise, Machine, and User policies are configured by an administrator. AppDomain policy, which is implemented only programmatically and used for special cases, is not discussed.

Despite their names, policies can be configured in any way an administrator chooses. The User policy could be set up to define enterprise security and the Machine policy to define user security. However, an administrator should take advantage of the names and use them to apply security to their intended target. As you will see in the discussion of the .NET Framework Configuration Tool (see "The .NET Framework Configuration Tool" on page 704), the security policy is granular enough to allow custom security policies on individual machines and users.

How .NET Applies Security Policies

Each security policy level is made up of one or more code sets. Each code set, in turn, contains a set of permissions that are mapped to a specific evidence type. Figure 15-8 illustrates how code sets and policy levels are combined to yield a permission set for an assembly.

Figure 15-8. A permission set is created from the intersection of policy level permissions

The .NET security manager is responsible for evaluating evidence and policy to determine the permissions granted. It begins at the enterprise level and determines the permissions in it that can be granted to the assembly. In this example, enterprise contains three code groups—two of which the assembly's evidence satisfies. The logical union of these permissions produces the permission set at this level. The other two policy levels are evaluated in the same way, yielding their associated permission set. The logical intersection of the three permission sets produces the permission set that is assigned to the assembly. In this case, the final set consists of permissions 2 and 5—the only permissions present on each level.

Configuring Security Policy

Physically, each policy level is stored as a configurable XML file that defines a hierarchy of code groups for the policy. The Enterprise policy file, enterprisec.config , and the Machine policy file, security.config[3], are stored in the same folder on a Microsoft Windows system:

[3] Not to be confused with the machine.config file that holds machine-wide configuration data.

<Windows Directory>\Microsoft.NET\Framework\<Version>\config\

The User policy file is named security.config and is located on the path

<Documents and Settings>\<User Name>\Application Data\Microsoft\

      CLR Security Config\<Version>\

Listing 15-2 contains an extract from the Machine policy file that illustrates the file layout. It comprises four major sections:

<SecurityClasses> | Defines all individual permissions.
<NamedPermissionSets> | Defines named permission sets.
<CodeGroup> | Provides a name and permission set for code group.
<FullTrustAssemblies> | List of trusted assemblies.

The code group section is structured as a multi-level hierarchy, with the conditions for granting permissions becoming more restrictive at each lower level.

Listing 15-2. Extracts from the Machine Policy File Security.Config

<SecurityClasses>

   <SecurityClass Name="WebPermission"

         Description="System.Net.WebPermission, System,

         Version=2.0.3600.0, Culture=neutral,

         PublicKeyToken=b77a5c561934e089"/>

   <SecurityClass Name="EventLogPermission"

         Description="System.Diagnostics.EventLogPermission,

         System, Version=2.0.3600.0, Culture=neutral,

         PublicKeyToken=b77a5c561934e089"/>

   ...

</SecurityClasses>

<NamedPermissionSets>

   <PermissionSet class="NamedPermissionSet"

         version="1"

         Name="LocalIntranet"

         Description="Default rights given to applications on

         the local intranet">

      <IPermission class="EnvironmentPermission"

         version="1" Read="USERNAME"/>

      <IPermission class="FileDialogPermission"

         version="1" Unrestricted="true"/>

   ...

</PermissionSet>

...

</NamedPermissionSets>

<CodeGroup class="UnionCodeGroup"

         version="1"

         PermissionSetName="Nothing"

         Name="All_Code"

         Description="Code group grants no ...">

       <IMembershipCondition class="AllMembershipCondition"

         version="1"/>

   <CodeGroup class="UnionCodeGroup"

         version="1"

         PermissionSetName="FullTrust"

         Name="My_Computer_Zone"

         Description="Code group grants ...">

         ...

   </CodeGroup>

   ... additional code groups here

</CodeGroup>

<FullTrustAssemblies>

   <IMembershipCondition class="StrongNameMembershipCondition"

         version="1"

         PublicKeyBlob="00000000000000000400000000000000"

         Name="mscorlib.resources"

         AssemblyVersion="Version=2.0.3600.0"/>

   <IMembershipCondition class="StrongNameMembershipCondition"

         version="1"

         PublicKeyBlob="00000000000000000400000000000000"

         Name="System"

         AssemblyVersion="Version=2.0.3600.0"/>

   ...

</FullTrustAssemblies>

.NET provides two ways to work with these policy files: a command-line Code Access Security Policy (caspol) and a graphical Configuration tool (MSCorCfg.msc). Both can be used to modify the default configuration, create custom code groups, create custom permission sets, and export policies to be deployed with an application. We'll look at both tools in this section, with an emphasis on the Configuration tool because its visual interface makes it more popular and easier to learn than the command-line approach.

The .NET Framework Configuration Tool

On a Microsoft Windows system, you start the Configuration tool by selecting Microsoft .NET Framework Configuration from the Administrative Tools folder or by selecting Run and typing MSCORCFG.MSC. The program interface consists of a window divided into two panes. As shown in Figure 15-9, the left side contains a tree structure comprising multiple folders. Of these, the Runtime Security Policy folder expands to display a hierarchy of security information.

Figure 15-9. Interface for the .NET Framework Configuration tool

[View full size image]

At the top level of the hierarchy are the three folders representing the Enterprise, Machine, and User policies. Beneath each of these are folders that contain code groups, permission sets, and policy assemblies. This hierarchy is, of course, simply a visual representation of the underlying XML policy files. You can observe this by comparing the raw XML tags in Listing 15-2 with the items displayed under the Machine policy.

The Default Configuration Policies

The Enterprise and User policies contain a single default code group named All_Code. If you click it, you'll see this description in the right panel:

"Code group grants all code full trust and forms the root of the code group tree."

Specifically, this code group binds All Code evidence with the FullTrust permission set. Recall that all assemblies qualify as members of code groups that use All Code evidence and that the FullTrust permission set offers unrestricted permissions. The net effect of binding these two is to create Enterprise and User policies that offer unlimited permissions to all assemblies. In other words, these two policies offer no security at all by default.

Core Note

The code groups provided by .NET are named after the evidence they represent. Because no two code groups may have the same name, custom code groups must use a modified naming convention.

The Machine security policy is far more interesting and instructive. At its root is the All_Code code group. Unlike the other two policies, it binds All Code evidence to the Nothing permission set, which means the code group grants no permissions. To find the permissions, you must look to the code groups nested beneath this root. The first level contains six groups: My_Computer_Zone, LocalIntranet_ Zone, Internet_Zone, Restricted_Zone, TRusted_Zone, and Application_ Security_Manager. All except Restricted_Zone have one or more child code groups. Let's look at how default permissions are granted for two of these: My_Computer_Zone and LocalIntranet_Zone. To view details about the other code groups, simply right-click their name to bring up a properties window.

My_Computer_Zone Code Group

This code group grants the FullTrust permission set to assemblies that satisfy the My Computer zone evidence. Its two child code groups grant FullTrust to assemblies that have Strong Name evidence containing either the Microsoft public key or the ECMA public key, also known as the Standard Public Key.[4] It is important to understand that an assembly can satisfy the Strong Name evidence without satisfying the My Computer zone evidence. This is because .NET evaluates child code groups even if the conditions for the parent group are not satisfied. Conversely, a code group's properties can be set to instruct .NET not to evaluate child groups if the conditions of the parent code group are satisfied.

[4] Assemblies based on the European Computer Manufacturers Association (ECMA) specifications for the Common Language Infrastructure (CLI) contain an ECMA public key. These include system.dll and mscorlib.dll. The key is actually a placeholder that is mapped to a key pair provided by the particular CLR installation.

LocalIntranet_Zone Code Group

This code group grants the LocalIntranet permission set to assemblies that satisfy the Local Intranet zone evidence—any computers on the same local area network as the machine on which the host code runs. This code group has two child code groups: Intranet_Same_Site_Access and Intranet_Same_Directory_Access. The former permits code to access the site of its origin; the latter permits code to access its original install directory. The practical effect of these permissions is to permit code to perform I/O on its local storage.

Configuring Code Access Security with the Configuration Tool—An Example

To illustrate how to use the Configuration tool, we'll create a new permission set and assign it the Reflection and Execution permissions. Next, we'll create a code group that maps the new permission set to Url evidence that specifies a directory on the local machine. The effect is to grant the Reflection permission to any assembly that runs from this directory.

For testing, we'll create a simple application that uses reflection to access a private field in this assembly's class:

// (movieclass.dll) Will use reflection to access this class

using System;

public class Movies

{

   private int ID;

   private string Director;

   public string title;

   public string year;

}

Listing 15-3 contains the code that accesses the private Director field. This is done by calling the Type.GetField method and passing as arguments the field name and flags that request access to a private field in a class instance.

Listing 15-3. Assembly Requiring Reflection Permission

// configtest.cs

using System;

using System.Reflection;

class ClassEvidence

{

   public static void Main()

   {

      Assembly ClientAssembly;

      ClientAssembly = Assembly.Load("movieclass");

      // Get the desired Type in the Assembly

      Type myType = ClientAssembly.GetType("Movies");

      // Get the FieldInfo for private field "Director".

      // Specify nonpublic field and instance class.

      // Accessing private members requires Reflection

      // Permission.

      FieldInfo myFieldInfo = myType.GetField("Director",

            BindingFlags.NonPublic | BindingFlags.Instance);

      if (myFieldInfo !=null)

         Console.WriteLine("Field: {0} Type: {1}",

                           myFieldInfo.Name,

                           myFieldInfo.FieldType);

      {

         // output: Field: Director  Type: System.String

      } else {

         Console.WriteLine("Could not access field.");

      }

   }

}

Creating a Permission Set

Follow these steps to create the permission set:

1. | Right-click the Permission Sets folder under Machine policy and select New.
2. | Enter Reflection as the name of the set and provide a description.
3. | On the next screen, select Security from the Available Permissions. Check Enable Assembly Execution and Allow Evidence Control from the permission settings window.
4. | Select Reflection from the Available Permissions. Check Grant Assemblies Unrestricted Permission to Discover Information About Other Assemblies.
5. | Click Finish and the permission set named Reflection appears in the left pane.

You now have a permission set that allows an assembly the rights associated with the Reflection permission.

Creating a Code Group

Follow these steps to create the code group:

1. | Right-click the All_Code node—under Machine-Code Groups—and select New.
2. | Enter My_Computer_Url as the name of the group and provide a description.
3. | For the Condition Type, choose URL.
4. | For the URL, enter file://c:/cas/*. This specifies the folder from which an assembly must originate in order to satisfy the Url evidence.
5. | Next, assign the new Reflection permission set to the code group and click Finish.
6. | Click the new code group in the left pane and select Edit Code Group Properties. Check the option This Policy Level Will Only Have the Permissions from the Permission Set Associated with the Code Group.

The final step is necessary to make this example work. Setting this Exclusive option tells .NET to assign only the permissions of the new code group to any code found in the specified directory path. If this option is not set, the code is evaluated against all Machine level code groups and receives permissions from all whose evidence it matches. Also, note that code not located in the specified subdirectory is unaffected by the new code group and receives the default Machine policy permissions.

Testing the New Code Group

To demonstrate the effects of this new code group, compile the program and store a copy of configtest.exe in C:\ and C:\CAS\. Run both copies from the command line, and they should succeed. Now, use the Configuration tool to change the permission set for the My_Computer_Url code group from Reflection to Internet. When you now run the program from the CAS subdirectory, it fails because the Internet permission set does not include the Reflection permission. The program still runs fine from C:\, because its permissions come from the other code groups in the Machine policy.

This is a simple example, but it illustrates the core principle behind Code Access Security of assigning permissions to an assembly based on an analysis of its evidence. In this case, identical assemblies receive different permissions based on the directory in which the code resides. The evidence is a Url specified directory, but could just as easily be an Internet site, the assembly's Strong Name, or its security zone.

Determining the Permissions Granted to an Assembly

It is not easy to empirically determine the permissions that are granted to a given assembly. You must gather the evidence for the assembly, determine the security policies in effect from the XML policy files, and evaluate the evidence in light of the policies. Fortunately, the .NET Configuration tool and Caspol utility can perform this evaluation for you.

To use the Configuration tool, right-click the Runtime Security Policy folder and select Evaluate Assembly from the context menu. Use the Browse button to locate the assembly in the file directory. Select View Permissions Granted to Assembly, and click the Next button to display the individual permissions. For c:\cas\configtest.exe from the preceding example, Security and Reflection are listed. For c:\configtest.exe, Unrestricted is listed.

Caspol is run from the command line. Among its numerous options is –rsp, which resolves permissions for an assembly. To determine permissions for the configtest assembly, enter this command:

C:\>caspol –rsp c:\cas\configtest.exe

Output is in an XML format and correctly includes Security and Reflection, as well as two identity permissions: UrlIdentity and ZoneIdentity (MyComputer). The identity permissions can be ignored because specific permissions are provided.

You can also use caspol to understand why an assembly qualifies for its permissions by identifying the code groups that the assembly belongs to. Enter the previous command, except replace the –rsp option with the –rsg (resolve code group) option:

C:\>caspol –rsg c:\cas\configtest.exe

This output shows that a code group uses Url evidence to grant its permission set (Reflection) exclusively to any assembly in the specified directory. Because our assembly is in that directory, the Security and Reflection permissions must come from this code group:

Level = Enterprise

Code Groups:

1.  All code: FullTrust

Level = Machine

Code Groups:

1.  All code: Nothing

   1.1.  Zone - MyComputer: FullTrust

   1.6.  Url - file://C:/cas/*: Reflection (Exclusive)

Level = User

Code Groups:

1.  All code: FullTrust

Note that you can view the contents of the Reflection permission set by executing

C:\>caspol –m -lp

The serialized XML output lists the permissions associated with all permission sets at the machine policy level—including the Reflection permission set.

Determining the Permissions Required by an Assembly

One of the objectives of designing an effective security policy is to grant an assembly only those permissions it requires, and no more. The ease of using predefined permission sets should be resisted when it allows an assembly to access resources that it does not require. In the preceding example, the configtest assembly required only the capability to execute and access members of the Reflection namespace. To satisfy these narrow requirements, we created a custom permission set containing the Security and Reflection permissions. This custom permission set was clearly a better choice than the unrestricted permissions offered by Everything and FullTrust—the only predefined permission sets granting the Reflection permission.

Given the obvious advantage of using customized permission sets, the question becomes how an administrator identifies the minimum permissions required by an assembly. .NET offers two utilities for this purpose: PermView and PermCalc. PermView, which is discussed in the next section, displays the permissions explicitly requested—using attributes—by an application; PermCalc evaluates an assembly and produces serialized XML output of the required permission classes. It is run from the command line, as shown here:

C:\>permcalc c:\cas\configtest.exe

Evaluating an assembly to determine its required permissions is a difficult task and not always guaranteed to provide accurate results. In fact, when the utility is run against configtest, it displays FileIOPermission—but not Reflection—as a required permission. However, if we add an attribute (described next) to the code requesting the Reflection permission, the utility correctly displays this as a required permission. As a rule, the only way to be certain which permissions an assembly requires is to declare them in the code—our next topic.

Requesting Permissions for an Assembly

Including permission attributes in code is referred to as declarative security, and it serves two primary purposes: When applied at the assembly level, the attribute serves to inform the runtime which permissions the assembly requires—or does not require—to function; when applied at the class or method levels, it protects resources by demanding that callers possess specific permissions to access a resource through the current assembly. Attributes used for this latter purpose trigger or modify a stack walk that verifies all assemblies in the call chain can access the called assembly. We'll examine this in "Programmatic Security" on page 715. For now, the focus is on how to use assembly-level attributes to adjust security within a program.

Although the term request is broadly used to describe the use of permission attributes, it's better described as a way for an assembly to publish (in metadata) its permission requirements. The effectiveness of including permission attributes in code is governed by three rules:

  • An assembly cannot receive any more permissions than are defined by the security policy rules—no matter what it "requests."

  • Although requests cannot cause code to receive extra permissions to which it is not entitled, it can influence the runtime to deny permissions.

  • An assembly receives permissions whether it requests them or not. The example shown in Listing 15-3 does not include a permission attribute, but it receives all the permissions that it qualifies for based in the evidence it provides the runtime.

Despite the fact that you do not have to include permission requests in your code, there are important reasons for doing so:

  • It allows PermView to display the permissions required by an assembly and also improves the reliability of PermCalc. This provides a quick way for administrators to collect assembly requirements and design an appropriate security policy.

  • Permission attribute information is stored in the assembly's manifest where it is evaluated by the CLR when loading the assembly. This permits the runtime to prevent an assembly from executing if it does not have the requested permissions. In most cases, this is preferable to having the program begin execution and then shut down unexpectedly because it has inadequate permissions.

  • It enables code to specify only those permissions it needs. So why are extra permissions a problem if you don't use them? Because the code may have a bug, or exploitable feature, that malicious calling code can use to take advantage of the "extra" permissions.

For a full trust environment where code has unrestricted permissions—typically in-house applications—security is not a significant factor, and it may not be necessary to apply security attributes. However, if you operate in a security-conscious environment or are creating components for use by other software, you should include security attributes as a means of providing self-documenting security.

How to Apply a Permission Attribute

Because the permission attribute is to have assembly scope, it is declared in the first line of code following the using statement(s):

[assembly : PermissionAttribute(

               SecurityAction.membername,

               PermissionAttribute property)

]

Let's examine its construction:

Assembly

Indicates the attribute has assembly scope.

Permission Attribute

The permission class being requested.

SecurityAction

An enumeration describing the type of permission request. Three values can be assigned to the assembly scope:

RequestMinimum— The minimum permissions required to run.

RequestOptional— Permissions the code can use but does not require. This implicitly refuses all other permissions not requested. Be careful with this, because it is not obvious that a request for one permission causes all other non-requested permissions to be denied.

RequestRefuse— Permissions that should not be assigned to the assembly even if it qualifies for them.

The final argument to the constructor sets a property of the permission attribute class to a value that describes the specific permission requested. The following example should clarify this.

Testing a Permission Attribute

To demonstrate the effects of applying a permission attribute, let's add this statement to the source for configtest, shown in Listing 15-3:

//place on line preceding: class ClassEvidence

 [assembly : ReflectionPermission(

              SecurityAction.RequestMinimum,

              Flags=ReflectionPermissionFlag.TypeInformation)

]

The ReflectionPermission attribute class is used to request the minimum permissions required to run. The second parameter, TypeInformation, is an enumeration property of the class that permits reflection on nonvisible members of a class. (Recall that the sole purpose of this code is to use reflection in order to access a private field on a class.)

Compile and run the code from the C:\ root directory. Because applications run on the local machine have unrestricted permissions, the program runs successfully. Let's see what happens if the second parameter does not specify a permission that enables access to a private field. To test this, change the enumeration value to ReflectionEmit—an arbitrary property that only permits the assembly to emit metacode. When run with this parameter, the assembly again succeeds because it continues to receive unrestricted permissions on the local machine. As a final test, change the first parameter to SecurityAction.RequestOptional. This causes the assembly to fail, because only the requested ReflectionEmit permission is granted to it. Table 15-3 summarizes how content of the permission attribute affects the assembly's operation.

Table 15-3. Effect of Changing Permission Attribute on Assembly

Attribute Parameters

Result

Explanation

SecurityAction.RequestMinimum

Flags = ReflectionPermission-

Flag.TypeInformation

Succeeds

RequestMinimum ensures that assembly is granted all permissions determined by security policy. Includes Reflection.

TypeInformation permits reflection on members that are not visible.

SecurityAction.RequestMinimum

Flags = ReflectionPermission-

Flag.ReflectionEmit

Succeeds

RequestMinimum ensures that assembly is granted all permissions determined by security policy. Includes Reflection.

ReflectionEmit parameter does not specify the permission required by the assembly.

SecurityActionRequestOptional

Flags = ReflectionPermission-

Flag.ReflectionEmit

Fails

RequestOptional causes all non-requested permissions to be denied.

Because ReflectionEmit does not provide permission to access a non-public field, the assembly fails.

Programmatic Security

The .NET Configuration tool provides a broad stroke approach to defining security for a computing environment. In most cases, this is satisfactory. If two assemblies come from the same security zone, it usually makes sense to grant them the same permissions. However, suppose you are developing components for clients with an unknown security policy, or you are using third-party components whose trustworthiness is unknown. In both cases, programmatic security offers a way to enforce security specifically for these components.

Programmatic security is implemented by using .NET permission classes to control and enforce security on a calling assembly or a called assembly. This is an important point: The calling assembly can override permissions granted by the CAS and prevent the assembly from accessing a resource; conversely, a called assembly can refuse to perform an operation unless the calling assembly—or assemblies—has permissions it requires. The key to implementing programmatic security is a mechanism known as a stack walk.

Stack Walk

As mentioned earlier in the chapter, a stack walk refers to steps the CLR follows to verify that all methods in a call stack have permission to perform an operation or access a system resource. This ensures that an immediate client with the proper permissions is not called by malicious code further up the stack that does not have permission.

As shown in Figure 15-10, a stack walk is triggered when code invokes a permission's Demand method. This method is inherited from the IPermission interface (refer to Figure 15-7), which includes other methods such as Intersect and Union that allow permission objects to be logically combined.

Figure 15-10. Invoking the Permission.Demand method triggers a stack walk

The Demand method is used extensively throughout the .NET Framework to ensure that applications requesting operations such as file I/O or database access have the proper permissions. Although most calls to the Framework Class Library (FCL) are protected in this way, it can still be useful to demand a stack walk within your code. You may want to apply even stricter permission requirements than the FCL demands, or you may want to ensure that a lengthy operation has all the required permissions before it is launched.

To provide a simple illustration, let's create a component that returns information about a requested movie from our Films database (see Chapter 11). Listings 15-4 and 15-5 contain the component and client code, respectively. For brevity, the ADO.NET code is excluded, but is available as downloadable code.

Because the SqlClient permission is required to access a SQL Server database, the component includes the following code to ensure that the calling assembly has this permission:

SqlClientPermission sqlPerm=

      new SqlClientPermission(PermissionState.Unrestricted);

sqlPerm.Demand();  // Trigger stack walk

The code simply creates an instance of the desired permission class and calls its Demand method. There is no limit on the number of permission objects that can be created and used to demand a stack walk. However, it is an expensive process and should be used judiciously.

Listing 15-4. Component to Illustrate Code Access Security

// filmcomponent.cs  (.dll)

using System.Data.SqlClient;

using System;

namespace moviecomponents

{

   public class MovieData

   {

      // Return MovieProfile object

      public static MovieProfile GetMovie(string movietitle)

      {

         // Return null if movie not found

         MovieProfile mp = null;

         // Demand SqlClient permission from methods on call stack

         SqlClientPermission sqlPerm= new

               SqlClientPermission(PermissionState.Unrestricted);

         sqlPerm.Demand();

         //*** Code here to query database for movie information

         //*** Requires SqlClient permission

         return mp;

      }

   }

   public  class MovieProfile

   {

      private string pTitle;

      private int pYear;

      private int pRank;

      private string pOscar;

      public MovieProfile(string title, int year,

                          int afiRank, string bestFilm)

      {

         pTitle= title;

         pYear = year;

         pRank = afiRank;

         pOscar = bestFilm;

      }

      // Readonly properties

      public string Title

      {

         get{return pTitle;}

      }

      public int Year

      {

         get{return pYear;}

      }

      public int Ranking

      {

         get{return pRank;}

      }

      public string BestPicture

      {

         get{return pOscar;}

      }

   }     // class

}       // namespace

Listing 15-5. Assembly to Access Film Component

// movieclient.cs  (.exe)

using System;

using moviecomponents;

namespace MovieClient

{

   class MovieMgr

   {

      static void Main(string[] args)

      {

         string myMovie;

         if(args.Length>0)

         {

            myMovie= args[0];

            // Call component to fetch movie data

            MovieProfile mp = MovieData.GetMovie(myMovie);

            if(mp==null)

            {

               Console.WriteLine("Movie not found");

            }

            else

            {

               Console.WriteLine("Year:{0} AFI Rank: {1}",

                                 mp.Year, mp.Ranking);

            }

         }

      }

   }    // class

 }     // namespace

This code can be tested from the command line. Compile both the component and client. Then, run the client by passing it the movie name as a parameter:

C:\>csc /t:library filmcomponent.cs

C:\>csc /r:filmcomponent.dll movieclient.cs

C:\>movieclient casablanca

Year: 1942  AFI Rank:2

Because code running on a local machine has unrestricted permissions (by default), the stack walk verifies that movieclient has the necessary permission. To make the example more interesting, let's run the client when it does not have the SqlClient permission. The easiest way to remove this permission is by adding an assembly-level permission attribute that explicitly refuses the permission. You do this by passing the SecurityAction.RequestRefuse enumeration to the constructor of the permission attribute you do not want granted to the assembly. To refuse the SqlClient permission, place this statement before the namespace statement in the client code:

[assembly : SqlClientPermission(SecurityAction.RequestRefuse)]

Building and running the modified SqlClient results in a security exception being thrown.

Stack Walk Modifiers

Just as a called assembly has the rights to initiate a stack walk, the objects on the call stack have the right to modify the behavior of the stack walk. For example, they can cause the walk to fail, or stop the walk at the current object so that it does not check objects further up the stack. These capabilities have significant security implications: inducing a stack walk failure is a way for a client to restrict what actions a called assembly may perform; and terminating a stack early can improve performance by eliminating unnecessary steps in a repeated stack walk.

The ability to modify a stack walk is provided by methods in the IStackWalk interface (refer to Figure 15-7) that all permissions must implement. This interface defines the Demand method that initiates a stack walk, and three other methods, Assert, Deny, and PermitOnly, that modify a stack walk's normal operation. Assert stops the stack walk at the current stack frame; Deny causes the walk to fail; and PermitOnly specifies the only permission that an object will allow a stack walk to verify.

To demonstrate these methods, let's modify the code in Listing 15-5 to implement each method. First, we add namespaces so that we can access IStackWalk and the permission:

using System.Security;

using System.Data.SqlClient;

using System.Security.Permissions;

Then, the code is changed to create an IStackWalk object that is set to the permission being checked by the stack walk—in this case, SqlClientPermission:

myMovie= args[0];

//

IStackWalk stackWalker;

stackWalker = new

      SqlClientPermission(PermissionState.Unrestricted);

stackWalker.Deny();      // Deny use of SqlClient by GetMovie

// Call component to fetch movie data

try{

   MovieProfile mp = MovieData.GetMovie(myMovie);

In our first example, we call the permission's Deny() method that causes the stack walk's verification of the SqlClient permission to fail. Filmcomponent cannot access the database and throws an exception.

Now, replace the call to Deny with a call to Assert. Assert prevents the stack walk from continuing further up the code stack—unnecessary in this case because there is no other object on the stack. The stack walk succeeds because movieclient has the required permission, and database access is permitted.

Core Note

On the surface, the use of Assert seems to undermine the reason for a stack walk—to ensure that all objects on the call stack have a required permission. To make sure the method is not used to hide potentially malicious code on the call chain, .NET requires that the asserting code have a special security permission before it can assert; then, as added protection, it triggers a stack walk when code makes an assertion that verifies that all code above it has the asserted permission.

The final way to modify the stack walk is by using the PermitOnly method to indicate specific permissions that the code is willing to let the called assembly use. In the current example, the calling assembly has unrestricted permissions; the component filmcomponent has need for only the SqlClient permission. By using PermitOnly to specify this permission, movieclient thwarts any stack walks that attempt to verify other permissions.

An assembly may be required to call multiple components, each with its own permission requirements. You may need to permit SQL access for one and file I/O for another. If you call PermitOnly a second time to override the first call, an exception is thrown. Instead, you must clear the effects of the first call by calling CodeAccessPermission's static RevertPeritOnly method; then, you call PermitOnly with a new permission. The following segment could be added to our code to remove SqlClient as the only allowed permission, and replace it with the Reflection permission.

// Remove effects of previous PermitOnly call

CodeAccessPermission.RevertPermitOnly();

stackWalker = new ReflectionPermission(

      ReflectionPermissionFlag.TypeInformation);

// Allow stack walk to verify Reflection permission only

stackWalker.PermitOnly();

// Now call a method that requires Reflection

Declarative Security Using Permission Attributes

The security technique just described that explicitly calls a permission's Demand method to invoke a stack walk is referred to as imperative security. An alternative form of security that uses attributes to achieve the same effect is known as declarative security. We looked at one version of this in the earlier discussion of how to use permission attributes to request permissions. In this section, we'll see how the other form of declarative security is used to attach attributes to classes or methods to verify that clients have the necessary permissions to use current code.

The easiest way to explain declarative security is to compare it with its imperative counterpart. Here are the statements from Listing 15-4 that trigger a stack walk. Below it is an equivalent attribute declaration that results in the same stack walk.

// Imperative Security

SqlClientPermission sqlPerm= new

      SqlClientPermission(PermissionState.Unrestricted);

sqlPerm.Demand();

// Declarative Security

[SqlClientPermission(SecurityAction.Demand)]

public static MovieProfile GetMovie(string movietitle){

The syntax for the attribute constructor is straightforward. It consists of the permission attribute class name with a parameter specifying a SecurityAction enumeration member. Depending on the permission type, there may also be a second parameter that specifies a property value for the permission class.

The most interesting feature of the attribute declaration is its SecurityAction parameter that specifies the action caused by the attribute. Its three most important members and their uses are the following:

  • Demand. TRiggers a stack walk at runtime that requires all callers on the call stack to have a specified permission or identity.

  • LinkDemand. Only the immediate caller is required to have a specified permission or identity. This check occurs during loading and eliminates the performance penalty of using a stack walk. However, because all objects on the call stack are not checked, this should only be used when you are certain the call stack is secure.

  • InheritanceDemand. Requires that any subclass inheriting from the current code has required security permissions. Without this check, malicious code could use inheritance or the capability of overriding protected methods, for its own purposes. This check occurs during loading.

The final two enumeration members result in checks that occur during loading and have no comparable statements that can be executed at runtime. However, the use of demand security to trigger a stack walk is common to both and leaves the developer with the decision of which to use. In where the component knows which permissions it needs at compile time, declarative security is recommended. Its syntax is simpler, and it provides a form of self-documentation. Also, declarative security information is placed in an assembly's manifest where it is available to the CLR during loading. As a rule, the sooner the CLR has information about code, the more efficiently it can operate.

Imperative security is recommended when variables that affect security are unknown at compile time. It's also easier to use when more granular security is required: attributes usually apply to a class or method, whereas demand statements can be placed anywhere in code.


15.4. Application Deployment Considerations

The purpose of this section is to take a broad look at the issues that should be considered when deciding on a strategy for installing your application. Much of the discussion centers on practical issues such as how deployment is affected by the use of public or private assemblies, how configuration files are used to tell the CLR where to search for an assembly, and how configuration files ease the problems of distributing new versions of an assembly.

Microsoft Windows Deployment: XCOPY Deployment Versus the Windows Installer

Almost always included in the list of the top .NET features is the promise of using XCOPY deployment to install your application on client machines. The idea is to create a simple script that uses the XCOPY utility to copy your assemblies and resource files to the specified directories on the client's machine. It is a terrific idea and works—if you have an application with only a few assembly files that can be stored in a local directory. However, there are a lot of installation scenarios where XCOPY does not work: It cannot be used to register a COM component in your application, and it cannot be used to store an assembly in the GAC. In addition, for a professional application, XCOPY does not provide the well-designed interface that customers expect.

The most popular alternative to XCOPY is the Microsoft Windows Installer that ships with most of its operating systems and is also available as a separate download. The installer is an installation service that processes files having a special format (.msi) and performs install operations based on their content. An .msi file is referred to as a Windows Installer Package, and an install can contain more than one of these files.

The easiest way to create an MSI file for your application is using Visual Studio .NET. It has a wizard that steps you through the process. If your application requires special Code Access Security policies, use the Configuration tool to create an install package. To do so, right-click the Runtime Security Policy node and select Create a Deployment Package. The ensuing wizard steps you through the process. You can also create a custom installer by creating a class that inherits from the System.Configuration.Install.Installer class and overrides its Install and Uninstall methods.

Core Note

.NET 2.0 and Visual Studio 2005 introduce a ClickOnce technology that is designed to simplify the installation and update of Windows applications. The idea is to create the application and use VS.NET to "publish" it to a File or Web Server. The location is provided as a URL to users who link to a Web page that provides install instructions. Included with the application is a manifest that describes the assembly and files that comprise the application. This information is used to determine whether an update is available for the application. The update process can be set up to automatically check for updates each time the application is run, or only run when requested.

ClickOnce works for straightforward installations that do not need to access the registry or install assemblies in the GAC. It is also targeted for the Microsoft Windows environment, as it contains shortcuts and an uninstall feature used by Windows. Although VS.NET is the easiest way to publish an application for installation, it can be done manually.

Deploying Assemblies in the Global Assembly Cache

.NET supports two assembly deployment models: private and shared. A private assembly is identified by its file name—without the extension—and is stored locally with other assemblies that it references or that reference it. Shared assemblies are stored in the Global Assembly Cache. Although this is a convenient way to make an assembly available to multiple applications, it does present an installation problem. Recall from the earlier discussion that an assembly cannot be copied into the GAC; instead, a special tool is required to install or "register" it. During code development, the GACUtil.exe utility is used for this purpose. However, this tool is not included with the end-user version of the .NET Framework redistributable package, so there is no assurance that it will exist on the user's machine. Instead, use the Windows Installer.

Deploying Private Assemblies

The easiest way to deploy an application with multiple private assemblies is to place all of the assemblies in the same directory. This home directory, referred to as the application's base directory, is the first place the CLR looks when trying to locate an assembly. However, using a single directory prevents assemblies from being grouped into folders that logically describe their purpose. For example, an application may have a set of assemblies dedicated to graphics and another dedicated to database access. Placing these in \Graphics and \Data subfolders represents a more logical and easier-to-maintain code deployment.

To see how to set up a meaningful directory structure for an application, let's first look at how the CLR locates private assemblies. This discovery process, called probing, begins with the application's base directory. If the assembly is not located there, it searches for an immediate subdirectory having the same name as the target assembly. For example, if the assembly is myClass, it looks first for the directory myClass.dll and then myClass.exe. The CLR loads the assembly from the first directory in which it locates it.

In our case, we want to use a directory structure that is not based on the name of the assemblies. To force the CLR to extend its probe into other folders, we must add a special <probing> element to the application's configuration file. For an application named myApp.exe, we create a configuration file named myApp.exe.config and store it in the application's base directory. Included in the file is a <probing> tag with a privatePath attribute that specifies the subdirectories (below the base directory) for the CLR to search.

<configuration>

   <runtime>

      <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">

         <probing privatePath="\graphics;\data" />

      </assemblyBinding>

   </runtime>

</configuration>

By default, the specified search paths are relative to the application's base directory. You can also specify an absolute path, but the path must specify a folder below the application's base directory.

The application configuration file in this example is rather simple, and you may choose to build it manually. However, because the file often contains other configuration elements, manual manipulation can quickly become an unwieldy task. As an alternative, you can use the same Configuration tool described in the CAS discussion. It has an Applications folder that can be opened and will lead you through the steps to create a configuration file for a selected assembly. One of its more useful and instructive features is an option to view a list of other assemblies that the selected assembly depends on.

Using CodeBase Configuration

A <codebase> element provides another way to specify the location of an assembly. It differs from the <probing> element in that it can specify any directory on the machine, as well as a location on a file or Web server across a network. The CLR uses the URI information provided by this element to download an assembly the first time it is requested by an application. Notice how <codebase> is used in the following sample configuration file.

The <codebase> element, along with a companion <assemblyIdentity> element, is placed inside the <dependentAssembly> block. The <assemblyIdentity> element provides information about the assembly the CLR is attempting to locate: its simple name, public key token, and culture. The <codebase> element provides the assembly's version number and location where it can be found. If the assembly is not strongly named, the version information is ignored.

<configuration>

  <runtime>

    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">

     <dependentAssembly>

        <assemblyIdentity name="movieclass"

           publicKeyToken="1F081C4BA0EEB6DB"

               culture="neutral" />

        <codeBase version="1.0.0.0"

                   href="http://localhost/scp/movieclass.dll" />

     </dependentAssembly>

    </assemblyBinding>

  </runtime>

</configuration>

The HRef can also refer to the local file system:

<codeBase version="1.0.0.0"

                  href="file:///e:\movieclass.dll"  />

Using a Configuration File to Manage Multiple Versions of an Assembly

One of the advantages of using a strongly named assembly is the version binding feature it offers. When Assembly A is compiled against Assembly B, A is bound to the specific version of B used during the compilation. If Assembly B is replaced with a new version, the CLR recognizes this at load time and throws an exception. It is a good security feature, but can make updates to B more time-consuming because all assemblies dependent on it must be recompiled.

A configuration file can be used to override version binding by instructing the runtime to load a different version of the assembly than was originally bound to the calling assembly. This redirection is achieved by adding a <bindingRedirect> element that specifies the original version and the new version to be used in place of it. Here is how the previous configuration file is altered to have a newer version of the movieclass assembly loaded:

<configuration>

  <runtime>

    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">

      <dependentAssembly>

        <assemblyIdentity name="movieclass"

           publicKeyToken="1F081C4BA0EEB6DB"

               culture="neutral" />

        <bindingRedirect oldVersion="1.0.0.0"

                         newVersion="2.0.0.0"/>

        <codeBase version="2.0.0.0"

                   href="http://localhost/scp/movieclass.dll" />

      </dependentAssembly>

    </assemblyBinding>

  </runtime>

</configuration>

Use of the <bindingRedirect> element inside an application configuration file offers a flexible and practical approach to matching applications with new or older versions of a component. For example, both versions of the component can run side by side on the same machine. One application can continue to use the older version, whereas the configuration file of a second application is modified to direct it to the newer component. Moreover, if the second application encounters problems with the new component, the <bindingRedirect> element can be removed and the application will revert to using the original component.

Assembly Version and Product Information

As a final note on deployment, it is good practice to include assembly-level attributes to your assembly to provide useful information to clients that want to examine the properties of your .exe or .dll file. As an example, the attributes in the following code yield the properties shown in Figure 15-11:

using System;

using System.Reflection;

[assembly:AssemblyVersion("2.0.0.0")]

[assembly:AssemblyCompany("Acme Software")]

[assembly:AssemblyCopyright("Copyright (c) 2005 Stephen Perry")]

[assembly:AssemblyTrademark("")]

// Set the version ProductName & ProductVersion fields

[assembly:AssemblyProduct("Core C# Examples")]

[assembly:AssemblyInformationalVersion("2.0.0.0")]

[assembly:AssemblyTitle("Core C# movieclass.dll")]

[assembly:AssemblyDescription("This is a sample C# class")]

//

public class Movies

{

// ... Remainder of code

Figure 15-11. Attributes provide information identifying an assembly

[View full size image]


15.5. Summary

There is more to developing an application or component than simply implementing code to handle a specific task or set of tasks. Ancillary issues such as code correctness, adherence to code standards, code efficiency, security, and code deployment must be addressed. This chapter has presented an overview of tools and approaches available to handle these issues in .NET.

A useful tool for determining how your code meets .NET coding standards is FxCop. It's included with the .NET SDK and evaluates an assembly against a set of rules that define good coding practice. You can add custom rules and disable rules that don't apply.

To secure an application, .NET employs a concept known as Code Access Security. Unlike role- or user-based security—which .NET also supports—CAS restricts what resources code can access. When an assembly loads, .NET gathers information about its identity known as evidence. It evaluates the evidence against security policies and maps it into a set of permissions that are granted to the assembly. A security administrator typically configures the security policy using the .NET Configuration tool, although a command-line utility is also available. Security can also be implemented in code. Permissions are nothing more than classes, and can be used by developers to request permissions for an assembly and demand that calling assemblies have the necessary permissions to access a secure resource.

The final step in developing an application is settling on a deployment strategy. In .NET, this can be as easy as creating an install based on copying files to a machine (XCOPY deployment); alternatively, MSI files can be created for use with the Microsoft Windows Installer. One of the key install decisions is how to deploy assemblies. Private assemblies are placed together in a common directory path; shared assemblies are stored in the Global Assembly Cache. In addition, a configuration file can be set up to instruct the runtime to search for assemblies in directories on the local computer or a remote Web server located across a network.


15.6. Test Your Understanding

1:

Indicate whether the following are true or false:

  1. A strongly named assembly must be stored in the GAC.

  2. When compiling an application, the CLR automatically searches the GAC for a referenced assembly.

  3. When loading an assembly, the CLR automatically searches the GAC for a referenced assembly.

  4. A public key token must exist in the manifest of an assembly referencing a strongly named assembly.

2:

You are developing a component for a class library and want to follow good coding practices. Evaluate the assembly with FxCop. The output includes the following message. What is missing from your code?

TypeName= "AssembliesShouldDeclareMinimumSecurity"

3:

What are the four parts of an assembly's strong name?

4:

How do you assign a version number to an assembly? How are default values for the build number and revision determined?

5:

What three methods are used to modify a stack walk? Describe the role of each.

6:

What is delayed signing, and why is it used?

7:

Identify each of the following: predefined permission, named permission set, and security zone.

8:

Indicate whether the following statements about assembly deployment are true or false:

  1. Only a strongly named assembly can be placed in the GAC.

  2. The <probing> element must specify a directory below the application's base directory.

  3. The <codeBase> element is used to redirect the CLR to use a newer version of an assembly.

  4. XCOPY deployment can be used to install shared assemblies.

9:

What element or elements need to be added to this application configuration file so that the application will access the new version 2.0.0.0 of this component, rather than the previous 1.0.0.0 version? Both the application and component reside in c:\data\.

<dependentAssembly>

   <assemblyIdentity name="movieclass"

      publicKeyToken="1F081C4BA0EEB6DB"

               culture="neutral" />

   ---> Insert code here

</dependentAssembly>


    Part: Programming for the Internet

    Chapter 16 ASP.NET Web Forms and Controls.

    Chapter 17 The ASP.NET Application Environment

    Chapter 18 XML Web Services


Chapter 16. ASP.NET Web Forms and Controls

Topics in This Chapter

  • Web Client-Server Communications: Web page requests and responses rely on HTML to represent content, and HTTP to define the protocol that governs client-server interaction. JavaScript, ASP, and ASP.NET are compared as ways to develop an application.

  • Structure of an .aspx Page: An .aspx Web page is compiled as a class. It contains a variety of directives that control the page's behavior and can link it to external code. This section looks at the basic elements that comprise an .aspx file, including how Viewstate is used to maintain state information for the form.

  • Inline and Code-Behind Pages: ASP.NET offers three models for designing a Web application: inline code, code-behind, and code-behind with partial classes. The latter two permit the page interface to be separated from the program logic. The implementation code is placed in a code-behind file that is referenced by an .aspx page.

  • HTML Server-Side Controls and Web Controls: ASP.NET provides controls that correspond to HTML tags and are accessed as classes in the server-side code. It also provides a number of native "Web" controls that include features not found on standard HTML controls. These include validation controls, data source controls, and sophisticated data display controls such as the DataList, GridView, and TReeView.

  • Master and Content Pages: ASP.NET supports the creation of a master Web page that serves as a template for other "content" pages that visually inherit from it. This chapter explains how to create master pages containing special "place holder" controls that define where derived content pages may insert their custom content.

Developing applications to run on the Internet is a broad topic, and this book devotes its last three chapters to the subject. This chapter introduces key features of ASP.NET and focuses on using controls to create Web pages; Chapter 17, "The ASP.NET Application Environment," looks at application development issues such as managing sessions and configuring ASP.NET control files; and the book's final chapter discusses Web Services.

ASP.NET is technically regarded as the next generation of ASP. There are syntactic similarities and compatibilities, but the differences are even greater. Thus, this chapter makes no attempt to explain ASP.NET in terms of ASP. There are some comparisons, but no prior knowledge of ASP is assumed. You will also find traces of JavaScript sprinkled in a couple of applications, but the code is easily understood within the context of the examples.

The first section provides a tour of client-server Web interaction. It begins with a simple JavaScript application that demonstrates the fundamental techniques used to transfer information between a client and Web server. It then shows how the ASP.NET model encapsulates these principles and adds an object-oriented approach to Web page design and implementation. Subsequent sections survey the array of Web presentation and validation controls. Special attention is given to the DataList and GridView controls.

One note: IIS (Microsoft Internet Information Server) is conspicuous by its absence in our discussion. Although all of the applications were tested in an IIS environment, and the preponderance of ASP.NET applications will run on this Web server, they are not bound to it. Microsoft has created an open-source HTTP server named Cassini that is written in C#. It's fully HTTP/1.1 compliant, supports directory browsing, as well as many of the standard MIME types, and most importantly, supports ASP.NET. It has been tested on Apache servers and is clearly geared toward making ASP.NET the Web development tool of choice for multiple Web platforms.


16.1. Client-Server Interaction over the Internet

At its core, the Internet consists of resources and users who want to access Web-based resources. Three mechanisms are required to enable this access: a naming scheme (URI) to locate the resources, a protocol (HTTP) defining how the request/response process works, and a language (HTML) for publishing information and providing a means to navigate to the resources. This environment is quite different than that facing the Windows Forms programmer and presents the Web application developer with three major challenges:

  • HTML. The developer must be familiar with the Hypertext Markup Language in order to create Web Forms. Certainly, HTML generators are a useful accessory, but at some point, the developer has to understand the raw HTML.

  • HTTP. The primary task of a Web application is to respond to HTTP requests. These requests may be sent by either an HTTP GET or POST method and may contain headers in addition to the main message body. The language used by the Web developer must provide access to the full HTTP message. For example, to process a received form, a Web application must be able to extract data from fields on the form. Header information can be equally important. It can be used to control caching, identify the source of the request, and pass cookies between the client and server.

  • State. There is no intrinsic feature in HTML or HTTP that maintains the state of variables during the request/response operation. Figure 16-1 illustrates this. The client receives an empty survey form, fills it out, and posts it back to the server. The server detects an error and returns the same Web page—which is blank again. Preserving data (state) over the round trip(s) between client and server is, perhaps, the biggest challenge facing the Web programmer.

    Figure 16-1. Request/response process

One of the best ways to understand and appreciate a technology is to compare it with other technologies that perform a similar task. With that in mind, this section takes a simple Web application and implements it in JavaScript, ASP, and ASP.NET. The objectives are twofold: to understand the problems associated with implementing a Web Forms application, and to illustrate the evolutionary approach that ASP.NET uses to meet these challenges. You do not need experience with ASP or JavaScript to understand the examples, but it helps. Their function is to demonstrate how the HTTP GET and POST methods send data to a server and maintain state information. The examples could just as easily use Perl or PHP scripting.

Web Application Example: Implementing a BMI Calculator

This application calculates the Body Mass Index (BMI) for a given height and weight. The user enters this information (see Figure 16-2) into a form and then clicks Submit Form to send the request to a server. The server calculates the BMI and returns a Web page that includes the BMI and original data entered.

Figure 16-2. BMI Web calculator

Realistically, the calculation could be done using JavaScript on the client's page without requiring a trip to the server. But let's suppose that we also want to record each time the calculator is used. For that, a trip to the server is necessary.

A JavaScript BMI Calculator

JavaScript remains the primary language used for client-side coding on a Web application. It is platform and browser independent (although there are a few exceptions). As we will see later, ASP.NET automatically generates JavaScript for its client-side processing.

The first example uses JavaScript on both the client and server side. Listing 16-1 contains the HTML and JavaScript that comprise the client-side page. For brevity, some of the less important code is excluded.

The HTML in the <body> defines the fields and buttons on a form bmi_input. This form defines GET as the HTML method for delivering the contents of the form to the server. When the button is clicked, control passes to the JavaScript function post that verifies the content of the form and sends it to the server. We'll get to the other JavaScript code after we look at the server side.

Listing 16-1. JavaScript/HTML to Display a BMI Calculator— bmi.htm

<HTML>

<HEAD><TITLE>BMI Calculator</TITLE>

<SCRIPT LANGUAGE="Javascript" TYPE="text/javascript">

<!—

// hrefstr is set to querystring values—if any

var hrefstr= location.search.substring(1,

      location.search.length);

function showbmi(){

   if (hrefstr )   // display values in form fields

   {

      var parms   = hrefstr.split('&');

      var f = self.document.forms[0];

      f.bmi.value = eval(parms[0]);

      f.hti.value = eval(parms[2]);

      f.wt.value  = eval(parms[3]);

      f.htf.value = eval(parms[1]);

   }

}

// -->Code for Verify goes here.

// Post Form to Web Host

function post() {

   if (verify()) //Call function to verify values in form fields

   {

      var f = self.document.forms[0];

      f.bmi.value=0;

      f.submit();  // Use HTTP GET to send form to server

   }

}

//-->

</script>

</HEAD>

<BODY bgcolor=#ffffff>

<FORM NAME="bmi_input" method=GET action=bmicalculator.htm>

   <table border=0 cellpadding=2 cellspacing=0 width=180

      bgcolor=#cccccc>

   <tr><td colspan=3 align=center><font size=2 color=#33333>

      <b>BMI Calculator</b> </td></tr>

   <tr><td><font size=2 ><b>BMI:</b></td>

      <td colspan=2 ><input type=text size=5 name=bmi>

   </td></tr>

   <tr><td colspan=3><hr size=1></td></tr>

   <tr><td><font size=2 >Height:</td>

      <td><input type=text size=3 name=htf maxlength=1></td>

      <td><input type=text size=3 name=hti maxlength=2></td>

   </tr>

   <tr><td>&nbsp;</td><td valign=top><font size=2>feet</td>

       <td valign=top><font size=2>inches</td>

   </tr>

   <tr><td><font size=2 >Weight:</td>

      <td colspan=2><input type=text size=3 name=wt

            maxlength=3></td>

   </tr>

   <tr><td colspan=3 align=center>

   <INPUT TYPE="button" VALUE="Submit Form" ONCLICK=

            "self.post()";>

   </td></tr>

   <tr><td colspan=3>&nbsp;</td></tr>

   </table>

</FORM>

<SCRIPT LANGUAGE="Javascript" TYPE="text/javascript">

<!--

   showbmi();   // Fills form with values if they exist

//-->

</script>

</body>

</html>

Using the GET method causes the form's data to be sent to the server as part of the URL. This string of data, referred to as a query string, contains name-value pairs that correspond to the name of the fields on the form, followed by their value. Figure 16-3 shows how form data is passed to the Web page bmicalculator.htm that calculates the BMI.

Figure 16-3. Passing data in a query string

Data is returned to the client in the same manner—by appending it to the URL of the page containing the original form. Note that bmi is now set to the calculated value. Here is the server-side code that creates this response:

<html><head></head>

<body >

<script language="javascript">

<!—

   // Use location.search to access the query string

   var hrefstr = location.search.substring(1,

         location.search.length);

   var parms   = hrefstr.split('&');

      feet    =  parms[1];

      inch    =  parms[2];

      pounds  =  parms[3];

      totinches = eval(feet)*12 + eval(inch);

      // ..... Calculate BMI

      var h2 = totinches * totinches;

      bmi = Math.round(eval(pounds) * 703 * 10/ h2)/10;

      // --> Place code here to maintain count of visits.

      //... Return value and original parameters as a query string

      ndx = hrefstr.indexOf('htf');

      self.location = 'bmi.htm?bmi=

            '+bmi+"&"+hrefstr.substring(ndx);

   //-->

</script>

</body></html>

This code grabs the values from the query string using location.search, parses them, calculates the BMI, and then creates a URL for the page to be returned. The final step is for the client browser to display the calculated BMI value.

At the bottom of the code in Listing 16-1 is a call to the JavaScript function showbmi(). This function operates much like the preceding server code. It extracts the data from the query string and displays it in the appropriate fields on the form. Note that the function first confirms that the query string is not empty, as will be the case the first time the form is loaded.

The use of a query string is popular for applications that transfer small amounts of data. However, it becomes a problem with large forms because the query string is limited by the 2K maximum string length imposed by some browsers (IE). In addition, it raises obvious security concerns by exposing data in a URL line that can be altered by the user. Query string encryption can mitigate this problem and should be considered where it makes sense. A more robust solution is to use the HTTP POST method in place of GET.

An ASP (Active Server Pages) BMI Calculator

JavaScript is intended primarily for client-side scripting and cannot process data sent by a POST method. This example replaces the JavaScript server code with an ASP file that handles the POST request. We change one statement in the client code to indicate that POST is being used and to provide the name of the ASP server page:

<FORM NAME="bmi_input" method=POST action=bmicalculator.asp>

The code for the ASP server file is quite simple. It accesses the data passed from the client by referencing a request object. The data is passed to a VBScript function that calculates the BMI. A URL is then constructed that contains the query string with the BMI value and other parameters. The response.redirect method sends the form to the client's browser.

<script language="VBScript" runat="server">

function getBMI(inch,feet,pounds)

   totinches = feet*12 + inch

   h2 = totinches * totinches

   getBMI= (pounds * 703 * 10/ h2)/10

end function

</script>

<%

   ' ... POST data is available in request object

   inch    = request("hti")

   feet    = request("htf")

   pounds  = request("wt")

   bmi= left(cstr(getBMI(inch,feet,pounds)),4)

   ' ... return value and original parameters as a query string

   hrefstr = "&htf=" +cstr(feet) + "&hti=" + cstr(inch) +

             "&wt="&cstr(pounds)

   response.redirect ("bmi3.htm?bmi="+bmi+hrefstr)

%>

This solution illustrates the fundamental ASP approach of using VBScript to interact with HTTP response and request objects. In addition, HTML and JavaScript can be intermixed with the ASP code to create Web Forms. Although this offers a degree of flexibility, it often results in a babel of code and inconsistent coding techniques.

Using ASP.NET to Implement a BMI Calculator

Before building an ASP.NET solution for the calculator, let's first take a general look at the features of ASP.NET and how they affect Web Forms programming. One way to understand the Web Forms model is to look at the requirements faced by the ASP.NET designers—and how they met them.

  • Integration with the .NET environment. Web Forms are built on the Common Language Runtime (CLR) and have access to all types in the Framework Class Library, as well as special namespaces, such as System.Web and System.Web.UI that support Web applications. An important aspect of .NET integration is that it is required only on the server side; users are not required to install the .NET Framework on their computers. The emphasis is on rendering code that runs unaided on a browser. In fact, the emitted code is tailored to recognize the browser it is running on, so it can take full advantage of its features.

  • Linking client-side HTML controls with server-side controls. All controls in ASP.NET are classes. Controls that appear on a browser window are renderings based on HTML tags. To reconcile this differing technology, ASP.NET includes server-side controls that map directly to HTML tags. For example, the <input type = text> tags we saw in the earlier examples are represented by the HtmlInputText class found in the System.Web.UI.HtmlControls namespace. When information on a Web page is sent to a server, all HTML tags designated to run on the server are converted into their corresponding .NET class and compiled as members of the Web page. We'll see how HTML tags are mapped to server controls in the next example.

  • Compiled rather than interpreted Web pages. Traditionally, Web pages have been rendered by JavaScript, VBScript, Perl, and other scripting interpreters. In contrast, an ASP.NET Web page consists of a user interface defined by HTML, and interface logic written in a .NET language such as C# or VB.NET. The first request for an ASP page (.aspx file) results in the page being compiled into a .NET class. Further requests are then handled by the assembly created by compilation. For the user, it means faster Web access; for the developer, it means applications are developed using the same .NET tools available for desktop and component applications.

ASP.NET offers three models for implementing a Web application:

  • Inline code. The HTML markup code and application code (C#) coexist in a single .aspx file.

  • Code-behind. The markup code and application code are placed in separate files. The markup is in an .aspx file and the logic code resides in a .cs or dll file.

  • Partial classes. This is a variation of the code-behind model that places the markup and code in separate files. The difference is that the code-behind file is implemented using partial classes. It is stored as a .cs file and is compiled along with the markup file. This model is available only with ASP.NET versions 2.0 and later.

None of the models offers a performance advantage over the others. This leaves the choice of model up to one's preference—or need—for code separation. Let's now examine the models by using each to implement the BMI application.

Inline Code Model

The code for the BMI Web Form application is shown in Listing 16-2. Although it resembles an HTML page, an .aspx page is actually an XML-formatted page. It is processed on the server and used to generate a mixture of HTML and JavaScript that is sent to the browser.

Notice that this example contains actual C# code—between the <script> </script> tags—that calculates the BMI value. This method is executed on the server and is not visible as source code to the client.

Listing 16-2. ASP.NET Inline Code Implementation of a BMI Calculator

<%@ Page Language="C#"  %>

<HTML>

<HEAD><TITLE>BMI Calculator</TITLE>

<script  runat="Server">

   // Calculate BMI from values on form

   private void getBMI(object sender, System.EventArgs e)

   {

      try

      {

         decimal f =  Convert.ToDecimal(htf.Value);

         decimal inch = Convert.ToDecimal(hti.Value);

         decimal w = Convert.ToDecimal(wt.Value);

         decimal totinches = f * 12 + inch;

         decimal h2 = totinches * totinches;

         decimal massIndex = (w * 703 * 10/ h2)/10;

         bmi.Value = massIndex.ToString("##.##");

      }catch (Exception ex)

         { bmi.Value=" "; }

    }

</script>

</HEAD>

<BODY bgcolor=#ffffff>

<FORM NAME="bmi_input" runat=server>

   <table border=0 cellpadding=2 cellspacing=0 width= 180

         bgcolor=#cccccc>

   <tr><td colspan=3 align=center><font size=2>

         <b>BMI Calculator</b>

         </td></tr>

   <tr><td><font size=2 ><b>BMI:</b></td>

         <td colspan=2 ><input type=text size=5 id=bmi

                  runat=server></td></tr>

   <tr><td colspan=3><hr size=1></td>

   </tr>

   <tr><td><font size=2 >Height:</td>

         <td><input type=text size=3 id=htf maxlength=1

                  runat=server></td>

         <td><input type=text size=3 id=hti maxlength=2

                  runat=server></td>

   </tr>

   <tr><td>&nbsp;</td><td valign=top><font size=2>feet</td>

         <td valign=top><font size=2>inches</td>

   </tr>

   <tr><td><font size=2 >Weight:</td>

         <td colspan=2><input type=text size=3 id=wt maxlength=3

                  runat=server></td>

   </tr>

   <tr><td colspan=3 align=center>

   <INPUT TYPE="button" VALUE="Submit Form"

            OnServerClick="getBMI"

            id=bmiButton runat=server>

         </td>

   </tr>

   <tr><td colspan=3>&nbsp;</td>

   </tr>

   </table>

</FORM>

</body>

</html>

To understand the differences between standard HTML and ASP.NET code, let's compare how controls are specified in Listing 16-1 versus Listing 16-2.

  • The most obvious difference is the addition of the runat=server attribute in the <Form> and <Input> tags. This designation converts any HTML elements to HTML server controls that can be processed by the server-side code prior to sending them to the browser. The following code illustrates how an <input> tag is transformed before it emitted to the client's browser:

ASP.NET statement:

   <input type=text size=3 id=wt maxlength=3 runat=server>

HTML statement emitted:

   <input name="wt"  type="text" size="3"

      maxlength="3" value="168" />

  • The <Form> tag does not include the method or action attribute. By default, the POST method is used. As the following code shows, the page is self-referencing, which means the page sends the form to itself.

ASP.NET statement:

   <FORM NAME="bmi_input" runat=server>

HTML statement emitted:

   <form name="_ctl0" method="post" action="bmi.aspx"

      >

  • The <button> tag contains an OnServerClick event delegate. This indicates that a method on the server will handle the event. The resulting HTML references a JavaScript function that has been added to post the form when the button is clicked.

HTML statement emitted:

   <input language="javascript" onclick =

      "__doPostBack('_ctl1','')" name="_ctl1"

      type="button" value="Submit Form" />

ASP.NET also adds three hidden fields to the HTML page it returns to the browser:

<input type="hidden" name="__EVENTTARGET" value="" />

<input type="hidden" name="__EVENTARGUMENT" value="" />

<input type="hidden" name="__VIEWSTATE"

     value="dDwxMzc5NjU4NTAwOzs+iIczTTLHA74jT/02tIwU9FRx5uc=" />

The first field, __EVENTTARGET, specifies the control that invoked the request (known as a postback) to the server; the second, __EVENTARGUMENT, contains any parameters required by the event. The __VIEWSTATE field is by far the most interesting.

View State

View state is the feature in ASP.NET that automatically maintains the state of server-side controls (controls declared with runat=server) as a form makes the round trip between the client and the server. In other words, it allows a page to place data in a control such as a ListBox or GridView one time—usually when the page is first loaded—and ensures the data is retained as subsequent postbacks occur. Here is how it works.

When a page is posted back to the server, the data in the hidden __VIEWSTATE field is deserialized and used to set the state of controls and the overall Web page. Data received in the HTTP request as part of a POST operation is used to set the values of those related controls (note that for controls whose contents are posted—TextBox, CheckBox, RadioButtons—the posted data overwrites the view state data). The __VIEWSTATE field is then updated before it is passed back to the client. The returned view state value plays no role on the client other than to represent a snapshot of control values at the time the page is received by the browser.

Because the view state string can be viewed in a source listing, questions about security become a legitimate issue. However, unlike the query string, the value is not represented as clear text.

value="dDwxMzc5NjU4NTAwOzs+iIczTTLHA74jT/02tIwU9FRx5uc="

By default, a machine-specific authentication code is calculated on the data and appended to the view state string. The full string is then Base64 encoded. It is possible to decode the string, but the difficulty in doing so will thwart most casual efforts. Tampering with the string can also be detected by the server and results in a security exception being thrown. As always, use Secure Sockets Layer (SSL) to ensure absolute security for Internet communications.

Core Note

Maintaining view state data within a Web page makes the page independent of the server. This means that a Web page request can be sent to any server in a Web farm—rather than restricting it to a single server.

Performance is another issue that must be considered when working with the view state value. By default, it maintains data for all server-side controls on the form. The control information is not limited to only the data value associated with the control. For example, when a DataGrid is used, the view state includes not only the data in each cell, but also column and row headers, and related style attributes. The view state data can easily add several thousand bytes to a Web page and slow performance.

To improve performance, you may want to disable view state for the Web page and apply it only to selected controls. Set the EnableViewState attribute in the @Page directive to disable it at the page level:

<%@ Page Language="C#" EnableViewState="False" %>

Then, to enable view state for an individual control, apply the attribute as shown in the following code:

<input type=text size=3 id=wt maxlength=3 EnableViewState=true

      runat=server >

Of course, you can also take the opposite tact and leave view state on for the page and turn it off for selective controls.

The decision to enable or disable view state is one of the key decisions in designing a Web page that displays large amounts of data in a server-side control. The easiest approach is to allow ViewState to take care of the details. However, this can result in a large HTML payload being transferred repeatedly between browser and server. An alternative is to design the code to reload the data into the controls on each postback. The data may be fetched from the original source—usually a database—or it may be stored on the server in session state or a cache. These last two options are described in Chapter 17, "The ASP.NET Application Environment."

Core Note

ViewState maintains not only a control's data but also its state. For example, it keeps track of the last item(s) selected in a ListBox and permits the ListBox to be redisplayed in its most recent state. The drawback of manually repopulating a control, rather than using ViewState, is that this state information is lost.

The @Page Directive

The last element to discuss in Listing 16-2 is the @Page directive. An .aspx file can contain only one @Page directive, and it is typically—although not required—the first statement in the file. The purpose of this directive is to assign attribute values that control the overall behavior of the Web page, as well as specify how the page is assembled and compiled on the server. Table 16-1 lists selected attributes for the @Page directive.

Table 16-1. Attributes for the @Page Directive

Attribute/Value

Description

EnableSessionState = value

Specifies the type of access the page has to the session state information. Sessions are discussed in Chapter 17.

EnableViewState = bool

Enables or disables view state for the page. Individual controls can override this value.

EnableViewStateMac = bool

Is used to make the view state more secure by adding a validator hash string to the view state string that enables the page to detect any possible attempt at corrupting original data.

SmartNavigation = bool

Setting this to true can improve the rendering of pages for users of Internet Explorer 5.0 and later. Its improvements include

  • Eliminating flickering when a page loads.

  • Retaining the input focus on the field last having it.

  • Preserving the scroll position on pages longer than one screen.

ErrorPage = url

Specifies the URL of a Web page that is called when an unhandled exception occurs.

Culture = string

A culture setting for the page based on the CultureInfo class. This attribute affects how culture-dependent functions, such as numbers and dates, are displayed. The following setting causes a DateTime object to be displayed in a European format using German months and days: Culture="de-DE". These settings can also be set in the Web.config file as described in the next chapter.

UICulture = id

Specifies the user interface culture for this page.

Example: UICulture="de"

trace = bool

Turns tracing on or off for the page. Default is false. When tracing is on, diagnostic information about a single request for an .aspx page is collected. The results of the trace are available programmatically and are appended as a series of tables at the bottom of the browser output. Tracing is discussed in Chapter 17.

Inherits = class name

Specifies the base class used to generate a class from the .aspx file. The default is System.Web.UI.Page. If code-behind is used, the class name from this code is used.

MasterPageFile = master page

Specifies the "master page" from which the current page visually inherits its layout. Introduced with 2.0.

theme = theme name

Specifies the subdirectory containing the .skin file (specifies the appearance of controls) and any other images and style sheets that define the look and style (theme) of a page. The theme file is stored in the /app_themes subdirectory. Introduced with 2.0.

Language = language

Specifies the language for inline code.

Codebehind = *.dll

Specifies the name of a compiled code-behind file. This file must be in the \bin subdirectory of the application.

Codefile = *.cs

Specifies a code-behind file containing a partial class. Introduced with 2.0.

Src = path

Specifies a code-behind file containing source code.

The Codebehind, Codefile, and Src attributes specify the assembly or source file containing the business logic code for the page. Instead of placing code between <script></script> tags as we did in Listing 16-2, the code is placed in a separate code-behind file that is referenced by these attributes. Before discussing code-behind, let's look at some additional directives that are frequently used in .aspx pages.

Other Directives
@Import Directive

This directive is used to import a namespace in an .aspx page. It serves the same purpose as the C# using statement.

<%@ Import namespace="System.Net" %>

Several namespaces are automatically imported in a page, making this directive unnecessary in most cases. These namespaces include

  • System, System.IO

  • System.Web.UI, System.Web.UI.HtmlControls, System.Web.UI.WebControls

  • System.Web, System.Web.SessionState, System.Web.Caching

  • System.Text, System.Text.RegularExpressions

@Assembly Directive

This directive links an assembly to the current page while the page is being compiled. This provides the page with access to all types in the assembly. It takes two forms:

<%@ Assembly Name="webfunctions" %>

<%@ Assembly Src="webfunctions.cs" %>

The first version references an assembly that may be private or deployed in the Global Assembly Cache; the second statement causes the source to be dynamically compiled into an assembly that is linked to the Web page. Note that assemblies in the application's \bin subdirectory are automatically linked to a page and do not need to be referenced.

@Register Directive

This directive associates alias names with namespaces and classes. Its purpose is to provide a convenient syntax for adding custom controls to a Web page. The directive takes two forms:

Syntax:

<%@ Register Tagprefix="tagprefix" Namespace="namespace"

      Assembly="assembly" %>

<%@ Register Tagprefix="tagprefix" Tagname="tagname"

      Src="pathname" %>

Attributes:

Tagprefix | Alias for a namespace.
Namespace | The namespace to associate with Tagprefix.
Assembly | Assembly in which namespace resides.
Tagname | Alias to associate with a class.
Src | The file containing the user control

The first form of the directive is used to add an ASP.NET server control to a page; the second form is used with a custom control contained in a source file. In the latter case, the TagPrefix and TagName are always used together as a colon-separated pair. Here is a code segment that places a user control defined in the file hdr.ascx on a Web page. The @Register directive defines the alias pair that is used to declare the control on the Web page.

<%@ Register TagPrefix="uc1" TagName="hdr" Src="hdr.ascx" %>

<form  method="post" runat="server">

      <uc1:hdr  runat="server"></uc1:hdr>

</form>

We'll make use of this directive in Section 16.4, which provides examples of how to create and use custom controls. Note that @Register directive information also can be stored in the Web.config file (see Chapter 17), eliminating the need to place it in a Web page.

The Code-Behind Model

The example in Listing 16-2 contains both C# to implement program logic and HTML to render the user interface. A Web page can also be configured as an .aspx file, containing only the interface code and a separate code-behind file that contains the program logic and serves as the base class for compiling the .aspx file (see Figure 16-4). This code-behind file takes the .cs extension.

Figure 16-4. How ASP.NET responds to a Web page request

The code-behind page is linked to the .aspx file as an assembly or source file using the Codebehind or Src attributes of the @Page directive. If the Codebehind attribute is used, the assembly must be stored in the \bin directory of the application.

Let's now look at how the code in Listing 16-2 can be changed to use a code-behind file. We create a code-behind file named bmicb.cs (see Listing 16-3) to replace the code currently between the <script/> tags. The @Page directive links this file to the .aspx file:

<%@ Page Language="C#" Src="bmicb.cs" Inherits="BMI" %>

The code-behind page is always structured as a class whose name must be specified by an Inherits attribute. This class is shown in Listing 16-3. Let's take a close look at it, because knowledge of how the code-behind file and the .aspx file interact on the server is essential to understanding the ASP.NET Web page model.

Listing 16-3. Code-Behind File for BMI Calculator—bmicb.cs

using System;

using System.Web.UI.HtmlControls;

public class BMI : System.Web.UI.Page

{

   //     <input type=text id=htf runat=server>

   protected HtmlInputText htf;

   //     <input type=text id=hti runat=server>

   protected HtmlInputText hti;

   //     <input type=text id=wt runat=server>

   protected HtmlInputText wt;

   //     <input type=text id=bmi runat=server>

   protected HtmlInputText bmi;

   //     <input type="button" VALUE="Submit Form" id=bmiButton

   //      runat=server>

   protected HtmlInputButton bmiButton;

   override protected void OnInit(EventArgs e)

   {

      // Delegate to handle button click on client

      bmiButton.ServerClick += new EventHandler(getBMI);

   }

   protected void getBMI(object sender, System.EventArgs e)

   {

      decimal f =  Convert.ToDecimal(htf.Value);

      decimal inch = Convert.ToDecimal(hti.Value);

      decimal w = Convert.ToDecimal(wt.Value);

      decimal totinches = f * 12 + inch;

      decimal h2 = totinches * totinches;

      decimal massIndex = (w * 703 * 10/ h2)/10;

      bmi.Value = massIndex.ToString("##.##");

   }

}

The first thing to observe from this listing is that it consists of one class—BMI—that derives from the System.Web.UI.Page class. The Page class is to Web Forms what the Form class is to Windows Forms. Like the Form, it has a sequence of events that are fired when it is initialized and loaded. It also has several properties that control its behavior—many of which correspond to the @Page directive attributes already discussed. We'll look at the Page class later in this section.

One of the trickiest aspects of learning ASP.NET is grasping how server-side controls work—specifically, how the content and action of controls displayed on a browser are managed on the server. Figure 16-5 illustrates the relationship. Each server control is declared as a field in the BMI class. When the values of the controls are posted to the server, they are assigned as field values. In this example, all of the controls are HTML controls—that is, standard HTML controls with the runat=server attribute.

Figure 16-5. Binding between code-behind page and .aspx page

The id value in the tag must match the field name identically. The field types are defined in the System.Web.UI.HtmlControls namespace. Each HTML control has a one-to-one mapping with an HTML tag, as shown in Table 16-2.

Table 16-2. HTML Controls and Their Tags

Control

HTML Tag

HtmlAnchor

<a>

HtmlSelect

<select>

HtmlTextArea

<textarea>

HtmlButton

<input type=button> <input type=submit>

HtmlCheckBox

<input type=checkbox>

HtmlRadio

<input type=radio>

HtmlHidden

<input type=hidden>

HtmlInputText

<input type=text>

HtmlInputFile

<input type=file>

HtmlForm

<form>

HtmlImage

<img>

HtmlTable

<table>

HtmlTableRow

HtmlTableCell

<tr>

<td>

HtmlGenericControl

All other unmapped tags such as <div> and <p>

Handling Events on the Server

In our example, clicking the Submit button sends a request to the server to calculate and return a BMI value based on the form's content. The .aspx code for the button looks like this:

<INPUT TYPE="button" VALUE="Submit Form"

      id=bmiButton runat=server>

Compare this with the tag defining the button in Listing 16-2:

<INPUT TYPE="button" VALUE="Submit Form" OnServerClick="getBMI"

      id=bmiButton runat=server>

This earlier code defines a method (getBMI) to be called when the click event occurs. Because our current example has the method placed in a code-behind file, there is no reference to it in the .aspx file. Instead, the server-side code handles the event using a standard delegate-based event handling approach. An event handler method (getBMI) is defined that matches the signature of the EventHandler delegate. Then, using the button's id value, we create a delegate instance that registers the method for the ServerClick event of the bmiButton control:

bmiButton.ServerClick += new EventHandler(getBMI);

When the button is clicked on the browser, the contents of the form are posted to the server, which recognizes the button click event and calls the appropriate event handler. This raises one obvious question: Because a form can contain any number of buttons, how does the server determine the event that triggered the post-back? The answer is that the name of the control causing the event is passed to the server in the __EVENTTARGET hidden field that was discussed earlier. This is handled automatically by ASP.NET; the developer's responsibility is to create the delegate and server-side control event handler.

Code-Behind with Partial Classes

The problem with the preceding code-behind model is that each control in the markup page must be explicitly mapped to a protected member in the code page. Changes to the markup page require that the code page members be kept in sync. The use of a partial class in the code-behind page eliminates the need for the protected class member. The partial class is compiled with the markup page, which permits the markup and code sections to directly access each other's members. The effect is the same as using the inline model. To demonstrate this, extract the getBMI method from the inline code (refer to Listing 16-2) and place it in its own file inside a partial class. The result is a code-behind partial class as shown in Listing 16-4.

Listing 16-4. Code-Behind Partial Class File for BMI Calculator

//file: bmi.aspx.cs

using System;

partial class BMICalc: System.Web.UI.Page{}

{

   void getBMI (Object sender, EventArgs e)

   {

      try

      {

         decimal f =  Convert.ToDecimal(htf.Value);

         decimal inch = Convert.ToDecimal(hti.Value);

         decimal w = Convert.ToDecimal(wt.Value);

         decimal totinches = f * 12 + inch;

         decimal h2 = totinches * totinches;

         decimal massIndex = (w * 703 * 10/ h2)/10;

         bmi.Value = massIndex.ToString("##.##");

      } catch (Exception ex)

         { bmi.Value=" "; }

   }

}

The client markup page links to this code by specifying the file name and the partial class name in its @Page declaration:

<%@ Page codefile="bmi.aspx.cs" inherits="BMICalc" %>

ASP.NET 2.0 continues to support the original code-behind model, but it should be used only for preexisting code. New development should employ the inline or partial class model.

Page Class

The first time an ASP.NET page is accessed, it is parsed and compiled into an assembly. This is a relatively slow process that results in the delay one notices the first time the .aspx page is called. Subsequent requests receive much faster responses because the assembly handles them. This assembly consists of a single class that contains all of the server-side code, as well as static HTML code.

The compiled class derives either directly from the System.Web.UI.Page class or indirectly via an intermediate code-behind class. It is important to understand the members of the Page class. Its methods and properties define much of the functionality the .aspx code relies on for handling requests, and its events define junctures at which the code must perform initialization and housekeeping tasks.

Table 16-3 summarizes some of the important properties of the Page class.

Table 16-3. Selected Properties of the Page Class

Control

HTML Tag

Application

Returns the HttpApplicationState object that contains information about the executing application.

EnableViewState

Boolean value that indicates whether controls retain their values between requests. Default is true.

IsPostBack

Boolean value indicating whether the page is being loaded and accessed for the first time or in response to a postback.

PreviousPage

Provides a reference to the page originating the call. Information on the calling page is available when the page is reached by cross-page posting or is invoked by HttpUtilityServer.Transfer().

Request

Gets the HttpRequest object that provides access to data contained in the current request.

Response

Gets the HttpResponse object that is used to programmatically send HTTP responses to the client.

Server

Gets the HttpServerUtility object provided by the HTTP runtime.

Session

Gets the HttpSessionState object, which provides information about the state of the current session.

The Application and Session properties provide state information for a Web application and are discussed in the next chapter.

HttpRequest and HttpResponse Objects

The Request and Response properties expose underlying objects (HttpRequest and HttpResponse) that are used to interact with the incoming HTTP request and the outgoing HTTP response. These classes include properties and methods to read cookies from the client's machine, receive uploaded files, identify the browser, get the IP address of the client, and insert custom content into the outgoing HTTP body. Chapter 17 discusses these in detail, but as an introduction, let's look a simple code example that illustrates the fundamentals of accessing these objects within an .aspx page.

The next code segment uses the Response.Output.Write method to write information into the HTTP content returned to a browser. The information comes from Request.Browser properties that indicate the type of client. Note that by using <% %> tags in an .aspx file, we can intersperse these statements with the HTML code. Do this judiciously. An .aspx file is much easier to read when the C# code is segregated between <script> tags, as shown in Listing 16-2, or placed in a code-behind file.

<table border=0 cellpadding=0 cellspacing=0>

   <%

      Response.Output.Write(@"<tr><td>Browser Version: {0}

           </td></tr>", Request.Browser.Type); //  IE6

   %>

</table>

Using IsPostBack

This useful property enables a Web application to distinguish between a postback and the first time a Web page is invoked. It lets the program know when to initialize values that only need to be set once. A typical example is a ListBox that contains unchanging values, such as all of the states in the United States. When assigned, these values are subsequently maintained in the __VIEWSTATE hidden field and do not need to be re-initialized.

To demonstrate, let's extend our BMI inline code example to display the date and time when the user first requests the Web page. When displayed, this date and time remain unchanged no matter how many times the BMI calculation is subsequently requested.

The date and time are displayed using a <span/> tag, which is added beneath the opening <FORM> tag (refer to Figure 16-2) in the bmi.aspx file. (This is equivalent to a Web control Label.)

<FORM NAME="bmi_input" runat=server>

<span id=sessionstart runat=server/><br>

In the code section, we must assign the date and time to the inner contents of the <span> tag the first time that the page is called. Here is the code to do this:

void Page_Load(object sender, EventArgs e)

{

   if(!this.IsPostBack) sessionstart.InnerText =

         DateTime.Now.ToString();

}

Recall that the code on the server is compiled into a class that inherits from the Page class. This class includes several events—discussed in the following section—that are triggered when a request is sent to the Web page. The most useful is the Page_Load event that is raised each time the page is loaded. Applications typically include an event hander for it that checks IsPostBack and performs initialization if the call is not a postback. In this example, the InnerText field of the <span> tags is set to the date and time the first time the page is loaded.

Page Events

The preceding example demonstrates how the Page_Load event handler provides a convenient place to initialize variables. The Load event is only one of four events defined by the System.Web.UI.Page class. The others are Init, PreRender, and UnLoad. These occur in a fixed sequence as shown in Figure 16-6.

Figure 16-6. System.Web.UI.Page events

To best understand the role of each, let's look at what happens when a form is posted to a server:

  1. The temporary class for the page is generated, compiled, and loaded. The ASP.NET runtime begins processing the page and fires the Page.Init event.

  2. The state of the page object and its controls are set from data in the POST variables and the __VIEWSTATE field. The Page.Load event is fired. Typically, the OnLoad event handler is overridden to initialize control values and establish database connections.

  3. Events related to all controls are fired. The last event that occurs is the one that caused the postback. After all server-side control events have been processed, the Page.PreRender event occurs.

  4. The page enters its rendering phase. The class that represents the page calls the Render method on all the individual controls included in the page.

  5. The HTTPResponse is issued, sending the rendered HTML to the client. The Page.Unload event completes the process. This event should be used to release resources by closing files and database connections.

Although Web Forms provide an event-based model that is similar to the Windows Forms model, the key difference is that the events do not actually fire until control returns to the server. There they must occur in the fixed order described in this section. Your code interacts with these events by overriding or replacing the event handlers for the events described in Figure 16-6.

Cross-Page Posting

A Web page in .NET 1.x could only post to itself. In 2.0, you can designate a target page, other than the current page, by setting the PostBackUrl property of a Button, ImageButton, or LinkButton to the address of the target page. Posting occurs when the button is clicked. To demonstrate, the button defined in this code posts the contents of the current form to nextstep.aspx when the button is clicked.

<asp:button

   text="redirect"

   postbackurl="nextstep.aspx"

   runat="Server">

</asp:button>

The page to which a form is posted can view the contents of the calling form using the PreviousPage property. In this example, nextstep.aspx uses PreviousPage to get the text value of the button that initiated the post. Of course, this technique would be used more often to gather data from text boxes on the posted form.

if(!this.IsPostBack)

{

   // Sets text to the value of the Text property of the button

   // from the calling form having the id postBtn: "redirect"

   string text =

         ((Button)PreviousPage.FindControl("postBtn")).Text;

}


16.2. Web Forms Controls

The designer of an ASP.NET Web page can choose from three types of controls for the GUI design: client-side HTML controls, HTML server controls, and Web Forms controls. Client-side controls are the traditional controls defined by HTML tags (<table>, <button>, <input>, and others). These controls are still useful for client-side form validation and creating hyperlinks.

HTML server controls are server-side versions of the standard HTML controls (see Listing 16-2). As we have seen, they're created by adding a runat = server attribute to the HTML tag. The main reason they are included in .NET is to ease the transition from legacy HTML files to ASP.NET. However, except for incurring less overhead, there is no real advantage in using HTML server controls. Web controls—a much more powerful alternative—are nearly as easy to implement and unlock the full capabilities of ASP.NET.

Web controls are native ASP.NET controls that extend the features of the HTML server controls and add non-HTML controls, such as a calendar and data grid. They are actual classes in the .NET Framework and expose properties that enable the developer to exert much more control over their appearance and behavior than is possible with server controls. With the exception of the Table—the HTML table is easier to work with for general use—Web controls should be the control of choice for ASP.NET pages.

Web Controls Overview

Web controls behave much like the HTML server controls: Each has a corresponding class; those with a user interface render themselves in HTML understandable by a browser; and most expose events that are handled on the server. A significant difference from the HTML controls is that Web controls provide a richer object model. From the WebControl class, they inherit properties that affect their appearance: ForeColor, BackColor, BorderColor, BorderStyle, Height, and Width. In addition, many can be bound to a data source that provides their content.

A Web control is declared using the asp: prefix. For example, a TextBox and Button are declared as

<asp:TextBox  type="text" runat="server" />

<asp:Button   Text="Save Data" runat="server"

      OnClick="SaveBtn_Click" />

ASP.NET Web controls are defined in the System.Web.UI.WebControls namespace. There are a lot of them—more than 70 with the release of ASP.NET 2.0. They naturally fall into functional categories that provide a convenient way to present them (see Figure 16-7):

  • Simple controls. Enhanced alternatives to the HTML Server controls and standard HTML tags.

  • List controls. These inherit directly from the System.Web.UI.WebControls.ListControl class and are populated by ListItem objects.

  • Data Display controls. These complex controls are designed to display multiple rows of data (GridView, DataList, Repeater) or multiple fields for a single row of data (FormView, DetailsView).

  • Data Source controls. Introduced with ADO.NET 2.0, these controls serve as source of data for other controls—primarily the data display controls—that bind to them. As their names imply, these controls provide data from a variety of sources.

  • Validation controls. These helper controls perform predefined and custom data validation on other controls.

  • Login controls. A set of controls designed to control the login process by managing and authenticating users.

Figure 16-7. ASP.NET Web controls

In addition to these, there are a few of highly specialized controls: Calendar, AdRotator, and FileUpload. This section provides examples of commonly used visual controls, as well Validation controls and the new (.NET 2.0) Data Source controls. Before examining specific controls, let's look at the properties shared by all Web controls that govern their appearance.

Specifying the Appearance of a Web Control

All Web controls inherit numerous properties from the base WebControl class that can be used to set their appearance and behavior. The properties are most effective when used to decorate simple controls where their effect is more pronounced than with complex controls. Table 16-4 demonstrates how these properties can be used to alter the appearance of a simple Button control.

Table 16-4. Properties That Affect the Appearance of a Web Control

Property

Button1

Button2

Button3

Width

80

100

100

Height

20

20

24

BackColor

#ffffff

#efefe4

#cccccc

BorderStyle

Double

Dotted

None

BorderWidth

1

1

1

BorderColor

Red

Black

Black

Font-Names

Sans-Serif

Sans-Serif

Courier

Font-Size

8pt

10pt

11pt

Font-Bold

TRue

TRue

true

Button

Displayed

Other useful properties include Enabled, Visible, and TabIndex. The latter indicates the tab order of a control. Note that browsers may render these properties differently or ignore some of them altogether. For example, Firefox and Netscape tend to ignore the Width property.

Simple Controls

The simple controls are typically used in combination to create an interface for users to enter data or make selections. The easiest way to present and manage controls that are related by function and style is to place them in a common container. In traditional HTML, the <DIV> element is used for this purpose; in ASP.NET, the panel control serves as a generic container (and is often rendered in browsers as a <DIV> element). There are many advantages to using a panel to layout controls:

  • It eliminates the need for multiple Web pages. Because ASP.NET is designed around posting back to the calling Web page, it's an easy way for a single Web page to present multiple interfaces by simply toggling panels' visibility on or off.

  • Controls on a panel maintain data that can be referenced even if the panel is not visible. This eliminates the use of multiple hidden fields.

  • It serves to unify the appearance of grouped controls by providing a common background or border. A screen is easily sectioned into multiple panels.

The screens in Figure 16-8 are created using a combination of button, label, text box, and panel controls. The page consists of a form that accepts name and address information. Controls to accept name fields are contained on one panel, whereas address fields are on another. Clicking the Name and Address buttons toggles the visibility property of the two panels. The effect is to reuse the same display space for both types of data—obviating the need for a long scrolling form or multiple Web pages.

Figure 16-8. Using panels to manage simple controls

A look at the underlying code reveals the syntax and mechanics of working with Web controls. We'll look at the three most interesting areas of the code: the Page_Load event handler, the button declarations and event handlers, and the layout of the panels.

Using the Page_Load Event to Initialize the Screen

When the page is loaded for the first time, the panel that accepts name fields, pnlName, is made visible, whereas pnlAddress has its visibility turned off.

<script  runat="Server">

   void Page_Load(object sender, EventArgs e)

{

   if(!this.IsPostBack) {

      pnlName.Visible = true;

      pnlAddress.Visible = false;

   }

}

Buttons

The two menu buttons across the top of the form are declared as

<asp:Button  CommandName="name"

      OnCommand="Button_Command" text="Name" Runat="server" />

<asp:Button  CommandName="address"

      OnCommand="Button_Command" text="Address" Runat="server" />

The buttons specify Button_Command as an event handler to be called when each button is clicked. As shown in the event handler code, the buttons' CommandName property identifies the button raising the event. This enables a single method to handle multiple button clicks, which is a useful way to group code performing similar tasks. It's most commonly used with buttons that control column sorting in a grid.

void  Button_Command(Object sender, CommandEventArgs e) {

   // CommandName identifies control invoking event

   switch(e.CommandName)

   {

      case "address":

         pnlName.Visible = false;

         pnlAddress.Visible = true;

         break;

      case "name":

          pnlAddress.Visible= false;

          pnlName.Visible = true;

          break;

      default:

          break;

   }

}

The Clear and Submit buttons use the OnClick property—rather than OnCommand—to specify their Click event handlers as separate methods:

<asp:Panel

   style="Z-INDEX:103; LEFT:20px;POSITION:absolute; TOP: 240px"

      runat="server"

      BackColor=#cccccc

      Height="26px"

      Width="278px">

   &nbsp;

   <asp:Button  Text="Clear"  OnClick="clear_Form"

      runat="server"  />

   &nbsp;&nbsp;

   <asp:Button  Text="Submit" Font-Bold="true"

       OnClick="store_Form" runat="server" />

</asp:Panel>

The most important thing to note about the event handlers is their signature. The Click event requires an EventArgs type as the second parameter; the Command event requires a CommandEventArgs type.

private void clear_Form(object sender, System.EventArgs e)

{

   if(pnlName.Visible)

   {

      txtFirstName.Text  ="";

      txtLastName.Text   ="";

      txtMiddleName.Text ="";

   } else

   {

      // Clear fields on pnlAddress

   }

}

private void store_Form(object sender, System.EventArgs e)

{

   // Code to verify and store Form

}

Core Note

ASP.NET 2.0 adds an OnClientClick property to the Button, ImageButton, and LinkButton controls, which can be used to execute client-side script. The following code causes a JavaScript function to be executed when the button is clicked.

<asp:Button id=btn text="Client Click" OnClientClick="popup()"

   runat="server" />

Using Panels

The panel declaration specifies its location, size, and appearance attributes such as background color. Controls are placed on the panel by declaring them within the <asp:Panel /> tags. In this example, the text box and label controls are members of the panel's control collection:

<asp:Panel  runat="server"

   style="Z-INDEX: 101; LEFT: 20px;

   POSITION: absolute; TOP: 64px"

   BackColor = "LightGreen" Height="160px" Width="278px">

<TABLE>

   <TR>

      <TD><asp:Label  Runat="server"

              text="First Name:"></asp:Label></TD>

      <TD><asp:TextBox  MaxLength=30

              Runat="server"></asp:TextBox></TD></TR>

   <TR>

      <TD><asp:Label  Runat="server"

              text="Middle Name:"></asp:Label></TD>

      <TD><asp:TextBox  MaxLength=30

              Runat="server"></asp:TextBox></TD></TR>

   <TR>

      <TD><asp:Label  Runat="server"

              text="Last Name:"></asp:Label></TD>

      <TD><asp:TextBox  MaxLength=30

              Runat="server"></asp:TextBox></TD></TR>

</TABLE>

</asp:Panel>

It is interesting to note how ASP.NET tailors the HTML code to capabilities of the client browser. The HTML returned to Internet Explorer renders the panel as a <div> element.

<div  style="background-color:LightGreen;

      height:160px;width:278px;Z-INDEX: 103; LEFT: 20px;

      POSITION: absolute; TOP: 64px">

The Firefox and Netscape browsers receive HTML in which the panel is rendered as a table:

<table  cellpadding="0" cellspacing="0" border="0"

      bgcolor="LightGreen" height="160" width="278"

      style="Z-INDEX: 101; LEFT: 20px; POSITION: absolute;

      TOP: 64px"><tr><td>

Core Note

ASP.NET 2.0 adds a ScrollBars property to the panel control. It can be set to Vertical, Horizontal, or Both. Beware that not all browsers support this feature.

Text Box

With the exception of the MaxLength property that limits the amount of text entered in the control, the text box controls in this example rely on default property values. However, they can be customized easily to present a more meaningful interface. The most useful properties include Width and Column, which both specify the width of the control (Column is the better choice since all browsers recognize it); ReadOnly, which can be used to prevent the user from changing the content; Rows, which specifies the number of rows in a multi-line text box; and TextMode, which indicates whether the text box is SingleLine, MultiLine, or contains a Password. In the latter case, text entered into the text box is masked.

The text box also supports an OnTextChanged property that specifies an event handler method to call when text in the box is changed. However, this is a delayed event that is not processed on the server until a round-trip event occurs. You can prevent the event from being delayed by adding the AutoPostBack = true property to the control's declaration. However, for best performance, the program should be designed to process all control content in one trip.

List Controls

The four ASP.NET list controls—DropDownBox, ListBox, CheckBoxList, and RadioButtonList—provide alternative ways of representing a collection of data. All are data-bound controls that provide a visual interface for an underlying ListItemCollection; and all are derived from the System.Web.UI.WebControls.ListControl class that contributes the properties and methods used to populate the controls with data and detect selected control items (see Figure 16-9).

Figure 16-9. List control display data from underlying ListItem collection

Filling the Control with Data

Individual data items within a list control are represented as a ListItem control. There are several ways to specify the text displayed for an item in the list control. The most common method is by placing text between the opening and closing tags of the ListItem control—referred to as the inner HTML content. You can also use the Text property to specify the text displayed in the list control for the item.

<asp:RadioButtonList  BackColor="LightBlue"

       RepeatColumns=1 RepeatDirection="Vertical" runat="server">

   <asp:ListItem Value="1">Rembrandt />

   <asp:ListItem Value="2">Courbet />

   <asp:ListItem Value="3" Text="Manet" />

   <asp:ListItem Value="4">Degas />

</asp:RadioButtonList>

The ListItem control also exposes a Value property that allows you to associate a value with the item in the list control, in addition to the text displayed in the control. This is often used as a key when retrieving data related to the selected item from a database table.

As mentioned, list controls are data bound, which means they expose a DataSource property that can be set to reference a collection of data. The control's DataBind method is called to load the data from the source into the control. It is possible to bind to many kinds of data sources, such as database tables, hash tables, XML files, and even other controls.

As an example, let's declare a ListBox that will be bound to an array:

<asp:ListBox  runat="server" Rows=4 Width=150px>

</asp:ListBox>

In the script section of the .aspx page, we define an array and bind its contents to the ListBox. The DataBind method copies the data from the array into the control, creating a ListItem collection (see Figure 16-9).

<script  runat="Server">

void Page_Load(object sender, EventArgs e) {

   string[] artists = new string[4] {"Rembrandt","Courbet",

                   "Manet","Degas"};

   //

   if( !this.IsPostBack) {

      // Bind the first time the page is loaded

      ListBox.DataSource = artists;

      ListBox.DataBind();

   }

}

Note that the binding occurs only when the page is loaded the first time. ViewState is used to populate the control on subsequent postbacks.

Selecting an Item

When an item in a list control is selected, the SelectedIndexChanged event occurs. A postback to the server occurs only if the control specifies an event handler to be called, and sets the AutoPostBack attribute to true.

<asp:ListBox  runat="server"

   SelectionMode="Multiple" Rows=4

   AutoPostBack="true"

   OnSelectedIndexChanged="ShowSelections"

   Width=150px>

</asp:ListBox>

The event handler declaration has the familiar parameters of the EventHandler delegate declaration.

public void ShowSelections(Object sender, EventArgs e) {

   Label1.Text="";

   foreach (ListItem item in ListBox.Items)

   {

      if(item.Selected)

      {

         Label1.Text += item.Text + "<br>";

      }

   }

}

The event handler iterates through the items in the ListBox and displays those that are selected. By default, the ListBox permits only one item to be selected, but this can be overridden by setting SelectionMode to Multiple.

A single selected item is available through three properties: SelectedIndex returns the index of a selected item in the list; SelectedItem returns the entire item; and SelectedValue returns the value of the selected item. If there are multiple selected items, these properties return information on the selected item having the lowest index.

The DataList Control

The DataList control makes it easy to display data in a repeating pattern. The idea is to create a template that defines the layout for items it contains. Each "item" is defined as a mixture of visual Web controls and HTML. Item content comes from one or more fields in a data source. When an instance of the DataList control is set to a data source, such as a DataReader, each row of the data source is represented as an item in the DataList. The DataList is processed much like a ListBox control: Items.Count provides the number of items in the control, and the SelectedIndex and SelectedItem properties reference a specific item.

To illustrate the use of this control, let's create a Web page that lists DVDs for sale. The items for sale are displayed in two-column rows as shown in Figure 16-10. The row/column layout is specified in the DataList declaration:

<ASP:DataList  RepeatColumns="2"

   RepeatDirection="Horizontal" OnItemCommand="Item_Command"

   runat="server">

Figure 16-10. DataList control is used to display items in repeated format

[View full size image]

The RepeatColumns property specifies the number of columns displayed, and RepeatDirection indicates the direction in which items are displayed. OnItemCommand specifies the method to be called when a button in the DataList control is clicked. In this example, the ItemCommand event fires when the user adds an item to the cart. Here is an example of the code to handle this event:

private void Item_Command(object source,

       System.Web.UI.WebControls.DataListCommandEventArgs e)

{

   // (1) Crucial: select an item in the DataList

   MyDataList.SelectedIndex = e.Item.ItemIndex;

   // (2) Get the value of a control in the selected item

   string id= ((Label)

        MyDataList.SelectedItem.FindControl("movID")).Text;

}

An item selected in the DataList is indexed by the SelectedIndex property. After an item is selected, the FindControl method can be used to obtain a reference to any control in the item definition. In this case, the value of the Label containing the movie's ID is assigned to a variable.

The ItemTemplate shown in Listing 16-5 describes the appearance of each item. The most important thing to note in this code is the use of DataBinder.Eval to bind to a property or column in the data source. As shown here, the method has two overloads, one of which takes a formatting parameter.

<%# DataBinder.Eval(Container.DataItem, "movie_ID") %>

<%# DataBinder.Eval(Container.DataItem,"r_price", "  {0:c2}") %>

Note that ASP.NET 2.0 offers a simpler, but equivalent version:

<%# Eval("movie_ID") %>

Either construct is replaced by the corresponding data from the data source. It can be displayed directly in the HTML stream or assigned to a control's property.

Listing 16-5. Defining an ItemTemplate for a DataList Control

<ItemTemplate>

   <table cellpadding=10 style="font: 10pt verdana" width=380>

      <tr>

         <td width=1 bgcolor="00000"/>

         <td valign="top">

         <a href=showdvd.aspx?id=<%# DataBinder.Eval(

            Container.DataItem, "movie_ID") %>>

         <img align="top" border=0

            src='./images/<%# DataBinder.Eval(

            Container.DataItem, "movie_ImgID") %>' ></a>

         </td>

         <td valign="top">

         <b>Title: </b><%# DataBinder.Eval(

               Container.DataItem, "movie_title") %><br>

         <b>ID: </b>

            <asp:Label

               Text=<%# DataBinder.Eval(Container.DataItem,

                  "movie_ID") %>

               runat="server" >

            </asp:Label></b><br>

         <b>Year:  </b><%# DataBinder.Eval(

               Container.DataItem, "movie_Year") %><br>

         <b>Price: </b><%# DataBinder.Eval(Container.DataItem,

               "r_price", "  {0:c2}") %><br>

         <%# DataBinder.Eval(Container.DataItem,

               "movie_shortdesc") %> <br>

         <asp:Button  Value="Select"  Text="Add to Cart"

               BackColor="#bda563"

               CommandName="cart"

               runat=server>

         </asp:Button> <br>

         </td>

      </tr>

   </table>

</ItemTemplate>

The easiest part of working with a DataList is binding it to a data source. In this example, we use a DataReader to load rows from a database. To display these rows as items in the control, set the DataSource to the reader and then bind it:

rdr= cmd.ExecuteReader();

MyDataList.DataSource= rdr;

MyDataList.DataBind();

The contents of the DataList can be changed at any time by reassigning and binding a new data source.

Core Note

DataBinder.Eval and Eval use reflection to perform late-bound evaluation. The advantage of this approach is that it can determine the data type (making casting unnecessary) and has a simple formatting syntax. The disadvantage is that it is much slower than an early-bound approach. As an alternative—particularly when formatting is not needed—consider this early-bound syntax:

<%# ((IDataRecord)Container.DataItem)["movie_ID"] %>

IDataRecord casting is used when a DataReader is the data source. Cast with DataRowView when the data source is a DataSet.

In summary, the DataList acts as composite control that can be configured to display data in just about any format. In addition to the ItemTemplate, it supports header, footer, edit, and select templates that can be used to expand its capabilities beyond those described in this section.


16.3. Data Binding and Data Source Controls

Data binding enables the contents of a control to be populated by data from a designated data source. Technically speaking, the data source is any collection of data that implements the IEnumerable interface. In simple cases, the source may be an object such as an Array, ArrayList, Hashtable, or SortedList. More often, the data comes from an ADO.NET DataTable, DataSet, or IDataReader object. The control may bind directly to one of these objects, or indirectly, using the special data source controls introduced with ASP.NET 2.0.

Binding to a DataReader

The data reader provides a forward-only, read-only resultset that is the most efficient way to present data. It is generated by issuing the ExecuteReader method of the IDbCommand object. A control binds to the results by setting its DataSource property to the data reader object and executing its DataBind method.

The example in Listing 16-6 illustrates how a data reader is used to populate a ListBox with movie titles from the Films database (described in Chapter 10). The values displayed in the ListBox are based on the value assigned to the DataTextField property—in this case, the Movie_Title column in the movies table. The DataValueField property specifies an additional column value that is assigned to each item in the ListBox. When an item is selected, this latter value is returned by the SelectedValue property.

Listing 16-6. Data Binding a List Box to a Data Reader

<%@ Page Language="C#" %>

<%@Import namespace="System.Data.SqlClient" %>

<%@Import namespace="System.Data" %>

<html>

<body>

<head><TITLE>Bind Films data to a ListBox</TITLE>

<script  runat="Server">

   void Page_Load(object sender, EventArgs e) {

      if(!this.IsPostBack) {

        getMovies();

      }

   }

   private void getMovies()

   {

      string cnstr=GetConnString(); // Get a connection string

      SqlConnection conn = new SqlConnection(cnstr);

      IDataReader rdr=null;

      IDbCommand cmd = new SqlCommand();

      cmd.Connection= conn;

      conn.Open();

      cmd.Connection=conn;

      cmd.CommandText="SELECT movie_id, movie_title FROM movies

          ORDER BY AFIRank";

      conn.Open();

      rdr = cmd.ExecuteReader();

      // Bind DataReader to ListBox

      ListBoxMovie.DataSource = rdr;

      ListBoxMovie.DataBind();

      conn.Close();

      rdr.Close();

   }

   </script>

   </head>

   <FORM NAME="FORM1" runat=server>

   <asp:ListBox

      dataValueField = "movie_ID"

      dataTextField  = "movie_title"

      AppendDataBoundItems=true

      Rows="10"

      BackColor=#efefe4

      font-size=9pt

      runat="server" >

    <asp:ListItem Value=-1 Text="Select Movie" />

   </asp:ListBox>

</FORM>

</body>

</html>

Observe the presence of the AppendDataBoundItems property in the ListBox declaration. This property, a new feature added by ASP.NET 2.0, provides a way to specify whether data binding should overwrite any preexisting items in a list control. This is particularly useful for placing an entry in a list that precedes the actual data. In the preceding example, the ListBox declaration includes a ListItem that causes Select Movie to be placed in it first row.

Binding to a DataSet

It is often preferable to bind a server control to a data reader, rather than to a DataSet. To understand why, let's compare the two (see Figure 16-11). The data reader connects to a table and streams data into the control when the DataBind method is called. A data set, on the other hand, is a cache in memory that is filled with the resultset when the DataAdapter.Fill method is invoked. Its contents are then copied into the control when the control's DataBind method is called. In a WinForms application, the data set remains available to the application until it is closed; in a Web application, the data set disappears after a reply is sent to the browser. As we discuss in the next chapter, it can be saved as a Session variable, but this requires memory and can lead to scalability problems.

Figure 16-11. DataReader versus DataSet as a control's DataSource

There are a couple of situations in which using a DataSet makes sense. One is when multiple controls are bound to the contents of a DataSet. In this code segment, a data set is built that contains a list of all the movies in a table. Two ListBox controls are then populated with different views of the data. One ListBox contains movies produced prior to 1951, and the other contains those produced on or after that year. The advantage of the DataSet in this case is that only one query is applied against the database.

string sql ="SELECT movie_id, movie_title,movie_year FROM movies

      ORDER BY movie_year";

SqlDataAdapter da = new SqlDataAdapter(sql,conn);

DataSet ds = new DataSet();

da.Fill(ds,"Movies");  // DataSet and DataTable

DataView dview = new DataView(ds.Tables["Movies"]);

dview.RowFilter = "movie_year < 1951";

// List box containing movies before 1951

ListBoxMovie.DataSource= dview;

ListBoxMovie.DataBind();

// List box containing movies produced after 1950

dview.RowFilter = "vendor_name > 1950";

ListBoxMovie2.DataSource= dview;

ListBoxMovie2.DataBind();

A DataSet is also useful when the Web application is designed as a three-tier Web site in which the presentation layer accesses a database through an intermediate data access layer. The data access layer contains a method or methods that return a DataSet in response to the call.

Let's look at how the .aspx file in Listing 16-6 can be converted from its current two-tier structure to a three-tier design. The first step is to remove the getMovies method and place it in a separate assembly. In the following code, the method is part of the DataMethods class and rewritten to return a data set containing the vendor data. This code is compiled into a DLL file and placed in the bin subdirectory below the Web page.

// datalayer.dll – place in \bin subdirectory below application

using System.Data.SqlClient;

using System.Data;

namespace myUtil{

   public class DataMethods{

      public DataSet getMovies(){

        string cnstr= GetConnString(); // Get a connection string

         SqlConnection conn = new SqlConnection(cnstr);

         IDbCommand cmd = new SqlCommand();

         cmd.Connection= conn;

         string sql="SELECT movie_id, movie_title FROM

               movies ORDER BY AFIRank";

         SqlDataAdapter da = new SqlDataAdapter(sql,conn);

         DataSet ds = new DataSet();

         da.Fill(ds,"Movies");

         return (ds);

      }

   }

}

The code changes in the .aspx file are minimal. An @Import directive is added so that the namespace in datalayer.dll can be accessed.

<%@Import namespace="myUtil" %>

The getMovies call is replaced with the following:

if(!this.IsPostBack) {

   DataMethods dm = new DataMethods();

   DataSet ds = dm.getMovies();

   ListBoxMovie.DataSource = ds;

   ListBoxMovie.DataBind();

}

The use of a data presentation layer promotes code reusability, hides the messy ADO.NET connection details, and results in cleaner code.

DataSource Controls

As we have seen, ASP.NET makes it easy to bind a control to data by simply setting the control's DataSource property to the collection of data it is to display. However, it is still up to the developer to assemble the collection of data. For example, using the data reader as a data source requires the following pattern of operations:

1. | Create a data connection.
2. | Create a Command object.
3. | Build a query and use the Command object to retrieve data into a data reader.

Data source controls encapsulate the functionality required to perform these operations—eliminating the need for coding by the developer. The data-bound control is no longer bound to the data collection, but to a data source control. To illustrate, Figure 16-12 shows how a grid can be populated by binding it to a data reader or a SqlDataSource control.

Figure 16-12. Comparison of data binding using ADO.NET code versus DataSource control

Data controls are not limited to database access. In fact, ASP.NET 2.0 supports data source controls that attach to six types of data:

  • AccessDataSource. Binds to a Microsoft Access database.

  • DataSetDataSource. Binds to non-hierarchical XML data.

  • ObjectDataSource. Binds to data through custom classes implemented in a data access layer.

  • SiteMapdataSource. Binds to XML site maps.

  • SqlDataSource. Binds to a SQL database.

  • XmlDataSource. Binds to XML documents.

We'll look at the SqlDataSource, ObjectDataSource, and XmlDataSource controls in this section.

SqlDataSource Control

This control represents a connection to a relational data store, such as SQL Server, DB2, or Oracle. It requires a .NET managed data provider with the capability to return a SQL resultset.

The SqlDataSource control is declared using standard Web control syntax.

Control Declaration:

<asp:sqldatasource  runat="server" docEmphasis">controlID"

Properties:

ConnectionString | Connection string to access database.
ProviderName | Managed provider. Default is SqlClient.
DataSourceMode | Controls how the select command retrieves data. Is a SqlDataSourceMode enumeration: DataSet or DataReader. Default is DataSet.
EnableCaching | True or false. Default is false. Can only be used if DataSourceMode is DataSet.
CacheDuration | How long the contents of the data source aremaintained in memory. Value is in seconds.
SelectCommand | SQL statement that retrieves data from associated data store.
DeleteCommand | SQL statement to delete row(s) from data store.
InsertCommand | SQL statement to insert row(s) into data store.
UpdateCommand | SQL statement to update row(s) in data store.

The four command properties are strings that contain either a SQL command or the name of a stored procedure (if the database supports it) to be executed. Each command can contain parameters whose values are defined by an associated collection of parameters. In an upcoming example, we'll see how the SelectParameters collection is used with SelectCommand. Before that, let's look at how the SqlDataControl is used to populate a Web control.

As in our earlier example, Listing 16-7 fills a ListBox with the name of movies from the Films database. However, in place of raw ADO.NET coding, it defines a data source control and assigns to its SelectCommand property a SQL select string that retrieves a list of movies from the database. A ListBox control is declared with its DataSourceID property set to the ID of the data source control. When the page is loaded, the list control is populated with the resultset.

Listing 16-7. Binding a ListBox to a SqlDataSource Control

<%@ Page Language="C#"  %>

<%@Import namespace="System.Data.SqlClient" %>

<%@Import namespace="System.Data" %>

<HTML>

<HEAD><TITLE>Using a DataSource Control</TITLE>

</HEAD>

<body>

<form  runat="server">

   <asp:sqldatasource runat="server"

      connectionstring="SERVER=(local);DATABASE=FILMS;

      Integrated Security=SSPI; "

      providername = "System.Data.SqlClient"

      selectCommand= "SELECT movie_ID, movie_Title FROM movies

                        ORDER BY AFIRank" >

   </asp:sqldatasource>

   <table border=0>

   <tr><td>

   <asp:ListBox runat="server"

      dataSourceid   = "SqlDataSource1"

      dataValueField = "movie_ID"

      dataTtextField = "movie_Title" />

   </td></tr>

   </table>

</form>

</body>

</html>

Let's extend this example so that when a movie is selected from the list, its cast members are displayed. To do this, we add a GridView, as shown in Figure 16-13.

Figure 16-13. Using data source controls to depict a parent-child relationship

The data for the GridView also comes from a new data source control. The purpose of this control is to dynamically retrieve a list of actors for any movie selected in the ListBox. The challenge is to identify the movie selected and to specify it in the query. Here's how it's done.

The query assigned to the SelectCommand property contains a parameter (@movieID) that serves as a placeholder for the actual movie ID. Within the data source control is another control, ControlParameter, that has the same name as the parameter in our query. It links the data source control to the ListBox via two properties: ControlID, which specifies the ListBox ID, and PropertyName, which specifies the value to assign to the query parameter. When an item in the ListBox is selected, ASP.NET replaces the parameter with the current SelectedValue of ListBoxMovie (which is a movie ID) and executes the query to retrieve the actor data.

<asp:SqlDataSource  RunAt="server"

   connectionstring="SERVER=(local);DATABASE=FILMS;

      Integrated Security=SSPI; "

   SelectCommand= "SELECT actor_first, actor_last, actor_sex FROM

      actor_movie LEFT JOIN actors ON actor_movie.actor_ID=

      actors.actor_ID WHERE movie_ID=@movieID">

      <SelectParameters>

         <asp:ControlParameter Name="movieID"

            ControlID="ListBoxMovie"

            PropertyName="SelectedValue"

         </asp:ControlParameter>

      </SelectParameters>

</asp:SqlDataSource>

The GridView control contains three columns that are bound to the data source control:

<asp:GridView  DataSource

   Width="100%" runat="server" AutoGenerateColumns="false"

   SelectedIndex="0" AutoGenerateSelectButton="true" >

      <Columns>

         <asp:BoundField HeaderText="First Name"

              DataField="actor_first" />

         <asp:BoundField HeaderText="Last Name"

                 DataField="actor_last" />

         <asp:BoundField HeaderText="Sex"

                 DataField="actor_sex" />

      </Columns>

</asp:GridView>

Core Note

In addition to the ControlParameter that specifies the control from which a query's parameter value comes, ASP.NET recognizes five other parameter sources: CookieParameter, FormParameter, ProfileParameter, SessionParameter, and QueryStringParameter.

ObjectDataSource Control

The ObjectDataSource is used when data is retrieved through a data access layer, rather than directly from the database. To demonstrate, recall the three-tier structure we created earlier by placing the getMovies method in a separate assembly. An ObjectDataSource control can be declared to access this method by setting its typename field to the class containing the method and its selectmethod field to the name of the method.

<asp:objectdatasource

   runat="server"

   typename="myUtil.DataMethods"

   selectmethod="getMovies">

</asp:objectdatasource>

This could be used to populate the ListBox control in Listing 16-7 by replacing the SqlDataSource control with the ObjectDataSource control and resetting DataSourceID to ObjectDataSource1 in the ListBox declaration.

It is also worth noting that an ObjectDataSource can be used to fetch data from a Web Service. Because Web Services (described in Chapter 18) are nothing more than classes that expose remotely accessible methods, the typename and selectmethod properties can be used to refer to the class and method as if they were in a local assembly.

XmlDataSource Control

The XmlDataSource control is likely to be the most popular of the data source controls. It reads XML data from a local file or a stream transmitted across a network. It's particularly useful for handling the XML formats that are becoming standards for exchanging information across the Internet. To illustrate, we'll create an example that uses the control to read data in the increasingly popular RSS format.

RSS, which stands for Really Simple Syndication, is an XML formatting standard designed originally for news feeds. However, it is now used as a generic way for Web sites to periodically publish information feeds that can be picked up by RSS readers. It not only provides an easy way to distribute data, but the simple format makes it easy for the recipient to determine when any updates have occurred. Because the data is sent as an XML stream, the XmlDataSource control can play the role of a simple RSS reader. As an example, we pair it with a DataList control to capture and display a sample RSS feed from the BBC news network (see Figure 16-14).

Figure 16-14. Display RSS feed using XmlDataSource and DataList controls

[View full size image]

The underlying XML conforms to the RSS standard (several versions are now in use). At the top level is the <rss> element that contains a required version attribute. Subordinate to it is a single <channel> element that contains a description of the channel along with its content. The content is supplied by <item> elements that have three mandatory subelements: <title>, <link>, and <description>. Their purpose should be clear from the portion of the XML feed shown here:

<rss version="0.91">

   <channel>

      <title>BBC News | Science/Nature | World Edition</title>

      <link>

         http://news.bbc.co.uk/go/click/rss/0.91/public/-

               /2/hi/science/nature/default.stm

      </link>

      <description>Updated every minute of every day</description>

      <item>

         <title>Huge 'star-quake' rocks Milky Way</title>

         <description>

            Astronomers say they are stunned by the explosive

            energy released by a super-dense star on the

            far side of our galaxy.

         </description>

      <link>

         http://news.bbc.co.uk/go/click/rss/0.91/public/-

                 /2/hi/science/nature/4278005.stm

      </link>

      </item>

      <item>

      ... other items go here

   </channel>

</rss>

Listing 16-8 shows how the XML is displayed using only a DataList and XmlDataSource control. The XmlDataSource control identifies the data source with the DataFile property. As mentioned, this can be a file or a URL. The purpose of the XPath property is to set a filter for the XML document so that only a subset of the document is returned. In this case, we are interested in only the <item> data.

The DataList control identifies the data source component it is bound to by setting the DataSourceID property to the component's ID—XmlDataSource1. The XPathBinder object is used to select items or nodes from the XML document. Its XPath method returns a single node, whereas the XPathSelect method returns an ArrayList of matching values. Both take an XPath expression (see Chapter 10) to identify the desired item.

<%# XPath("xpath-expression"[, "format"]) %>

<%# XPathSelect("xpath-expression") %>

Listing 16-8. Displaying RSS Feed with a DataList and XmlDataSource Control

<asp:DataList  Runat="server"

   RepeatColumns=1

   RepeatDirection="Horizontal"

   GridLines="Horizontal"

   BorderWidth="1px" BackColor="White" CellPadding="2"

   BorderStyle="None" BorderColor="#E7E7FF"

   DataSourceID="XmlDataSource1">

   <ItemTemplate>

      <asp:HyperLink  Runat="server"

         Text=<%# XPath("title") %>

         NavigateUrl=<%# XPath("link") %>

         Target="_blank" Font-Names="Sans-Serif"

         Font-Size="X-Small">

      </asp:HyperLink><br/>

      <i><%# XPath("description")%></i><br /><br />

   </ItemTemplate>

   <AlternatingItemStyle BackColor="#F7F7F7">

   </AlternatingItemStyle>

   <ItemStyle ForeColor="#4A3C8C"

      Font-Size=9pt BackColor="#E7E7FF">

   </ItemStyle>

   <HeaderTemplate>BBC RSS Feed: Nature</HeaderTemplate>

   <HeaderStyle ForeColor="#F7F7F7"

      Font-Bold="True" BackColor="#4A3C8C">

   </HeaderStyle>

</asp:DataList>

<asp:XmlDataSource

   Runat="server"

   XPath="rss/channel/item"

   DataFile=

      "http://news.bbc.co.uk/rss/newsonline_world_edition/

      science/nature/rss091.xml">

</asp:XmlDataSource>

By binding a data component to a sophisticated visual data control, we are able to create an application in which all data binding is specified through declarations. This can eliminate the need for code to input data, iterate through it, and parse it into a format that can be displayed. In addition, data components have built-in caching features that improve the efficiency of accessing data and eliminate code-managed caching. We look at caching in the next chapter.


16.4. Validation Controls

One of the more frustrating Internet experiences is to fill in a long form, submit it, and—after a lengthy wait—have it rejected due to an invalid field entry. Any well-designed form should attempt to avoid this by including client-side verification to check fields before the form is submitted. Validation is typically used to ensure that a field is not empty, that a field contains a numeric value only, that a phone number or credit card has the correct format, and that an e-mail address contains the @ character. JavaScript is traditionally used for this purpose.

ASP.NET offers validation controls as a flexible alternative to implementing your own client-side JavaScript functions. The purpose of these controls is to perform a specific type of validation on an associated control. For example, a RequiredFieldValidator control checks an input control to ensure it is not empty.

Table 16-5 lists the six built-in validation controls along with their unique properties and values.

Table 16-5. Validation Controls

Control

Properties

Description and Possible Values

RequiredField

Validator

Checks whether input control field contains a value.

CompareValidator

Compares the value of an input control with a constant or other control.

Operator

Has value of:

Equal, NotEqual, GreaterThan, GreaterThanEqual, LessThan, LessThanEqual, DataTypeCheck

Type

Currency, Date, Double, Integer, String.

ValueToCompare

Constant value used for comparison.

ControlToCompare

Other control to compare value with.

RangeValidator

Checks the value of the input control against a range of values.

MaximumValue

Constant value that represents upper value.

MinimumValue

Constant value that represents lowest value.

Type

Currency, Date, Double, Integer, String.

RegularExpression

Validator

Matches the value of the input control against a regular expression.

ValidationExpression

Regex to match input control's value against.

CustomValidator

Checks a field's value against custom validation logic.

ClientValidationFunction

JavaScript client-side function to perform validation.

OnServerValidate

Server-side method to perform validation.

ValidationSummary

Collects and lists all the error messages from the form validation process.

DisplayMode

Format of error messages:

BulletList, List, SingleParagraph

HeaderText

Title for error message summary.

ShowMessageBox

Display errors in pop-up box: true or false.

ShowSummary

Display errors on control: TRue or false.

Only the final control in the table, ValidationSummary, does not perform a validation. Instead, it displays a summary of the errors generated by the other validation controls.

Using Validation Controls

Validation controls are used only with controls that have a single input field type. These include the HTMLInputText, HTMLSelect, TextBox, DropDownList, and ListBox controls. ASP.NET implements the validation function on both the server and client side. To handle the client side, it includes a .js file containing validation code in the response to the browser. Note, however, that if scripting is disabled on the browser, only client-side checking will occur.

To illustrate the mechanics of using a validation control, here is the code to associate a RequiredFieldValidator and RangeValidator control with a TextBox control. The range validator control uses its MaximumValue and MinimumValue properties to ensure that the value entered in the text box is a numeric value from 0 to 12.

<td><asp:TextBox Width=30 id=hti maxlength=2

   runat=server></td>

<asp:RequiredFieldValidator  runat=server

   ControlToValidate="hti"

   ErrorMessage="Must enter height value."

   Display="dynamic">

</asp:RequiredFieldValidator>

<asp:RangeValidator

   ControlToValidate="hti"

   MaximumValue="12"

   MinimumValue="0"

   Type="Integer"

   Display="dynamic "

   ForeColor="Blue"

   ErrorMessage="Invalid Height."

   runat=server>

</asp:RangeValidator>

This example also illustrates useful properties that all validator controls inherit from the BaseValidator class:

  • ControlToValidate. Set to the identifier of the control to be validated.

  • Display. static, dynamic, or none. This specifies how the error message takes up space on the form: static reserves space and makes the message visible when it is needed; dynamic uses no space on the form until it is displayed; none is used when the error message is to be displayed by the ValidationSummary control.

  • ForeColor. Sets the color of the error message text. Red is the default.

  • ErrorMessage. The message displayed when a validation exception is detected.

  • ValidationGroup. A string value that enables validation controls to be grouped by assigning the same value to this property. When a Button control that has its ValidationGroup property set to a group value submits a form, only controls with validators in the group are validated. This allows sections of a page to be validated separately. This property is introduced in ASP.NET 2.0.

Of the controls listed in Table 16-4, the CustomValidator and ValidationSummary exhibit unique behavior that requires further explanation.

CustomValidator Control

There are many common validation patterns that the built-in validation controls do not handle. For example, the contents of one control may be dependent on another, such as when a credit card number format depends on the type of card selected. Another common example is to require that a string entered in a field conform to a minimum and maximum length. Cases such as these are handled with a CustomValidator control that points to server-side and/or client-side routines that implement custom validation logic. To demonstrate how this control works, let's use it to validate a field that accepts a password, which must be between 8 and 12 characters in length.

The declaration of the control is similar to that of the other validation controls. The main difference is the ClientValidationFunction and OnServerValidate fields that specify client and server routines to validate the associated password field.

<input type=password  runat="server" />

<br>

<asp:CustomValidator

   ControlToValidate="pw"

   ClientValidationFunction="checkPWClient"

   OnServerValidate="checkPW"

   Display=dynamic

   ErrorMessage="A password must be between 8 and 12 characters."

   runat="server"/>

The validation routines contain identical logic. The client side is written in JavaScript and the server side in C#. They are contained in separate <script/> blocks in the .aspx file.

<script language=javascript>

<!—

   // Client side function to check field length

   function checkPWClient(source, args)

   {

      var pw = args.Value;

      var ln= pw.length;

      args.IsValid=true;

      if(ln <8 || ln > 12) args.IsValid=false

   }

//-->

</script>

<script Language="C#"  runat="Server">

private void checkPW(object source, ServerValidateEventArgs args){

   if(args.Value.Length<8 || args.Value.Length>12)

         {args.IsValid=false;

   } else{

      args.IsValid=true;

   }

}

</script>

Two parameters are passed to the validation routines: source and args. Args is the more important of the two. Its value property exposes the content of the form field being validated. In this example, the length of the field value is checked. If it falls within the bounds, IsValid is set to true; otherwise, it is set to false. A false value triggers the error message defined by the CustomValidator control.

For consistency with the built-in validation controls, include both server- and client-side routines for custom validation. If only one is to be implemented, server side is always preferred.

ValidationSummary Control

This control collects all of the error messages generated by the validation controls and displays them as a list or customizable paragraph. The messages are displayed either within the control (as a <span> element within the HTML) or as a pop-up window by setting the ShowMessageBox to true.

<asp:ValidationSummary id=validsumm runat="server"

   ShowMessageBox=true

   DisplayMode=List

   ShowSummary=false>

</asp:ValidationSummary>

The most important factor to consider when deciding how to display validation error messages is that the ValidationSummary control displays messages when a form is submitted, and individual validation controls display a message when their associated control loses focus. Thus, displaying the message at a validation control provides immediate feedback to the user. Also note that if the validation control has its Display property set to static or dynamic, the error message is displayed by both the validation control and the ValidationSummary control.


16.5. Master and Content Pages

A principal design objective when creating a multi-page Web site is visual and functional consistency. Headers and footers should look the same, and the layout and use of controls should be similar from page to page. One way to achieve this is to drag and drop common controls on to each new Web page; but in many cases, a better approach is to create a template from which new pages can be derived. In Windows Forms programming, a form can be filled with controls and used as a base class to create other interfaces. ASP.NET has a similar feature known as master pages. The idea is to create a template, or master page, containing visual elements that will be common to other pages, as well as placeholders that will be filled in with the unique content from other pages. Pages that provide content for the master page are referred to as content pages.

The major advantages to this approach are that any changes made to a master page are automatically reflected in the content pages and that the act of creating content pages is limited to providing statements that specify the content to be associated with the placeholders in the master page.

To illustrate the fundamentals of using master pages, we'll create a master page that defines the layout for the Web page shown in Figure 16-15. In addition, we'll create two content pages that correspond to the menu items shown on the left side of the page.

Figure 16-15. Web page derived from a master page

Creating a Master Page

Master pages are so similar to regular .aspx pages that converting an .aspx page to a master page requires only three changes: the file must have the .master extension; the @Page directive is replaced with the @Master directive; and one or more ContentPlaceHolder controls are added to serve as containers that will be filled at runtime with content from content pages.

Listing 16-9 shows the master page used by content pages to create the Web page in Figure 16-15. The two menu items are implemented as HyperLink controls that reference the two content pages. Two ContentPlaceHolder server controls designate the area in the page where a content page's title and main body are placed.

Listing 16-9. Master Page Definition—shell.master

<%@ Master  %>

<html>

<head>

   <title>Master Page for Shell Design Studio</title>

</head>

<body bgcolor=#ffffff link=#ffffff alink=#ffffff vlink=#ffffff>

<FORM  runat="server">

<table width="500" cellpadding="0" cellspacing="0">

   <tr><td bgcolor=black align=center>

         <img src=./images/sdslogo.gif>

      </td><td>&nbsp;&nbsp;

      </td><td>

         <font size=4 face=Verdana> <b>SHELL</b> DESIGN STUDIO

      </td></tr>

   <tr>

      <td width=120 height=300 bgcolor=red valign=top>

         <asp:HyperLink  NavigateUrl="home.aspx"

            Text="Home Page"

            Font-Bold="true" Font-Size=9pt

            Font-Names="Verdana"

            runat=server />

         <br><br>

         <asp:HyperLink  NavigateUrl="clients.aspx"

            Text="Our Clients"

            Font-Bold="true" Font-Size=9pt

            Font-Names="Verdana"

            runat=server />

      </td>

      <td>&nbsp;&nbsp;</td>

      <td valign=top>

         <hr size=1 color=red>

         <asp:contentplaceholder  runat="Server">

            <b>Introduction</b>

         </asp:contentplaceholder>

         <hr size=1 color=red>

         <asp:contentplaceholder  runat="Server">

            This is Default Content to be overridden

            by content pages

         </asp:contentplaceholder>

      </td></tr>

</table>

</FORM>

</body>

</html>

Creating a Content Page

A content page is an .aspx file containing <asp:Content> tags (instances of Content controls) that override corresponding <asp:contentplaceholder> tags in the master page. The ContentPlaceHolderID property of the content tag matches the ID of the placeholder where the content is to be inserted. The home.aspx content page in our example illustrates this. It contains two content tags that define the content for the Header and PageBody placeholders, respectively. The masterpagefile attribute specifies the master page from which this page inherits.

[home.aspx]

<%@ Page language="C#" masterpagefile=~/shell.master %>

<asp:content  runat="server"

   contentplaceholderdocEmphStrong">Header">

   <font size=3 face=Verdana> <b>Introduction </b>

</asp:content>

<asp:content  runat="server"

   contentplaceholderdocEmphStrong">PageBody">

   <font face=Verdana size=2>

   Shell Design Studios specializes in interior decorating for

   homes and offices. Our staff contains experts in art, color

   theory, architectural design and home technology.

</asp:content>

The content may consist of any combination of standard HTML markup code, images, managed code, and server controls. In this example, the MainBody placeholder is replaced with literal text for the home.aspx page and a list of clients—using the <UL> tag—for the clients.aspx content page.

[clients.aspx]

<%@ Page language="C#" masterpagefile=~/shell.master %>

<asp:content  runat="server"

   contentplaceholder>

   <font size=3 face=Verdana> <b>Our Clients </b>

</asp:content>

<asp:content  runat="server"

   contentplaceholder>

   <font face=Verdana size=2>

   <ul>

      <li>Swanburg Medical </li>

      <li>Lombard & Gable Law </li>

      <li>Coble Architectural Design</li>

   </ul>

</asp:content>

There are only a few commonsense rules to keep in mind when using master/content pages:

  • A content page does not have to provide content for all placeholders. When content is not mapped to a placeholder, its default value is used.

  • Content may include ASP.NET server controls. However, controls cannot be placed outside of the content tags.

  • A placeholder's ID is unique. You cannot, for example, map the same content to two sections in a master page.

Accessing the Master Page from a Content Page

Recall that .aspx files are compiled at runtime into (System.UI.Web) Page objects. The Page object serves as the naming container for all objects on the page. When a master page is involved, one of the objects included is a MasterPage object that, in turn, serves as a container for the ContentPlaceHolder and Content objects. This hierarchy of objects comprises the Web page that is rendered to the client.

To support master pages, the Page object includes a Master property that can be used to reference objects in a master page's control collection. Content pages have access to this property and can thus alter the appearance of the master page template when they are loaded. As an example of how this can be used, consider the menu on our sample Web page (see Figure 16-15). The items have the same appearance no matter which page is loaded. As a rule, Web pages should distinguish the menu item for the currently loaded page from the other items. One popular technique is to highlight it. This requires adding only a few lines of code to our content files:

<%@ Import namespace=System.Drawing %>

<script runat="Server">

// Highlight link for home.aspx page

void Page_Load(object sender, EventArgs e)

{

   // Change color of link to indicate current page

   HyperLink h = (HyperLink)Master.FindControl("homepage");

   h.BackColor=Color.Goldenrod;  // highlight menu item

}

</script>

The Master.FindControl method is used to reference the link pointing to the current page. In this case, the returned object's BackColor property is set to highlight the link.


16.6. Building and Using Custom Web Controls

This section provides an overview of how to write and consume a custom Web control. Its objective is twofold: to introduce the fundamentals of implementing a control and, in the process, provide insight into the architecture of the intrinsic .NET controls.

In its simplest form, a custom control is a class that inherits from the System.Web.UI.Control class, implements (overrides) a Render method, and uses its HtmlTextWriter parameter to emit the HTML code that represents the control. Add some public properties to define the control's behavior, and you have a custom control that functions like the built-in ASP.NET controls. More complex controls may require features such as data binding and caching support, which are not covered in this chapter. For a full understanding of those topics, refer to a good ASP.NET book.[1]

[1] Essential ASP.NET by Fritz Onion (Addison-Wesley, 2003) is a good choice.

A Custom Control Example

Listing 16-10 defines a custom control that we will use to illustrate the basics of control creation. The purpose of this control is to display a large or small version of a company logo, along with the company's name. Two properties determine its appearance and behavior: LogoType takes a value of small or large to indicate the size of the image to be displayed, and Link specifies the URL to navigate to when the small logo is clicked.

Listing 16-10. A Custom Control—logocontrol.cs

using System;

using System.Web;

using System.Web.UI;

namespace CompanyControls

{

   // (1) Inherit from the System.Web.UI.Control class

   public class CompanyLogo : Control

   {

      // Custom control to display large or small company logo

      private string logo_sz;   // "small" or "large"

      // Page to go to when logo is clicked

      private string myLink;

      public string LogoType

      {

         get {return logo_sz;}

         set {logo_sz = value;}

      }

      public string Link

      {

         get {return myLink;}

         set {myLink = value;}

      }

      // (2) Override the Render method

      protected override void Render(HtmlTextWriter output)

      {

         // (3) Emit HTML to the browser

        if (LogoType == "large"){

            output.Write("<a href="+Link+">");

            output.Write("<img src=./images/logo_big.gif

               align=middle border=0>");

            output.WriteLine("</a>");

            output.Write("&nbsp;&nbsp;");

            output.Write("<b style=font-style:24;");

            output.Write("font-family:arial;color:#333333;>");

            output.Write("STC Software</b>");

         } else {

            output.Write("<a href="+Link+">");

            output.Write("<img src=./images/logo_small.gif

              align=middle border=0>");

            output.WriteLine("</a>");

            output.Write<br>");

            output.Write("<b style=font-style:12;");

            output.Write("font-family:arial;color:#333333;>");

            output.Write("Shell Design Studio</b>");

         }

      }

   }

}

Let's examine the three distinguishing features of a custom control class:

  1. Inherits from System.Web.UI.Control.

    This class provides properties, methods, and events that the custom control requires. The most important of these is the Render method that we describe next. Other members include the Page events (Init, Load, PreRender, Unload) and members for managing child controls.

    If you are using Visual Studo.NET for control development, the base class will be System.Web.UI.WebControls.WebControl. This class derives from the Control class and adds several members, most of which affect appearance.

  2. Overrides the Render method.

    Each control must implement this method to generate the HTML that represents the control to the browser. Note that the Render method is not called directly; instead, a call is made to RenderControl, which then invokes Render. For controls that contain child controls, the RenderChildren method is available. This is called automatically by the Render method, and an implementation that overrides this method should include a base.Render() call if the control contains child controls.

  3. Uses HtmlTextWriter object to generate HTML code.

    This example uses the HtmlTextWriter.Write method to generate HTML for the control. This is the simplest approach, but HtmlTextWriter offers several other methods that you may prefer. One alternative is to use a set of helper methods that eliminate writing full literal strings.

//Following yields: <table border=0>

Output.WriteBeginTag("table")

Output.WriteAttribute("border","0");

Output.WriteEndTag("table")

A third approach uses "stack-based" methods to render code. It uses AddAttribute methods to define attributes for a tag that is then created with a RenderBeginTag method call and closed with a RenderEndTag call. Although the approach is verbose, it has the advantage of automatically detecting which version of HTML a browser supports and emitting code for that version.

The following code generates the same HTML as is in Listing 16-7 for the large image. It relies on a mixture of HtmlTextWriter methods and special tag, attribute, and style enumerations. Refer to the documentation of HtmlTextWriter for the lengthy list of methods and enumerations.

output.AddAttribute(HtmlTextWriterAttribute.Href,Link);

output.RenderBeginTag(HtmlTextWriterTag.A);  // <a

output.AddAttribute(HtmlTextWriterAttribute.Src,bgImg);

output.AddAttribute(HtmlTextWriterAttribute.Align,"middle");

output.AddAttribute(HtmlTextWriterAttribute.Border,"0");

output.RenderBeginTag(HtmlTextWriterTag.Img);

output.RenderEndTag();

output.RenderEndTag();                       // </a>

output.Write (" &nbsp;&nbsp;");

output.AddStyleAttribute(HtmlTextWriterStyle.FontSize,"24");

output.AddStyleAttribute(HtmlTextWriterStyle.FontFamily,"arial");

output.AddStyleAttribute(HtmlTextWriterStyle.Color,"#333333");

output.RenderBeginTag(HtmlTextWriterTag.B);

output.Write("Shell Design Studio");

output.RenderEndTag();

Using a Custom Control

The key to using a custom control is the @Register directive, which was discussed in Section 16.1. Its Assembly and Namespace attributes identify the assembly and namespace of the custom control. Its TagPrefix attribute notifies the ASP.NET runtime that any tag containing this prefix value refers to the control specified in the directive. Here is a Web page that includes the custom CompanyLogo control:

<%@ Page Language="C#" %>

<%@ Register Namespace="CompanyControls" TagPrefix="logo"

   Assembly="logocontrol"   %>

<script runat="server">

   protected void SendPage(object src, EventArgs e)

   {

      // Process page here.

   }

</script>

<html>

<body>

   <logo:CompanyLogo runat="server"

      Link="products.aspx"

      LogoType="large"/>

   <hr>

   <font size=2 face=arial color=black><center>

   This page contains ways to contact us

<br>

   <asp:Button runat="server" text="submit"

      OnClick="SendPage" />

</body>

</html>

The control that we have created behaves similarly to the built-in Web controls, but lacks one important feature that they all have: the capability to maintain its state during a postback operation.

Control State Management

Let's change the preceding code to set the LogoType property when the page is first loaded, rather than within the body of the code. We use the IsPostBack property for this purpose:

protected void Page_Load(object src, EventArgs e)

{

   if (!IsPostBack) {

      lgc.LogoType="large";

   }

}

On the initial request, LogoType is set and the page is returned with a large image. However, subsequent postbacks result in the small image being displayed because the value is not retained, and the code defaults to the small image. Recall that state is maintained between postbacks in the hidden _VIEWSTATE field. This field contains the values of the ViewState collection, so the secret to state control is to place values in this collection. It operates like a hash table—accepting name/value pairs—and is accessible by any control. The following code demonstrates how property values are placed in ViewState as a replacement for the simple fields used in Figure 16-7.

public class CompanyLogo : Control

{

   // Custom control to display large or small company logo

   public CompanyLogo() {

      ViewState["logo_sz"] = "small";

      ViewState["myLink"] = "";

   }

   public string LogoType

   {

      get {return (string) ViewState["logo_sz"]; }

      set {ViewState["logo_sz"]= value;  }

   }

   public string Link

   {

      get {return (string) ViewState["myLink"]; }

      set {ViewState["myLink"]= value;  }

   }

   // Rest of class code is here...

The property values are now maintained in the _VIEWSTATE field and persist between postbacks.

Composite Controls

At the beginning of the chapter, we created a Web page (refer to Figure 16-2) that calculates the Body Mass Index. This calculator consists of text boxes, labels, and a button. To turn this into a custom control, we could take our previous approach and override the Render method with a lengthy list of statements to generate the appropriate HTML. In addition, special code would have to be added to maintain the state of the control during postback operations. A better solution is to create a custom composite control.

A composite control is created from existing controls. Its advantage is that these controls, referred to as child controls, are very low maintenance. They render themselves—eliminating the need to override Render—and they maintain their state during postbacks. In addition, they let you program with familiar objects and their members, rather than output statements.

There are two major differences in the code used for the "from scratch" custom class in our preceding example and that of a composite control:

  • The composite does not have to override the Control.Render method to display controls. Instead, it must override the Control.CreateChildControls method to add existing controls to the collection of controls making up the composite control.

  • The custom control class should inherit from the INamingContainer interface. Its purpose is to indicate to ASP.NET that child controls exist and they should be placed in a separate namespace. This prevents name collision problems when a page contains more than one composite control.

Listing 16-11 contains the code to implement the BMI calculator as a composite control. The calculator comprises three text boxes, a label to display the result, and a button to invoke a method to perform the calculation. Most of the code of interest is in the overridden CreateChildControls method. It adds the standard controls to the collection, and uses LiteralControl to add HTML and descriptive information that helps format the control. To simplify the listing, code validation is included on only one control.

Listing 16-11. A Composite Control—bmicompos.cs

using System;

using System.Web;

using System.Web.UI;

using System.Web.UI.WebControls;

namespace CompositionControls {

   public class BMIComposition : Control, INamingContainer {

       TextBox htf;

       TextBox hti;

       TextBox wt;

       Label bmi;

   private void getBMI(object sender, System.EventArgs e)

   {

      if (Page.IsValid) {

         decimal f =  Convert.ToDecimal(htf.Text);

         decimal inch = Convert.ToDecimal(hti.Text);

         decimal w = Convert.ToDecimal(wt.Text);

         decimal totinches = f * 12 + inch;

         decimal h2 = totinches * totinches;

         decimal massIndex = (w * 703 * 10/ h2)/10;

         bmi.Text = massIndex.ToString("##.##");

      }

   }

   protected override void CreateChildControls() {

      htf = new TextBox();

      hti = new TextBox();

      wt  = new TextBox();

      bmi = new Label();

      bmi.Width= 50;

      bmi.BorderStyle= BorderStyle.Solid;

      bmi.BorderWidth=2;

      htf.Width= 30;

      hti.Width= 30;

      hti.ID = "hti";

      wt.Width = 40;

      // Display calculator interface

      Controls.Add(new LiteralControl

              ("&nbsp;&nbsp;<b>BMI Calculator</b>"));

      Controls.Add(new LiteralControl

              ("<br>BMI: &nbsp;&nbsp;"));

      Controls.Add(bmi);

      Controls.Add(new LiteralControl("<br>Height: &nbsp;"));

      Controls.Add(htf);

      Controls.Add(new LiteralControl("&nbsp;&nbsp"));

      Controls.Add(hti);

      Controls.Add(new LiteralControl(" (feet/inches)"));

      Controls.Add(new LiteralControl("<br>Weight: "));

      Controls.Add(wt);

      Controls.Add(new LiteralControl("<br>"));

      // Validation control for inches accepted

      RangeValidator rv = new RangeValidator();

      rv.ControlToValidate="hti";

      rv.MaximumValue="12";

      rv.MinimumValue="0";

      rv.Type=ValidationDataType.Integer;

      rv.ErrorMessage="Inches must be 1-12";

      Controls.Add(rv);

      // Button to invoke BMI calculation routine

      Button calcBMI = new Button();

      calcBMI.Text = "Submit Form";

      calcBMI.Click += new EventHandler(this.getBMI);

      this.Controls.Add(calcBMI);

      }

   }

}

Note that getBMI performs the BMI calculation only if the Page.IsValid property is TRue. Include this check when validation controls are used, because server-side validation sets the IsValid flag to false if validation tests fail, but does not prevent code execution.

To make the control available, compile it and place it in the \bin directory of the Web page.

csc /t:library bmicompos.cs

The control is included in a page using the @Register directive as described earlier:

<%@ Register Namespace="CompositionControls" TagPrefix="bmicon"

   Assembly="bmicompos" %>

<HTML>

   <table border=0 color=#cccccc>

   <tr><td>

      <bmicon:BMIComposition runat="server"  />

   </td></tr></table>

Figure 16-16. BMI composite control

    Previous Section < Day Day Up > Next Section


16.7. Selecting a Web Control to Display Data

ASP.NET developers have an overflowing toolbox of controls to choose from—many with overlapping functionality. Table 16-6 offers some recommendations for selecting a control, or combination of controls, to handle frequently encountered Web page tasks. Some of the suggested controls have been introduced in this chapter; but for other important ones, such as the GridView, DetailsView, and FormView, you should refer to an in-depth ASP.NET 2.0 book or reference source.

Table 16-6. Controls Recommended for Typical Web Page Applications

Page Requirement

Controls

Display multiple data records in a spreadsheet or grid format.

GridView. Provides sorting, pagination, and editing. Permits controls to be embedded in cells. Performance can be slow, so use only when these features are required. If displaying data is the only objective, use the DataList or Repeater.

Display multiple data records in a custom format using simple controls.

DataList or Repeater. Use the DataList if editing and event handling is required. Use the Repeater strictly to display data in a repeated format.

Display a parent-child relationship between data.

ListBox or DropDownList and a GridView. See Figure 16-13 on page 779.

Display a hierarchical view, such as a directory or XML elements.

treeView. Allows nodes to be expanded to display data on lower level.

View, edit, or delete data, one record at a time.

DetailsView or FormView.

Web page with multiple sections or one that steps through a process in multiple steps.

Use multiple panels as containers for simple controls. Panels in ASP.NET 2.0 include a scrolling capability for IE browsers.


16.8. Summary

ASP.NET is a development platform that offers a variety of techniques to overcome the inherent problems that plague client-server interaction over the Internet. These problems include browser incompatibility, the difficulty of retaining an application's state between requests, and a reliance on interpreted script rather than compiled code to implement program logic.

The ASP.NET answer to these problems is server-side based model whose code is written in C# or VB.NET. A Web page is a class deriving from the Page class. Controls are all classes that implement a Render method to generate the HTML code that represents them in the browser. An important design feature of this model is the ability to separate presentation logic from the business logic by placing the latter in a code-behind file.

Controls fall into two categories: HTML server controls and Web controls. The former correspond to traditional HTML tags, but include a runat=server attribute that indicates they are run on the server. Web controls are much richer than HTML controls. They include list controls; data display controls—DataList and GridView—that are usually bound to data sources; and validation controls, which are helper controls that validate the content of other controls. If none of these controls meet an application's need, custom controls can be designed that extend existing controls or present a new interface.


16.9. Test Your Understanding

1:

What are the two standard HTTP methods of sending data to a Web host? Which does ASP.NET use as its default?

2:

Indicate whether the following statements are true or false.

  1. All controls in ASP.NET are classes.

  2. An .aspx page must be compiled before it can respond to a request.

  3. A user must have the .NET runtime installed to take advantage of ASP.NET Web pages.

  4. ASP.NET may render different HTML code for different browsers.

  5. A Web page may contain both HTML Server and Web controls.

3:

Which collection class contains the data for List controls?

4:

You set up an event handler to be called when the contents of a TextBox change. If the contents are changed, when is the event handler code executed?

5:

Which property enables the controls on a master page to be programmatically accessed?

6:

Which property must be set and which method executed to bind a control to a data source?

7:

Indicate whether the following statements are true or false with regard to a data-bound control.

  1. It can bind directly to a DataAdapter.

  2. It can bind directly to a DataReader.

  3. The control is populated with data when its Fill method is invoked.

  4. Its DataSource property can specify a DataSet or a data source control.

8:

You have a form containing a text box field that is used to input a phone number. Which control would you use to ensure that the phone number includes an area code?

9:

Which directive must a Web page include to use a custom control on the page?

  1.
@Page

  2.
@Import

  3.
@Register

  4.
@Assembly

10:

What role does the HtmlTextWriter class play in the implementation of a custom control?

11:

What are the advantages of creating a composite control versus a non-composite custom control?


Chapter 17. The Asp.Net Application Environment

Topics in This Chapter

  • HTTP Request and Response Objects: These ASP.NET classes correspond closely to the HTTP request and response specifications, and expose information about a request or response through class properties.

  • Configuration Files: Configuration files provide an easy way to specify the behavior and performance of a Web application. Focus is on the web.config file.

  • ASP.NET Security: Forms Authentication is a platform-neutral technique that can be used to control who can access a Web page. An example demonstrates the use of Forms Authentication to manage authentication and authorization.

  • State Management: Both the Application and Session classes can be used to maintain information during a Web session or during the life of a Web application.

  • Caching: Data caching and output caching can be used to improve Web application performance.

  • Accessing Web Resources: The WebRequest and WebResponse classes provide a simple way to request and process Web pages for the purpose of extracting page content.

  • HTTP Pipeline: The HTTP pipeline is the combination of events, HTTP modules, and HTTP handlers that affect how requests and responses are handled as they pass between a client and server.

Chapter 16, "ASP.NET Web Forms and Controls," dealt with the most visible aspect of ASP.NET—the Web page. It described a Web application in terms of the controls, HTML, and compilable C# code that comprise it. Much of the emphasis was on how to construct the interface presented to a client. This chapter shifts the focus to the underlying details of ASP.NET support for the Hypertext Transfer Protocol (HTTP), which defines how Web requests and responses are transported between client and host.

You'll find that a discussion of the ASP.NET environment is heavily tilted toward those issues that fall under the responsibility of the Web server—as opposed to a Web client. This is only natural because one of the purposes of ASP.NET is to enable developers and architects to manage the difficult side of Web performance, while allowing Web clients to remain blissfully thin and unaffected.

The chapter begins with a little background information on the structure of a response and request—as defined by the HTTP standard. It shows how the HttpRequest and HttpResponse objects serve as proxies that expose and extend the information you'll find in the standard HTTP message structure. Next, the role of configuration files is discussed—a significant role that allows XML elements to be used to manage the behavior and performance of Web applications at an application, domain, and machine level. Controlling who can gain access to a Web page or resource is the next topic. Included is an overview of the .NET options for user authentication and authorization, as well as a detailed example of forms authentication.

One of the challenges of designing a Web application is determining how to maintain and manage state information. A discussion of how to persist data between Web requests includes examples of using both Session and Application objects to store information. ASP.NET also offers data and output caching as a way to store state information or to buffer frequently used data or Web pages. When used correctly, caching can greatly improve Web performance. Its advantages and disadvantages are considered.

The client side is not totally abandoned. The penultimate section demonstrates how to access Web resources using the WebRequest and WebResponse classes. An example uses an HttpWebRequest object to retrieve a Web page and glean information about the server.

The final section discusses the HTTP pipeline—a metaphor for the series of internal events that occur along the roundtrip journey that a request and response travel.


17.1. HTTP Request and Response Classes

HTTP specifications[1] concisely define messages as "requests from client to server and responses from server to client." At its most elemental level, the role of ASP.NET is to enable server-based applications to handle HTTP messages. The primary way it does this is through HTTPRequest and HttpResponse classes. The request class contains the HTTP values sent by a client during a Web request; the response class contains the values returned to the client.

[1] RFC 2616—Hypertext Transport Protocol—HTTP/1.1

HttpRequest Object

An HttpRequest object is available to a Web application via the Page.Request or Context.Request property. The object's properties represent the way that .NET chooses to expose the content to an underlying HTTP request message. Consequently, the best way to understand HttpRequest is to examine the layout of the message it represents.

HTTP Request Message Structure

Figure 17-1 represents the general structure of the HTTP request message as defined by the HTTP/1.1 specifications.

Figure 17-1. Structure of a request message

Unless you are writing a browser, it is not necessary to understand the full details of the standard. However, a general understanding is useful to a developer who needs to extract information from the HttpRequest object. A few observations:

  • The Request-Line consists of three parts: the method token, the Request-URI, and the protocol version. Several methods are available, the most common being the POST and GET methods described in Chapter 16. The Uniform Resource Identifier (URI) specifies the resource being requested. This most commonly takes the form of a Uniform Resource Locator (URL), but can be a file or other resource. The protocol version closes out the Request-Line.

  • The general-header is used to pass fields that apply to both requests and responses. The most important of these is the cache-control field that specifies, among other things, what can be cached and how long it can be cached.

  • The request-header is used by the client to pass additional information about the request, as well as the client. Most of the HttpRequest object's properties correspond to values in this header.

Viewing the Request Message

For debugging—or simply out of curiosity—you may want to view the raw contents of the HTTP request. A simple way to do this is to use the SaveAs method of the HttpRequest class to write the request to a file where it can be viewed with a text editor. The method, as shown here, takes two parameters: the name of the output file and a bool type switch that indicates whether HTTP headers are to be included in the output.

this.Request.SaveAs("c:\\myrequest.txt",true);

Posting a form containing two text boxes to the Web server generates this sample output. Of most interest are the browser description, referring Web page, and text box content values.

POST /ideas/panel.aspx HTTP/1.1

Cache-Control: no-cache

Connection: Keep-Alive

Content-Length: 158

Content-Type: application/x-www-form-urlencoded

Accept: image/gif, image/x-xbitmap, image/jpeg, */*

Accept-Encoding: gzip, deflate

Accept-Language: en-us

Cookie: ASP.NET_SessionId=uszrfs45z4f20y45v0wyyp45

Host: localhost

Referer: http://localhost/ideas/panel.aspx

User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0;

.NET CLR 1.0.3705; .NET CLR 1.1.4322; .NET CLR 2.0.40607)

__VIEWSTATE=%2FwEPDwULLTEwODExMTgwOTAPZBYCA ...

   &txtFirstName=joanna&txtLastName=larson&btnSubmit=Submit

HttpRequest Class Structure

Table 17-1 summarizes selected properties of the HttpRequest class. Where applicable, it includes the underlying header and message field on which its content is based.

Table 17-1. HttpRequest Properties

HttpRequest Property

Request Message Field

Description

AcceptTypes

—

String array of client-supported MIME accept types.

Browser

(request-header) User-Agent

Identifies software program making request.

ClientCertificate

—

Returns the client's security certificate as an HttpClientCertificate object. Used with SSL when a client is configured with a personal certificate.

ContentEncoding

(entity-header) Content-Type

Character set of the entity body.

ContentLength

(entity-header) Content-Length

Size of client request.

ContentType

(entity-header) Content-Type

MIME content type of request.

Cookies

Not defined in HTTP protocol

Collection of client's cookie variables.

FilePath

Request-Line URI

The virtual path of the currently requested page.

Files

—

Files uploaded by the client.

Form

Body

The collection of form variables including ViewState.

Headers

general and request-header fields

Collection containing content of headers contained in request message.

HttpMethod

Request-Line method

HTTP data transfer method.

IsAuthenticated

(request-header) Authorization

Has user been authenticated. true or False.

IsSecureConnection

Request-Line

True if HTTPS used.

Path

Request-Line URI

Virtual path of current request.

PhysicalPath

—

Physical path of current page.

QueryString

Request-Line URI

Query string arguments.

RawUrl

Request-Line URI

Part of a URL following the domain name.

RequestType

Request-Line method field

HTTP data transfer method (GET, POST).

TotalBytes

(entity-header) Content-Length

Number of bytes in input stream.

Url

Request-Line URI field and Host field of header

URL of current request.

UrlReferrer

(request-header) Referer field

Information about the URL of the client's previous request that linked to the current URL.

UserAgent

(request-header) User-Agent

String containing raw information about the client software used for request—usually a browser.

UserHostAddress

—

IP host address of the remote client.

The built-in features of ASP.NET reduce the amount of direct interaction required between an application and the HttpRequest object. For example, we saw in Section 16.2, "Web Forms Controls," that ASP.NET Web controls generate code that is automatically tailored to the client's browser—eliminating the need for the application code to implement logic to identify the browser. There are, however, some cases where an application must access the object fields directly. Logging Web statistics is one; another is working with cookies.

Cookies are used to store information on the computer of a client that has cookies enabled on his browser. The browser is responsible for passing the cookie value as part of the request and handling any cookies returned from the server. The Cookies attribute references the collection of cookies returned by the browser. The following code loops through the collection displaying the name, value, and expiration date of cookies in the collection.

// Read cookies returned by browser

foreach(string cookieName in Request.Cookies.AllKeys){

   HttpCookie cookie = Request.Cookies[cookieName];

   Response.Write(cookie.Name+" = "+cookie.Value

                  +"<br>Expires: "+cookie.Expires);

}

The only cookie in this collection is the ID used by ASP.NET to identify sessions. ASP.NET's use of cookies is discussed further in the next section.

.NET_SessionId = avsqkn5501m3u2a41e3o4z55

Expires: 1/1/0001 12:00:00 AM

HttpResponse Object

The HttpResponse class contains properties that encapsulate the information returned in a response message. It also provides methods that construct the response and send it to the requestor. As we did with the Request object, let's begin by taking a quick look at the underlying HTTP response message represented by the Response object (see Figure 17-2).

Figure 17-2. Structure of a response message

HTTP Response Message Structure

The server responds with a status line that includes the message's protocol version, a success or error code, and a textual description of the error code. This is followed by the general header and the response header that provides information about the server. The entity header provides metainformation about the body contents or the resource requested.

Viewing an HTTP Response Message

ASP.NET provides the HttpWebRequest and HttpWebResponse classes for working with HTTP requests and responses, respectively. They are discussed in detail later in the chapter, but let's take a preliminary look at how they can be used to display portions of a response message.

Listing 17-1 contains a simple application that sends a request to a user-entered URL, receives the response, and extracts the status code and server description. Run the program from the command line by typing in the program name and domain name you want to contact:

Example: | showserver www.addison-wesley.de/
Output: | Web Host: Microsoft-IIS/5.0 Response Status: OK

Listing 17-1. Getting Status and Server Information from a Response Message

//File: showserver.cs

using System;

using System.Net;

class WebClient

{

   // To run, type in:  showserver <domain name>

   public static void Main(string[] args)

   {

      HttpWebRequest request;

      HttpWebResponse response;

      if(args.Length>0)

      {

         string url="http://"+args[0];

         // Create a request to the URL

         request = (HttpWebRequest) WebRequest.Create(url);

         try

         {

            response = (HttpWebResponse) request.GetResponse();

            Console.WriteLine("Web Host: "+response.Server);

            Console.WriteLine("Response Status: "+

                              response.StatusCode);

         } catch ( Exception ex)

         {

            Console.Write(ex.Message);

         }

      } else

      {

         Console.Write("You must enter a domain name.");

      }

   }

}

HttpResponse Class Properties

Table 17-2 lists selected properties of the HttpResponse class. Some of those excluded exist only for ASP compatibility and have been deprecated by newer properties.

Table 17-2. Selected HttpResponse Properties

Property

Description

BufferOutput

Set to true or false to indicate whether output is buffered and sent only when a page has been completely processed. Default is true.

Cache

HttpCachePolicy object that describes the caching policy features such as expiration time and privacy.

Example: Response.Cache.SetExpires( DateTime.Parse("6:00:00PM"));

CharSet

Gets or sets the character set of the output stream.

ContentEncoding

An Encoding object that contains information about the character set of the current response. Encoding type includes ASCII, UTF-7, UTF-8, and others.

ContentType

String value containing MIME type of the output—for example, "text/html".

Cookies

Collection of cookies sent to the client.

Filter

A developer-written Stream object that filters all data being sent to the client.

IsClientConnected

Indicates whether client is still connected to the server.

Output

TextWriter object that sends text output to the client software. Example: Response.Output.Write("{0} Years Old", age);

StatusCode

Status code returned in the response message status line.

StatusDescription

Status code description returned in the response message status line.

Particularly noteworthy is the Cache property, which can greatly affect how pages are displayed to a browser. It is used to define a caching policy that dictates if and how long pages are cached. We'll look at this property in Section 17.5, "Caching."

An example that displayed the contents of a cookie was presented in the discussion of the HttpRequest class. Let's extend that example by demonstrating how the response object is used to create and return a cookie to the client.

// Create a cookie

HttpCookie myCookie = new HttpCookie("userid","007");

// Cookie will live for 10 minutes

// Timespan(days, hours, minutes, seconds)

myCookie.Expires = DateTime.Now.Add(new TimeSpan(0,0,10,0));

// Add to collection

Response.Cookies.Add(myCookie);

// Later... Read specific cookie

myCookie = Request.Cookies["userid"];

if(myCookie !=null) Response.Write("userid: "+myCookie.Value);

Using HttpResponse Methods

The Response.Write method, which we have used in numerous examples to emit output into the response stream, is the most commonly used method. Other useful methods include the following:

  • Redirect. Redirects the client's request to another URL.

  • AppendHeader. Adds an HTTP header to the response stream.

  • ClearContent. Clears the contents of the response stream.

  • End. Stops page execution and sends buffered output to the client.

  • WriteFile. Places the contents of a file directly into the output stream.

A simple, but useful, example (see Listing 17-2) illustrates how these methods can be used to download a file. The client request for this page includes a query string with the name of a requested file. The page locates the file (or returns an error message) and uses WriteFile to send it to the user. The AppendHeader, ClearContent, and End methods prepare and manage the response stream.

Listing 17-2. Using HttpResponse to Download a File

//File: requestfile.aspx

<%@ Page Language="C#"  %>

<script Language="C#"  runat="Server">

   private void Page_Load(object sender, EventArgs e)

   {

      //http://localserver/ideas/requestfile.aspx?file=notes.txt

      string fileRequest = Request.QueryString["file"];

      if(fileRequest!=null) {

         // File is store in directory of application

         string path = Server.MapPath(fileRequest);

         System.IO.FileInfo fi = new System.IO.FileInfo(path);

         if (fi.Exists) {

            Response.ClearContent(); // Clear the response stream

            // Add a header to indicate attachment type

            Response.AppendHeader("Content-Disposition",

                                  "attachment;filename="+fi.Name);

            Response.AppendHeader("Content-Length",

                                  fi.Length.ToString());

            // Use octet-stream to indicate unknown media type

            Response.ContentType="application/octet-stream";

            // Write file to output stream

            Response.WriteFile(fi.FullName);

            Response.End(); // Flush buffer output to the client

         }  else {

            Response.Write(path+" does not exist.");

         }

      } else {

         Response.Write("No Download file was specified.");

      }

   }

</script>


17.2. ASP.NET and Configuration Files

One of the advantages of working with ASP.NET, as a developer or administrator, is the ease of configuring and customizing the settings. The use of XML-based configuration files replaces the previous ASP approach that required shutting down and restarting server software to change a setting or replace DLLs. Now, changes that affect the underlying behavior of a Web page can be made dynamically. The beauty of this approach is that changes can be made using a simple text editor—although an XML editor is preferable—and that ASP.NET automatically detects and applies these changes when they occur.

Configuration information is stored in a required machine.config file and optional web.config files. The format of these files is the same; the difference is their scope.

As shown in Figure 17-3, configuration files can be placed in a hierarchy on the Web server. Each file lower down in the hierarchy overrides the settings (with a few exceptions) of files above it. The machine.config file is at the top of the hierarchy, and its settings apply to all applications running on the machine. Web.config files placed in application directories and subdirectories apply their settings to a specific application or pages in an application.

Figure 17-3. Hierarchy of configuration files

Core Note

As an alternative to a text editor, ASP.NET includes a configuration editor with the IIS management console. It is accessed from the Properties menu of a virtual directory.

A Look Inside web.config

The web.config file is an XML-formatted text file comprising numerous sections and subsections. Listing 17-3 offers a sampling of sections you're likely to find most useful: <configSections>, <appSettings>, <location>, <system.web>, and <connectionStrings>. After summarizing key features of these predefined sections, we'll look at how to create a custom configuration section.

Listing 17-3. Sample web.config File (continued)

<configuration>

<!-- (1) Define custom configurations                -->

   <configSections>

      <section name="RewriterConfig"

         type="RewriteSectionHandler, URLRewriter" />

   </configSections>

   <RewriterConfig>

      <!-- Contents of custom configuration section -->

   </RewriterConfig>

<!-- (2) Place application data in here              -->

   <appSettings>

      <add key="mainFont" value="arial" />

      <add key="fontSize" value="2" />

   </appSettings>

<!-- (3) Define system.web settings for a directory  -->

   <location path="calculators\bmi">

      <system.web>

         <trace enabled="true"

         pageOutput="true" />

      </system.web>

   </location>

<!-- (4) Main ASP.NET Application settings            -->

   <system.web>

      <sessionState

         cookieless="false"

         timeout=20  />

   ...

   </system.web>

<!-- (5) Connection string for database               -->

   <connectionStrings>

      <!-- connection string description  -->

   </connectionStrings>

</configuration>

<appSettings> Configuration Section

This area is used by developers to hold constant data values required by an application. These values typically include preference settings that define the appearance of a Web page. This entry illustrates how font settings are added using an <add> element and a key-value pair of attributes:

<add key="mainFont" value="arial" />

Values are retrieved from the appSettings section using the static AppSettings property of the ConfigurationSettings class:

myFont = ConfigurationSettings.AppSettings["mainFont"];

<location> Configuration Section

As mentioned previously, web.config files can be placed hierarchically within a directory structure, with files at lower levels overriding the settings of files in parent directories. This approach permits you to tailor the actions of ASP.NET down to the application and page level. However, this flexibility brings with it the potential problem of keeping track of the settings in multiple configuration files. As an alternative, ASP.NET provides a way to configure the settings for multiple directories in a single web.config file by using <location> elements. The <location> section operates as a configuration file within a configuration file. The element has a path attribute that takes the value of a virtual directory name. The settings within the contained <system.web> block apply to the virtual directory.

As an example, suppose that during application development we want to view the trace information as we test our applications. Because we don't want this voluminous output in the final product, we can set up a virtual "test" directory and enable tracing for that directory only.

<location path="test">

   <system.web>

      <trace enabled="true" pageOutput="true" />

   </system.web>

</location>

This has the same effect as placing a web.config file with this trace setting in the physical directory associated with the "test" virtual directory.

<system.web> Configuration Section

This is the area in web.config where an administrator can configure just about any aspect of the ASP.NET environment. It can be used to set session time-out length, user authentication and authorization rules, the type of session state management used, and the default culture used for processing requests. Table 17-3 summarizes the more important elements in this section.

Table 17-3. Selected system.web Elements

Element

Use

<compilation>

Sets default language and debug option.

<customErrors>

Defines custom errors and error pages.

<trace>

Enables or disables the trace feature for an application.

<pages>

Sets default Web page attributes.

<globalization>

Defines response/request encoding and culture-specific setting for Web pages.

<processModel>

Used to configure process setting for an IIS Web server.

<authentication>

Specifies authentication mode for a client: Windows, Forms, Passport, None.

<authorization>

Allows or denies access to resources.

<membership>

Defines how the Membership class manages user credentials and authentication.

<identity>

Specifies whether client impersonation is used on a Web page request.

<sessionState>

Configures the session state features of ASP.NET.

Of particular interest are the authentication, authorization, and membership elements that are discussed in Section 17.3, "ASP.NET Application Security." Other useful sections are summarized here.

The <compilation> Section

Use the debug attribute to indicate whether debugging symbols will be included in the compiled code. The defaultLanguage attribute sets the default language used in a Web page's script blocks. Multiple languages can be selected by using a semicolon to separate them.

<compilation  debug="true"  defaultLanguage="C#"  />

The <customErrors> Section

The primary use of this section is to redirect errors to custom Web pages. Here is an entry that establishes a default error page, and a page to handle the HTTP 404 status code that arises when a resource cannot be found:

<customErrors defaultRedirect = "defaultError.aspx" mode="On" >

   <error statusCode="404" redirect="Error404.aspx" />

</customErrors>

The mode attribute takes the value On, Off, or RemoteOnly. The latter value, which is also the default, specifies that the default page is called only if the request comes from a machine other than the Web server. This permits local developers to see details of the actual error, whereas clients see only the custom page. Off should be used when a developer is testing pages on a remote machine. Note that multiple <error> elements can be included in the section.

The <trace> Section

The <trace> element is used to enable application-level trace logging. This is in contrast to the trace attribute of the <%@Page> directive that provides only page-level tracing. Here is an example that illustrates the use of the <trace> element and its associated attributes:

<trace

   enabled="true"

   pageOutput="false"

   traceMode="SortByTime"

    requestLimit="5",

   localOnly="false"

</trace>

Aside from the enabled attribute that turns tracing on or off, the most interesting attributes are pageOutput and localOnly.

  • pageOutput specifies whether the trace log is appended to the Web page for display on the client's browser. If set to false, the output can be viewed in the TRace.axd page of the application's root. Here is an example:

    Application URL:

    http://localhost/calculation

    Trace log:

    http://localhost/calculation/trace.axd

  • The requestLimit attribute sets the maximum number of trace logs maintained.

  • localOnly indicates whether tracing is enabled for localhost users only or all users. The default is true.

The <sessionState> Section

One of the more powerful features of ASP.NET is the ability to maintain state information for a session; and the <sessionState> element plays a definitive role in selecting how it is implemented. By setting its mode attribute to one of four values, the software architect can specify where the session state information is stored.

  • off disables the use of session state management.

  • Inproc enables in-process state management (aspnet_state.exe).

  • StateServer state is stored by a surrogate process running on a selected server.

  • SqlServer state is maintained in a temporary SQL Server table.

These options are described in detail in Section 17.4. Our interest in this section is to understand the elements and attributes that define how ASP.NET manages session state. To illustrate, here are the default settings for the <sessionState> element:

<sessionState

   mode="InProc"

   stateConnectString="tcpip=127.0.0.1:42424"

   sqlConnectionString="data

       source=127.0.0.1;Trusted_Connection=yes"

   cookieless="false"

   timeout="15"

/>

The timeout attribute specifies the number of minutes the session can be idle before it is abandoned. The cookieless attribute indicates whether the identification of the client session is maintained using a cookie or by mangling the URL (placing encoded session information in the URL line). The other two attributes are dependent upon the mode setting: stateConnectString specifies the IP address of the machine running the Session State Server process, and sqlConnectionString contains the connection string value used to access SQL Server if it is used to maintain state information.

The <connectionStrings> Section

Prior to .NET version 2.0, developers usually resorted to storing connection strings as a key-value pair in the <appSettings> section. ASP.NET 2.0 now provides a predefined <connectionStrings> section for this purpose. As shown in this example, the section takes three attributes: name, connectionString, and providerName.

<connectionStrings>

  <add name="movies"

    connectionString=

      "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=/movies.mdb;"

    providerName="System.Data.OleDb"/>

</connectionStrings>

The connection string is retrieved using the ConfigurationSettings class, which retrieves the string associated with a specified name attribute. Note that multiple strings may be defined.

string cs=

ConfigurationSettings.ConnectionStrings["movies"].ConnectionString;

Adding a Custom Configuration Section

The web.config file can be a convenient place to stash information needed by an application. We saw earlier how simple name/value pairs can be stored in the <appSettings> section. Suppose, however, your application requires data represented by a more complex XML structure. The solution is to add a custom configuration section to web.config. However, it is not as simple as choosing a configuration name and inserting appropriately formatted code into the configuration file. When you try to access a Web application that uses the configuration file, you'll receive an Unrecognized Configuration Section error.

It turns out that each section in web.config has a special handler that parses the section and makes its content available to an application. This handler is a class that implements the System.Configuration.IConfigurationSectionHandler interface and is declared in the <configSections> section of the web.config file.

Core Note

You can view the section handlers for the standard predefined sections, such as <appSections> in the machine.config file.

To demonstrate how section handlers work, let's create one for the <RewriterConfig> section shown in the code that follows. The first thing to note is the <configSections> element that contains a definition of the new section. Its type attribute specifies the handler (class) that parses the <RewriterConfig> section. The data section represents URLs that have been changed and need to be replaced with a new URL.

<configSections>

   <sectionGroup name="RewriterConfig">

      <section name="RewriterRules"

         type="ConfigRewriter.RulesHandler,RulesConfigHandler" />

   </sectionGroup>

</configSections>

<!--   Custom Configuration Section for URL Rewriting -->

<RewriterConfig>

   <RewriterRules>

      <Rule>

      <OldPath>/ideas/calculator.aspx</OldPath>

      <NewPath>/utilities/calculator.aspx</NewPath>

      </Rule>

      <Rule>

         <OldPath>/ideas/bmi.aspx</OldPath>

         <NewPath>/utilities/bmi.aspx</NewPath>

      </Rule>

   </RewriterRules>

</RewriterConfig>

After the XML data structure is determined, the next step in developing a section handler is to create a class that is populated with the contents of the section and contains members that make the data available to an application. In this example, data is defined by any number of <Rule> elements. Consequently, the class must provide some sort of collection to return multiple values. We'll use a Hashtable for the purpose. The GetNewPath method accepts a string value that it uses as a key to retrieve and return its associated value from the hash table.

// File: SectionRules.cs

using System.Collections;

namespace ConfigRewriter

{

   // Class to contain content of configuration section

   public class RulesData

   {

      private Hashtable paths;

      // Constructor accepts hash table as parameter

      public RulesData (Hashtable pathCollection){

         paths = pathCollection;

      }

      // Use old path as key and return hash table value

      public string GetNewPath(string OldUrl)

      {

         return ((string)paths[OldUrl]);

      }

   }

}

The final step, shown in Listing 17-4, is to create the section handler code. Its primary function is to implement the Create method. This method is passed an XMLNode parameter that corresponds to the <RewriterRules> node in our section. It is used to parse its child nodes and fill the hash table with its contents. An instance of the RulesData class is instantiated by passing the hash table to its constructor. This object is then available to a Web application.

Listing 17-4. Configuration Section Handler

//File: RedirectConfigHandler.cs

using System.Xml;

using System.Configuration;

using System.Collections;

namespace ConfigRewriter

{

   public class RulesHandler: IConfigurationSectionHandler

   {

     public object Create(object parent, object input,

                          XmlNode section)

     {

        Hashtable paths = new Hashtable();

        string oldUrl="";

        XmlElement root = (XmlElement) section;

        foreach (XmlNode node in root.ChildNodes){

           foreach(XmlNode child in node.ChildNodes)

           {

              if(child.Name=="OldPath") oldUrl= child.InnerText;

              if(child.Name=="NewPath")

              {

                 paths[oldUrl]= child.InnerText;

              }

           }

        }

        RulesData urlRules = new RulesData(paths);

        return urlRules;

      }

   }

}

After the section has been constructed, accessing its content is trivial: use the ConfigurationSettings.GetConfig method to return an instance of the section object, and use its properties to retrieve values.

// Retrieve object returned by section handler

// and use it to access content of section

RulesData r;

r = ConfigurationSettings.GetConfig(

      "RewriterConfig/RewriterRules")

      as RulesData;

if(r!=null)

// Pass old path to method and receive redirected path

string newURL = r.GetNewPath("/ideas/bmi.aspx");


17.3. ASP.NET Application Security

Web-based applications must address many of the same security issues that are faced by applications situated on a local network. The most important of these—and the subject of this section—is client authentication. By this, we mean the capability to identify the user attempting to access a Web resource. Unlike traditional static Web sites that permit anonymous user access, an application that wants to tailor its content for the user, or restrict access based on authorization rights, must first authenticate the user.

ASP.NET offers three forms of client authentication:

  • Windows Authentication. Relies on the relationship between IIS (Internet Information Server) and the underlying operating system's directory security. Its options include using the native Microsoft Windows login authentication.

  • Microsoft Passport Service. Microsoft's proprietary "single sign-on" authentication service. Requires subscribing to the Passport network and installing the Passport SDK to link .NET to the service.

  • Forms Authentication. Allows an application to define user credentials and handle authentication based on it own requirements. This approach relies on .NET classes to create and manage a cookie that maintains user authentication information during the lifetime of a user's session.

The most flexible—and least proprietary—of these techniques is Forms Authentication. It represents the natural evolution from a do-it-yourself approach that requires creating a login screen, writing code to match user credentials against a database or file, and building a cookie to maintain client data during a session. ASP.NET offers substitutes for each of these tasks, which can be easily plugged into a developer's custom solution. The remainder of this section examines this form of authentication.

Forms Authentication

The general framework for implementing Forms Authentication is based on a mixture of configuration files, authentication modules, ASP.NET security classes, and the script (C# code) to work with the methods and properties of these classes. Figure 17-4 illustrates how it works.

Figure 17-4. Forms Authentication steps that occur when a page is first requested

[View full size image]

A web.config file defines a login page to which a user's browser is redirected when it first attempts to access a Web resource. This login page accepts the client's credentials—usually name and password—and determines if they are valid. If so, the user is authenticated and ASP.NET methods are used to create a FormsAuthenticationTicket that contains the user's security information. The ticket is encrypted and stored as a cookie. The cookie may be configured to expire at the end of the session, or remain on the client's machine so that the user can be automatically identified in future sessions. An optional step in this process is to add authorization information about a client to the ticket. For example, users may be assigned to groups that define their access rights. The code to add this role information can be placed in the global.asax file to handle the application's AuthenticateRequest event.

After these steps are complete and the user is authenticated, control passes to the Web page originally requested. Subsequent session authentication uses information in the authentication cookie and avoids these steps.

Security Settings in web.config for Forms Authentication

The web.config file plays an important role in supporting authentication and authorization in ASP.NET security. Listing 17-5 presents many of its elements and optional attribute values that are used to select and implement a security method.

The first thing to note is the mode attribute in the <authentication> element, which specifies the type of authentication being used. Contained in the <authentication> section is a <forms> tag or <passport> tag (not shown) that corresponds to the type of authentication chosen. The <forms> tag, which is used in a later example, contains several key attributes. The loginurl specifies the login page that a user is directed to when she has not been authenticated. The timeout attribute specifies how long a user may wait between requests before the authentication ticket expires. Thus, if the value is set to 5 minutes and the user has not requested a page in that interval, she is forced to log in again at her next request. The protection attribute specifies how, or if, ticket authentication information is processed before it is written to a cookie. You'll normally set this to All.

The <forms> element may contain a <credentials> element that can be used as a mini-database to list each user and his password. The advantage of placing them here, as we see in a later example, is that .NET authentication classes provide methods to automatically perform authentication against these names. To add a measure of security for storing the passwords, the passwordFormat attribute is provided to specify how the passwords are encrypted.

Web.config may also contain an <authorization> section that explicitly allows or denies access to users based on their name or role (think of role as a group the user belongs to). The <allow> and <deny> tags are processed in the order they occur in the file. When the tag's policy can be applied to the user, processing stops. If no allow or deny policy matches the user, the user is authorized to access the resource.

Listing 17-5. Authentication and Authorization Configuration Sections

<Configuration>

   <system.web>

      <authentication mode="{Windows|Forms|Passport|None}">

         <forms name="Cookiename"

            loginurl="path to login file"

            timeout="minutes"

            protection="{None|All|Encryption|Validation}"

            path="Cookiepath"  >

         <credentials passwordFormat="Clear|MDS|SHA1}">

            <user name="User Name" password="password" />

         </credentials>

         </forms>

      </authentication>

      <authorization>

         <allow users="comma-separated list"

            roles="roles list" />

         <deny  users="comma-separated list"

            roles="roles list />

      </authorization>

      ...

   </system.web>

   ...

</configuration>

An Example of Forms Authentication

We'll now create a simple Web page that can only be accessed by users who provide a valid name and password. The example is then extended to demonstrate how client role information can be incorporated in the authorization process. As you work through the example, the most important things to observe are

  • The interrelationship between the web.config file and authentication methods used in the login screen code.

  • How roles can be assigned to users by the use of global.asax.

  • How to access authentication ticket information through the underlying .NET classes that manage authentication.

Using web.config for Authentication and Authorization

The configuration file segment in this example is used for both user authentication and authorization. The <credentials> element contains the names and passwords of three users who may access the system. The <authorization> element contains rules that deny login access to all anonymous users as well as one individual user. Only users Joanna and Ruth are permitted to access the Web page.

<system.web>

   <authentication mode="Forms">

      <forms name="AppCookie"

         loginUrl="applogin.aspx" protection="All"

         timeout="10" path="/"   >

         <credentials passwordFormat="Clear">

            <user name="joanna" password="chicago" />

            <user name="ruth"   password="raleigh" />

            <user name="kim"    password="newyork" />

         </credentials>

      </forms>

   </authentication>

   <authorization>

      <deny users="?,kim" />

   </authorization>

The same authorization rights can be granted by a combination of allow/deny rules:

<allow users="joanna,ruth"  />

<deny users="*" />

This denies access to all users (the asterisk (*) wildcard selects all users) except for those that are explicitly allowed. When both <allow> and <deny> are present, the order in which they are placed can be important. In this example, placing the <deny> rule first would deny access to all users, and the <allow> rule would be ignored.

The Login Screen—Using the FormsAuthentication Class

The web.config file used in this example redirects the initial request for a Web page to applogin.aspx, which accepts login credentials and performs user authentication. The login part is easy—two text boxes are used to accept a user name and password. The technique for authenticating the user is more interesting.

Traditionally, login screens validate a user-supplied name and password against a database or file containing valid users. You are still free to do this in ASP.NET, but there are other options. These include storing names and passwords in the web.config file, or using the ASP.NET Membership class and its preconfigured database to manage user credentials. For demonstration purposes, this example uses web.config as the data store. Keep in mind that this is not the recommended approach if there are more than a few users, or multiple servers are used, which would require synchronizing the configuration file across machines.

As Listing 17-6 illustrates, the FormsAuthentication class plays the key role in authenticating a user and creating the authentication ticket that identifies the user. This important class provides static properties and methods that are used to manipulate and manage authentication information. Two of its methods are used here: Authenticate accepts a name and password, which it attempts to validate against the data in web.config; RedirectFromLoginPage redirects the user to the page originally requested, and creates an authentication cookie for the user. The second parameter to this method indicates whether the cookie should persist on the user's machine beyond the lifetime of the session.

Listing 17-6. Login Screen Using ASP.NET Authentication

<%@ Page Language="C#" %>

<html>

<head>

<title>login</title>

<script  runat="Server">

  void verify_Form(object sender, System.EventArgs e)

  {

    // Code to verify user against a database goes here...

    // or use .NET to verify user using web.config info.

    // Name and password are compared against web.config content

    if( FormsAuthentication.Authenticate(

           txtName.Text,txtPW.Text))

    {

      // Redirect to original form and

      // create authentication ticket

      bool persistCookie=false;

      FormsAuthentication.RedirectFromLoginPage(txtName.Text,

            persistCookie);

    }

    else

    { errmsg.Text="Cannot log in user."; }

  }

</script>

</head>

<body >

<form id=Form1 method=post runat="server">

  <asp:Panel

        style="Z-INDEX: 101; LEFT: 20px; POSITION: absolute;

        TOP: 64px"

        BackColor = "#efefe4"

        Height="120px" Width="278px"

        runat="server" >

  <TABLE>

    <TR>

      <TD><asp:Label  Runat="server"

                 text="User ID:"></asp:Label></TD>

      <TD><asp:TextBox  Runat="server">

             </asp:TextBox></TD></TR>

    <TR>

      <TD><asp:Label  Runat="server"

                 text="Password:"></asp:Label></TD>

      <TD><asp:TextBox  TextMode="Password"

           Runat="server"> </asp:TextBox></TD></TR>

    <TR>

      <TD colspan=2 align=center>

          <asp:Button

              Text="Submit" Font-Bold="true"

              OnClick="verify_Form" Runat="server" /></TD></TR>

    <TR>

      <TD colspan=2 align=center>

             <asp:Label id=errmsg runat="server" /></TD>

    </TR>

  </TABLE>

</asp:Panel>

</FORM>

</body>

</html>

Adding Role-Based Authorization with global.asax

Authentication is often only the first step in permitting a user to access Web resources; the second step is authorization—the process of determining what resources the user is authorized to access. If all users have full access, this step can be ignored. However, if a user's access rights, or role, are determined by membership in a group, this group must be identified and associated with the user.

After a user is created in ASP.NET, information about that user is available through the Page.User property. This property returns a principal object, so named because it implements the IPrincipal interface. This interface has two important members (see Figure 17-5) that the principal object implements: Identity, which can be used to get the name of the user, and the IsInRole method that is used to check a user's group membership.

Figure 17-5. User information is encapsulated in a GenericPrincipal object

The IsInRole method is passed a role name and returns a bool value indicating whether the user is in that role. It is the developer's task to assign the roles to the authenticated user object so that this method will have an internal list to check against. Looking at Figure 17-5, you would expect this list to be part of the GenericPrincipal object—and you would be right. The constructor for this class accepts as one of its parameters a string array of roles to be associated with the user. To assign these roles, the application can take advantage of the AuthenticateRequest event that occurs when the identity of a user is established. A handler for this event is placed in the global.asax file.

Listing 17-7 shows how code in the event handler assigns roles to the current user. It determines the user roles—in this case, calling GeTRoles—and places them in a string array. This array, along with the current Identity, is passed to the GenericPrincipal constructor to create a new object that updates the value of the User property.

Listing 17-7. Using global.asax to Add Role Information for a User

<%! file:global.asax %>

<%@ Import Namespace="System.Security" %>

<%@ Import Namespace="System.Security.Principal" %>

<%@ Application   %>

<script language = "C#" runat=server>

protected void Application_AuthenticateRequest(

                   object src, EventArgs e)

{

   if (Request.IsAuthenticated)

   {

      string currUser= Context.User.Identity.Name;

      string roles= GetRoles(currUser);

      string[] appRoles = roles.Split(new char[]{'|'});

      // Create GenericPrincipal class and add roles to it

      // GenericPrincipal(IIdentity identity, string[] roles)

      Context.User = new GenericPrincipal(

                         Context.User.Identity, appRoles);

   }

}

private string GetRoles( string username )

{

   // Code here would query database for user's role(s).

   // Return role as delimited string that can split into array.

   return "Administrator|Operator";

}

Viewing Authentication Information

After the authentication steps are complete, an application may want to log information or make decisions based on information about the user. The User property exposes all of the members shown in Figure 17-5. Further authentication details, such as when the authentication cookie expires or when it was issued, can be obtained from properties exposed by the authentication ticket. As shown in Listing 17-8, an instance of the cookie is created and then converted to a FormsAuthenticationTicket object by using the Decrypt method of the FormsAuthentication class. The properties of this object are then displayed.

Listing 17-8. Displaying Authentication Details

// Display authentication details

Response.Write("Client: "+ User.Identity.Name+"<br>");

if(User.IsInRole("Operator")) Response.Write(

          "Role: Operator<br>");

string cookieName = FormsAuthentication.FormsCookieName;

HttpCookie authCookie = Context.Request.Cookies[cookieName];

if(authCookie !=null)

{

   // Create ticket from cookie and display properties

   FormsAuthenticationTicket authTicket = null;

   authTicket = FormsAuthentication.Decrypt(authCookie.Value);

   Response.Write("Issued: "+authTicket.IssueDate+"<br>");

   Response.Write("Expiration: "+authTicket.Expiration+"<br>");

   Response.Write("Persistent: "+authTicket.IsPersistent);

}


17.4. Maintaining State

The standard for the HTTP protocol is contained in a document known as RFC 2616. In printed form, it is more than 120 pages in length. If you search this document, you'll find that not one page, section, or sentence discusses a method for maintaining state information between HTTP requests. The term cookie is completely absent. So, unlike human memory where "our thoughts are linked by many a hidden chain" (Alexander Pope), the Internet protocol is designed to be stateless—each request unlinked to the preceding one.

Yet, for Internet applications to flourish, a means has to exist to identify specific users. Online purchasing, surveys, and the need to recognize user preferences depend on it. The first efforts to maintain state information relied on using features defined in the HTTP protocol: hidden fields that could be sent back and forth using the POST method, and a string full of values that could be placed after the ? character in an HTTP URL path. The former technique is used to maintain .NET ViewState information; the latter constitutes a "query string" consisting of name/value pairs.

A third client-side approach to managing state is the use of cookies, a small (4K) text-only string that is stored in browser memory during a session and may be written to disk. Many applications maintain session-related state information, such as a shopping cart's identification, in cookies. ASP.NET identifies each session by creating a cookie containing a unique session ID.

These approaches are plagued by a common weakness: They can be compromised by the client. Query strings and hidden fields can be altered; cookies can be altered or simply turned off on the browser.

To overcome these shortcomings, ASP.NET offers two general types of server-side state management: session state, which maintains information for the life of the session; and application state, which maintains information across multiple sessions. Although these techniques maintain information on a server—rather than passing it back and forth between requests—they still rely on a cookie, where possible, to uniquely identify a user's session. However, the server-based solution presents another problem: All session activity must be handled by that server possessing the state information. This is incompatible with large Web sites where a server-selection algorithm assigns requests to any machine in a Web farm. An ASP.NET solution to this is the use of a central database to store session state information.

Table 17-4 summarizes the client- and server-side state management approaches. Of those listed, this section focuses on the application and session management techniques.

Table 17-4. State Management Techniques for Web Applications

Technique

Web Farm

Compatible

Description

Query String

Yes

Data passed as part of URL.

View State

Yes

ASP.NET hidden View_State field used.

Cookie

Yes

Information passed to/from browser in a cookie.

Application

No

An Application object maintains information available to all sessions accessing an application.

Session: In-Process

No

Session state is stored and managed using the Aspnet_wp.exe process.

Session: State Server

Yes

Session state is stored in the Aspnet_state.exe process.

Session: SQL Server

Yes

State information is maintained in a temporary table in a database.

Application State

Application state data is maintained in an instance of the HttpApplicationState class—an in-memory dictionary holding key-value objects. The contents of this dictionary object are available to all sessions using that application.

The data is usually loaded within the global.asax file when the Application_ Start event fires.

protected void Application_Start(object sender, EventArgs e)

{

   Application["targetBMI"]= 21;

   Application["obeseBMI"] = 26;

}

Users of the application then have read and write access to the data without having to regenerate it. This is particularly useful when working with data from a database. It can also be a convenient way to initialize variables used for collecting usage statistics. (See Section 17.2 for background on global.asax file.)

Accessing and Updating Application State Data

All sessions using the application may access the Application object through the Application property of the System.Web.UI.Page type. The only thing to note about accessing data is that it must be cast, because the information is stored as an object. Here is a segment that illustrates the primary properties and methods for working with the Application object:

int target = (int) Application["targetBMI"];

// Change value

Application["targetBMI"] = 22;

// List all HttpApplicationState values

foreach( string s in Application.AllKeys){

       Response.Output.Write("<br>{0} = {1}",s,Application[s]);

}

// Remove Application item

Application.Remove("obeseBMI");

Not illustrated are the Lock and UnLock methods that allow application variables to be updated in a thread-safe manner.

Considerations in Using Application State

The variables contained in application state have two primary uses: as read-only data globally available across all of the application's sessions, and as variables that are updated to keep statistics regarding the application's use. Both of these uses can be handled more efficiently using a different approach.

The ASP.NET data cache provides a more flexible approach for making read-only data available on an application-wide basis. Its contents are as accessible as those in the application object, and it offers the advantage of having its contents automatically refreshed at a specified time interval. (Its use is described in Section 17.5.) As a rule-of-thumb, use application state to preload small amounts of read-only data required by an application; use the data cache when working with large amounts of data.

The problem with maintaining usage data in application state is that the data is available only during the life of the application. When it ends, the data is gone. In addition, if there are multiple servers, the data will apply to only one server. A better approach is store the information in a central database.

Core Note

Application recycling refers to shutting down and restarting an application based on criteria such as time, memory usage, and number of client requests. It is set using the <processModel> tag in the web.config file. The following setting recycles the application every two hours.

<processModel timeout="120">

Session State

When a new client requests an ASP.NET application, ASP.NET creates a cookie containing a unique session ID and returns it to the client's browser. This ID, or session key, is used to identify subsequent requests from the client. State information unique to each session is maintained in an instance of the HttpSessionState object. The contents of this object are available through the Session properties of both the Page and HttpContext class. Note that Session can be used in place of the fully qualified HttpContext.Session. Working with session state is analogous to working with application state. Many applications perform state session initialization in a Session_Start event handler in the global.asax file. This segment initializes variables for an online survey:

// global.asax file

protected void Session_Start(object sender, EventArgs e)

{

   Session["Start"]= DateTime.Now;

   Session["QuestionsAnswered"] = 0;

}

The variables can be updated in the application with the same syntax used for the Application object:

int ict= (int)Session["QuestionsAnswered"]+1;

Session["QuestionsAnswered"]=ict;

The HttpSessionState class contains several members that are useful in manipulating and interrogating the Session object. The following code demonstrates their use:

// Session ID is unique to each session

Response.Write("<br>Session ID: "+Session.SessionID);

// Mode may be InProc, StateServer, SqlServer, Off

Response.Write("<br>Processing Mode: "+Session.Mode);

// Session times out after 20 minutes (default) of inactivity

Response.Write("<br> Timeout: "+Session.Timeout);

// Number of items in session state

Response.Write("<br>Items: "+Session.Count);

// List key-values in session state

foreach( string key in Session.Keys)

      Response.Output.Write("<br>{0} = {1}",key, Session[key]);

The most interesting of these properties, by far, is Session.Mode, which indicates where the Session object is configured to store its data. Session state information can be stored using one of three methods: InProc, StateServer, or SqlServer. Although the choice does not affect the way information is accessed through the Session object, it does affect application performance, manageability, and extensibility. Let's look at the details of using all three backing stores (see Figure 17-6).

Figure 17-6. Session state data store options

In-Process Session State

By default, ASP.NET stores session state in-process using the aspnet_wp.exe process. This means that the information is managed in memory space where the application is running. This has the advantage of providing fast access to the data. The drawbacks are that it must run on a single server—precluding its use with Web farms—and that all session data is lost if the server reboots. Given this, it is recommended for single-server sites that need to maintain moderate amounts of data in state.

To select in-process session state management, set the mode attribute to InProc in the <sessionState> element of the web.config file. As shown here, you can also specify a time limit specifying how long the process may be idle before the session data is discarded.

<configuration>

   <system.web>

      <sessionState mode="InProc" timeout="30"/>

   </system.web>

</configuration>

Out-of-Process Session Using a State Server Process

The concept of out-of-process session state management entails the use of a mechanism outside of the application's process to store state data. ASP.NET provides two such mechanisms: a separate process running on a selected state server and the use of a data server to store state information.

When session state mode is used, session state is stored in the Aspnet_ state.exe process. This allows an application running on multiple servers to share state information. Because the server running this process is accessed via a network on the Internet, performance is slower than using the in-process approach. Also, the data is susceptible to a server reboot.

To access a state server, make sure the machine selected is running the Aspnet_state.exe process. Any server accessing the state server must set the <sessionState> element in its web.config file to point to the state server. The default port used is 42424. On a Windows-based operating system, this can be changed through the registry.

<configuration>

   <system.web>

      <sessionState mode="StateServer"

         stateConnectionString="192.168.1.109:42424"

      />

   </system.web>

</configuration>

Out-of-Process Session Using SQL Server

Using a SQL Server database to store session state data also offers the capability of sharing data among machines. In addition, SQL Server security features can be used to selectively provide access to the data; and, of course, the data is not transient because it exists in tables. The disadvantages of this approach are that it only works with Microsoft SQL Server and it is the slowest of the three session state modes.

To prepare a machine running SQL Server to handle session state requires no more than creating the tables that ASP.NET expects. The InstallPersistSqlState.sql script takes care of this. By default, the script is located in the \systemroot\Microsoft.NET\Framework\version folder. It creates a database named ASPState that contains the state tables.

Web.config is used to tell ASP.NET that session information for the application(s) is stored on a SQL Server. The <sessionState> element is set to specify the mode and connection string.

<configuration>

  <system.web>

    <sessionState mode="SQLServer"

      sqlConnectionString="datasource=x; user id=sa; password="/>

  </system.web>

</configuration>

SQL Server should be considered when there is a need to maintain data beyond the life of the session. For example, sites often persist the content of a shopping cart so it is available when the user logs in again.

Because the use of SQL Server slows Web page performance, it should be used only on those pages in an application that require state data. The @Page directive has an EnableSessionState attribute that dictates how state is used on the page. It can be set to false to disable session state, TRue to enable read and write session state, or readOnly.


17.5. Caching

Consider a Web page with a list box containing the names of a thousand movie actors whose movies are displayed when an actor is selected. The first time the page is requested, a query is sent to the database, the list box is populated with the results, and the rendered HTML is returned to the user's browser. This is a relatively slow and expensive process. Multiply it by a thousand user requests and Web server response time will slow measurably.

One solution is to use caching to bypass most, or all, of this reprocessing by keeping an HTML page or data in memory. Subsequent requests for the page are handled using the cached information—obviating the need to fetch data from the database. A sound caching strategy can improve Web server response more than any other single factor. This section presents the factors to be weighed in designing a caching strategy and the two broad categories of caching available in ASP.NET: output caching and data (or request) caching.

Page Output Caching

ASP.NET permits a developer to indicate whether all or part of a Web Form should be cached, where it should be cached, and how long it should remain in the cache before it expires and must be refreshed. The key to cache control for a Web Form is the @OutputCache directive:

<%@OutputCache Duration="30" Location="Any"

   VaryByParam="none" %>

Its attributes declare the specific caching policies in effect for that Web page. Let's look at how these attributes are used.

Core Note

ASP.NET processes the @OutputCache directive by translating its attributes into HTTPCachePolicy method calls.

Specifying Cache Duration

The mandatory Duration attribute specifies the length, in seconds, that the page or control is cached. Setting this to 20 seconds generates the underlying statement:

Response.Cache.SetExpires(DateTime.Now.AddSeconds(20));

Specifying the Caching Location

The Location attribute specifies where the caching can occur. In general, caching occurs on a server, browser, or somewhere in between, such as on a proxy server. The values of this attribute correspond to these locations: Any, Client, DownStream, Server, ServerAndClient, or None. DownStream refers to a cache-capable device other than the origin server. None disables caching everywhere.

Conditional Caching with VaryByParam, VaryByHeader, and VaryByCustom

In Chapter 16, we developed a BMI calculator Web page in which the user enters a height and weight value and the BMI value is returned. Because there are thousands of combinations of height (htf, hti) and weight (wt) parameters, several slightly different versions of the same page are likely to be created. The VaryByParam attribute is available for cases such as this. Its value indicates the parameter(s) (from a POST or query string) that should be considered when selecting a page to cache. For example, the following statement causes a page to be cached for each unique combination of wt, hti, and htf values:

<%@ OutputCache Duration="60"  VaryByParam="wt;hti;htf" %>

Figure 17-7 shows four requests that result in three unique pages being cached with their calculated BMI value. Note that we could have also assigned an asterisk (*) to VaryByParam to indicate that each parameter's value affects the cache.

Figure 17-7. Cachedpage: VaryByParam="wt;hti;htf"

Be aware that you can create some unexpected results if you set VaryByParam incorrectly. If only the wt parameter were specified in this example, requests with a weight of 168 and heights of 5'1" and 6'1" would return the same Web page. The page returned to both would be the one requested earliest.

VaryByHeader caches a different version of a page, based on the value of the Request Header fields (refer to Figure 17-1). This is the most commonly used with the Accept-Language field to ensure that different language versions of a Web page are cached.

The final conditional attribute to be familiar with is VaryByCustom. This is most useful when you have a Web page that is rendered differently depending on the client's browser type and version. To create different page versions by browser type, set the VaryByCustom attribute to "Browser".

<%@ OutputCache Duration="60"  VaryByParam="*"

                               VaryByCustom="Browser" %>

This attribute also can be set to recognize custom values that your program generates. Please refer to caching documentation for details on this.

Caching a Partial Page (Fragment Caching)

All parts of a Web page are not created equally. Some, such as headers and links, rarely change, whereas other sections, such as the latest stock exchange quotes, change by the minute. A desirable caching strategy is to identify the static objects on the page and cache only those. This can be done in ASP.NET by creating custom controls (.ascx files) for the sections of the Web page to be cached. Custom controls have their own OutputCache directive that determines how they are cached, irrespective of the form containing them.

To illustrate the concept, let's create a simple control to be embedded within a Web page (see Figure 17-8). This segment simply contains a couple of links to other Web sites.

<!-- File: CacheFrag.ascx  -->

<%@ OutputCache Duration="30" VaryByParam="None" %>

<b>My Links</b><br>

<a href=http://www.moviesites.org>Movies</a><br>

<a href=http://www.imdb.com>Movie Reviews</a>

<br>

Figure 17-8. Example of custom control with its own caching policy

The application Web page to hold this control displays a title and date. Note that its caching is turned off by setting Location to None. The result is that only the control is cached.

<!-- File: CacheMain  -->

<%@ Page Language="C#"  %>

<%@ OutputCache Location="None" VaryByParam="*" %>

<%@ Register TagPrefix="frag" TagName="fraglinks"

                Src="CacheFrag.ascx" %>

<HTML>

   <HEAD><TITLE>Sample Web Site</TITLE>

   <body>

      <center> <b>Fragment Sample</b>

      <br>

      <% Response.Output.Write(DateTime.Now.ToString("f")); %>

      </center>

      <frag:fraglinks  runat="server" />

   </body>

</html>

Data Caching

Data caching, sometimes referred to as request caching, is often an alternative to using application state or ViewState to store read-only data. It is as easy to use as the Application object, and adds the flexibility of assigning expiration and priority values to the stored data.

Adding Items to a Data Cache

The simplest way to add data to a data cache is to use the key-value syntax of the Cache property of the Page class:

Cache["userid"] = "rigoletto";

The Cache.Insert method is often a better approach since it takes full advantage of the fact that each cache entry is actually an instance of the CacheEntry class. This class contains many useful properties that are set by passing parameters to the Insert method:

Syntax:

Example:

public void Insert(

   string key,

   object value,

   CacheDependency dependencies,

   DateTime absoluteExpiration,

   TimeSpan slidingExpiration,

   CacheItemPriority priority,

   CacheItemRemovedCallBack cb

);

Cache.Insert (

   "userid",

   "rigoletto",

   null,

   DateTime.Now.AddMinutes(20),

   TimeSpan.Zero,

   CacheItemPriority.Normal,

      null

);

Either the absoluteExpiration or slidingExpiration parameter can be used to determine when the cached data expires. The former sets a specific date and time; the latter sets the expiration to occur a specified amount of time after the value is last accessed. To disable sliding expiration, assign the value Cache.NoSlidingExpiration.

The dependencies parameter allows cached data to be tied to files, directories, or other objects in the cache. Establishing this dependency causes the cached data to be removed when the object it is dependent upon changes. Thus, if cached data comes from a file and the file contents are modified, the data in the cache is removed.

string filename = "actors";

CacheDependency dep = new CacheDependency(fileName);

cache.Insert("key", "value", dep);

The priority parameter makes sense when you understand that data in a cache is not guaranteed to remain there. Because there is no limit on what can be placed in the cache, ASP.NET periodically invokes a resource scavenging process to remove less important data from the cache. The determination of what is important is based on the priority of the resource. By setting this priority to a CacheItemPriority enum value, you can reduce, or increase, its likelihood of being removed through scavenging. Enum values may be set to AboveNormal, BelowNormal, Default, High, Low, Normal, and NotRemovable.

Retrieving Items from the Data Cache

To retrieve data from the cache, simply specify the key (case is ignored) of the desired data:

String user = (string) Cache["userid"];

Because the data is stored as an object, it is necessary to cast the result. If no data exists for the key, a null value is returned.

Data Caching as a Substitute for ViewState

The true value of data caching comes into play when large amounts of data need to be saved to restore a page's state. As was explained in the previous chapter, ViewState automatically saves data and state information for Web controls on a page as long as the page posts to itself. Figure 17-9 illustrates this. In it, we have Web page A containing a list box populated with several hundred items from a database. In steps 1 and 2, a user performs some action that results in a modified version of the same page being returned to the client. State is retained automatically because the contents of the page's list box—and any other fields—are passed in the ViewState field.

Figure 17-9. Use a data cache to preserve information between separate pages

Suppose the user selects an item in the list box on page A that requires loading page B to show more details about the item. The server constructs the description as page B and returns it to the browser, discarding page A. After viewing this page, the user now wants to return to A. In the absence of caching, page A must be reconstructed by retrieving data from the database. To avoid this, the contents of the list box can be cached prior to navigating to page B (step 3):

// Step 3: Save listbox data before making server request

Cache["listbox"]= actors.Items;

In step 6, page A is constructed using data from the cache to restore the list box.

// Step 6: Fill list box from cache

if(Cache["listbox"] !=null){

   ListItemCollection li= (ListItemCollection)Cache["listbox"];

   for(int i=0;i< li.Count; i++){

      ListItem a= li[i];

      actor.Items.Add(a);

   }

}

Cache["listbox"] = null;  // Clear cache

The cache is cleared after the page is restored; otherwise, both the cache and view state will contain the contents of the ListBox. It is worth noting that another option is to turn view state off for the ListBox (EnableViewState=false) and rely on caching to maintain its content.

Core Note

Data caching should be considered as a substitute for ViewState when data is read-only, or there is a large amount of it, or its update frequency is in minutes or hours rather than seconds, or when the data is not unique to individual users.


17.6. Creating a Web Client with WebRequest and WebResponse

As you would expect in a chapter on ASP.NET, the role of the Web client does not receive much attention. It's usually assumed to be a browser that requests a Web page specified by the user, and renders the returned HTML content. Despite the evolution of browser features, its primary role remains to display what it receives. It is of less use if you want to parse and extract portions of the received content—or want to examine the HTTP message headers. For this, you need a Web client—a program that communicates with an HTTP server and includes custom methods to perform operations on the response object. This section demonstrates how easy it is to write a Web client using .NET classes to handle the HTTP request/response duties.

WebRequest and WebResponse Classes

The System.Net namespace contains classes that are intended for applications involved with network communications. Included in these are the abstract WebRequest and WebResponse classes that contain generic properties to upload and download data given a specific Uniform Resource Identifier (URI). These classes are designed to support the response/request model—not a specific protocol. The details of that are left to descendant classes. To handle the HTTP protocol, we call on the HttpWebRequest and HttpWebResponse classes. Note that other classes are available to support the file:// URI scheme and FTP operations.

Web Client Example

This example accepts a URL, sends a request for its Web page, and uses the response object to display the server description, IP address(es) of the server, and source code for the requested Web page. Figure 17-10 shows the interface for the application.

Figure 17-10. Example using HttpWebRequest and HttpWebResponse classes

[View full size image]

The basic steps for communicating synchronously with the HTTP server are quite simple:

1.
Create an HttpWebRequest object that identifies the URL using the Create method:

request = (HttpWebRequest) WebRequest.Create(url);

2.
Get a response from the Internet resource using the WebRequest.GetResponse method:

response = (HttpWebResponse) request.GetResponse();

3.
Get the Stream used to read the body of the response and read its contents.

Stream s = response.GetResponseStream();

string strContents = new StreamReader(s).ReadToEnd();

Note that request and response are cast to HttpWebRequest and HttpWebResponse types, respectively. As mentioned, these subclasses deal specifically with the HTTP protocol (see Listing 17-9).

Listing 17-9. Using WebRequest and WebResponse to Scrape a Web Page

private void btnURL_Click(object sender, System.EventArgs e)

{

   // Fetch web page for requested URL

   HttpWebRequest request;

   HttpWebResponse response;

   if(txt_URL.Text.Length>0)

   {

      lblServer.Text="";

      tbIP.Text="";

      string serverPath= txt_URL.Text;

      string url="http://"+serverPath;

      // create a request to the url

      request = (HttpWebRequest) WebRequest.Create(url);

      request.Timeout= 7000; // timeout after 7 seconds

      try

      {

         response = (HttpWebResponse)

                  request.GetResponse();

         lblServer.Text= response.Server;

         // Get a stream to send the web page source

         Stream s = response.GetResponseStream();

         string strContents = new

                   StreamReader(s).ReadToEnd();

         // Place Web page source in text box

         HTMLViewer.Text= strContents;

         s.Close();

         ListIP(serverPath);   // List IP address(es)

      }

      catch ( Exception ex)

      {

         lblServer.Text= ex.Message;

      }

   }

   else

   {

      lblServer.Text= "Please enter a domain name.";

   }

}

private void ListIP(string uri)

{

   // List IP addresses for this domain

   // Use only server name part of URI for IP resolution

   int ndx= uri.IndexOf("/");

   if(ndx>0) uri= uri.Substring(0,ndx);

   string ips="";

   // Get a list of IP addresses for the URI

   // Dns contacts the Internet Domain Name System

   IPHostEntry IPHost = Dns.GetHostByName(uri);

   foreach(IPAddress addr in IPHost.AddressList)

   ips+= addr.ToString()+"\r\n";

   tbIP.Text= ips;

}

You may encounter Web pages that check the UserAgent property of the request object and do not allow their pages to be downloaded unless they can identify the browser. You can assign a legitimate browser value to overcome this.


17.7. HTTP Pipeline

The purpose of this section is to provide background information about the components and classes that come into play between the time a Web server receives a request and the time it finishes crafting a response. An understanding of this HTTP pipeline can be crucial to improving Web application performance, because it enables a developer to create components that affect how ASP.NET responds to requests. The references to HTTP modules and handler factories can make the subject appear daunting, but underneath there is a simple logic at work that is rooted in event handling.

Processing a Request in the Pipeline

As a request makes its way to a server, several events occur. ASP.NET permits a developer to interact with the request or response at the point of these events—through special code referred to as an HTTP module or by script in a global.asax file. Both the module and file function as event handlers, and the nature of what goes on in these event handlers is the topic of this section. Figure 17-11 depicts how a request flows from the beginning of the HTTP pipeline to the endpoint, where a handler provides a response.

Figure 17-11. Handling a request within the HTTP pipeline

Let's take a simplified look at the sequence of events that occur.

  1. Request is received by Web server. The HTTP request for the bmi.aspx resource is passed to the ASP.NET Worker Process (aspnet_wp.exe). It creates an instance of the HttpRuntime class, which initiates processing of the request.

    The processing begins with the creation of an HttpContext object. You can think of this as the central repository of all information pertaining to the request. When another class needs information about the request, it can find it in an HttpContext field or property. In addition to the HttpRequest and HttpResponse fields, the class also exposes state information through HttpSessionState and HttpApplicationState properties.

  2. HttpApplicationFactory provides the HttpApplication object to process the request. After the HttpContext object is created, the HttpRuntime calls on the static factory method HttpApplicationFactory.GetApplicationInstance to create or find an HttpApplication object to process the request.

  3. HttpApplication is initialized with HTTP modules that filter request and response. HTTP modules are used to process a request and response as they pass through the pipeline. Predefined ASP.NET modules provide authentication, caching, and session state services.

  4. Locate handler factory. After the HttpApplication object takes over the processing from the HttpRuntime, it locates the HandlerFactory that finds or creates the HTTP handler object. It does this by examining the type of request (.aspx) and searching the machine.config file or web.config for the name of the handler that maps to this request type. Different handlers are needed for the different types of resources that may be requested. For example, a request for a Web page is handled differently than one for a text or XML file.

  5. Handler processes request. In our example, the handler class is the requested .aspx page. It may seem strange that the page is a handler, but the main requirement for a handler is that it implement the IHttpHandler interface—which the .aspx page does via its Page class.

  6. Response is constructed and returned. The ProcessRequest method of the handler is called to generate the response. This method takes an HttpContext object parameter that gives it access to the requested information needed to complete the processing.

As requests and responses move through the HTTP pipeline, an ordered chain of deterministic events—starting with the HttpApplication.BeginRequest event and concluding with the HttpApplication.EndRequest event—defines each stage of the processing. In addition, random error events can arise at any time from unhandled exceptions.

A developer can greatly improve the robustness and effectiveness of an application by selectively developing handlers for these events. Handlers are typically used to trap error events and direct them to a custom error page, log session lengths and number of users, authenticate users, and adorn pages with common header and footer information. These events can be handled using either a custom application class or custom HTTP modules. We'll look at both, and see how they compare.

A third place to customize the pipeline is at the endpoint, where an HTTP handler processes a resource request. As we will see, different resources, such as .aspx or .soap files, use their own handler components. ASP.NET makes it easy to write your own handler to manage special resources

HttpApplication Class

The HttpApplication object handles most of the duties required to process a request. Its properties expose caching and application/session state information, as well as a collection of predefined HttpModules. Equally important are the set of events it exposes. We can customize an application by including code in a global.asax file to handle these events.

Table 17-5 lists the HttpApplication events in the order that they occur in the HTTP pipeline. The Error event is an exception because it can occur at any time during the process.

Table 17-5. HttpApplication Events

Event

Description

BeginRequest

Fires when ASP.NET receives a request. A request can be modified here before it reaches the page.

AuthenticateRequest

The identity of the user has been established.

AuthorizeRequest

User authorization has been verified.

ResolveRequestCache

Fires when ASP.NET determines whether to handle request from a cached page or pass request to a handler.

AcquireRequestState

Acquires the state of the current session.

PreRequestHandlerExecute

Fires before the request is sent to a handler. In the case of a request for an .aspx file, the handler is the page itself.

PostRequestHandlerExecute

Fires after the handler has packaged a response. The response object contains the text returned to the client.

ReleaseRequestState

Fires after all request handlers have been executed and ASP.NET is ready to store session state information.

UpdateRequestCache

Data being sent to client is stored in the cache for future requests.

EndRequest

Occurs when request has been processed.

Error

Occurs in response to unhandled application exception.

Your code can provide a handler for these events by creating a component that uses a delegate to subscribe to the event or define event handlers in the global.asax file. We'll demonstrate the former approach when discussing HTTP modules, but first let's look at how to use the global.asax file.

global.asax File

This file is stored in the root of the virtual directory containing the application(s) it is to be associated with. Its primary purpose is to hold the handlers that respond to the Application object's events. It is similar to an .aspx file in that it is compiled into an assembly when the Web application is first accessed. Unlike the Web page, however, it inherits from the HttpApplication class.

Here is a simple global.asax file that displays copyright information at the bottom of each page returned by an application in its virtual directory. It does this by implementing a custom handler for the EndRequest event. Notice that the format of the event handler for this, and all Application events in the global.asax file, is Application_eventname.

<%! file:global.asax %>

<%@ Application Language="C#" %>

<script runat=server>

protected void Application_EndRequest(object sender, EventArgs e)

{

   this.Context.Response.Output.Write(

      "<br>&copy2005 BMI Software");

}

</script>

Global.asax supports four other important events that are not members of the HttpApplication class: Application_Start, Application_End, Session_Start, and Session_End. The purpose of these events, which should be clear from their name, is to mark the beginning and end of an application and the sessions using it. They are the most frequently implemented event handlers in the global.asax file—performing the bookend operations of state initialization and cleanup. To illustrate this, let's extend the preceding global.asax example to keep track of the number of sessions that request an application and display this session number below the copyright.

The session counters are kept in an HttpApplicationState object represented by the Application property of the HttpApplication base class. It acts like a dictionary to hold information during the lifetime of an application (refer to Section 17.4, "Maintaining State").

As shown in Listing 17-10, counters that track the number of sessions and sessions cancelled are initialized to zero when the Application_Start event fires. The session counter is incremented when a Session_Start occurs, and the counter for terminated sessions is incremented when a Session_Ends event occurs.

Listing 17-10. Using global.asax to Count Sessions

<%! global.asax %>

<%@ Application Language="C#" %>

<script language = "C#" runat=server>

// Can also use Application_OnStart syntax

void Application_Start(Object Sender, EventArgs e)

   {

      // Initialize counters with application scope

      Application["Sessions"] = 0;

      Application["TerminatedSessions"] = 0;

   }

void Session_Start(Object Sender, EventArgs e)

   {

      Application.Lock();

      Application["Sessions"]=(int) Application["Sessions"] + 1;

      Application.UnLock();

   }

void Session_End(Object Sender, EventArgs e)

   {

      Application.Lock();

      Application["TerminatedSessions"] =

         (int) Application["TerminatedSessions"] + 1;

      Application.UnLock();

   }

protected void Application_EndRequest(object sender,

                                      EventArgs e)

   {

      string sessionCt= "Session #:

            "+Application["Sessions"].ToString();

      // Display copyright and current session number

      this.Context.Response.Output.Write(

            "<br>&copy2005 BMI Software<br>"+sessionCt);

   }

</script>

Using a Code-Behind File with global.asax

As with the .aspx file, code can be separated from the global.asax file and placed in a code-behind file. The code is compiled into a DLL and placed it in the \bin subdirectory of the application. The Inherits attribute of the Application directive points to the class implemented in the DLL.

To convert the preceding example to use a code-behind file, we remove code from the global.asax file, leaving only the Application directive:

<%! file:global.asax %>

<%@ Application  Inherits="sessionctr" %>

The code-behind file must be implemented as a class that inherits from HttpApplication. Here is a segment of the code:

// file: sessionctr.cs

using System;

using System.Web;

public class sessionctr: HttpApplication

{

   void Application_OnStart(Object Sender, EventArgs e)

   {

      Application["Sessions"] = 0;

      Application["TerminatedSessions"] = 0;

   }

   // Remainder of assembly code goes here

}

HTTP Modules

An HttpModule is a component that implements the IHttpModule interface. The role of the module is similar to that of the global.asax file: It processes events (refer to Table 17-2) associated with response and request messages in the HTTP pipeline. In many cases, you can implement the same functionality using either the module or global.asax file. We discuss the factors that affect your choice later in this section.

Listing 17-11 shows the predefined modules that ship with ASP.NET. The physical modules (assemblies) are stored in the Global Assembly Cache (GAC) and are made available by listing them in the <httpModules> section of the machine.config file. They perform such diverse tasks as user authentication, caching, and state management. Although you do not work with the modules directly, they should give you an idea of the type of pre- and post-request processing that can be performed.

Listing 17-11. HTTP Modules in machine.config File

<httpModules>

   <add name="OutputCache"

      type="System.Web.Caching.OutputCacheModule" />

   <add name="Session"

      type="System.Web.SessionState.SessionStateModule" />

   <add name="WindowsAuthentication"

      type="System.Web.Security.WindowsAuthenticationModule" />

   <add name="FormsAuthentication"

      type="System.Web.Security.FormsAuthenticationModule" />

   <add name="PassportAuthentication"

      type="System.Web.Security.PassportAuthenticationModule" />

   <add name="UrlAuthorization"

      type="System.Web.Security.UrlAuthorizationModule" />

   <add name="FileAuthorization"

      type="System.Web.Security.FileAuthorizationModule" />

   <add name="ErrorHandlerModule"

      type="System.Web.Mobile.ErrorHandlerModule,

      System.Web.Mobile, Version=1.0.5000.0, Culture=neutral,

      PublicKeyToken=b03f5f7f11d50a3a" />

</httpModules>

Implementing a Custom HttpModule

There are two minimal requirements for building a functioning HttpModule: It must contain a class that implements the IHttpModule interface and it must register an event handler to process one of the HttpApplication events. An example of this is shown in Listing 17-12. The code adds copyright information to the bottom of all pages returned by the application—exactly the same function performed by the earlier global.asax file.

The IHttpModule interface consists of two methods—Init and Dispose—that must be accounted for. We use Init to register the event handler that will be called when the EndRequest event occurs. Note that access to the events is provided through the HttpApplication object that Init receives as a parameter.

Listing 17-12. Module to Append Copyright to Web Pages

using System;

using System.Web;

namespace CustomModules{

public class FooterModule: IHttpModule

{

   public void Init(HttpApplication httpApp)

   {

      httpApp.EndRequest += new EventHandler(this.OnEndRequest);

   }

   public void Dispose() {}

   //

   public void OnEndRequest (Object obj, EventArgs e)

   {

      HttpApplication httpApp = (HttpApplication) obj;

      httpApp.Context.Response.Output.Write(

               "<br>&copy2005 BMI Software");

   }

}

}

Deploying a Custom HttpModule

To use the module with a Web application, compile it and place it in the \bin subdirectory of the application or in the Global Assembly Cache (GAC). Next, the assembly must be registered in either the web.config or machine.config file. In this example, we use a web.config file, which is placed in the root of the application's virtual directory. A portion of the file is shown here:

<configuration>

   <system.web>

      <httpModules>

         <add name="copyright"

         type="CustomModules.FooterModule, footmodule" />

      </httpModules>

   </system.web>

</configuration>

The key part is the <add> element that takes the form:

<add name=friendly name  type=class name, assembly />

This element is enclosed by the <httpModules> elements. Multiple <add> elements can be used to specify multiple modules.

URL Rewriting with a Custom Module

URL rewriting is the process of changing a requested URL so that it is redirected to another resource. This is commonly used to ensure that bookmarked links continue to work when directories and resource names are changed on a Web server.

An HTTP module provides a convenient and easy-to-use approach for rewriting a URL. The implementation logic in the module is straightforward:

  1. An HttpApplication event invokes event handler code.

  2. The event handler examines the Context.Request.Path property for the requested path.

  3. If the path URL needs to be redirected, the Context.RewritePath method is called with the substitute path.

The most important issue to be resolved in developing a rewriter is selecting an event in the request's lifecycle where the URL checking and modification should occur. The three HttpAppliction event candidates are shown in Table 17-6, along with the criteria for selecting them. (Authentication is discussed in Section 17.5.)

Table 17-6. Event Choices for Implementing a URL Rewriter Module

Event

Criteria for Selecting Event

BeginRequest

Use unless forms authentications is also used.

AuthenticateRequest

Use if Windows authentication is used.

AuthorizeRequest

Use if Forms Authentication is used and Windows authentication is not.

In general, if no authentication is being used, place the rewrite logic in an event handler for the BeginRequest event.

Listing 17-13 contains code for a simple rewriter module with the rewriting logic included in the AuthorizeRequest event handler. Look closely at this code. It first calls a static method Rewrites.getRewrites() that returns a hash table in which the keys are old URLs and the associated value is the replacement URL. The hash table is checked to see if it contains the currently requested path and if it's in the table, a call is made to Context.RewritePath. The new URL is passed to it, and the method automatically takes care of details such as including any query string with the new URL.

Listing 17-13. Custom HTTP Module to Rewrite a URL

using System;

using System.Web;

using System.Collections;

namespace CustomModules

{

   public class ReWriteModule: IHttpModule

   {

      public void Init(HttpApplication httpApp)

      {

         httpApp.AuthorizeRequest += new

               EventHandler(this.OnAuthorizeRequest);

      }

      public void Dispose() {}

      // Determine if requested URL needs to be rewritten

      public void OnAuthorizeRequest( Object obj, EventArgs e)

      {

         // Get hash table containing old and replacement URLs

         Hashtable urls = Rewrites.getRewrites();

         HttpApplication httpApp = (HttpApplication) obj;

         // Get path of requested URL

         string path = httpApp.Context.Request.Path.ToUpper();

         // See if path is in hash table

         if(urls.ContainsKey(path))

            path= (string)urls[path];

         httpApp.Context.RewritePath(path);   // Rewrite URL

      }

   }

}

The module is registered by placing the following entry in the web.config file:

<add name="redirector"

       type="CustomModules.ReWriteModule, rewritemodule" />

The process of identifying URLs to be rewritten can be implemented in various ways. This example stores the old and replacement URLs in an XML file using the following format:

<!--  File: redirector.xml   -->

<RewriterConfig>

   <RewriterRules>

      <Rule>

         <OldPath>/ideas/calculator.aspx</OldPath>

         <NewPath>/utilities/calculator.aspx</NewPath>

      </Rule>

      <Rule>

         <OldPath>/ideas/bmi.aspx</OldPath>

         <NewPath>/utilities/bminew.aspx</NewPath>

      </Rule>

   </RewriterRules>

</RewriterConfig>

This information could also be stored in the web.config file, although some overhead is required to do this. For details, refer to "Adding a Custom Configuration Section" on page 824.

Choosing Between an HTTP Module and global.asax

In many cases, the functionality you want to add to a Web server environment can be implemented using HTTP modules or the global.asax file. As a guide to making the choice, let's review their features:

  • Both can contain handlers to service HttpApplication events.

  • Global.asax can receive notification of session and application start and end events. An HTTP module cannot service these events.

  • Modules are deployed at the machine (machine.config) or virtual directory (web.config) level; global.asax operates at a directory level only.

Either can be used to handle HttpApplication events. If the feature being added applies to all applications on the server, a module is the better choice; if the feature is specific to an application, use a global.asax file.

HTTP Handlers

When a request is made for a resource, ASP.NET directs control to the appropriate handler for that resource. For example, if the request is for an .aspx file, control passes to the PageHandlerFactory component, which returns the requested .aspx page. The default ASP.NET handlers are registered in the machine.config file within the <httpHandlers> section. An extract of that file shows how the resource type is mapped to the class capable of creating a handler for it:

<httpHandlers>

   <add verb="*" path="trace.axd"

           type="System.Web.Handlers.TraceHandler" />

   <add verb="*" path="*.aspx"

           type="System.Web.UI.PageHandlerFactory" />

   <add verb="*" path="*.ashx"

           type="System.Web.UI.SimpleHandlerFactory" />

   <add verb="GET,HEAD" path="*"

           type="System.Web.StaticFileHandler" />

   ...

</httpHandlers>

An HTTP handler is a component that implements the System.Web.IhttpHandler interface. Unlike an HTTP module, only one handler is called to process a request. ASP.NET maps a request to the target handler based on information in a machine.config or web.config file. The request can be mapped directly to a handler or to a handler factory that creates or retrieves the appropriate handler. As shown in the preceding machine.config file, .aspx requests are handled by a PageHandlerFactory class. We'll look at both techniques.

Implementing a Custom HTTP Handler

ASP.NET provides handlers for .aspx, .soap, .asmx (Web services), and other standard ASP.NET file types. So why create a custom handler? The primary motivation is to support new file extensions or existing file extensions that require added processing features. For example, when a text file (.txt) is requested, ASP.NET simply returns the contents of the file. As an exercise, let's improve this with a handler that accepts a URL with the name of the text file and a query string parameter that specifies a keyword to search for in the file. The output from the handler is a listing of the text file with all instances of the keyword highlighted, as well as a count of the number of times the keyword occurs in the document.

The IHttpHandler interface contains two members that must be implemented in our custom handler class: the ProcessRequest method and the Reuseable property. As shown in Listing 17-14, ProcessRequest contains the code that processes the HTTP request; IsReusable indicates whether the instance of the handler can be reused for other requests. This is applicable only when a handler factory is providing handlers and pools them for reuse.

This handler responds to URL requests that contain a text file and query string containing a keyword to search for in the file:

HTTP://www.scilibrary.com/films.txt?kwd=Bogart

The context object provides access to the file name through the Request.PhysicalPath property. An attempt is made to open the text file and read it line by line. Each line is searched for the keyword using Regex.Match. If a match is found, the method BoldWord is called to place HTML bold (<b>) tags around the text. This method also increments the word counter.

Listing 17-14. Custom HTTP Handler to Display a Text File

// file: txthandler.cs

using System;

using System.Web;

using System.IO;

using System.Text.RegularExpressions;

public class TextHandler: IHttpHandler

{

   static int wordCt=0;

   static string BoldWord(Match m)

   {

      // Get the matched string and place bold tags around it

      string kwd = m.ToString();

      kwd="<b>"+kwd+"</b>";

      wordCt += 1;

      return kwd;

   }

   public void ProcessRequest(HttpContext ctx)

   {

   // Get file to be opened

   string filePath= ctx.Request.PhysicalPath;

   int ndx= filePath.LastIndexOf(@"\");

   string fileName = filePath.Substring(ndx+1);

   // Keyword to search for in file

   string keyWord = ctx.Request["kwd"];

   // Create HTML response

   ctx.Response.Output.Write("<html><body >");

   ctx.Response.Output.Write("File: "+fileName+

            " &nbsp;Keyword: <b>"+keyWord+"</b><hr size=1>");

   string line;

   try {

      StreamReader reader= new StreamReader(filePath);

      // Read lines of file and display in response

      while ((line = reader.ReadLine()) != null)

      {

         if (keyWord!= null) {

         // search for keyword and highlight

            string newLine = Regex.Replace(line, keyWord,

                 new MatchEvaluator(TextHandler.BoldWord),

                                    RegexOptions.IgnoreCase);

            line= newLine;

         }

         ctx.Response.Output.Write("<br>"+line);

      }

      reader.Close();

   } catch (Exception e)

   {

      ctx.Response.Output.Write("<br>"+e.Message);

   }

   // Display number of matches

   ctx.Response.Output.Write("<br><br>Word Matches: " +

          wordCt.ToString());

   ctx.Response.Output.Write("</body></html>");

   wordCt=0; // reset since it is static

   }

   public bool IsReusable

   {

      get {return false;}

   }

}

The final code is compiled and placed in the \bin subdirectory of the application.

Deploying a Custom HTTP Handler

For ASP.NET to be aware of the handler, an entry is made in the machine.config or application's web.config file. In this example, we place an <add> element in the web.config file. This element contains three attributes that define the handler and the conditions for using it. Verb specifies the type of request to be handled, such as GET or POST. The * is a wildcard character that represents any type request. The path attribute indicates the requested resource name that is mapped to the handler; and type specifies the handler class or handler factory and its containing assembly.

<httpHandlers>

   <add verb="*" path="*.txt" type="TextHandler,txthandler"/>

</httpHandlers>

The final step in deployment is to make IIS (Internet Information Service) aware that requests for the .txt extension are to be handled by ASP.NET. This is done by using Internet Services Manager to map the file extension to the ISAPI extension DLL (aspnet_isapi.dll). Follow these steps:

1. | Invoke Internet Services Manager and right-click Default Web Site.
2. | Select Properties – Home Directory – Configuration.
3. | Click Add and a pop-up window appears.
4. | Fill in the Executable field with the path to the aspnet_isapi.dll file. This file should be in the version directory of the Framework installation.
5. | Fill in the Extension field with *.txt.

With these steps completed, TextHandler is now called to process all requests for .txt files. Figure 17-12 shows output from a sample request.

Figure 17-12. Output from HTTP handler that displays text files

[View full size image]

Core Note

If an extension used by an HTTP handler is not mapped in IIS to ASP.NET, IIS attempts to return the contents of any file requested having that extension. ASP.NET never sees the request.


17.8. Summary

The ASP.NET environment offers a variety of types and configuration files that enable a developer or software architect to customize how Web requests are handled by a server. Configuration files such as web.config provide a flexible and easy way to customize the ASP.NET settings. These files contain sections that enable/disable tracing, specify the default culture of a page, define the way session state information is stored, and define an authentication/authorization security model. The security model enables an application to limit page access to users based on credentials such as their name, password, and role. Credential information may be stored in the web.config file, an XML file, or an application's proprietary database.

ASP.NET provides several mechanisms for handling both Application and Session state data. The application data is available to all sessions using that application; session data is limited to the individual session and can be configured to support Web farms and permanent storage in a database. An alternative to using an application state object is data caching. ASP.NET provides an area in memory where CacheEntry objects are stored and made available to all sessions. Output caching is also supported to enable an entire Web page or Web page fragment to be stored in a cache on the server, a proxy server, or the browser. This reduces the load on a server by allowing a Web page to be retrieved from memory rather than being recreated on the server.

Advanced Web application development can benefit from an understanding of the underlying ASP.NET architecture that supports HTTP communications. Central to this is the HTTP pipeline through which requests and responses are transported. After a request or response enters this pipeline, it can be parsed, modified, rerouted, or rejected. Both the global.asax file and HTTP module can be designed to respond to pipeline events. The event handlers they supply are used to do such things as append standard information to all responses, maintain Web statistics, and transparently reroute requests when necessary. At the end of the pipeline, a handler is responsible for providing the requested resource. Custom handlers can be written to handle specially defined resources or enhance the existing handlers.


17.9. Test Your Understanding

1:

True or False: A Web application requires at least one web.config file in its directory tree.

2:

Name the two places that trace output can be viewed.

3:

Regarding ASP.NET security:

  1. List the three types of .NET authentication available.

  2. Where is the authentication type specified?

  3. What is the difference between authentication and authorization?

  4. Which property of the Page class provides information about an authenticated user?

4:

Which mechanism(s) for storing session state information supports multiple servers?

5:

You have a reasonably static Web page that is rendered differently for different browsers and you want to refresh the cached page every three hours. What would be included in your OutputCache directive for this page?

6:

What factors influence whether state data is maintained in a data cache or an application state variable?

7:

What is resource scavenging, and how can a data cache prevent it from occurring?

8:

Fill in the blanks:

As a _________ or __________ passes through the HTTP pipeline, an __________ object fires a series of events that may be handled by a/an ___________ or in the _________ file.


Chapter 18. XML Web Services

Topics in This Chapter

  • Web Service Overview: The popularity of Web Services stems from the fact that they are implemented using the non-proprietary technologies of HTTP, XML, and SOAP. For them to become truly useful, users must have a way to locate them on the Internet. UDDI offers one such discovery service.

  • Creating a Web Service: In C#, a Web Service is created by implementing a method that is exposed to clients across the Internet. To indicate a method is available, a WebService directive is required on the Web page, and a WebMethod attribute is attached to the Web Service method.

  • Building an XML Web Service Client: The easiest way to create a Web Service client is to build a proxy using the Web Services Description Language (WSDL) provided by the Web Service. The proxy handles the physical connection to the Web Service and provides asynchronous and synchronous access.

  • WSDL and SOAP: WSDL provides the information that describes a Web Service; Simple Object Access Protocol (SOAP) is the format in which the requests and responses are delivered. An understanding of both offers added insight into how Web Services operate.

  • Web Service Examples: To satisfy real-world business needs, Web Services must deliver more than simple data types. Examples in this section show how to build a service that delivers images and how to build a client to retrieve datasets from the publicly available Amazon Web Services.

XML Web Services provide a relatively simple technique for accessing a method on an object that is running on a local or remote computer. Many embrace this lightweight approach to making Remote Procedure Calls (RPC) as technology that will spell the end to much heavier and complex solutions for distributed communications such as DCOM and CORBA. At the root of its appeal is the fact that Web Services are based on standardized technologies such as HTTP and XML that are designed to promote seamless interoperability among different operating systems. Not everyone is sold on them. Critics point out the lack of security standards, the heavy bandwidth requirements of XML, and the lack of notification and transaction services.

The chapter takes a practical look at the pluses and minuses of implementing and consuming Web Services in a .NET environment. It presents Web Services from the perspective of both the server and client. On the server side, the chapter explores how to define a Web Services class, make its methods available to HTTP clients, and access the ASP.NET Application and Session objects. The emphasis on the client side is how to use the Web Services Description Language (WSDL) contract to create a proxy class that implement calls to the Web Service.

HTTP and SOAP—a protocol that codifies how XML is used to package the request and response data that comprise a Web Service operation—are the two cornerstones of Web Services. Other protocols such as GET and POST are available, but the Simple Object Access Protocol (SOAP) has fewer limitations and is used in the majority of real-world applications. This chapter takes a look at the SOAP format as well as several issues related to SOAP, including the handling of complex data types, exception handling, and security.

We begin the exploration of the .NET Web Services architecture by creating a simple Web Service and Web Service consumer. Our first examples are created "by hand" to emphasize the underlying principles. After these are understood, we see how Visual Studio.NET simplifies the development. The chapter also demonstrates how to use the wsdl.exe tool and .NET classes that are integral to the development process. Closing out the chapter is a sample application to access the Amazon Web Services.


18.1. Introduction to Web Services

The Web Service architecture is a service-oriented architecture that enables applications to be distributed across a network or the Internet to clients using any language or operating system. As shown in Figure 18-1, Web Service communications are implemented using existing technology and standards that require no proprietary vendor support. This technology either formed the basis of the Internet or evolved from it. HTTP and XML have been discussed in earlier chapters, but let's take a brief look at them—from a Web Services perspective—along with TCP/IP and SOAP:

  • TCP/IP (Transmission Control Protocol/Internet Protocol). A communications protocol suite that forms the basis of the Internet. It's an open system that governs the flow of data between computers by breaking data into chunks that are easily routed over a network. A Web Service user or developer rarely has direct contact with this layer.

  • HTTP (Hypertext Transfer Protocol). Technically, this text-based protocol is a Remote Procedure Call (RPC) protocol that supports request/response communications. The .NET Framework as well as most production Web Services use it because it has wide support and generally allows information to sail unimpeded through firewalls. Both the HTTP POST and HTTP GET methods are supported in .NET as a way to call a Web Service. Most applications use POST, because it packages the request inside the request body (in a SOAP envelope), rather than as part of a less secure query string.

    Core Note

    The rules that describe how a message is carried within or on top of a protocol is referred to as a binding. The default protocol binding used by .NET for Web Services is HttpSoap, which should be used to access Web Services that understand SOAP. It is specified inside the System.Web section of the machine.config file.

  • XML (Extended Markup Language). We have seen in earlier chapters how data can be serialized into XML format for storage or transmission. The fact that it is text based makes it easy to work with. Just about every programming environment supports tools for encoding and decoding XML formatted data. Its inherent flexibility, extensibility, and validity checking make it attractive to Web Services that must deal with simple data types such as strings, as well as more complex data structures. There are currently two XML-based protocols used for delivering Web Services: XML-RPC and SOAP. Because .NET supports SOAP, this chapter focuses on it. After you understand SOAP, you should have no difficulty with XML-RPC if you encounter it.

  • SOAP (Simple Object Access Protocol). SOAP is defined as "a lightweight protocol for exchange of information in a decentralized, distributed environment."[1] It is not designed specifically for Web Services, nor restricted to HTTP; but its RPC specifications define a model for invoking methods on a specified target machine, passing parameters, handling faults, and receiving a method response. We will look at SOAP in detail later in the chapter. For now, keep in mind that the details of XML and SOAP are typically handled transparently by .NET. The provider code only needs to focus on implementing the method that returns the desired data; the requestor code simply places a method call and processes the returned data.

    [1] Simple Object Access Protocol (SOAP) 1.1—W3C Note, May 8, 2000.

Figure 18-1. Web Service transport

Discovering and Using a Web Service

To use a Web Service, a client must have a description of how to access the service. This information is provided by a Web Services Description Language (WSDL) document that provides the name of the service, the signature of the method(s) that can be called, the address of the service (usually a URL), and binding information that describes how the transport operation will occur. In practical terms, the WSDL information contains the methods that actually call a Web Service. We implement these methods in our client code (as source or a DLL reference) and use them as a proxy to access the service. Section 18.2, "Building an XML Web Service," provides a concrete example of using .NET to retrieve WSDL information and incorporate it into a client application.

Introduction to UDDI

Web sites are identified by a domain name and IP address that are maintained by a distributed network service known as the Domain Name System (DNS). Servers in this network are responsible for controlling e-mail delivery and translating domain names into IP addresses. The combination of DNS and Web search engines enables users to quickly locate Web content. However, neither DNS servers nor search engines provide a formal way of identifying Web Services.

To organize Web Services into a publicly searchable directory, a public consortium (www.uddi.com) comprising hundreds of companies has defined a standard known as Universal Description Discovery and Integration (UDDI). This standard defines a SOAP-based interface that can be used to publish a service or inquire about services in a UDDI-compliant registry. The registry has a business-to-business flavor about it—containing information about a company, its services, and interface specifications for any Web Services it offers. Importantly, there is no single UDDI registry. IBM, SAP, and Microsoft maintain the most prominent registries. Users may query each separately by entering a business name or service as a search term. Figure 18-2 provides an overview of the inquiry process.

Figure 18-2. Discovering and accessing a Web Service

The dialog between the client and UDDI registry server is conducted using SOAP messages. Overall, UDDI defines approximately 40 SOAP messages for inquiry and publishing.

UDDI Discovery Example

To demonstrate how to use UDDI, w'e'll look at the SOAP messages sent between client and server as we seek to discover a Web Service that can provide a stock quote. A Web-based UDDI browser (described shortly) sends and receives the messages.

Step 1: Send Discovery Request

For our example, we'll search the UDDI registry provided by Microsoft:

http://uddi.microsoft.com/inquire

An inquiry may request business information or tModel information. The former contains information about a business, including contact information and a description of services. The tModel structure is much simpler: It consists of a unique key, optional description, and a URL or pointer to a Web page where information for using the service is described. The following message requests a list of tModel keys for companies whose service includes providing a stock quote.

<Envelope>

   <Body>

      <find_tModel generic="1.0" maxRows="100">

         <findQualifiers/>

         <name>stock quote</name>

      </find_tModel>

   </Body>

</Envelope>

Step 2: Registry Service Responds with List of Services Matching Request

A list of tModel keys is returned. These keys are used for the subsequent query:

<soap:Envelope>

  <soap:Body>

    <tModelList generic="1.0" operator="Microsoft Corporation" >

      <tModelInfos>

        <tModelInfo

          tModelKey="uuid:7aa6f610-5e3c-11d7-bece-000629dc0a53">

          <name>Stock Quote</name>

        </tModelInfo>

        <tModelInfo

          tModelKey="uuid:265973ab-31cb-4890-83e0-34d9c1b385e5">

          <name>Stock Quotes and Information</name>

        </tModelInfo>

      </tModelInfos>

    </tModelList>

  </soap:Body>

</soap:Envelope>

Step 3: Retrieve Overview Document Containing WSDL

Send a request for tModel details for the service with the specified tModelKey:

<Envelope>

   <Body>

      <get_tModelDetail generic="1.0">

         <tModelKey>uuid:7aa6f610-5e3c-11d7-bece-000629dc0a53

         </tModelKey>

      </get_tModelDetail>

   </Body>

</Envelope>

The response message includes the OverviewURL element that points to a WSDL document. This document contains the information needed to create an application to access the service.

<overviewDoc>

   <description xml:lang="en">

      Get Stock quote for a company symbol

   </description>

   <overviewURL>

      http://www.webservicex.net/stockquote.asmx?WSDL

   </overviewURL>

</overviewDoc>

You can display the WSDL by pointing your browser to this URL. To invoke the Web Service, remove the query string (?WSDL) from the URL, and navigate to it with your browser.

How to Communicate with a UDDI Service Registry

To interact with a UDDI registry—whether to publish or inquire—you need a way to generate SOAP requests and interpret responses. One option is to write your own application using the Microsoft UDDI 2.0 SDK. It contains excellent C# examples that demonstrate the API and can be run against a test UDDI registry hosted by Microsoft.

For making registry inquires, Web-based UDDI browsers are available. A publicly available one—as well as a wealth of accompanying UDDI information—can be found at

http://www.soapclient.com/UDDISearch.html


18.2. Building an XML Web Service

In this section, we demonstrate how to build a Web Service by hand and then access it using a browser. We also show how to create the same Web Service using Visual Studio.NET. Although IIS is used, the examples run on any Web server.

Creating a Web Service by Hand

The first step is to select or create a virtual directory under IIS that will hold the Web Service source code file(s). Any physical directory can be mapped to an IIS virtual directory. You can use the Internet Service Manager or simply right-click the directory and select Sharing-Web Sharing. Then, assign it an alias that will be used in its URL. In our example, we will place the service in the \ws subdirectory.

After the directory has been set up, the next step is to use a text editor to create a file in this directory with the .asmx extension to contain the Web Service code. Listing 18-1 contains the code for our simple Web Service. This service exposes a method GetdayBorn that accepts three integer parameters that represent the month, day, and year for birth date. A string value containing the day of the week (Monday, Tuesday, and so on) for this date is returned. The service performs rudimentary error checking and returns an error message if the date is invalid.

Listing 18-1. Web Service to Return a Date's Day of Week—BirthDayWS.asmx

<%@ WebService Language="C#" Class="BirthDayWS.BirthDay" %>

using System;

namespace BirthDayWS

{

   public class BirthDay

   {

      [System.Web.Services.WebMethod

      (Description="Return day of week for a date")]

      public string GetDayBorn(int mo, int day, int yr)

      {

         bool err = false;

         string dob;

         if (mo <1 || mo >12) err=true;

         if (day < 1 || day > 31) err=true;

         if (err)

            dob = "Invalid Date";

         } else {

            DateTime dt = new DateTime(yr,mo,day);

            dob = dt.ToString("dddd"); // Get day

         }

         return(dob);

      }

   }

}

The code consists of a single class and a method that is invoked by a client to return the day-of-week value. In addition to the C# code that implements the logic of the service, two other elements are required: a WebService directive and a WebMethod attribute.

WebService Directive

The WebService directive identifies the file as defining a Web Service:

<%@ WebService Language="C#" Class="BirthDayWS.BirthDay" %>

The directive specifies the class implementing the XML Web Service and the programming language used in the implementation. In this example, the directive and the code for the class are present in the BirthDayWS.asmx file. Note, however, that the class can be in a separate assembly. In that case, the separate assembly is placed in a \bin directory below the Web application where the Web Service resides. If the class were in bdAssembly.dll, the WebService directive would look like this:

<%@ WebService Language="C#"

      Class="BirthDayWS.BirthDay, bdAssembly" %>

This statement would be the only line needed in the .asmx file.

WebMethod Attribute

The WebMethod attribute identifies a method as being accessible to clients making HTTP requests—that is, as an XML Web Service. Although not required, it is a good practice to include the Description property to describe the purpose of the method:

[System.Web.Services.WebMethod

       (Description="Return day of week for a date")]

The description is added to the WSDL for the service and—as we will see next—is displayed when a Web Service is accessed via a browser. The WebMethod attribute has other optional parameters, which are described later in this chapter.

Testing the Web Service

A quick way to test the newly developed Web Service is to point a browser to its location. For this example, we enter the address:

http://localhost/ws/BirthDayWS.asmx

This brings up a Web page that lists all the services (methods) available through this .asmx file as well as a Service Description link that displays WSDL information. For our example, there is only one method, GetdayBorn. Clicking it yields the page shown in Figure 18-3. This page contains the name of the class implementing the Web Service, the name of the method to be invoked, a description of the method, and text boxes for entering values to be passed to the Web Service method.

Figure 18-3. Using a browser to access the BirthDay Web Service

[View full size image]

To use the Web Service, fill in the parameter values and select the Invoke button. This causes the parameters to be sent to the Web Service using the HTTP POST protocol. The output received from the service is an XML document shown in Figure 18-4.

Figure 18-4. BirthDayWS output

[View full size image]

The output from the method is included in the string element of the XML wrapper. Fortunately, we do not have to parse the XML to retrieve this value when writing our own SOAP client. The WSDL contract provides information that allows our client to treat the remote Web Service as a method that returns data conforming to the method's type—not as XML.

Core Note

It is unnecessary to compile the .asmx file containing the Web Service in order to deploy it. On a Microsoft Windows server, the ASP.NET runtime automatically compiles the file the first time it is requested and places it in a subdirectory—named after the virtual directory—on the following path:

<%windir%>\Microsoft.NET\Framework\<version>\

Temporary ASP.NET Files\

To view the WSDL associated with this Web Service, open your browser and append ?WSDL to the URL of the .asmx file:

http://localhost/ws/BirthDayWS.asmx?WSDL

Creating a Web Service Using VS.NET

Aside from the usual advantages of IntelliSense and single key compilation (F5), the major advantage of VS.NET over a manual approach is that it automatically creates a new virtual directory under IIS to host the Web Service. This directory takes the same name as the project.

To create a Web Service with VS.NET, open it up and select ASP.NET Web Service as the project type. Assign it a project name (BirthDayWS, in this case) that reflects the purpose of the Web Service. When the project opens, select View Code and you will find that the following template class is predefined:

namespace BirthDayWS

{

   public class Service1 : System.Web.Services.WebService

   {

      public Service1()

      { InitializeComponent(); }

      private IContainer components = null;

      private void InitializeComponent()   {   }

      protected override void Dispose( bool disposing )

      {

         if(disposing && components != null)

         {

            components.Dispose();

         }

         base.Dispose(disposing);

      }

         // --> Place BirthDayWS code here

   }

}

To implement the service, rename the class to Birthday and add the code for the GeTDayBorn method. Use the browser to test the service by either compiling the code, which automatically invokes the browser, or directly pointing the browser to

http://localhost/BirthDayWS/BirthDayWS.asmx

The only significant difference between this code and our handcrafted version is that the Web class now inherits from WebService rather than the default System.Object. The service works either way, but by inheriting from WebService it gains the functionality of an ASP.NET application.

System.Web.Services.WebService Class

The WebService base class exposes properties to a Web Service that enable it to access the intrinsic ASP.NET objects introduced in Chapter 17, "The ASP.NET Application Environment."

  • Application. Provides access to an Application object that can be used to maintain state information for all sessions accessing the Web Service. For example, it can be used to keep track of how many times the service is called.

  • Session. Provides access to the HttpSessionState object that maintains session state information. It can be used, for example, to track how many times a service is called while the current session is alive. This property is available only if the EnableSession property of the WebMethodAttribute is set to true.

  • Context. Exposes the HttpContext class. Recall that this class provides a wealth of information about the current request.

  • User. Returns the IPrincipal object that provides user authentication information and can be used to determine whether the request is authorized (see Chapter 17).

To demonstrate the use of state information, let's extend our example to use the Application object to keep track of the number of times the Web Service is called.

Our first step is to add the statement in bold to the GeTDayBorn method:

dob = dt.ToString("dddd"); // extracts day

this.Application["count"] = GetCount()+1;

Next, add a method to the class that internally returns the "count" value of the Application object. Finally, add a method, GetVisitors, which allows clients to view the number of calls to the Web Service as a Web Service call:

private int GetCount()

{

   object ob = this.Application["count"];

   if (ob == null) {

      return(0);

   } else {

      return((int)ob);

   }

}

[WebMethod(Description="Number of times Web Service called.")]

public int GetVisitors()

{

   return (GetCount());

}

Extending the Web Service with the WebService and WebMethod Attributes

The BirthDayWS example demonstrates that a functioning Web Service can be created with a minimal use of .NET classes. However, before releasing a Web Service to the Internet, there are additional features that should be considered. For instance, each XML Web Service should be given a unique namespace rather than relying on the default <http://tempuria.org>, caching can be implemented to improve performance, and overloaded methods can be implemented using the WebMethod attribute.

The WebService Attribute

The optional WebService attribute (not to be confused with the WebService directive) is applied to the class implementing the XML Web Service. It has two useful properties: Description, which describes the overall Web Service, and Namespace, which sets the default XML namespace for the service. In general terms, namespaces are used to avoid naming conflicts among XML elements and attributes. In this case, it is used to make the Web Service name unique.

Let's add a WebService attribute containing a namespace and description to the class in our BirthDayWS example:

[WebService(Namespace="http://www.dateservices.com/ws/",

   Description="<b>Web Service that Provides Date

   Functions.</b>")]

public class BirthDay : System.Web.Services.WebService

It is important to note that the namespace is intended to be a unique identifier, and does not need to actually point to anything. Domains are typically used to take advantage of their uniqueness.

Figure 18-5 shows the page that is returned when we call the Web Service that is now updated with the WebService attribute and the GetVisitors method.

Figure 18-5. Updated BirthDayWS Web Service

The WebMethod Attribute

As mentioned previously, the WebMethod attribute is required to expose a method as part of a Web Service. Its properties include Description, which we have already used, EnableSession, MessageName, CacheDuration, and TRansactionOption. The latter property applies to applications that call transactional COM+ components—a topic not discussed in this book. The other three properties are useful for developing general Web Services. Let's look at the role of each.

EnableSession: Activate the Use of Session State Information

This property is used with Web Services that inherit from the WebService class. Turning it on allows a Web Service method to access session information through WebService.Session or HttpContext.Current.Session. By default, this is set to false, because maintaining session data increases memory consumption and reduces performance.

Example: [WebMethod(EnableSession=true)]   // Default is false

Be aware that Windows Forms clients do not provide the same support for session state variables that a browser does. Sessions rely on cookies to preserve state data. Unlike browsers, Windows Forms applications do not store cookies. Thus, the service thinks that each request is the first request. There is a work-around, however, which is described in the following section on building a Web Services client.

MessageName: Create Overloaded Web Operations

Suppose we want to add a method to our Web Service that accepts the month as a string rather than an integer. Because the signature is different than the original GeTDayBorn method, we can give it the same method name and C# will compile it as an overloaded method. However, when we try to access it from a browser, we receive a system exception indicating that the two methods have the same name.

The problem lies in the WSDL, which requires that each Web method in its XML elements be uniquely named, irrespective of its signature. By default, the routine in ASP.NET that generates WSDL code uses the name for both. The solution—aside from renaming the method—is to use the MessageName property to indicate a surrogate name.

[WebMethod(Description="Return day of week for any date",

       MessageName="GetDayBorn2")]

public string GetDayBorn(string month, int day, int yr){

   // Code to convert string month to int value

   string allMos= "JANFEBMARAPRMAYJUNJULAUGSEPOCTNOVDEC";

   month = month.Substring(0,3).ToUpper();

   int ndx = allMos.IndexOf(month);

   if(ndx <0) err=true; else mo = ndx/3 +1;

   // Remainder of code goes here...

CacheDuration: Caching Output from a Web Operation

Just like ASP.NET Web pages, output from a Web method can be cached to obviate executing it each time the method is called. The CacheDuration property specifies how long (in seconds) the output is to be cached. For methods that accept arguments, cached output is saved for each unique set of arguments.

Example: [WebMethod(CacheDuration=1800)]   // Value is in seconds

The only rule to follow when (if) setting up caching is to do it judiciously. Caching greatly improves performance for methods that are requested frequently and implement logic that requires lengthy processing. On the other hand, methods such as GetdayBorn in our example can actually hurt performance because most requests to it will yield unique results.

Using web.config to Configure Web Service Options

.NET Web Services support four protocols: HttpGet, HttpPost, HttpSoap, and HttpLocalHost. Of these, HttpPost and HttpGet are disabled for security reasons. The effect of this is to limit Web Service access to URLs located on http://localhost; an attempt by a remote machine to access a local Web Service results in this message being displayed:

The test form is only available for requests from the local

machine

To make a Web Service available to remote users, it is necessary to enable the HttpPost and HttpGet protocols. This is easily done by configuring the <protocol> section of the web.config file to "add" the two protocols.

<system.web>

   <webServices>

      <protocols>

         <add name="HttpGet"/>

         <add name="HttpPost"/>

      </protocols>

   </webServices>

</system.web>

Recall that navigating to a Web Services page with no parameters brings up a help page that describes how to use the service, and provides links to invoke the Web Service methods or display the WSDL. If you do not want to expose this information, you can disable the display of help pages by removing the Documentation protocol. To do so, place the following statement in the <protocols> element of the web.config file:

<remove name="Documentation" />


18.3. Building an XML Web Service Client

This section describes how to create a client application that consumes a Web Service. The sobjective is to make the client's call to the remote Web method as simple as calling a method in its own code. Although the actual implementation does not reach quite that level of simplicity, the code is straightforward, and much of it can be generated automatically using .NET tools.

Before delving into details, let's first take a high-level view of how a .NET Web Services client interacts with a Web Service.

The most important thing to observe in Figure 18-6 is that the client does not directly invoke the Web Service method. Instead, it calls a proxy object that performs this task. The proxy class is created from the WSDL information provided by the Web Service. We'll see how to do this shortly. The proxy code is a class that has the same class name and method names as the Web Service class does. It contains the transport logic that allows it to make the actual connection to the Web Service. It may do this either synchronously (without receiving confirmation) or asynchronously. The messages exchanged between the proxy and server are bundled within an HTTP request and transmitted using either the HTTP or the SOAP wire protocol.

Figure 18-6. Overview of how a client accesses a Web Service

Creating a Simple Client to Access the Web Service Class

To demonstrate the basic principles involved in creating a Web Service client, let's develop a console client application to access the BirthDayWS Web Service (refer to Figure 18-1). The client passes three arguments to the service and prints the string it returns. Recall that the service consists of the Web class Birthday and a single method GeTDayBorn:

public class BirthDay

{

   [System.Web.Services.WebMethod

        (Description="Return day of week for a date")]

public string GetDayBorn(int mo, int day, int yr)

Although the Web Service method can reside on a remote machine anywhere on the Internet, we can approach the client design as if we were within the same assembly. Here is the client code contained in the file bdClient.cs:

using System;

using System.Web.Services;

public class BirthDayClient

{

   static void Main(string[] args)

   {

      BirthDay bd = new BirthDay();

      string dayOfWeek = bd.GetDayBorn(12,20,1963);

      Console.WriteLine(dayOfWeek);

   }

}

Compiling this, of course, results in an error stating that BirthDay and bd cannot be found. We resolve this by creating a proxy class that performs the remote call, yet can be accessed locally by the client. The code for the proxy class is obtained by feeding the WSDL information that defines the Web Service into the .NET wsdl.exe utility.

Using wsdl.exe to Create a Proxy

The wsdl.exe utility reads the WSDL describing a Web Service and generates the source code for a proxy class to access the service; it can also use the information to create the skeleton code for a Web Service. This latter feature is designed for developers who prefer to design the WSDL as the first step in creating a Web Service. We do not take that approach in this chapter, but you should be aware that there are WSDL editors available for that task.

The wsdl.exe utility is run from the command line and has numerous flags or options to govern its execution. Table 18-1 lists those most commonly used.

Table 18-1. wsdl.exe Command-Line Options

Option

Description

/appsettingurlkey

/urlkey:

Specifies a key within the client's *.config file that contains the URL of the Web Service. The default is to hardcode the URL within the proxy class.

/language

/l:

Specifies the language to use for generating the proxy class. Choices are:

CS (C#), VB (Visual Basic), JS (JScript), and VJS (Visual J#).

C# is the default.

/namespace

/n:

Specifies the namespace for the proxy.

/out

Specifies file in which the generated proxy code is placed. The default is to use the XML Web Service name with an extension reflecting the language used for the code.

/protocol

The wire protocol to use within the proxy code:

/protocol:SOAP SOAP 1.1 is generated

/protocol:SOAP12 SOAP 1.2 is generated

/protocol:HttpGet or /protocol:HttpPost

/server

Generates an abstract class for the Web Service. This is used when the WSDL document is used to create a Web Service.

/serverinterface

Generates interfaces—rather than abstract classes—for the Web Service. Available only in 2.0 and later.

The following statement creates a proxy class from the BirthDayWS.asmx Web Service and places it in c:\BDProxy.cs:

wsdl.exe /out:c:\BDProxy.cs http://localhost/ws/

BirthDayWS.asmx?WSDL

This proxy source code can be used in two ways: include it in the client's source code, or compile it into a DLL and add a reference to this DLL when compiling the client code. Let's look first at the DLL approach. This command line compiles the source code and links two DLL files containing classes required by the proxy:

csc /t:library /r:system.web.services.dll /r:system.xml.dll

         BDProxy.cs

We are now ready to compile the client source code into an executable file, bdClient.exe:

csc /r:BDProxy.dll  bdClient.cs

If we add the proxy code directly to the bdClient.cs file and compile it, the result is a module that produces the same output as a version that links the proxy as a separate DLL file.

Core Note

When wsdl.exe creates proxy code, it checks the associated Web Service for compliance with the WS-I Basic Profile 1.0 standards and emits a warning if the service is noncompliant. For information on this standard, refer to the Web Services Interoperability Organization's Web site at http://www.ws-i.org.

Listing 18-2 examines the proxy source code to understand how it converts a client's call into a remote Web Service invocation.

Listing 18-2. Proxy Source Code to Access BirthDayWS.asmx Web Service

using System;

using System.ComponentModel;

using System.Diagnostics;

using System.Web.Services;

using System.Web.Services.Protocols;

using System.Xml.Serialization;

//

// Auto-generated by wsdl, Version=2.0.40607.16.

[System.Diagnostics.DebuggerStepThroughAttribute()]

[System.ComponentModel.DesignerCategoryAttribute("code")]

[System.Web.Services.WebServiceBindingAttribute(

       Name="BirthDaySoap",

       Namespace="http://tempuri.org/")]

public class BirthDay : SoapHttpClientProtocol {

   private System.Threading.SendOrPostCallback

         GetDayBornOperationCompleted;

   public BirthDay() {

         this.Url = "http://localhost/ws/BirthDayWS.asmx";

   }

   public event GetDayBornCompletedEventHandler

         GetDayBornCompleted;

   // (1) Synchronous Call to Web Service

   [SoapDocumentMethodAttribute(

      "http://tempuri.org/GetDayBorn",

      RequestNamespace="http://tempuri.org/",

      ResponseNamespace="http://tempuri.org/",

      Use=

      System.Web.Services.Description.SoapBindingUse.Literal,

      ParameterStyle=

      System.Web.Services.Protocols.SoapParameterStyle.Wrapped)]

   public string GetDayBorn(int mo, int day, int yr)

   {

      object[] results = this.Invoke("GetDayBorn", new object[] {

            mo,

            day,

            yr});

      return ((string)(results[0]));

   }

   // (2) Asynchronous call to Web Service

   public System.IAsyncResult BeginGetDayBorn(int mo, int day,

         int yr, System.AsyncCallback callback,

         object asyncState) {

      return this.BeginInvoke("GetDayBorn", new object[] {

            mo,

            day,

            yr}, callback, asyncState);

   }

   public string EndGetDayBorn(System.IAsyncResult asyncResult) {

         object[] results = this.EndInvoke(asyncResult);

         return ((string)(results[0]));

   }

   // (3) Call this for event-based asynchronous handling

   public void GetDayBornAsync(int mo, int day, int yr) {

         this.GetDayBornAsync(mo, day, yr, null);

   }

   public void GetDayBornAsync(int mo, int day, int yr,

         object userState) {

      if ((this.GetDayBornOperationCompleted == null)) {

            this.GetDayBornOperationCompleted = new

            System.Threading.SendOrPostCallback(

               this.OnGetDayBornOperationCompleted);

      }

      this.InvokeAsync("GetDayBorn", new object[] {

            mo,

            day,

            yr}, this.GetDayBornOperationCompleted,

            userState);

   }

   private void OnGetDayBornOperationCompleted(object arg) {

      if ((this.GetDayBornCompleted != null)) {

            InvokeCompletedEventArgs invokeArgs =

               ((InvokeCompletedEventArgs)(arg));

            this.GetDayBornCompleted(this, new

               GetDayBornCompletedEventArgs(invokeArgs.Results,

               invokeArgs.Error, invokeArgs.Cancelled,

               invokeArgs.UserState));

      }

   }

   public new void CancelAsync(object userState) {

         base.CancelAsync(userState);

   }

}

public delegate void GetDayBornCompletedEventHandler(

      object sender, GetDayBornCompletedEventArgs args);

public class

      GetDayBornCompletedEventArgs :AsyncCompletedEventArgs

{

   private object[] results;

   internal GetDayBornCompletedEventArgs(object[] results,

      System.Exception exception, bool cancelled,

      object userState) : base(exception, cancelled, userState) {

         this.results = results;

   }

   // Results are available as a property

   public string Result {

     get {

           this.RaiseExceptionIfNecessary();

           return ((string)(this.results[0]));

      }

   }

}

Observe that the proxy class has the same name as the Web Service class (Birthday) it represents, and implements a method (GeTDayBorn) having the same name as the Web Service method. The supporting code is quite different, however. It contains transport logic rather than application logic. This code uses methods provided by the System.Web.Services.Protocols.SoapHttpClientProtocol class from which the proxy class derives. Table 18-2 summarizes of the more useful members of the class.

Table 18-2. Selected Members of SoapHttpClientProtocol

Member

Description

BeginInvoke()

Begins an asynchronous invocation of the Web method.

EndInvoke()

Ends an asynchronous invocation of the Web method.

Invoke()

Invokes the Web method synchronously.

CookieContainer

Gets or sets the collection of cookies. This permits a proxy-based client to accept cookies and allow a server to maintain state information.

Proxy

Gets or sets proxy information needed to make a Web Service call through a firewall.

Timeout

Gets or sets the timeout (in milliseconds) a client waits for a synchronous call to a Web Service to be completed.

Url

Gets or sets the URL used to access the Web server.

Let's look at how these members are used in a proxy class.

Synchronous Calls to a Web Service Method

A proxy provides the capability to invoke a Web Service method synchronously or asynchronously. Recall from Chapter 13, "Asynchronous Programming and Multithreading," that a synchronous operation uses only one thread, which is blocked until control returns from the called method. An asynchronous call, on the other hand, creates a worker thread that handles the operations of the called method. Control returns immediately to the main thread while a separate thread handles the auxiliary task. The calling routine is notified when the task is completed.

Synchronous communication is implemented with a proxy method that has the same name as the Web method and uses the Invoke method to request the service.

public string GetDayBorn(int mo, int day, int yr) {

     object[] results = this.Invoke("GetDayBorn", new object[] {

           mo, day, yr});

     return ((string)(results[0]));

}

This is the most intuitive way to access a service because a client can be totally unaware of the proxy and execute a call to the Web Service method, using its actual name. However, because a synchronous call by definition does not require a response, the client code should include a timeout interval and handle any exception that occurs if the interval is exceeded. The following code sets a timeout of four seconds and handles any System.Net.WebException that is thrown.

// Must add using System.Net;

BirthDay bd = new BirthDay();

bd.Timeout= 4000;         // Set timeout to 4 seconds

try

{

   string dayOfWeek = bd.GetDayBorn(12,20,1963);

   Console.WriteLine(dayOfWeek);

}

catch (WebException ex)

{

   Console.WriteLine(ex.Message);  // Will report timeout

}

catch (Exception ex)

{

   Console.WriteLine("Unknown error occurred.");

}

Core Note

An easy way to test the timeout exception is to add a Thread.Sleep() call within the Web method being called on the Web server. This suspends thread execution for a specified amount of time. For example:

System.Threading.Thread.Sleep(3000);  // Sleep 3 seconds

Asynchronous Calls to a Web Service Method

Asynchronous communication is performed using the Begin methodname and End methodname methods supplied by the proxy. Internally, these call the BeginInvoke and EndInvoke methods, respectively. BeginInvoke queues the method to be run on a separate (worker) thread. It returns an IAsyncResult type that is subsequently passed to the EndInvoke method to retrieve the results. As shown in the following code, the IAsyncResult type is also used to determine if the call has been completed.

BirthDay bd = new BirthDay();

// Pass null callback argument since we will poll status

IAsyncResult dayOfWeek =bd.BeginGetDayBorn(12,20,1963,null,null);

// Perform other operations here while waiting for response

If (dayOfWeek.IsCompleted)

      Console.Write(bd.EndGetDayBorn(dayOfWeek));

This "polling" approach can be used to periodically check the status of the request. Another approach is to have the Web Service call a method in the client code when the response is ready. The AsyncCallback delegate is used to specify the method that receives the callback.

// Create delegate that specifies method to be called back

AsyncCallback wscb = new AsyncCallback(DayReturned);

BirthDay bd = new BirthDay();

IAsyncResult dayOfWeek =

      bd.BeginGetDayBorn(12,20,1963,wscb,null);

// This method is called back when the web method completes

public static void DayReturned(IAsyncResult result)

{

      BirthDay bd = new BirthDay();

      Console.WriteLine(bd.EndGetDayBorn(result));

}

As with polling, the method implementing EndInvoke (EndGetDayBorn, in this example) is called to return the result.

Event-Based Asynchronous Calls

If you take a close look at the proxy code in Listing 18-2, you'll notice that it includes a delegate and corresponding event declaration:

public event GetDayBornCompletedEventHandler

      GetDayBornCompleted;

public delegate void GetDayBornCompletedEventHandler(

      object sender, GetDayBornCompletedEventArgs args);

These enable client code to treat the Web Service invocation as an event that can be handled by a local event handler. Only a few lines of code are required to hook up an event handler to the event defined in the proxy:

Birthday service = new Birthday();

// (1) Associate event handler with event

service.GetDayBornCompleted += new

      GetDayBornCompletedEventHandler(this.ShowOutput);

// (2) Invoke service asynchronously

service.GetDayBornAsync();

// (3) Event handler called when service returns value

private void ShowOutput(object sender,

      GetDayBornCompletedEventArgs args)

{

   Console.WriteLine(args.Result);

}

The Web Service is invoked asynchronously by calling the proxy provided method GetdayBornAsync (<web service name>Async). When the Web Service finishes execution, .NET returns the results to the event handler through the args parameter.

Using the CookieContainer Property to Maintain Session State

Setting a Web Service method's EnableSession attribute to true enables the method to maintain state information. However, this is only effective if the client can accept cookies. This is not usually a problem with browsers, but Windows Forms (WinForms) applications do not store cookies by default. The CookieContainer property is a way to permit WinForms clients to accept cookies.

To permit the client to accept cookies, set the CookieContainer property of the proxy class instance to a CookieContainer object. The code shown here permits the Web Service to maintain state information over all queries made by this client.

// BirthDay bd = new BirthDay() .. must have class level scope

if (bd.CookieContainer == null)

   bd.CookieContainer = new CookieContainer();

IAsyncResult dayOfWeek =  bd.BeginGetDay-

Born(mo,dy,yr,null,null);

Console.WriteLine(bd.EndGetDayBorn(dayOfWeek));

// List all cookies in CookieContainer collection

// Create a Uniform Resource Identifier object

Uri hostURL = new Uri("http://localhost");

foreach (Cookie ck in bd.CookieContainer.GetCookies(hostURL))

Console.WriteLine(ck.Name+" "+ck.TimeStamp.ToString());

Notice that the CookieContainer class has a method GetCookies that returns a collection of cookies stored in the container. In the example, foreach is used to enumerate the collection and list the name of each cookie along with the date it was created.

Creating a Proxy with Visual Studio.NET

We have shown how the proxy code can be generated using wsdl.exe and compiled into a DLL or used as source code within the client application. If you use VS.NET to create your Web Service client, you have no need for the wsdl.exe utility. Instead, VS.NET lets you select a Web Service for which you want to generate the proxy code. It adds the code to the project, and you can use the techniques described in the preceding examples to access it. Here are the details:

1.
Open up .NET and select a Windows application.

2.
Select Add Web Reference under the Project menu tab. The subsequent screen contains a text box at the top in which you enter the URL of the Web Service. For example:

http://localhost/ws/BirthDayWS.asmx

3.
Press the Enter key and a screen appears that includes a Web Service Help page for the selected service. Click the Add Reference button and the source code containing the proxy is added to the project.

4.
The proxy code has its own namespace that should be added to the client code. In this example, wsdlclient is the name of the VS.NET project.

using wsdlclient.localhost;

You will see that a Web References folder has been added to the directory of the project. To view the proxy source, click the Show All Files buttons in the Solution Explorer and open the Reference.cs file beneath the localhost node. Notice that in addition to the proxy code, the directory includes the WSDL file from which the proxy was generated. We'll look at this file in detail in the next section to gain an understanding of how WSDL describes a Web Service. In addition, we will look at how SOAP is used to define the format of messages transported between the Web Service consumer and provider.


18.4. Understanding WSDL and SOAP

The purpose of this section is to add some depth to the topic of Web Services—to look underneath at the XML grammar that is used to describe the service (WSDL) and the protocol (SOAP) used for transporting data between the service and client. Be aware that both of these have been submitted to the World Wide Web Consortium (W3C), but neither is an official standard.

Web Services Description Language (WSDL)

WSDL is broadly defined as "an XML format for describing network services as a set of endpoints operating on messages containing either document-oriented or procedure-oriented information."[2] In our case, the endpoints are the client and a Web Service, and WSDL defines how the client interacts with the service.

[2] Web Services Descriptive Language (WSDL) 1.1—W3C Note, March 15, 2002.

When developing a Web Service from the ground up, it is good practice to develop the interface definition first—in the form of WSDL—and then map it to the implementation code. Although the sheer complexity of WSDL demands that WSDL editors be used for the task, there is still a need for the developer to have a general understanding of the WSDL structure. The same is true even if you work only on the client side and use wsdl.exe to create source code directly from the WSDL. Problems can arise using utilities and editors that require a familiarity with the format in order to perform troubleshooting. For example, a well-designed WSDL interface is often built from multiple documents tied together by an XML import element. Being familiar with the semantics and existence of the import element goes a long way toward solving import issues—a not uncommon source of WSDL errors.

This section introduces the basic structure of WSDL, which should satisfy the curiosity of the occasional Web Service consumer or developer. Those interested in learning more should refer to the specification that is published by W3C at http://www.w3.org/TR/wsdl.

The WSDL Structure

Specifications for the WSDL grammar define six major elements: definitions, types, message, port type, binding, and service. Let's discuss these within the context of the WSDL file that describes the sample BirthDayWS Web Service.

<Definitions>

This is the root element of the WSDL document. It declares multiple namespaces used throughout the document, and contains all of the other elements:

<definitions xmlns:http="http://schemas.xmlsoap.org/wsdl/http/"

   xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"

   xmlns:s="http://www.w3.org/2001/XMLSchema"

   xmlns:s0="http://tempuri.org/"

   xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/"

   xmlns:tm="http://microsoft.com/wsdl/mime/textMatching/"

   xmlns:mime="http://schemas.xmlsoap.org/wsdl/mime/"

   targetNamespace="http://tempuri.org/"

   xmlns="http://schemas.xmlsoap.org/wsdl/">

Namespaces are used to distinguish elements because it is possible that elements from different namespaces could have the same name.

<Types>

This element contains an XSD (XML Schema Definition Language) schema that describes the data types publicly exposed by the service: the parameters passed in the Web Service request, and the response:

<types>

   <s:schema elementFormDefault="qualified"

       targetNamespace="http://tempuri.org/">

   <s:element name="GetDayBorn">

   <s:complexType>

   <s:sequence>

   <s:element minOccurs="1" maxOccurs="1" name="mo"

      type="s:int" />

   <s:element minOccurs="1" maxOccurs="1" name="day"

      type="s:int" />

   <s:element minOccurs="1" maxOccurs="1" name="yr"

      type="s:int" />

   </s:sequence>

   </s:complexType>

   </s:element>

   <s:element name="GetDayBornResponse">

   <s:complexType>

   <s:sequence>

   <s:element minOccurs="0" maxOccurs="1" name="GetDayBornResult"

      type="s:string" />

   </s:sequence>

   </s:complexType>

   </s:element>

   </s:schema>

</types>

<Message>

Defines the data that is exchanged between the Web Service provider and consumer. Each message is assigned a unique name and defines its parameters—if any—in terms of names provided by the types element:

<message name="GetDayBornSoapIn">

   <part name="parameters" element="s0:GetDayBorn" />

   </message>

   <message name="GetDayBornSoapOut">

   <part name="parameters" element="s0:GetDayBornResponse" />

   </message>

<PortType>

Each <portType> element defines the <Message> elements that belong to a communications transport. The name attribute specifies the name for the transport. The <portType> element contains <operation> elements that correspond to the methods in the Web Service. The <input> and <output> elements define the messages associated with the operation. Four types of operations are supported: one-way, in which the service receives a message; request-response, in which the client sends a request; solicit-response, in which the service first sends a message to the client; and notification, where the service sends a message to clients.

<portType name="BirthDaySoap">

   <operation name="GetDayBorn">

   <documentation>Return day of week for any date</documentation>

   <input message="s0:GetDayBornSoapIn" />

   <output message="s0:GetDayBornSoapOut" />

   </operation>

</portType>

<Binding>

A set of rules that describe how the <portType> operation is transmitted over the wire. Wire protocols available are HTTP GET, HTTP POST, and SOAP. This example demonstrates how SOAP is specified.

As acknowledgement of the importance of SOAP as a transport protocol, the WSDL 1.1 specification includes extensions for SOAP 1.1. These extension elements include <binding>, <operation>, and <body>.

<binding name="BirthDaySoap" type="s0:BirthDaySoap">

   <soap:binding transport=

      "http://schemas.xmlsoap.org/soap/http"

      style="document" />

   <operation name="GetDayBorn">

      <soap:operation soapAction="http://tempuri.org/GetDayBorn"

         style="document" />

      <input>

         <soap:body use="literal" />

      </input>

      <output>

         <soap:body use="literal" />

      </output>

   </operation>

</binding>

Note that the <operation> element specifies the entry point for the Web method that is called on the server. One other thing to be aware of is the style attribute in the binding element. This value, which may be document or rpc, specifies how an operation is formatted. By default, .NET sets this value to document. To specify rpc, you must apply the SoapRpcMethodAttribute to the Web method:

[SoapRpcMethod][WebMethod]

public string GetDayBorn(string month, int day, int yr)

Although there is a rather spirited debate among WSDL purists as to which is better, you can safely ignore the histrionics and use the .NET default. However, knowing your options will enable you to easily work with third parties that may have a preference.

<Service>

Identifies the location of the Web Service. Specifically, it lists the name of the Web Service class, the URL, and references the binding for this endpoint.

<service name="BirthDay">

   <port name="BirthDaySoap" binding="s0:BirthDaySoap">

      <soap:address location=

      "http://localhost/ws/BirthDayWs.asmx" />

   </port>

</service>

Simple Object Access Protocol (SOAP)

SOAP is a platform-neutral protocol for exchanging information. Its cross-platform capabilities are attributable to its use of XML to define the data being passed and support for HTTP as a communications protocol. SOAP is the most popular and flexible protocol for the exchange of information between Web Service consumers and providers. Its format allows it to define complex data structures not supported by the competing HTTP GET and POST protocols.

Our discussion of SOAP follows the same approach used with WSDL: We examine the basic features of SOAP using the request/response messages generated from the BirthDayWS Web Service example. The format of these messages is described on the Web page containing the desired method(s) of the Web Service.

A SOAP Request Message

The header for a SOAP request reveals that the SOAP request is packaged as an HTTP POST request to the server designated by the Host field. The length field specifies the number of characters in the body of the POST, and SOAPAction indicates the namespace and method to be contacted.

POST /ws/BirthDayWS.asmx HTTP/1.1

Host: localhost

Content-Type: text/xml; charset=utf-8

Content-Length: length

SOAPAction: "http://tempuri.org/GetDayBorn"

Listing 18-3 shows the XML template for the SOAP message that is sent to the server.

Listing 18-3. GetdayBorn SOAP Request Content

<?xml version="1.0" encoding="utf-8"?>

<soap:Envelope

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001

  /XMLSchema" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:tns="http:/

  /tempuri.org/" xmlns:types="http://tempuri.org/encodedTypes" xmlns:soap="http://schemas

  .xmlsoap.org/soap/envelope/">

   <soap:Body

      soap:encodingStyle=

         "http://schemas.xmlsoap.org/soap/encoding/">

      <tns:GetDayBorn>

         <mo xsi:type="xsd:int">int</mo>

         <day xsi:type="xsd:int">int</day>

         <yr xsi:type="xsd:int">int</yr>

      </tns:GetDayBorn>

   </soap:Body>

</soap:Envelope>

The overall structure of a SOAP message is not complex. It is an XML document that has a mandatory root element, <Envelope>, an optional <Header> element, and a mandatory <Body>.

A SOAP envelope, as the name implies, is conceptually a container for the message. The SOAP header represents a way to extend the basic message. It may contain additional information about the message and—as we will see later—can be used to add a measure of security. The SOAP body contains what one would regard as the actual data: the arguments sent to the service and the response. The contents of the <Body> element in this example consist of the method name and its three parameters that correspond to the call made within the client code:

string dayOfWeek = bd.GetDayBorn(12,20,1963);

A SOAP Response Message

The SOAP body of the response includes a <GetDayBornResult> element (see Listing 18-4) that contains the response from the Web Service and identifies it as a string type.

Listing 18-4. GeTDayBorn SOAP Response Content

<?xml version="1.0" encoding="utf-8"?>

<soap:Envelope

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xmlns:xsd="http://www.w3.org/2001/XMLSchema"xmlns:soap-

enc="http://schemas.xmlsoap.org/soap/encoding/"

xmlns:tns="http://tempuri.org/" xmlns:types="http://tem-

puri.org/encodedTypes" xmlns:soap="http://schemas.xml-

soap.org/soap/envelope/">

   <soap:Body

      soap:encodingStyle=

         "http://schemas.xmlsoap.org/soap/encoding/">

      <tns:GetDayBornResponse>

         <GetDayBornResult

            xsi:type="xsd:string">string</GetDayBornResult>

      </tns:GetDayBornResponse>

   </soap:Body>

</soap:Envelope>

Using the SOAP Header for User Authentication

The optional SOAP header is available for adding miscellaneous information about its associated SOAP message. One popular use for this header is to include identification information about the user making the request. This enables user authentication to be performed by the methods within the Web Service.

Core Note

A question that arises when a Web Service is being invoked from a Web page is whether Forms Authentication (Chapter 17) can be used. The short answer is yes, but it requires a special implementation. Unlike a regular Web page, a Web Service does not recognize the authentication cookie created by the separate login screen. The solution is to add a login method to the Web Services that creates the authentication cookie. In addition, each service must check user authentication before performing its operation. The coding requirements are comparable to using SOAP header authentication.

Listing 18-5 demonstrates a Web Service that checks the header for an ID and password before making the service available to the requestor. It consists of the code taken from Listing 18-1 and updated to include features required to access a SOAP header.

Listing 18-5. Authenticate Web Service User by Checking SOAP Header

using System;

using System.Web.Services;

// Required for SoapException

using System.Web.Services.Protocols;

namespace BirthDayWS

{

// (1) Class to hold data passed in the header

   public class SOAPHeaderContent : SoapHeader

   {

      public string UserName;

      public string PassWord;

   }

   public class BirthDay : System.Web.Services.WebService

   {

      // (2) Member class accessing header data

      public SOAPHeaderContent headerInfo;

      // (3) Add SoapHeader attribute

      [WebMethod(Description="Return day of week for any date"),

            SoapHeader("headerInfo")]

      public string GetDayBorn(int mo, int day, int yr)

      {

         if (!Verify())

         {

            throw new SoapException(

                  "Valid User info not included.",

                  SoapException.ClientFaultCode);

         } else {

            bool err = false;

            string dob;

            if (mo <1 || mo >12) err=true;

            if (day < 1 || day > 31) err=true;

            if ( ! err)

            {

               DateTime dt = new DateTime(yr,mo,day);

               dob = dt.ToString("dddd"); // extracts day

            }  else {

               dob = "Invalid Date";

            }

            return(dob);

         }

      }

      // Method to check password and ID in SOAP header

      private bool Verify()

      {

         // (4) Access data in header

         if (headerInfo.UserName != "Vincent" ||

             headerInfo.PassWord != "arles")

         { return(false);

         } else { return(true);}

      }

   }

}

This example illustrates the four steps that are followed to receive and access any SOAP header data:

1. | Create a class to represent the data in the header. This class must inherit from the SoapHeader class. SOAPHeaderContent serves the purpose in this code.
2. | Add a member to the Web Service class that is the same type as the class created in Step 1. The example uses headerInfo.
3. | Apply a SoapHeader attribute that references the member created in Step 2. Applying this to a method makes the information in the header available to the method.
4. | Process the header data by accessing the members of the class created in Step 1. The Verify method contains the simple logic to check PassWord and UserName. In reality, the comparison information would come from a database rather than being hardcoded.

The proxy for this Web Service includes the class that represents the header contents. The client creates an instance of this class and assigns a password and user name. The class instance is assigned to a field that is now a member of the proxy class representing the Web Service class. In this example, Birthday now has a field named SOAPHeaderContentValue. As you have probably guessed, .NET creates this field name by appending Value to the name of the class that accesses the header info (SOAPHeader).

using System;

using System.Web.Services;

using system.Web.Services.Protocols;

using System.Text.RegularExpressions;

public class BirthDayClient

{

   static void Main(string[] args)

   {

      SOAPHeaderContent acctInfo = new SOAPHeaderContent();

      acctInfo.UserName = "Vincent";

      acctInfo.PassWord = "arles";

      BirthDay bd = new BirthDay();

      bd.SOAPHeaderContentValue = acctInfo;

      try {

         string dayOfWeek = bd.GetDayBorn(12,20,1963);

         Console.WriteLine(dayOfWeek);

      } catch (SoapException ex)

         {

            // Extract Soap error message

            // Be sure to add:

            // using System.Text.RegularExperssions

            Match errMatch = Regex.Match(ex.Message,":(.*)");

            Console.WriteLine(errMatch.Groups[1]);

         }

   }

}

Handling SOAP Exceptions

As shown in Listing 18-5, the Web Service throws an exception if the user name and password cannot be authenticated:

throw new SoapException("Valid User info not included.",

      SoapException.ClientFaultCode);

The exception is rendered as a SoapException object. In this example, its constructor receives a message and a SOAP fault code that signifies the type of error. This information is returned to the client as a <Fault> element in the SOAP body and is converted by the .NET Framework back to a SoapException object that can be processed by the client's code.

The SoapException object includes four properties that provide information about the exception:

  • Message. The error message that explains the reason for the exception.

  • Actor. The URL of the Web Service that threw the exception.

  • Code. An XMLQualifiedName object that specifies one of four SOAP fault codes that categorize the exception. These fault codes are represented as static fields of the SoapException class.

  • Detail. An XMLNode object containing application-specific information about the error.

The Message and Code properties are used most frequently in processing a SOAP exception. The message is verbose: It consists of the full namespace qualification of the SoapException class followed by the actual message and the name of the Web method where the exception occurred. A regex was used in the preceding client code to extract the actual message.

The SoapException class contains static fields that can be compared with the Code value to broadly classify the exception. These fields include the following:

  • VersionMismatchFaultCode. The SOAP envelope has an invalid namespace.

  • ClientFaultCode. The message sent by the client is incorrectly formatted or contains incorrect information.

  • ServerFaultCode. The error occurs on the server and is not related to the SOAP message. An example would be a network or hardware problem.

  • MustUnderstandFaultCode. A SOAP element marked with the MustUnderstand attribute set to TRue is not processed. This is an attribute that indicates the particular element must be processed.

Here is a code sample that demonstrates using the Code property. It first extracts a message embedded in the Message property and prints it. Then it checks the Code property to classify the excepton.

try {

      string dayOfWeek = bd.GetDayBorn(12,20,1963);

      Console.WriteLine(dayOfWeek);

    }

catch (SoapException ex)

    {

       Match errMatch = Regex.Match(ex.Message,":(.*)");

       Console.WriteLine(errMatch.Groups[1]);

       // Check various fault codes here

       if(ex.Code == SoapException.ClientFaultCode)

       {

          Console.WriteLine("Problem with Client message.");

       }

       if(ex.Code == SoapException.ServerFaultCode)

       {

          Console.WriteLine("Problem with Server. Try again.");

       }

    }

SOAP Security

The preceding example illustrates a lightweight technique for using SOAP headers to authenticate a user. Its main drawback is that the contents of the SOAP headers are passed as cleartext. To overcome this, you can add a layer of encryption by using this technique in conjunction with Secure Sockets Layer (SSL).

As an alternative to this and other ad hoc approaches to SOAP security, there is now a Web Services Security (WS-Security) specification [3] that defines enhancements to SOAP messaging. Its objectives are to ensure the following:

[3] http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0.pdf

  • Authentication. How a SOAP message expresses the identity of its sender.

  • Integrity. How to verify that a SOAP message has not been tampered with.

  • Confidentiality. What protects the contents of a SOAP message from being read by an intermediary.

In support of this specification, Microsoft provides an add-on to the .NET Framework known as Web Services Enhancements 2.0 (WSE 2.0). This tool set comprises a .NET class library that can be used to integrate the WS-Security specification into Web Service applications. Note that the use of these enhancements requires .NET on both the client and service provider endpoints, so it's not yet a generic Web Service security solution. See the MSDN Web site, msdn.microsoft.com/webservices/webservices/building/wse/default.aspx, for whitepapers and a free download of the enhancements library


18.5. Using Web Services with Complex Data Types

The BirthDayWS Web Service used throughout this chapter accepts integers as input and returns a string value. This is useful for introducing Web Service principles because HTTP GET, HTTP POST, and SOAP all support it. However, Web Services also have the capability of serving up more complex data types such as data sets, hash tables, images, and custom objects.

Before data can be sent to or from a Web Service, it is serialized using XML serialization. Conversely, it is deserialized on the receiving end so it can be restored to its original type. As we saw in Chapter 4, "Working with Objects in C#," not all data can be serialized. Thus, when designing a Web Service, it is important to understand restrictions that apply to serialization:

  • XML serialization can only be used with classes that contain a public parameterless constructor. For example, you may have a Web Service that returns a hash table because it has the constructor public Hashtable(). On the other hand, the Bitmap class does not have a parameterless constructor and cannot be used as a return type.

  • Read-only properties in a class cannot be serialized. The property must have a get and set accessor and be public.

  • Fields must be public to be serialized; private ones are ignored.

In this section, we work with two Web Service examples that illustrate the use of complex data. Our first example creates a Web Service that accepts the name of an image and returns it as a byte stream. The second example creates a client to use the Amazon Web Services provided by Amazon.com, Inc. These services offer a rich—but practical—sampling of accessing multiple Web methods and processing a wide variety of custom classes.

A Web Service to Return Images

Image manipulation usually requires representing an image as a Bitmap object. However, because bitmaps cannot be serialized and transferred directly, we must find an indirect way to transport an image. The not-so-difficult solution is to break the image into bytes and return a byte stream to the client, who is responsible for transforming the stream to an image.

The logic on the server side is straightforward: a FileStream is opened and associated with the image file. Its contents are read into memory and converted to a byte array using

tempStream.ToArray()

This byte array is then sent to the Web client (see Listing 18-6).

Listing 18-6. Web Service to Return an Image as a String of Bytes

<%@ WebService Language="C#" Class="WSImages" %>

using System;

using System.Web.Services;

using System.IO;

using System.Web.Services.Protocols;

public class WSImages: System.Web.Services.WebService {

   [WebMethod(Description="Request an Image")]

   public byte[] GetImage(string imgName) {

      byte[] imgArray;

      imgArray = getBinaryFile("c:\\"+imgName+".gif");

      if (imgArray.Length <2)

      {

         throw new SoapException(

           "Could not open image on server.",

            SoapException.ServerFaultCode);

      } else

      {

         return(imgArray);

      }

   }

   public byte[] getBinaryFile(string filename)

   {

      if(File.Exists(filename)) {

         try {

            FileStream s = File.OpenRead(filename);

            return ConvertStreamToByteBuffer(s);

         }

         catch(Exception e)

         {

            return new byte[0];

         }

      } else { return new byte[0]; }

   }

   // Write image to memory as a stream of bytes

   public byte[] ConvertStreamToByteBuffer(Stream imgStream) {

      int imgByte;

      MemoryStream tempStream = new MemoryStream();

      while((imgByte=imgStream.ReadByte())!=-1) {

         tempStream.WriteByte(((byte)imgByte));

      }

      return tempStream.ToArray();  // Convert to array of bytes

   }

}

Our client code receives the byte stream representing an image and reassembles it into a Bitmap object. Because the Bitmap constructor accepts a stream type, we convert the byte array to a MemoryStream and pass it to the constructor. It can now be manipulated as an image.

WSImages myImage = new WSImages();

try {

   // Request an image from the Web Service

   byte[] image = myImage.GetImage("stanwyck");

   MemoryStream memStream = new MemoryStream(image);

   Console.WriteLine(memStream.Length);

   // Convert memory stream to a Bitmap

   Bitmap bm = new Bitmap(memStream);

   // Save image returned to local disk

   bm.Save("c:\\bstanwyck.jpg",

        System.Drawing.Imaging.ImageFormat.Jpeg);

}

catch (WebException ex)

{

   Console.WriteLine(ex.Message);

}

Using Amazon Web Services

To use the Amazon E-Commerce Service, you must register for a developer's token, which is required as part of all requests made to the Web Services. In addition, you should download (http://www.amazon.com/webservices) the developer's kit that contains the latest documentation, examples, and—most importantly—a WSDL file defining all the services.

An examination of the WSDL file reveals that AmazonSearchService is the Web Service class that contains the numerous search methods available to clients. These methods provide the ability to search the Amazon product database by keyword, author, artist, ISBN number, manufacturer, actor, and a number of other criteria. Each search method takes a search object as an argument that describes the request to the server and returns a ProductInfo object. For example, a request to search by keywords looks like this:

AmazonSearchService amazon = new AmazonSearchService();

KeywordRequest kwRequest = new KeywordRequest();

// Set fields for kwRequest

ProductInfo products = amazon.KeywordSearchRequest(kwRequest);

Sending a Request with the AmazonSearchService Class

Table 18-3 contains a sampling of the methods available for searching Amazon products. These methods are for accessing the Web Service synchronously. An asynchronous form of each method is also available that can be accessed using the techniques discussed earlier in this chapter.

Table 18-3. Selected Methods of AmazonSearchService Class

Method

Description

ProductInfo KeyWordSearchRequest

 (KeywordRequest req)

Method to return items that contain one or more keywords provided in request.

ProductInfo AsinSearchRequest

 (AsinRequest req)

Method to return a book having a requested Amazon Standard Identification Number (ASIN) that is the same as the book's ISBN. Represented as a 10-digit string.

ProductInfo AuthorSearchRequest

(AuthorRequest req)

Method to return names of all books by requested author.

ProductInfo ActorSearchRequest

(ActorRequest req)

Method to return video titles of movies in which a specified actor or actress was a cast member.

ProductInfo PowerSearchRequest

(PowerRequest req)

Method to retrieve book information based on a Boolean query that may include a combination of title, subject, author, keyword, ISBN, publisher, language, and publication date (pubdate).

Each call to a Web method passes an object that describes the search request. This object is different for each method—for example, AuthorSearchRequest requires an AuthorRequest object, whereas KeyWordSearchRequest requires an instance of the KeywordRequest class. These classes expose almost identical fields. Each contains a unique string field that represents the search query, five other required fields common to each class, and some optional fields for sorting or specifying a locale. Table 18-4 lists unique and shared fields for each method listed in Table 18-3.

Table 18-4. Selected Fields for Classes That Define a Search Request

Field

Description

KeywordRequest.Keyword

AsinRequest.Asin

ActorRequest.Actor

AuthorRequest.Author

PowerRequest.Power

These are string values containing the search value or query. For example:

PowerRequest pr = new PowerRequest();

   pr.Power = "author:Nabokov and

   keyword:butterfly";

string page

Page of results to display.

string mode

Type of products being searched—for example, "books".

string tag

Amazon associate's ID. Use "webservices-20" as default.

string type

"lite" or "heavy". Determines how much XML data is returned.

string devtag

Developer's token assigned to you by Amazon.

Using the ProductInfo Class to Process the Web Service Response

The Web Service responds to the search request with a ProductInfo object containing results from the search. This object exposes three important fields: a TotalResults string contains the number of products retrieved by the request, a TotalPages string that indicates how many pages these results are displayed in, and the important Details array that contains a detailed description of products that constitute one returned page of results. This array is of the Details type. Table 18-5 shows the fields that are related to books.

Table 18-5. Selected Fields of the Details Class

Field

Description

string ProductName

Name of a single product.

string SalesRank

Ranking of product based on sales of items of its type.

string Publisher

Publisher of book.

String ListPrice

string OurPrice

List and sales price of book.

Reviews[] Reviews

The Reviews class contains several fields relating to

reviews of the book:

string AvgCustomerRating

string TotalCustomerReviews

CustomerReview CustomerReviews

string Comment

string Rating

String[] Authors

One or more authors for the book.

This is only a small sample of the fields available in the Details class. There is a particularly rich set of fields worth exploring that define video products.

Creating a Proxy for the Amazon Web Services

Our first step is to create a proxy class from the Amazon WSDL information. The downloadable kit includes a WSDL file, and it can also be retrieved from the Internet, as we do here. Using the VS.NET command line, we place the proxy source code in the file AZProxy.cs.

wsdl.exe  /out:c:\client\AZProxy.cs

      http://soap.amazon.com/schema3/AmazonWebServices.wsdl

Next, we create an assembly, AZProxy.dll, containing the proxy that will be used by client code. It is linked to assemblies containing .NET Web classes required by the application.

csc/t:library /r:system.web.services.dll /r:system.xml.dll

      AZProxy.cs

You can make a quick test of the service using this barebones application, azclient.cs:

using System;

using System.Web.Services;

namespace webclient.example {

public class AmazonClient

{

   static void Main(string[] args)

   {

      // Search for books matching keyword "butterflies"

      AmazonSearchService amazon = new AmazonSearchService();

      KeywordRequest kwRequest = new KeywordRequest();

      kwRequest.keyword = "butterflies";

      kwRequest.type = "heavy";

      kwRequest.devtag= "*************"; // your developer token

      kwRequest.mode = "books";       // search books only

      kwRequest.tag = "webservices-20";

      kwRequest.page = "1";          // return first page

      ProductInfo products =

          amazon.KeywordSearchRequest(kwRequest);

      Console.WriteLine(products.TotalResults);  // Results count

   }

}

}

Compile and execute this from the command line:

csc /r:AZProxy.dll azclient.cs

azclient

When azclient.exe is executed, it should print the number of matching results.

Building a WinForms Web Service Client

Let's design a Windows Forms application that permits a user to perform searches on books using multiple search options. Open VS.NET and select a Windows application. After this is open, we need to add a reference to the proxy assembly AZProxy.dll. From the menu, select Project – Add Reference – Browse. Click the assembly when it is located and then click OK to add it as a reference. You also need to add a reference to System.Web.Services.dll, which contains the required Web Service namespaces.

The purpose of the application is to permit a user to search the Amazon book database by keyword, author, or title. The search can be on a single field on a combination of fields. Figure 18-7 shows the interface for entering the search values and viewing the results. The Search buttons submit a search request based on the value in their corresponding text box. The Power Search button creates a query that logically "ands" any values in the text boxes and submits it.

Figure 18-7. Overview of how a client accesses a Web Service

[View full size image]

A single page of results is displayed in a ListView control. Beneath the control are buttons that can be used to navigate backward and forward through the results pages.

Each Search button has a Click event handler that calls a method to create an appropriate request object and send it to the Amazon Web Service. A successful call returns a ProductInfo object containing information about up to 10 books meeting the search criteria. Listing 18-7 displays code that creates an AuthorRequest object, sends it to the Web Service, and calls FillListView to display the results in the ListView control.

Listing 18-7. Client Code to Display Results of Author Search—azwsclient.cs

// Fields having class-wide scope

int CurrPg;               // Current page being displayed

string SearchMode = "";   // Current search mode

int MaxPages =1;          // Number of pages available

// This method is called when author Search button is clicked

private bool AuthorReq()

{

   AmazonSearchService amazon = new AmazonSearchService();

   AuthorRequest auRequest = new AuthorRequest();

   auRequest.author = textBox2.Text;   // Get author from GUI

   auRequest.type = "heavy";

   auRequest.devtag= "****KLMJFLGV9";  // Developer token

   auRequest.mode = "books";

   auRequest.tag = "webservices-20";

   auRequest.page = CurrPg.ToString();

   try

   {

      // Call Web Service with author query

      ProductInfo products =

            amazon.AuthorSearchRequest(auRequest);

      FillListView(products);

      return(true);

   }

   catch (SoapException ex)

   {

      MessageBox.Show(ex.Message);

      return(false);

   }

}

private void FillListView(ProductInfo products)

   listView1.Items.Clear();   // Remove current entries

   label6.Text="";            // Clear any title

   label4.Text = products.TotalResults;

   label5.Text = CurrPg.ToString()+" of "+products.TotalPages;

{

   MaxPages =  Convert.ToInt32(products.TotalPages);

   ListViewItem rowItem;

   string auth,rev;

   for (int i=0; i< products.Details.Length; i++)

   {

      rowItem = new

            ListViewItem(products.Details[i].ProductName);

      // Add Author. Make sure author exists.

      object ob = products.Details[i].Authors;

      if (ob != null)  auth =

            products.Details[i].Authors[0]; else auth="None";

      rowItem.SubItems.Add(auth);

      // Add Price

      rowItem.SubItems.Add(products.Details[i].OurPrice);

      // Add Average Rating

      ob = products.Details[i].Reviews;

      if (ob != null)  rev =

            products.Details[i].Reviews.AvgCustomerRating;

            else rev="None";

      rowItem.SubItems.Add(rev);

      // Add Date Published

      rowItem.SubItems.Add(

            products.Details[i].ReleaseDate);

      listView1.Items.Add(rowItem);

   }

}

The keyword, title, and power searches use an identical approach: Each has a routine comparable to AuthorReq that creates its own request object. The only significant difference pertains to the power search that creates a Boolean query from the search field values. The format for this type query is field:value AND field2:value AND field3:value. For example:

"author:hemingway AND keywords:Kilimanjaro"

This application was designed as a Windows Forms application. It could just as easily be set up as Web page under ASP.NET. The code to reference the assembly AZProxy.dll is identical. The ListView control is not supported on a Web Form, but you could easily substitute a DataGrid for it.


18.6. Web Services Performance

The performance of a Web Service from both the client and server side is affected by a variety of factors. Some are .NET related and others are inherent in the technology. The solutions for improving Web Services performance range from shaving a few milliseconds off the way .NET sends a request to simply eschewing Web Services for an alternate protocol when transferring large amounts of data. We'll look at all of these.

Configuring the HTTP Connection

Connections (HTTP) to Internet resources are managed in .NET by the ServicePoint class. This class includes properties to specify a connection's timeout interval, set its security protocol, and manage the use of server security certificates. It also includes properties that directly affect how much delay is incurred before a Web Service request is transmitted over a network: UseNagleAlgorithm and Expect100Continue. Despite the awkward sounding names, setting their properties is as easy as assigning a TRue or false value to them. The default for both is true.

Expect100Continue

This property determines whether a POST request should expect to receive a 100-Continue response from the server to indicate that the data can be posted. If this value is set to TRue, only the request header portion of a request is sent to the server. If the server finds no problem with the header, it returns a 100-Continue response, and the data is then sent. Two trips are required. If the property is set to false, the initial request includes the headers and data. If it is rejected by the server, the data has been sent unnecessarily; if it is accepted, a second trip is not necessary.

Because Web Service calls tend to pass small amounts of data, it can be beneficial to turn this property off. Even if a request is rejected, only a small amount of data will have been sent.

The Nagle Algorithm

One way to improve network efficiency is to reduce the number of small data packets sent across a network. To accomplish this, the software layer controlling the underlying TCP (Transmission Control Protocol) connection attempts to accumulate, or buffer, small messages into a larger TCP segment before they are sent. The technique to do this is based on the Nagle algorithm.[4]

[4] RFC 896, "Congestion Control in IP/TCP Internetworks," by John Nagle, 1984.

The crux of the algorithm is that small amounts of data should continue to be collected by TCP until it receives acknowledgment to send the data. .NET institutes a delay of up to 200 milliseconds to collect additional data for a packet. For a typically small Web Service request, there may be no reason to include this delay. It's an option you can experiment with.

To set the Expect100Continue and UseNagleAlgorithm properties, it is necessary to get a reference to the ServicePoint object being used to handle the Web request. This is done in the proxy code on the client side. Refer to Listing 18-2, and you'll see that the proxy code consists of a class derived from the base SoapHttpClientProtocol class. By overriding the inherited GetWebRequest method, you can customize the WebRequest object before the request is sent to the Web Service.

Add the following code to override the GetWebRequest method. Inside the method, you use the Uri object to get the ServicePoint. Its properties can then be turned off or on to test performance:

// Using System.Net must be added

protected override WebRequest GetWebRequest( Uri uri)

{

   // Locate ServicePoint object used by this application

   ServicePoint sp =

         ServicePointManager.FindServicePoint(uri);

   sp.Expect100Continue = false;

   sp.UseNagleAlgorithm = false;

   return WebRequest.Create(uri);

}

Working with Large Amounts of Data

Although the XML format offers a great deal of flexibility in representing data, it can place a potentially large burden on a network because of the large files that can be generated. Moreover, processing XML data can require extensive memory resources on the client and server. Finally, there is the nature of a Web Service: If the transmission fails at any point, the entire response must be resent. This is in contrast to FTP and the HTTP GET verb that allow partial data transmission.

For large amounts of data, consider these options:

  • Use FTP or a Web client as described in Section 17.6, "Creating a Web Client with WebRequest and WebResponse."

  • Avoid calls as much as possible by caching data rather re-requesting it.

  • Look at compression techniques such as HTTP transport compression or the SOAP extensions that can compress part of a Web Service message.

  • Wait for new Web Service standards. Of particular note is Message Transmission Optimization Mechanism (MTOM), a new W3C recommendation—pushed by Microsoft—that details a method for attaching large binary data to a SOAP message.


18.7. Summary

.NET supports the development of both Web Service provider and Web Service consumer applications. On the server side, a .NET Web Service is easily constructed using a WebService directive to define the Web Service class and a WebMethod attribute to identify methods accessible to HTTP requests. For the Web Service to be used by clients, a description of the service(s) must be made available. This is the purpose of the Web Service Description Language (WSDL) contract. This XML file describes the service, the methods available, and a description of the arguments each method accepts. You can generate the contract by using a browser to navigate to a URL address that consists of the URL of the Web Service with ?WSDL appended to it.

The WSDL information is used on the client side to create a Proxy class that is used to actually communicate with the Web Service. This proxy is created using the wsdl.exe utility or within a Visual Studio.NET project as part of the Add Web Service option. The proxy defines methods that permit the Web Service methods to be accessed synchronously or asynchronously. The latter technique returns control to the application while the request is being processed.

SOAP (Simple Object Access Protocol) describes the XML format that is used to transport information between a Web Service provider and consumer. Its structure consists of an envelope, optional header, and body. The body contains the actual data or message; the header may contain annotation about the message. One such use is to include user authentication information. The main advantage of SOAP over the other two wire protocols—HTTP GET and HTTP POST—is that it supports the transmission of non-text data such as images and objects. We demonstrated this by building a client to access the Amazon Web Services.


18.8. Test Your Understanding

1:

Which base class must the Web Service class inherit from for the Web Service to use the ASP.NET Session and Application objects?

2:

What is the difference between a WebService attribute and a WebService directive?

3:

How can we create a Web Service that exposes two methods having the same name?

4:

Which SoapHttpClientProtocol member is used to make a synchronous call to a Web method? An asynchronous call?

5:

You are using a Web Service that has heavy traffic or is subject to downtime. How can you ensure that your client program waits no longer than 10 seconds for a request?

6:

Which WSDL element contains the URL of the target Web Service?

  1.
<Definitions>

  2.
<Message>

  3.
<Service>

  4.
<Binding>

7:

Which attribute must be applied to a Web method for the method to access a SOAP header?

8:

What three ways can a proxy be used to make asynchronous calls to a Web Service?


Appendix A. Features Specific to .NET 2.0 and C# 2.0

In This Appendix

Table A-1 provides a shortcut to reference the types and classes described in this book that are supported only in .NET 2.0 and C# 2.0.

Table A-1. .NET 2.0 and C# 2.0 Features Index

Chapter.Section

Feature

Description

2.2

float type

Now referred to as a single.

2.2

tryParse method

Has been added to return a Boolean value if a string can be successfully converted into the target type:

int iParse =

   Int32.TryParse("100", out  result);

3.2

Static class

A class containing only static members can be declared.

3.4

Property access modifier

The get and set accessors can now have modifiers:

protected set { name= value; }

">3.7

Anonymous methods

Eliminates the need for a separate event handler for delegates.

3.10

Generics syntax

New syntax that enables collection objects to be made type-safe. An application specifies the type of data a class can hold in a parameter:

myStack<T> = new myStack<T>();

4.4

Iterators

An easier way to implement enumeration on a custom class.

4.4

System.Collection. Generic namespace

Holds a generic version of the collection classes. New classes include List<>, Dictionary<>, and Stack<>.

7.1

DataGridView and MenuStrip controls

Some new WinForms controls.

7.6

StatusStrip and ToolStripProgressBar controls

StatusStrip control supersedes StatusBar. ToolStripProgressBar can be displayed on a StatusStrip.

10.2

XmlReaderSettings class

New class used to define the behavior of the XmlReader object. Notably, it allows XML validation to be performed automatically while reading nodes.

10.4

XmlReader.Create method

New method that returns an instance of an XmlReader.

11.1

ProviderFactory class

Used to return connection and command objects from a specified data provider:

string pvdr="System.Data.SqlClient";

DBProviderFactory factory;

factory = DBProviderFactories.GetFactory(pvdr);

DbConnection conn = factory.CreateConnection();

11.4

DataTable.Load() method

Accepts a DataReader object as a parameter and fills a table based on DataReader.

12.3

DataGridView class

WinForms class that supersedes the DataGrid control. Includes a virtual mode that permits the application to dynamically control the contents of the grid

13.2

GZipStream class

Class to perform text compression.

13.3

ParameterizedThreadStart delegate

Permits data to be passed to a thread.

13.4

Semaphore class

A new synchronization class that is used to control the number of threads that can access a resource.

16.1

Partial classes

A code-behind page can now contain a partial class that extends the class in the main Web page.

16.1

PostBackUrl property

Permits a Web page to easily post its content to a Web page other than itself.

16.3

GridView control

ASP.NET's new control that supersedes the DataGrid control.

16.3

AppendDataBoundItems property

Indicates whether data bound to a list control should overwrite existing data in the control.

16.3

DataSource controls

The logic for accessing a data source can now be encapsulated in controls designed to serve as a bridge to a variety of data sources including SQL and XML.

16.4

ValidationGroup property

Allows validation controls to be grouped so that when a page is submitted, only controls whose validators are in a specific group are validated.

16.5

Master pages

Allow a Web page to be created as a template that provides a consistent interface to users. New content pages are created by replacing the placeholders in the template.

17.2

<connectionStrings> section

New section in the web.config file reserved for connection strings.

18.3

Web Service response handled as event

.NET now includes in a Web proxy a delegate and event that fire when a Web Service returns a response. An application can process the results in its custom event handler.


Appendix B. DataGridView Events and Delegates

In This Appendix

This section contains two tables that describe the events and delegates associated with the System.Windows.Forms.DataGridView control. Table B-1 contains a list of the events (first column) and corresponding delegates (second column). Table B-2 contains the parameters for the delegate.

Table B-1. DataGridView Events and Delegates

DataGridView Event

Delegate

AllowUserToAddRowsChanged

System.EventHandler

AllowUserToDeleteRowsChanged

System.EventHandler

AllowUserToOrderColumnsChanged

System.EventHandler

AlternatingRowsDefaultCellStyle Changed

System.EventHandler

AutoGenerateColumnsChanged

System.EventHandler

AutoSizeChanged

System.EventHandler

AutoSizeColumnCriteriaChanged

Forms.DataGridViewAutoSizeColumnCriteriaEventHandler

AutoSizeColumnHeadersEnabledChanged

System.EventHandler

AutoSizeRowHeadersModeChanged

Forms.DataGridViewAutoSizeModeEventHandler

AutoSizeRowsModeChanged

Forms.DataGridViewAutoSizeModeEventHandler

BackColorChanged

System.EventHandler

BackgroundColorChanged

System.EventHandler

BackgroundImageChanged

System.EventHandler

BackgroundImageLayoutChanged

System.EventHandler

BindingContextChanged

System.EventHandler

BorderStyleChanged

System.EventHandler

CancelRowEdit

Forms.QuestionEventHandler

CausesValidationChanged

System.EventHandler

CellBeginEdit

Forms.DataGridViewCellCancelEventHandler

CellBorderStyleChanged

System.EventHandler

CellClick

Forms.DataGridViewCellEventHandler

CellContentClick

Forms.DataGridViewCellEventHandler

CellContextMenuStripChanged

Forms.DataGridViewCellEventHandler

CellContextMenuStripNeeded

Forms.DataGridViewCellContextMenuStripNeededEventHandler

CellEndEdit

Forms.DataGridViewCellEventHandler

CellEnter

Forms.DataGridViewCellEventHandler

CellErrorTextChanged

Forms.DataGridViewCellEventHandler

CellErrorTextNeeded

Forms.DataGridViewCellErrorTextNeededEventHandler

CellFormatting

Forms.DataGridViewCellFormattingEventHandler

CellLeave

Forms.DataGridViewCellEventHandler

CellMouseClick

Forms.DataGridViewCellMouseEventHandler

CellMouseDoubleClick

Forms.DataGridViewCellMouseEventHandler

CellMouseDown

Forms.DataGridViewCellMouseEventHandler

CellMouseEnter

Forms.DataGridViewCellEventHandler

CellMouseLeave

Forms.DataGridViewCellEventHandler

CellMouseMove

Forms.DataGridViewCellMouseEventHandler

CellMouseUp

Forms.DataGridViewCellMouseEventHandler

CellPainting

Forms.DataGridViewCellPaintingEventHandler

CellParsing

Forms.DataGridViewCellParsingEventHandler

CellStateChanged

Forms.DataGridViewCellStateChangedEventHandler

CellStyleChanged

Forms.DataGridViewCellEventHandler

CellStyleContentChanged

Forms.DataGridViewCellStyleContentChangedEventHandler

CellToolTipTextChanged

Forms.DataGridViewCellEventHandler

CellToolTipTextNeeded

Forms.DataGridViewCellToolTipTextNeededEventHandler

CellValidated

Forms.DataGridViewCellEventHandler

CellValidating

Forms.DataGridViewCellValidatingEventHandler

CellValueChanged

Forms.DataGridViewCellEventHandler

CellValueNeeded

Forms.DataGridViewCellValueEventHandler

CellValuePushed

Forms.DataGridViewCellValueEventHandler

ChangeUICues

Forms.UICuesEventHandler

Click

System.EventHandler

ColumnContextMenuStripChanged

Forms.DataGridViewColumnEventHandler

ColumnDataPropertyNameChanged

Forms.DataGridViewColumnEventHandler

ColumnDefaultCellStyleChanged

Forms.DataGridViewColumnEventHandler

ColumnDisplayIndexChanged

Forms.DataGridViewColumnEventHandler

ColumnDividerWidthChanged

Forms.DataGridViewColumnEventHandler

ColumnHeaderCellChanged

Forms.DataGridViewColumnEventHandler

ColumnHeaderMouseClick

Forms.DataGridViewCellMouseEventHandler

ColumnHeaderMouseDoubleClick

Forms.DataGridViewCellMouseEventHandler

ColumnHeadersBorderStyleChanged

System.EventHandler

ColumnHeadersDefaultCellStyleChanged

System.EventHandler

ColumnHeadersHeightChanged

System.EventHandler

ColumnMinimumWidthChanged

Forms.DataGridViewColumnEventHandler

ColumnNameChanged

Forms.DataGridViewColumnEventHandler

ColumnSortModeChanged

Forms.DataGridViewColumnEventHandler

ColumnStateChanged

Forms.DataGridViewColumnStateChangedEventHandler

ColumnToolTipTextChanged

Forms.DataGridViewColumnEventHandler

ColumnWidthChanged

Forms.DataGridViewColumnEventHandler

ContextMenuChanged

System.EventHandler

ContextMenuStripChanged

System.EventHandler

ControlAdded

Forms.ControlEventHandler

ControlRemoved

Forms.ControlEventHandler

CurrentCellChanged

System.EventHandler

CurrentCellDirtyStateChanged

System.EventHandler

CursorChanged

System.EventHandler

DataBindingComplete

Forms.DataGridViewBindingCompleteEventHandler

DataError

Forms.DataGridViewDataErrorEventHandler

DataMemberChanged

System.EventHandler

DataSourceChanged

System.EventHandler

DefaultCellStyleChanged

System.EventHandler

DefaultValuesNeeded

Forms.DataGridViewRowEventHandler

Disposed

System.EventHandler

DockChanged

System.EventHandler

DoubleClick

System.EventHandler

DragDrop

Forms.DragEventHandler

DragEnter

Forms.DragEventHandler

DragLeave

System.EventHandler

DragOver

Forms.DragEventHandler

EditingControlShowing

Forms.DataGridViewEditingControlShowingEventHandler

EditModeChanged

System.EventHandler

EnabledChanged

System.EventHandler

Enter

System.EventHandler

FontChanged

System.EventHandler

ForeColorChanged

System.EventHandler

GiveFeedback

Forms.GiveFeedbackEventHandler

GotFocus

System.EventHandler

GridColorChanged

System.EventHandler

HandleCreated

System.EventHandler

HandleDestroyed

System.EventHandler

HelpRequested

Forms.HelpEventHandler

ImeModeChanged

System.EventHandler

Invalidated

Forms.InvalidateEventHandler

KeyDown

Forms.KeyEventHandler

KeyPress

Forms.KeyPressEventHandler

KeyUp

Forms.KeyEventHandler

Layout

Forms.LayoutEventHandler

Leave

System.EventHandler

LocationChanged

System.EventHandler

LostFocus

System.EventHandler

MarginChanged

System.EventHandler

MouseCaptureChanged

System.EventHandler

MouseClick

Forms.MouseEventHandler

MouseDoubleClick

Forms.MouseEventHandler

MouseDown

Forms.MouseEventHandler

MouseEnter

System.EventHandler

MouseHover

System.EventHandler

MouseLeave

System.EventHandler

MouseMove

Forms.MouseEventHandler

MouseUp

Forms.MouseEventHandler

MouseWheel

Forms.MouseEventHandler

Move

System.EventHandler

MultiSelectChanged

System.EventHandler

NewRowNeeded

Forms.DataGridViewRowEventHandler

PaddingChanged

System.EventHandler

Paint

Forms.PaintEventHandler

ParentChanged

System.EventHandler

QueryAccessibilityHelp

Forms.QueryAccessibilityHelpEventHandler

QueryContinueDrag

Forms.QueryContinueDragEventHandler

ReadOnlyChanged

System.EventHandler

RegionChanged

System.EventHandler

Resize

System.EventHandler

ResizeBegin

System.EventHandler

ResizeEnd

System.EventHandler

RightToLeftChanged

System.EventHandler

RowContextMenuStripChanged

Forms.DataGridViewRowEventHandler

RowContextMenuStripNeeded

Forms.DataGridViewRowContextMenuStripNeededEventHandler

RowDefaultCellStyleChanged

Forms.DataGridViewRowEventHandler

RowDirtyStateNeeded

Forms.QuestionEventHandler

RowDividerHeightChanged

Forms.DataGridViewRowEventHandler

RowEnter

Forms.DataGridViewCellEventHandler

RowErrorTextChanged

Forms.DataGridViewRowEventHandler

RowErrorTextNeeded

Forms.DataGridViewRowErrorTextNeededEventHandler

RowHeaderCellChanged

Forms.DataGridViewRowEventHandler

RowHeaderMouseClick

Forms.DataGridViewCellMouseEventHandler

RowHeaderMouseDoubleClick

Forms.DataGridViewCellMouseEventHandler

RowHeadersBorderStyleChanged

System.EventHandler

RowHeadersDefaultCellStyleChanged

System.EventHandler

RowHeadersWidthChanged

System.EventHandler

RowHeightChanged

Forms.DataGridViewRowEventHandler

RowHeightInfoNeeded

Forms.DataGridViewRowHeightInfoNeededEventHandler

RowHeightInfoPushed

Forms.DataGridViewRowHeightInfoPushedEventHandler

RowLeave

Forms.DataGridViewCellEventHandler

RowMinimumHeightChanged

Forms.DataGridViewRowEventHandler

RowPostPaint

Forms.DataGridViewRowPostPaintEventHandler

RowPrePaint

Forms.DataGridViewRowPrePaintEventHandler

RowStateChanged

Forms.DataGridViewRowStateChangedEventHandler

RowUnshared

Forms.DataGridViewRowEventHandler

RowValidated

Forms.DataGridViewCellEventHandler

RowValidating

Forms.DataGridViewCellCancelEventHandler

RowsAdded

Forms.DataGridViewRowsAddedEventHandler

RowsDefaultCellStyleChanged

System.EventHandler

RowsDeleted

Forms.DataGridViewRowsDeletedEventHandler

Scroll

Forms.ScrollEventHandler

SelectionChanged

System.EventHandler

SizeChanged

System.EventHandler

SortCompare

Forms.DataGridViewSortCompareEventHandler

Sorted

System.EventHandler

StyleChanged

System.EventHandler

SystemColorsChanged

System.EventHandler

TabIndexChanged

System.EventHandler

TabStopChanged

System.EventHandler

TextChanged

System.EventHandler

UserAddedRow

Forms.DataGridViewRowEventHandler

UserDeletedRow

Forms.DataGridViewRowEventHandler

UserDeletingRow

Forms.DataGridViewRowCancelEventHandler

Validated

System.EventHandler

Validating

System.ComponentModel.CancelEventHandler

VisibleChanged

System.EventHandler

Note: System.Windows.Forms has been truncated to Forms in the Delegate description.

Table B-2. DataGridView Delegates and Parameters

Delegate

arameter 2

ControlEventHandler

Forms.ControlEventArgs

DataGridViewAutoSizeColumnCriteriaEventHandler

Forms.DataGridViewAutoSizeColumnCriteriaEventArgs

DataGridViewAutoSizeModeEventHandler

Forms.DataGridViewAutoSizeModeEventArgs

DataGridViewBindingCompleteEventHandler

Forms.DataGridViewBindingCompleteEventArgs

DataGridViewCellCancelEventHandler

Forms.DataGridViewCellCancelEventArgs

DataGridViewCellContextMenuStripNeededEventHandler

Forms.DataGridViewCellContextMenuStripNeededEventArgs

DataGridViewCellErrorTextNeededEventHandler

Forms.DataGridViewCellErrorTextNeededEventArgs

DataGridViewCellEventHandler

Forms.DataGridViewCellEventArgs

DataGridViewCellFormattingEventHandler

Forms.DataGridViewCellFormattingEventArgs

DataGridViewCellMouseEventHandler

Forms.DataGridViewCellMouseEventArgs

DataGridViewCellPaintingEventHandler

Forms.DataGridViewCellPaintingEventArgs

DataGridViewCellParsingEventHandler

Forms.DataGridViewCellParsingEventArgs

DataGridViewCellStateChangedEventHandler

Forms.DataGridViewCellStateChangedEventArgs

DataGridViewCellStyleContentChangedEventHandler

Forms.DataGridViewCellStyleContent ChangedEventArgs

DataGridViewCellToolTipTextNeededEventHandler

Forms.DataGridViewCellToolTipTextNeededEventArgs

DataGridViewCellValidatingEventHandler

Forms.DataGridViewCellValidatingEventArgs

DataGridViewCellValueEventHandler

Forms.DataGridViewCellValueEventArgs

DataGridViewColumnEventHandler

Forms.DataGridViewColumnEventArgs

DataGridViewColumnStateChangedEventHandler

Forms.DataGridViewColumnStateChangedEventArgs

DataGridViewDataErrorEventHandler

Forms.DataGridViewDataErrorEventArgs

DataGridViewEditingControlShowingEventHandler

Forms.DataGridViewEditingControlShowingEventArgs

DataGridViewRowCancelEventHandler

Forms.DataGridViewRowCancelEventArgs

DataGridViewRowContextMenuStripNeededEventHandler

Forms.DataGridViewRowContextMenuStripNeededEventArgs

DataGridViewRowErrorTextNeededEventHandler

Forms.DataGridViewRowErrorTextNeededEventArgs

DataGridViewRowEventHandler

Forms.DataGridViewRowEventArgs

DataGridViewRowHeightInfoNeededEventHandler

Forms.DataGridViewRowHeightInfoNeededEventArgs

DataGridViewRowHeightInfoPushedEventHandler

Forms.DataGridViewRowHeightInfoPushed EventArgs

DataGridViewRowPostPaintEventHandler

Forms.DataGridViewRowPostPaintEventArgs

DataGridViewRowPrePaintEventHandler

Forms.DataGridViewRowPrePaintEventArgs

DataGridViewRowStateChangedEventHandler

Forms.DataGridViewRowStateChangedEventArgs

DataGridViewRowsAddedEventHandler

Forms.DataGridViewRowsAddedEventArgs

DataGridViewRowsDeletedEventHandler

Forms.DataGridViewRowsDeletedEventArgs

DataGridViewSortCompareEventHandler

Forms.DataGridViewSortCompareEventArgs

DragEventHandler

Forms.DragEventArgs

GiveFeedbackEventHandler

Forms.GiveFeedbackEventArgs

HelpEventHandler

Forms.HelpEventArgs

InvalidateEventHandler

Forms.InvalidateEventArgs

KeyEventHandler

Forms.KeyEventArgs

KeyPressEventHandler

Forms.KeyPressEventArgs

LayoutEventHandler

Forms.LayoutEventArgs

MouseEventHandler

Forms.MouseEventArgs

PaintEventHandler

Forms.PaintEventArgs

QueryAccessibilityHelpEventHandler

Forms.QueryAccessibilityHelpEventArgs

QueryContinueDragEventHandler

Forms.QueryContinueDragEventArgs

QuestionEventHandler

Forms.QuestionEventArgs

ScrollEventHandler

Forms.ScrollEventArgs

System.ComponentModel.CancelEventHandler

System.ComponentModel.CancelEventArgs

System.EventHandler

System.EventArgs

UICuesEventHandler

Forms.UICuesEventArgs

Note: The first parameter is always System.Object, which is not shown in the table.

The contents of both tables are generated using reflection to extract all the events in the namespace associated with the DataGridView control and write them to a text file. The output files can be sorted in a text editor and imported into word processor's table format. Alternatively, you can extend the program to create XML or HTML formatted output. Here is the code to create Table B-1. The code used to create Table B-2 follows Table B-1.

//  Use reflection to extract DataGridView events

using System;

using System.Reflection;

using System.Collections;

using System.IO;

class MyApp

{

   public static void Main()

   {

      // This DLL is found in the Framework version folder

      // You will need to specify the path

      Assembly myassembly =

            Assembly.LoadFrom("System.windows.forms.dll");

      Type grid  = myassembly.GetType(

            "System.Windows.Forms.DataGridView");

      Hashtable events = new Hashtable();

      StreamWriter sw = new StreamWriter("c:\\events.txt");

      foreach(EventInfo gev in grid.GetEvents())

      {

         events.Add(gev.Name, gev.EventHandlerType);

      }

      // Tab allows columns to be imported into table

      string mytab= ((char)9).ToString();

      // Write event and delegate info to file

      foreach (DictionaryEntry de in events)

         sw.WriteLine(de.Key+mytab+de.Value);

      sw.Close();

   }

}

This code is added to the code for the first table in order to gather information about the delegates' signature. The signatures are stored in a hash table and written to a text file of your choice. The content of the output file can then be loaded into a table with three columns.

Hashtable signature = new Hashtable();  // Holds signatures

events.Add(gev.Name, gev.EventHandlerType);

Type deleg = gev.EventHandlerType;

if(!signature.Contains(deleg))

{

   // Get parameters

   MethodInfo invoke = deleg.GetMethod("Invoke");

   ParameterInfo[] pars = invoke.GetParameters();

   string sig = "";

   foreach (ParameterInfo p in pars)

   {

      Console.WriteLine(p.ParameterType);

      sig += mytab +p.ParameterType  ;

   }

   signature.Add(deleg, sig);

}


Answers to Chapter Exercises

    Chapter 1

    Chapter 2

    Chapter 3

    Chapter 4

    Chapter 5

    Chapter 6

    Chapter 7

    Chapter 8

    Chapter 9

    Chapter 10

    Chapter 11

    Chapter 12

    Chapter 13

    Chapter 14

    Chapter 15

    Chapter 16

    Chapter 17

    Chapter 18


Chapter 1

A1: | The .NET Framework Redistributable is required by a client to run a .NET application and is available as a free download on the Internet.
A2: | Managed code is produced by a compiler that meets the Common Type System (CTS) requirements that are necessary before code produced by it can be run by the Common Language Runtime (CLR). Unmanaged code does not meet the CTS standard. The Windows API and COM objects are examples of unmanaged code.
A3: | The Common Type System defines the types and their members that must be used by compilers that create code to run on the Common Language Runtime. The Common Language Specification provides stricter requirements that ensure interoperability between languages.
A4: | All .NET compilers generate an Intermediate Language (IL) code. Since IL is compatible, irrespective of its source, the CLR doesn't care which compiler produces it.
A5: | The Global Assembly Cache (GAC) holds shared assemblies—assemblies that can be used by more than one application. Assemblies in the GAC have a digital signature that uniquely identifies them, even if they have the same file name.
A6: | A strong name consists of an assembly name, a version, a culture setting, and a public key token that is required by a client to use the assembly.
A7: | A namespace usually identifies a group of types that provide related services. An assembly may contain one or more namespaces. Also, a namespace may contain types in more than one assembly.
A8: | CLR—Common Language Runtime FCL—Framework Class Library GAC—Global Assembly Cache IL—Intermediate Language (also, CIL or MSIL)


Chapter 2

A1:

A C# program must have an entry point defined by the static Main() method. It accepts a string array args as input.

A2:

// is used for single line comments; /* */ is used to enclose multi-line comments; and /// is used to create XML comments that can be exported to a text file.

A3:

A primitive refers to a simple value type such as an int or byte.

A4:

true

A5:

A do loop is evaluated at the end of the iteration and thus must execute at least once.

A6:

A break statement causes control to exit the enclosing loop immediately; a continue statement continues the same loop by skipping to the beginning of the loop.

A7:

a does not compile because an int cannot be converted to a char. c does not compile because a string cannot be converted to a char.

A8:

Each time concatenation occurs, a new string is created in memory. For a large number of concatenations, this wastes memory and also degrades performance. The StringBuilder class can be used as an alternative—but only where there are many concatenations. Otherwise, it will not outperform concatenation.

A9:

All classes inherit from System.Object. In addition, value types inherit from System.ValueType.

A10:

7


Chapter 3

A1:

A sealed class cannot be inherited. A sealed class is used primarily when the class contains static members. Note that a struct is implicitly sealed.

A2:

A class can inherit from one class explicitly and inherits from System.Object implicitly. It can inherit from any number of interfaces.

A3:

  1. ShowName is static and cannot be referenced from a class instance.

  2.
ShowName.ShowMe("My Name is Ishmael");

A4:

An abstract class may contain both abstract and non-abstract methods.

A5:

new is used to replace (not override) an inherited method with one of the same name.

A6:

(a) x=60 y=40. x is passed by reference; y by value.

A7:

Include a method that performs the same operation as the operator overloading.

A8:

A class cannot be instantiated if it is abstract or if it has a private constructor.

A9:

Example event handler:

private void PrintSale(object sender, SaleEvArgs e)

{

   decimal prc=  (decimal)e.saleprice;

   DateTime dt = (DateTime)e.date;

   int itemNum = (int)e.itemnum;

   // Now print the values

}

A10:

(a) Compilation error indicating that keyword new is required on Child.amethod(int, string) because it hides the inherited method. If new is used, the code prints "Base Constructor".


Chapter 4

A1:

The advantages of a class factory pattern are that it can control the number of objects created, encapsulates the logic required to create an object, and makes it easier to add new products by isolating the code in the factory.

A2:

Custom exceptions should inherit from the ApplicationException class. Three constructors should be included: a parameterless one, a constructor accepting a string parameter, and a constructor that accepts a string and Exception object parameter.

A3:

System.Object.Equals bases equality on objects having the same memory location.

A4:

A class must implement the IEnumerable and IEnumerator interfaces to support the foreach statement.

A5:

Generics permit a collection to be type-safe. This means the collection class is restricted to holding and processing objects of one type. Non-generic collections can hold any mixture of objects and require casting to detect their type.

A6:

The IDisposable interface is a convenient way to notify a client that an object's Dispose method should be called. The client only needs to check for the existence of the interface.

A7:

  1.
false

  2.
true

  3.
false

  4.
true

The Clone method creates a copy of an object at a new address.

A8:

Objects with a Finalize method implemented are placed in a special queue so that Finalize can be executed before Garbage Collection occurs. This delays the actual Garbage Collection by one cycle.


Chapter 5

A1:

CultureInfo represents information about a culture. Here is an example of its use:

CultureInfo ci = new CultureInfo("de-AT");  // German-Austria

A2:

CultureInfo() with no parameters and Thread.CurrentThread.CurrentCulture return objects with current culture.

A3:

  1. 2

  2. 3

  3. 1

A4:

Use the instance method when the expression is used repeatedly, because, unlike the static approach, it does not have to recompile the expression each time.

A5:

Equals() is used to check the memory location. If different, a character-by-character comparison is performed.

A6:

  1. 2

  2. 1

  3. 3

A7:

a. FileInfo is not created from a FileStream.

A8:

a

A9:

  1.
true

  2.
true

  3.
false

  4.
false


Chapter 6

A1:

System.Windows.Forms.Form is the required base class for a Windows application.

A2:

Docking attaches a control to an edge of its container; anchoring places a control in a fixed position relative to the edge(s) of a container. As the container is resized, the control remains a fixed distance from the edges.

A3:

x and y yield the coordinates. The Button property is set to a MouseButtons enumeration value.

A4:

The Form.TransparencyKey permits part of a form to appear transparent. Any part of the Form have the color assigned to this property becomes transparent. For example, to set the red portion of myForm to red, set the transparencyKey as follows:

myForm.TransparencyKey = Color.Red;

A5:

A modal form maintains focus until it is closed. A modeless form allows the parent form to regain focus.

A6:

The owned form is always on top, and closing or minimizing the parent also closes or minimizes the owned form.

A7:

Minimize and Maximize properties are set to false; the HelpButton property is set to TRue.

A8:

A MouseOver displays information associated with a control's ToolTip. Help text is enabled by pressing F1 or selecting the Help button and clicking a control to show context-sensitive Help.

A9:

Have the original event handler in the base Form call a virtual method to deal with the event. The inheriting Form then overrides the virtual method.


Chapter 7

A1:

Only one radio button in a group may be selected at a time. A GroupBox provides a way to group radio buttons logically.

A2:

SizeMode = PictureBoxSizeMode.StretchImage

A3:

To display selected fields of an object in a ListBox, override the object's ToString() method to display the desired fields.

A4:

The SelectedIndexChanged event is fired. Use the SelectedIndex to get the index of the selected item, or SelectedItem to get the object selected.

A5:

Set View property to View.Details.

A6:

The Tag property can be used to store objects.

A7:

DragEnter and DragDrop must be supported by a destination control for drag and drop to work.

A8:

Assign Browsable, Category, and Description attributes.

A9:

The ResourceManager class is used to access resources. Its GetString(name) method returns the value associated with a name in the text file.


Chapter 8

A1:

The Graphics object encapsulates the drawing surface and is used to draw; the ClipRectangle represents the area that needs to be redrawn.

A2:

Control.Invalidate().

A3:

C

A4:

b fails because Brush is an abstract class.

A5:

b is more transparent because its alpha value (200) is less than the value (255) of color a.

A6:

100%. The image is scaled to fit the rectangle.

A7:

Here is one solution:

Graphics g = panel1.CreateGraphics();

g.SmoothingMode = SmoothingMode.AntiAlias;

GraphicsPath gp = new GraphicsPath();

gp.AddLine(10, 170, 30, 170);

gp.AddLine(16, 100, 100, 100);

gp.AddLine(100, 100, 190, 180);

gp.AddLine(40, 50, 50, 20);

gp.StartFigure();

gp.AddLine(50, 20, 145, 100);

gp.StartFigure();

gp.AddArc(65, 10, 120, 180, 180, 80);

gp.StartFigure();

gp.AddArc(65, 5, 120, 100, 200, 70);

g.DrawPath(new Pen(Color.Black, 2), gp);

A8:

D


Chapter 9

A1:

Typeface

A2:

Point is the default measurement for a font. It is 1/72nd of an inch.

A3:

Graphics.DrawString method is used. It is passed a StringFormat object that has its Alignment property set to StringAlignment.Far.

A4:

The third column begins at x coordinate of 310 (usually in 1/100ths of an inch). The tab numbers are cumulative.

A5:

The BeginPrint event is fired when PrintDocument.Print is executed.

A6:

The PrintPageEventArgs parameter has a MarginBounds property that represents the margins.

A7:

To preview a document on the screen, you must create a PrintPreviewDialog object, set its Document property to the PrintDocument, and then call ShowDialog to display the preview:

PrintPreviewDialog prvDialog = new PrintPreviewDialog();

PrvDialog.Document = pd;

PrvDialog.ShowDialog();  // Show preview


Chapter 10

A1:

Use its static Create method to create an instance.

A2:

XmlReaderSettings defines how an XmlReader processes the input stream. It can specify node types to be ignored and a schema to be used for validation.

A3:

An XPathDocument is used for reading only.

A4:

  1. Two movies nodes are retrieved: Raging Bull and Taxi Driver.

  2. One movies node is retrieved: Taxi Driver.

  3. One movies node is retrieved: Raging Bull.

  4. Two movie_Title nodes are retrieved: Raging Bull and Taxi Driver.

A5:

Two validation techniques were discussed in this chapter. One approach is to use the XmlReader class directly by setting the Schemas and XsdValidate properties to enable validation. The other approach is to use the XmlValidatingReader class.


Chapter 11

A1:

A .NET data provider must supply a connection, command, DataReader, and DataAdapter object to provide access to its data source.

A2:

b is the correct choice. a and c are invalid statements.

A3:

ExecuteNonQuery() executes a command but does not return a resultset.

ExecuteReader() returns a resultset in response to a query.

ExecuteScalar() returns a single value—or null—if the query generates no value.

A4:

The DataReader remains connected to a data source and returns data in a forward, read-only cursor. A DataAdapter is a bridge between a data source and an internal data store. It typically loads data from a data source into a DataSet. It can also update the data source.

A5:

A DataSet object contains one or more DataTables.

A6:

Rejected and Changed are not valid DataRowState values. Detached, Unchanged, and Modified are the other values.

A7:

A DataSet schema can be created (without loading actual XML data) by

DataInferXmlSchema(xml file);


Chapter 12

A1:

  1. False

  2. True

  3. True

  4. False

  5. False

  6. False

  7. True

  8. False

  9. True

A2:

Simple binding occurs on a control that displays a single value; complex binding associates a control with a collection of data in a data source. In one-way data binding, the control is bound to the source for read-only purposes. Changes to the control's value are not reflected in the data source. Two-way binding permits the data source to be updated by changing the control's value(s).

A3:

The properties on a custom data source that expose the bound data must be writable, so the object can be updated.

A4:

DataGridView.SelectionMode =

  DataGridViewSelectionMode.FullRowSelect;

A5:

A ListBox cannot be included in a DataGridView cell. Other controls that can be included are a Link, CheckBox, and Image.

A6:

To freeze a column, set the column's Frozen property to TRue. The column and all to its left remain visible during scrolling.

dgv.Columns[1].Frozen=true;


Chapter 13

A1:

False. An asynchronous delegate may have a return value.

A2:

ia is an IAsyncResult object returned by BeginInvoke that is later passed to EndInvoke

p1 is a string parameter defined in the delegate's signature.

p2 is an optional callback method.

p3 is a value that can be passed to the callback method when the thread ends.

A3:

Thread Local Storage (TLS) holds state information about a thread. This is required when a thread is swapped out before completion.

A4:

The default number of threads in a thread pool is 25.

A5:

ThreadStart and ParameterizedThreadStart delegates are used to create a thread. The latter permits parameters to be passed to a thread.

A6:

lock (this) { } expands into an identical construct.

A7:

a. The message is never printed because the semaphore is created with zero initial threads, and then goes into a wait state.

A8:

b. It prints "Primary Thread" followed by "Worker Thread". The mutex in the main thread is created with ownership. The spawned thread's mutex must wait for the original one to finish before its thread continues processing.

A9:

There are many solutions. Here is one based on not grabbing a chopstick until both are available:

using System;

using System.Threading;

class Stick {

  //Sticks available are designated as true

  bool[] chopStick = {true, true, true, true, true};

  // Attempt to pick up left and right chopstick

  public void GetSticks(int left, int right)

  {

    lock (this)

    {

      // Release lock and wait until both chopsticks are free

      while (!chopStick[left] && !chopStick[right])

         Monitor.Wait(this);

      chopStick[right] = false; chopStick[left] = false;

    }

  }

  // Put chopsticks down

  public void FreeSticks(int left, int right)

  {

    lock(this)

    {

      chopStick[right] = true;

      chopStick[left]  = true;

      // Signal threads in queue that chopsticks are available

      Monitor.PulseAll(this);

    }

  }

}

class Philosopher

{

  int n;             // Philosopher number

  int eatDelay;

  int thinkDelay;

  int left, right;

  Stick chopSticks;

  public Philosopher (int n, int thinkTime,int eatTime,

                      Stick sticks)

  {

    this.n = n;

    this.eatDelay   = eatTime;

    this.thinkDelay = thinkTime;

    this.chopSticks = sticks;

    // Fifth philosopher has chopstick 1 on left

    left = n == 5 ? 1 : n+1;

    right = n;

    new Thread(new ThreadStart(Run)).Start();

  }

  public void Run()

  {

    while(true)

    {

      try

      {

        // Philosopher thinks for random amount of time

        Thread.Sleep(thinkDelay);

        chopSticks.GetSticks(left-1, right-1);

        Console.WriteLine("Philosopher {0} is eating for

                        [1} ms ",n, eatDelay);

        Thread.Sleep(eatDelay);

        chopSticks.FreeSticks(left-1, right-1);

      } catch { return; }

    }

  }

}    // End of class Philosopher

public class Diners

{

  public static void Main()

  {

    Stick sticks = new Stick();

    // Create thread for each philosopher

    // Eat time is random

    Random r = new Random(DateTime.Now.Millisecond);

    new Philosopher(1, 100, r.Next(500), sticks);

    new Philosopher(2, 200, r.Next(500), sticks);

    new Philosopher(3, 300, r.Next(500), sticks);

    new Philosopher(4, 400, r.Next(500), sticks);

    new Philosopher(5, 500, r.Next(500), sticks);

  }

}


Chapter 14

A1:

  1. True. A process may contain multiple AppDomains.

  2. True. An AppDomain may contain multiple assemblies.

  3. True. An AppDomain may contain .dll and .exe files.

  4. True. An AppDomain can be unloaded from a process.

A2:

Three activation modes: client activation, server activation (single call), and server activation (singleton). HTTP or TCP can be used with all.

A3:

Single call server activation creates a new object on each call; single call singleton creates an object on the first call only. The object is then used for other calls.

A4:

CurrentLeaseTime—Amount of time until object is available for Garbage Collection.

InitialLeaseTime—Initial lifetime of a lease.

RenewalOnCallTime—Amount that CurrentLeaseTime is increased on called object.

A5:

Set the LeaseManagerPoolTime in a configuration file to specify how frequently the lease manager checks for expirations.

A6:

Client-activated and server-activated singleton use leases.

A7:

c. A server-activated singleton object is created when the first call is made to the host object.

A8:

Channel registration indicates the type of protocol to be used—usually TCP or HTTP—in the remoting and the port number to be used. Type registration specifies the available remote object and the activation mode type.

A9:

An interface or SoapSuds is used to prevent having to place a full assembly on the remoting client's machine. The assembly provides metadata, but can also expose proprietary code. An interface provides only the metadata a client requires. SoapSuds extracts the metadata and places it in a file that is deployed on the client's machine.


Chapter 15

A1:

  1. False. Only a strongly named assembly may be placed in the GAC—but it does not have to be there.

  2. False. The compiler does not automatically check the GAC for references.

  3. True. The CLR checks the GAC for assembly references.

  4. True. A client must have a valid public key token to use an assembly in the GAC.

A2:

A Permission attribute should be added to the assembly, specifying the permission(s) required by the assembly.

A3:

A strong name consists of a simple name, version number, culture info, and public key token.

A4:

Assign version: [assembly: AssemblyVersion("1.1.0.1")]

Assign culture:  [assembly: AssemblyCulture("fr-CA")]

The default build number is the number of days since 1/1/2000; the default revision number is the number of seconds past midnight divided by 2.

A5:

The IStackWalk interface defines the Assert, Deny, and PermitOnly methods to modify a stack walk. Assert stops the walk; Deny specifies permissions that cause the walk to fail; and PermitOnly specifies the only permissions that do not cause the stack walk to fail..

A6:

Delayed signing refers to using a public key to create a digital signature, as opposed to using the private key. When the final assembly is ready, the private key replaces the public key for encryption. This reduces the need to expose the private key during development.

A7:

A predefined permission is an individual permission class provided by .NET. A named permission set is a predefined set of permission objects. A security zone classifies where an assembly originates: Internet, Intranet, MyComputer, NoZone, trusted, Untrusted.

A8:

  1. True.

  2. True.

  3. False. <Binding Redirect /> is used to redirect CLR to newer version.

  4. False.

A9:

Add to the configuration:

<binding Redirect OldVersion="1.0.0.0"

             NewVersion="2.0.0.0" />


Chapter 16

A1:

GET and PUT are used to transfer data from a client to a server. PUT is default for ASP.NET.

A2:

  1. True.

  2. True.

  3. False. ASP.NET returns HTML, therefore a client does not require the .NET runtime.

  4. True.

  5. True.

A3:

The ListItem class contains a collection for List controls.

A4:

If the TextBox.AutoPostBack property is set to true, the event handler is called immediately on the server; otherwise, it waits until the next round trip to the server occurs.

A5:

The .Master property allows access to the MasterPage object.

A6:

The DataSource property must be set to the data source, and DataBind must be executed to load the data into the control.

A7:

  1. False. You cannot bind directly to a DataAdapter.

  2. True. A control can bind to a DataReader.

  3. False. It is populated when DataBind is executed.

  4. True. A DataSet or data source control can bind to a control.

A8:

Use a validating control to manage input to a text box.

A9:

The @Register directive is required to specify a custom control.

A10:

The HtmlTextWriter class emits HTML that renders a control.

A11:

The controls in a composite control render themselves and offer the standard properties to work with.


Chapter 17

A1:

False. A web.config file is not required.

A2:

pageOutput in the web.config file specifies whether the trace log is appended to the Web page for display on the client's browser. If set to false, the output can be viewed in the trace.axd file in the application's root.

A3:

  4. Windows, Forms, and Microsoft Passport authentication are offered by .NET.

  5. Authentication is specified using the <authentication> element of the web.config file.

  6. Authentication verifies that a user is who he says he is; authorization determines what actions the user may perform or what resources he may access.

  7. The Page.User property provides information about a user.

A4:

Out-of-Process Session management supports storing state information on a server on process on another server.

A5:

<%@ OutputCache Duration="180"  VaryByParam="*"

                                VaryByCustom="Browser"

%>

A6:

A data cache is read-only versus read/write for application state data. However, a data cache is much more flexible: An expiration can be assigned to it, and it can be tied to other objects, causing the cache data to be removed if the objects' value changes.

A7:

ASP.NET periodically invokes a resource scavenging process that removes less important data from a data cache. Importance is determined by priority. Thus, setting a high priority reduces the chance of the data cache being removed.

A8:

As a request or response passes through the HTTP pipeline, an Application object fires a series of events that may be handled by an HTTP module or in the global.asax file.


Chapter 18

A1:

System.Web.Services.WebService

A2:

WebService attribute is applied to a class providing a Web Service. It describes the Web Service. WebService directive identifies a file as containing a Web Service.

A3:

Use the Web Service attribute to specify a namespace to uniquely identify a service.

A4:

The Invoke method is used for a synchronous call; BeginInvoke is used for asynchronous.

A5:

Use the Timeout property on the Web Service object to set the timeout in milliseconds.

A6:

The <Service> element contains the target URL for a Web Service.

A7:

A Web method requires the SoapHeader attribute to access the SOAP header info.

A8:

BeginInvoke with polling using the IsCompleted property to check for a response. BeginInvoke with EndInvoke to retrieve results. Call <web service name>Async and implement event handler to process response.


Index

[SYMBOL] [A] [B] [C] [D] [E] [F] [G] [H] [I] [J] [K] [L] [M] [N] [O] [P] [Q] [R] [S] [T] [U] [V] [W] [X]

Previous Section < Day Day Up >


index_SYMBOL

Index

[SYMBOL] [A] [B] [C] [D] [E] [F] [G] [H] [I] [J] [K] [L] [M] [N] [O] [P] [Q] [R] [S] [T] [U] [V] [W] [X]

.asmx files
.NET
ADO.NET [See ADO.NET]
ASP.NET [See ASP.NET]
assemblies 2nd 3rd 4th 5th 6th
built-in security permissions 2nd 3rd
compiling code 2nd
CTS 2nd 3rd
data providers 2nd 3rd 4th
FCL 2nd 3rd 4th 5th 6th
features specific to 2nd 3rd 4th
Framework
Configuration tool 2nd 3rd
overview of 2nd 3rd 4th
SDK 2nd 3rd 4th 5th 6th 7th 8th 9th
standards 2nd 3rd
tools 2nd 3rd 4th 5th 6th 7th 8th
updating
Garbage Collection 2nd 3rd 4th 5th 6th 7th
generics 2nd 3rd
OLE DB providers in 2nd
operator overloading
security policies 2nd 3rd 4th
2-D graphics 2nd 3rd 4th 5th 6th
@Assembly directive
@Import directive
@Page directives 2nd 3rd
@Register directive 2nd

Previous Section < Day Day Up >


index_A

Index

[SYMBOL] [A] [B] [C] [D] [E] [F] [G] [H] [I] [J] [K] [L] [M] [N] [O] [P] [Q] [R] [S] [T] [U] [V] [W] [X]

abstract classes, comparing to interfaces
abstract modifiers 2nd 3rd
accessor methods
activating forms
adding
data bindings
Help to forms 2nd 3rd 4th 5th 6th
items
data sources 2nd
to list boxes 2nd 3rd
nodes
on trees 2nd 3rd
radio buttons into groups 2nd
rows (DataGridView class) 2nd 3rd
ADO.NET
binding 2nd
connected models 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th 13th 14th 15th 16th
connection classes 2nd 3rd 4th 5th
data access models 2nd 3rd 4th 5th
disconnected models 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th 13th 14th 15th 16th 17th 18th 19th
overview of 2nd 3rd 4th 5th 6th
XML 2nd 3rd 4th 5th 6th 7th 8th
algorithms
DES
aligning
strings 2nd
tab stops 2nd
allocating
memory 2nd 3rd
StringBuilder class 2nd 3rd 4th
alpha values
AlphaRGB (ARGB)
analysis, FxCop tool 2nd 3rd 4th
anchoring controls 2nd
annotations, ToolTip controls
anonymous methods, delegates 2nd
AppDomain class 2nd 3rd 4th
applications
configuration files
data binding 2nd 3rd 4th 5th 6th 7th 8th 9th
deploying 2nd 3rd 4th 5th 6th
Windows 2nd 3rd 4th
architecture
ADO.NET [See ADO.NET]
CLI, 7 [See standards;CLI]
remoting 2nd 3rd 4th 5th
Web services, 870 [See Web services]
ARGB (AlphaRGB)
arithmetic operators 2nd
ArrayList class 2nd
binding 2nd 3rd
objects
arrays
C# 2nd 3rd 4th 5th
controls (binding) 2nd
declaring
System.Array class
ASCII 2nd
ASP.NET
binding 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th 13th 14th 15th 16th 17th 18th 19th 20th 21st 22nd 23rd 24th 25th 26th 27th 28th 29th 30th 31st 32nd 33rd 34th 35th 36th 37th 38th 39th 40th 41st 42nd 43rd 44th 45th 46th 47th 48th 49th 50th 51st 52nd 53rd 54th 55th 56th 57th
buttons 2nd
client-server Internet interaction 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th 13th 14th 15th 16th 17th 18th 19th 20th 21st 22nd 23rd 24th 25th
code-behind models 2nd 3rd 4th 5th 6th
content pages 2nd 3rd 4th 5th
DataList controls 2nd 3rd 4th 5th
HTTP
requests/responses 2nd 3rd 4th 5th
inline code models 2nd 3rd 4th 5th 6th 7th 8th 9th
master pages 2nd 3rd 4th 5th
Page class 2nd 3rd 4th 5th
panels 2nd
text boxes
validation controls 2nd 3rd 4th 5th
Web controls
customizing 2nd 3rd 4th 5th 6th 7th 8th 9th
selecting 2nd
Web Forms controls 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th 13th 14th 15th
assemblies
.NET 2nd 3rd 4th 5th 6th
application domains 2nd
clients 2nd 3rd
configuration file management
configuring
deploying 2nd
FCL
FxCop tool 2nd 3rd 4th
GAC 2nd 3rd
hosts 2nd 3rd
interfaces (SAO) 2nd 3rd
managing
multiple files 2nd 3rd
permissions
granting 2nd
requesting 2nd 3rd 4th
precompiling 2nd
private 2nd
satellite 2nd 3rd 4th 5th 6th 7th 8th
servers 2nd 3rd 4th
shared 2nd
strongly named 2nd 3rd 4th 5th 6th 7th
versioning 2nd 3rd
asynchronous programming 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th 13th 14th
calls
event-based
I/O (Input/Output) 2nd
implementing 2nd 3rd 4th 5th 6th 7th 8th 9th 10th
to Web service methods 2nd
delegate invocation
attributes
@Page directives 2nd 3rd
C# classes 2nd 3rd
conditional 2nd
permissions 2nd
applying 2nd
declarative security 2nd
testing 2nd
WebMethod 2nd 3rd 4th 5th
WebService
XML serialization 2nd
AutoSizeRows method
availability of interface members 2nd
avoiding deadlock 2nd 3rd

Previous Section < Day Day Up >


index_B

Index

[SYMBOL] [A] [B] [C] [D] [E] [F] [G] [H] [I] [J] [K] [L] [M] [N] [O] [P] [Q] [R] [S] [T] [U] [V] [W] [X]

background threads 2nd
backing store
base classes
BeginInvoke method 2nd
BeginPrint event 2nd
binary serialization 2nd 3rd 4th 5th
BinaryFormatter object
binding
ADO.NET 2nd
ArrayList class 2nd 3rd
ASP.NET 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th 13th
DataReader class 2nd 3rd
DataSet class 2nd 3rd 4th 5th
DataTables class 2nd 3rd
lists 2nd
managers 2nd 3rd 4th
policies
BindingManagerBase class 2nd
Bit Block Transfer 2nd
bit flags and enumerated types
bitmaps (BMP)
blocks
catch 2nd 3rd
finally
try
BMI (Body Mass Index)
calculating 2nd 3rd 4th 5th 6th 7th
BMP (bitmaps)
bool type
boxing 2nd 3rd
Break property
BreakBar property
brightness
brushes 2nd 3rd 4th 5th 6th
BufferedStream class 2nd
buffering images 2nd
built-in events 2nd
built-in named permission sets 2nd
built-in security permissions 2nd 3rd
Button class 2nd
buttons
ASP.NET 2nd
click events
controls
formatting 2nd
Help 2nd
byte type

Previous Section < Day Day Up >


index_C

Index

[SYMBOL] [A] [B] [C] [D] [E] [F] [G] [H] [I] [J] [K] [L] [M] [N] [O] [P] [Q] [R] [S] [T] [U] [V] [W] [X]

C#
applications
case sensitivity
embedding comments 2nd 3rd
naming conventions 2nd
arrays 2nd 3rd 4th 5th
classes
constants 2nd 3rd 4th 5th 6th 7th 8th 9th
constructors 2nd 3rd 4th 5th 6th 7th
defining 2nd 3rd 4th 5th 6th
delegates 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th
fields 2nd 3rd 4th 5th 6th 7th 8th 9th
generics 2nd 3rd 4th
interfaces 2nd 3rd 4th 5th
members 2nd
methods 2nd 3rd 4th 5th 6th 7th 8th 9th 10th
operator overloading 2nd 3rd 4th
properties 2nd 3rd 4th 5th 6th 7th 8th 9th
structures 2nd 3rd 4th 5th 6th
code-behind models (ASP.NET) 2nd 3rd 4th 5th 6th
compiling 2nd 3rd 4th 5th
enumerated types 2nd 3rd 4th
features specific to 2nd 3rd 4th
layouts 2nd 3rd 4th 5th 6th
loops 2nd 3rd 4th 5th
do
for 2nd
foreach
transferring control within 2nd
while
objects
.NET collection classes and interfaces 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th 13th 14th 15th 16th 17th 18th 19th 20th 21st
creating 2nd 3rd 4th 5th
exception handling 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th
implementing System.Object methods 2nd 3rd 4th 5th 6th 7th 8th
life cycle management 2nd 3rd 4th 5th
serialization 2nd 3rd 4th 5th 6th
operators
arithmetic 2nd
conditional 2nd
equality (==)
relational 2nd
preprocessing directives 2nd 3rd
primitives 2nd 3rd 4th 5th 6th
bool type
byte type
char type
decimal type
double type
int type
long type
Parse method 2nd
sbyte type
short type
single type
TRyParse method 2nd
statements
control flow 2nd
if-else 2nd
switch 2nd
strings 2nd 3rd 4th 5th 6th
text
comparing strings 2nd 3rd 4th
formatting DateTime/numeric values 2nd 3rd 4th 5th 6th 7th 8th 9th
modifying strings 2nd 3rd 4th 5th
regular expressions 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th
StringBuilder class 2nd 3rd 4th
System.IO namespace 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th 13th 14th 15th 16th 17th 18th 19th
System.String class 2nd 3rd 4th
Unicode 2nd 3rd 4th 5th 6th
CacheDuration property
caching
deploying 2nd
GAC 2nd 3rd
output from Web operations
callbacks 2nd
asynchronous calls 2nd 3rd
calls
asynchronous
event-based
to Web service methods 2nd
synchronous calls to Web service methods 2nd
CAO (client-activated objects) 2nd
carriage returns 2nd
case sensitivity of C# applications
casting
catch block 2nd 3rd
catching exceptions
cells
formatting 2nd
recognizing 2nd
channels
naming 2nd
remoting 2nd
char type
Char variables, assigning 2nd
characters 2nd 3rd 4th 5th
carriage returns 2nd
dates 2nd
encoding schemes 2nd
matching
positional
repetition
standards 2nd 3rd 4th 5th
strings
escape 2nd
tab stops 2nd
XML 2nd 3rd
CheckBox class 2nd
Checked property
CheckedListBox class 2nd
CIL (Common Intermediate Language)
classes
.NET collections 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th 13th 14th 15th 16th 17th 18th 19th 20th 21st
AppDomain 2nd 3rd 4th
ArrayList 2nd
binding 2nd 3rd
base
BindingManagerBase 2nd
BufferedStream 2nd
built-in permission 2nd
Button 2nd
C#
constants 2nd 3rd 4th 5th 6th 7th 8th 9th
constructors 2nd 3rd 4th 5th 6th 7th
defining 2nd 3rd 4th 5th 6th
delegates 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th
fields 2nd 3rd 4th 5th 6th 7th 8th 9th
generics 2nd 3rd 4th
interfaces 2nd 3rd 4th 5th
members 2nd
methods 2nd 3rd 4th 5th 6th 7th 8th 9th 10th
operator overloading 2nd 3rd 4th
properties 2nd 3rd 4th 5th 6th 7th 8th 9th
structures 2nd 3rd 4th 5th 6th
CheckBox 2nd
CheckedListBox 2nd
ComboBox 2nd
connections (ADO.NET) 2nd 3rd 4th 5th
ConnectionStringBuilder
ContextMenu
CryptoStream 2nd 3rd 4th
DataAdapter 2nd 3rd 4th 5th 6th 7th 8th
DataGridView
configuring master-detail 2nd 3rd 4th
data binding 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th 13th 14th 15th 16th 17th 18th 19th 20th 21st
delegates 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th 13th
events 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th 13th 14th 15th 16th 17th 18th 19th
parameters 2nd 3rd
properties 2nd 3rd 4th 5th 6th 7th 8th
virtual mode 2nd 3rd 4th 5th
DataReader 2nd 3rd 4th 5th
binding 2nd 3rd
DataRow 2nd 3rd
DataSet 2nd 3rd 4th 5th
binding 2nd 3rd 4th 5th
defining relationships 2nd 3rd
loading data into 2nd 3rd
XML 2nd 3rd 4th 5th 6th 7th
DataTables 2nd 3rd 4th 5th 6th 7th 8th
DateTimeFormatInfo
DbCommand
DbProviderFactories
DefaultPageSettings
Delegate 2nd 3rd
Directory 2nd 3rd 4th
DirectoryInfo 2nd 3rd 4th
DrawImage
events 2nd
exceptions 2nd 3rd
FCL 2nd 3rd 4th 5th 6th
File 2nd 3rd
FileInfo 2nd 3rd
FileStream 2nd 3rd
FileSystemInfo
Font 2nd 3rd 4th
FontFamily 2nd
Form 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th 13th 14th 15th 16th 17th 18th 19th 20th 21st
generics
Graphics
GDI+ 2nd 3rd 4th 5th
methods 2nd
GraphicsPath 2nd
GroupBox
HatchBrush 2nd
HttpRequest objects 2nd 3rd 4th 5th
identifiers 2nd
identity permissions
instances
Label 2nd
LinearGradientBrush 2nd 3rd 4th
ListBox 2nd 3rd 4th 5th 6th 7th
ListView 2nd 3rd 4th 5th 6th 7th 8th
MemoryStream 2nd
MenuItem properties
MessageBox 2nd
Monitor 2nd
Mutex 2nd
NumberFormatInfo
operators 2nd 3rd 4th
Page (ASP.NET) 2nd 3rd 4th 5th
PageSettings 2nd
Panel 2nd 3rd
Path 2nd 3rd 4th
Pen 2nd 3rd
PictureBox 2nd 3rd
PrintDocument 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th 13th 14th 15th 16th 17th 18th
customizing 2nd 3rd
events 2nd 3rd 4th
ProgressBar 2nd 3rd 4th
ProviderFactory
proxy
wsdl.exe utility 2nd 3rd 4th 5th 6th
Queue 2nd 3rd
RadioButton 2nd 3rd
Regex 2nd 3rd 4th 5th 6th
ResourceManager 2nd
ResourceWriter
ResXResourceReader 2nd
ResXResourceWriter
Semaphore 2nd
ShowDialog 2nd
Stack 2nd 3rd
StatusStrip 2nd 3rd 4th
Stream 2nd
StringBuilder 2nd 3rd 4th
StringFormat
StringReader 2nd
StringWriter 2nd
structures 2nd 3rd
System.Array
System.Collections
generic namespaces 2nd 3rd 4th
namespaces
System.Exception 2nd
properties 2nd
System.Object
customizing 2nd 3rd 4th 5th 6th 7th 8th
System.String 2nd 3rd 4th 5th 6th 7th
System.Web.Services.WebService 2nd
TextBox 2nd 3rd
Thread
Timer 2nd 3rd 4th
TReeNode 2nd 3rd 4th
TReeView 2nd 3rd 4th 5th 6th 7th
Windows.Forms.Form control classes 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th 13th 14th
XmlDataDocument 2nd 3rd
XmlDocument
XmlNodeReader 2nd 3rd
XmlReader 2nd 3rd 4th 5th 6th
XmlReaderSettings 2nd
XmlSerializer 2nd 3rd
XmlWriter 2nd 3rd
XPathDocument
XslTransform 2nd 3rd
CLI (Common Language Infrastructure)
standards 2nd 3rd
click event buttons
client-activated objects [See objects;CAO]
clients
assemblies 2nd 3rd 4th 5th 6th 7th
remoting 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th 13th 14th 15th 16th 17th 18th 19th 20th 21st 22nd 23rd 24th 25th 26th 27th 28th 29th
servers (Internet interaction) 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th 13th 14th 15th 16th 17th 18th 19th 20th 21st 22nd 23rd 24th 25th
UDDI 2nd 3rd
Web services
building 2nd 3rd 4th 5th 6th 7th 8th 9th
Clone method
cloning objects
closing forms
CLR (Common Language Runtime) 2nd 3rd 4th 5th 6th 7th 8th 9th 10th
.NET 2nd
reference/value type support 2nd 3rd 4th 5th
CLS (Common Language Specification)
code
.NET 2nd
design 2nd 3rd 4th 5th
exception handling 2nd 3rd
forms 2nd 3rd
groups
creating 2nd
testing
inline code models (ASP.NET) 2nd 3rd 4th 5th 6th 7th 8th 9th
isolation
libraries
managed
regions
code points (Unicode identifiers)
code-behind models
ASP.NET 2nd 3rd 4th 5th 6th
CodeBase configuration
collections 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th 13th 14th 15th 16th 17th 18th 19th 20th 21st
bindings
enumeration
members (iterators) 2nd 3rd
colors 2nd 3rd
multi-color gradients
objects
viewers 2nd 3rd 4th 5th 6th
wheels
columns
DataTable.Columns property 2nd 3rd
headers 2nd
recognizing 2nd
tab stops 2nd
types 2nd
value access 2nd
ComboBox class 2nd
command line, compiling from the 2nd 3rd 4th
CommandBuilder object 2nd
commands
objects 2nd 3rd 4th 5th
SQL 2nd
comments, embedding 2nd 3rd
Common Intermediate Language [See CIL]
Common Language Infrastructure [See standards;CLI]
Common Language Runtime [See CLR]
Common Language Specification [See types;CLS]
Common Type System [See types;CTS]
Compact Profile
comparing
abstract classes and interfaces
classes and structures 2nd 3rd
strings 2nd
compiling
C# 2nd 3rd 4th 5th
code (.NET) 2nd
from the command line 2nd 3rd 4th
multiple files
Web pages
components
HelpProvider 2nd
composite controls 2nd 3rd 4th
compressing files 2nd
concatenation
StringBuilder class
strings 2nd
conditional attributes 2nd
conditional compilation
conditional operators 2nd
configuration
application configuration files
assemblies
configuration files
assembly management
message servers 2nd 3rd
configuring
.NET Framework Configuration tool security 2nd 3rd
CodeBase configuration
column headers 2nd
DataGridViews class 2nd 3rd 4th
integrated help systems 2nd 3rd 4th 5th 6th
leasing 2nd
permissions
printer resolution
security 2nd 3rd 4th 5th 6th 7th 8th 9th
strongly named assemblies 2nd
Web services (Web.config file)
connected models
ADO.NET 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th 13th 14th 15th 16th
selecting 2nd
connections
classes (ADO.NET) 2nd 3rd 4th 5th
pooling
strings 2nd 3rd
ConnectionStringBuilder class
constants, C# classes 2nd 3rd
constraints, relationships 2nd
constructors
C# classes 2nd 3rd 4th 5th 6th 7th
Delegate class
Font class
inheritance
instances 2nd 3rd 4th 5th
private 2nd
static 2nd
StringBuilder class
content pages (ASP.NET) 2nd 3rd 4th 5th
context
devices
menus 2nd
ContextMenu class
control flow statements (C#) 2nd
controls
anchoring [See user controls]
arrays [See user controls]
buttons [See user controls]
classes [See user controls]
composite [See user controls]
CreateGraphics method [See user controls]
CustomValidator [See user controls]
data binding [See user controls]
data display [See user controls]
data sources [See user controls]
DataList [See user controls]
DataSource property [See user controls]
docking [See user controls]
dragging and dropping [See user controls]
events [See user controls]
extending [See user controls]
filling [See user controls]
FlowLayoutPanel [See user controls]
forms [See user controls]
buttons [See user controls]
CheckedListBox [See user controls]
ComboBox [See user controls]
customizing [See user controls]
dragging and dropping [See user controls]
group boxes [See user controls]
iterating [See user controls]
ListBox [See user controls]
ListView [See user controls]
panels [See user controls]
PictureBox [See user controls]
populating [See user controls]
ProgressBar [See user controls]
resources [See user controls]
StatusStrip [See user controls]
TextBox [See user controls]
Timer [See user controls]
treeView [See user controls]
interfaces [See user controls]
lists [See user controls]
login [See user controls]
ObjectDataSource [See user controls]
positioning [See user controls]
properties [See user controls]
servers (View state) [See user controls]
sizing [See user controls]
SqlDataSource [See user controls]
state management [See user controls]
TableLayoutPanel [See user controls]
ToolTip [See user controls]
validating [See user controls]
ValidationSummary [See user controls]
values [See user controls]
Web
customizing ASP.NET [See user controls]
selecting ASP.NET [See user controls]
Web Forms [See user controls]
Web pages [See user controls]
XmlDataSource [See user controls]
conventions, naming 2nd
converting
Char values
narrowing
numeric types 2nd
resource files
widening
Copy method
copying images
CreateGraphics method
creational patterns
cross-page posting
CryptoStream class 2nd 3rd 4th
CTS (Common Type System) 2nd 3rd
cultures
assembly settings
dates
strings 2nd 3rd
CurrentThread property
custom date formats 2nd
custom Web controls 2nd
customizing
ADO.NET Web controls 2nd 3rd 4th 5th 6th 7th 8th 9th
class exceptions 2nd 3rd
controls 2nd 3rd 4th 5th 6th
data sources 2nd
events 2nd
interfaces 2nd 3rd
list boxes 2nd 3rd
PrintDocument class 2nd 3rd
System.Object class 2nd 3rd 4th 5th 6th 7th 8th
CustomValidator control 2nd

Previous Section < Day Day Up >


index_D

Index

[SYMBOL] [A] [B] [C] [D] [E] [F] [G] [H] [I] [J] [K] [L] [M] [N] [O] [P] [Q] [R] [S] [T] [U] [V] [W] [X]

DashStyle property
data access models (ADO.NET) 2nd 3rd 4th 5th
data binding
applications [See binding]
DataGridView class [See binding]
overview of [See binding]
data display controls
data error handling 2nd
data providers
.NET 2nd 3rd 4th
factories 2nd 3rd
data sources
controls 2nd
customizing 2nd
for reports
items 2nd
data types
C# primitive 2nd
DataAdapter class 2nd 3rd 4th 5th 6th 7th 8th
databases
SQL, 507 [See commands;SQL]
synchronizing 2nd 3rd 4th
updating 2nd 3rd 4th 5th 6th 7th
DataBindings.Add method
DataError event
DataGridView class
data binding 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th 13th 14th 15th 16th 17th 18th 19th 20th 21st
delegates 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th 13th
events 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th 13th 14th 15th 16th 17th 18th 19th
master-detail 2nd 3rd 4th
parameters 2nd 3rd
properties 2nd 3rd 4th 5th 6th 7th 8th
virtual mode 2nd 3rd 4th 5th
DataList controls 2nd 3rd 4th 5th
DataReader class 2nd 3rd 4th 5th
binding 2nd 3rd
DataRow class 2nd 3rd
DataSet class 2nd 3rd 4th 5th
binding 2nd 3rd 4th 5th
defining relationships 2nd 3rd
loading data into 2nd 3rd
XML 2nd 3rd 4th 5th 6th 7th
DataSource property controls 2nd 3rd 4th 5th 6th 7th 8th 9th
DataTable.Columns property 2nd 3rd
DataTables class 2nd 3rd 4th 5th 6th 7th 8th
dates
DateTime class, formatting 2nd 3rd 4th 5th 6th 7th 8th 9th
DateTimeFormatInfo class
DbCommand class
DbProviderFactories class
deactivating forms
deadlock, avoiding 2nd 3rd
decimal type
declarative security
permission attributes 2nd
decoding, 219 [See encoding]
delayed signing 2nd
Delegate class 2nd 3rd
delegates
anonymous methods 2nd
asynchronous invocation
C# classes 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th
DataGridView class 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th 13th
events
defining 2nd
handling 2nd 3rd 4th 5th 6th 7th 8th 9th
MethodInvoker 2nd
multicasting
deleting
nodes 2nd 3rd 4th
subdirectories
deploying
applications 2nd 3rd 4th 5th 6th
CAO applications
GAC 2nd
private assemblies 2nd
XCOPY 2nd
derived classes
DES algorithm
devices, context
diagnostic directives 2nd
dialog boxes, forms 2nd 3rd 4th
Dijkstra, Edsger
directives
@Assembly
@Import
@Page 2nd 3rd
@Register 2nd
diagnostic 2nd
preprocessing C# 2nd 3rd
WebService
directories
System.IO namespace 2nd 3rd 4th 5th 6th 7th 8th
Directory class 2nd 3rd 4th
DirectoryInfo class 2nd 3rd 4th
disconnected models (ADO.NET) 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th 13th 14th 15th 16th 17th 18th 19th
DISconnected models, selecting 2nd
discovery
UDDI [See servers;UDDI]
Web services 2nd 3rd 4th
Dispose method 2nd 3rd 4th 5th
do loops
docking controls 2nd
Document Object Model [See DOM]
documenting XML tags
documents
fragments
printing, 441 [See printing]
XML, 463 [See serialization;XML]
DoDragDrop method
DOM (Document Object Model)
domains
AppDomain class 2nd 3rd 4th
applications 2nd 3rd 4th 5th 6th
assemblies 2nd
double type
dragging and dropping controls 2nd 3rd 4th 5th 6th
DrawImage class
drawing, 380
multi-line text [See GDI+]
rectangles [See GDI+]
text strings [See GDI+]
DrawRectangle method

Previous Section < Day Day Up >


index_E

Index

[SYMBOL] [A] [B] [C] [D] [E] [F] [G] [H] [I] [J] [K] [L] [M] [N] [O] [P] [Q] [R] [S] [T] [U] [V] [W] [X]

ECMA International
embedding comments 2nd 3rd
Enabled property
EnableSession property
encapsulation, monitoring 2nd
encoding
strings 2nd
Unicode 2nd 3rd 4th 5th
EndCap property
EndInvoke method
EndPrint event
enumerated types
bit flags and
C# 2nd 3rd 4th
enumeration
collections
iterators
System.Enum methods
XML
environments
.NET Framework [See standards;.NET Framework]
IDE
equality (==) operator
Equals( ) method 2nd 3rd
errors
data error handling 2nd
updating
escape characters
escape sequences
events
asynchronous calls
BeginPrint 2nd
binary serialization 2nd
built-in 2nd
buttons 2nd
controls 2nd 3rd 4th 5th 6th
customizing 2nd
DataError
DataGridView class 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th 13th 14th 15th 16th 17th 18th 19th
declaring
delegates 2nd
dragging and dropping 2nd 3rd 4th
EndPrint
handling 2nd 3rd 4th 5th 6th 7th 8th 9th
keyboard 2nd 3rd
mouse 2nd 3rd 4th
overriding 2nd
Page_Load 2nd
pages 2nd
Paint (GDI+) 2nd 3rd 4th 5th
PrintDocument class 2nd 3rd 4th
PrintPage 2nd
propertyChanged
QueryPageSettingsEvent 2nd
servers
Everything permission
evidence 2nd 3rd 4th 5th
exceptions
classes 2nd 3rd
handling 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th
guidelines 2nd
SEH
writing code to 2nd 3rd
System.Exception class 2nd
unhandled 2nd 3rd
ExecuteReader method 2nd
Executexxx methods
executing
SQL commands 2nd
stored procedures 2nd 3rd
Execution permission
Extended Markup Language [See serialization;XML]
Extended Style Language Transformation [See XSLT]
extending
controls 2nd
Web services 2nd 3rd 4th
extracting substrings 2nd

Previous Section < Day Day Up >


index_F

Index

[SYMBOL] [A] [B] [C] [D] [E] [F] [G] [H] [I] [J] [K] [L] [M] [N] [O] [P] [Q] [R] [S] [T] [U] [V] [W] [X]

F1 keys, invoking help 2nd
factories
creational patterns
data providers 2nd 3rd
multiple 2nd
familles of fonts
FCL (Framework Class Library) 2nd 3rd 4th 5th 6th
factory creational patterns
namespaces 2nd 3rd
fields
C# classes 2nd 3rd
initializing
static read-only
File class 2nd 3rd
FileInfo class 2nd 3rd
files
.asmx [See directories]
application configuration [See directories]
compressing [See directories]
configuration
managing assemblies [See directories]
message servers [See directories]
listing files and directories [See directories]
manifests [See directories]
multi-file assemblies [See directories]
multiple [See directories]
opening files [See directories]
resources [See directories]
schemas [See directories]
System.IO namespace [See directories]
text
reading from [See directories]
writing to [See directories]
web.config
configuring Web services [See directories]
XML, 463 [See directories, serialization;XML]
FileStream class 2nd 3rd
FileSystemInfo class
finalization 2nd
Finalize method 2nd 3rd
finally block
flags, bit and enumerated types
FlowLayoutPanel control 2nd
Focus and tab order
Font class 2nd 3rd 4th
FontFamily class 2nd
fonts
formatting 2nd
GDI+ 2nd 3rd 4th 5th 6th
metrics 2nd 3rd
for loops 2nd
foreach loops
foreground threads 2nd
foreign keys
Form class 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th 13th 14th 15th 16th 17th 18th 19th 20th 21st
formatters, remoting
formatting
BinaryFormatter object
buttons 2nd
cells 2nd
checkboxes
content pages 2nd
control buttons
DateTime values 2nd 3rd 4th 5th 6th 7th 8th 9th
files (XML)
fonts 2nd
forms 2nd
appearance 2nd 3rd 4th 5th
localized 2nd 3rd 4th
locations 2nd
MDI menus 2nd 3rd
items (ListView) 2nd
master pages 2nd
numeric values 2nd 3rd 4th 5th 6th 7th 8th 9th
objects 2nd 3rd 4th 5th
colors
FontFamily
pages for printing 2nd
regular expressions 2nd
schemas 2nd 3rd
String.Format method
strings 2nd 3rd 4th
tab stops 2nd
text 2nd 3rd
Web controls 2nd
forms
activating
ASP.NET [See ASP.NET]
closing
code 2nd 3rd
controls 2nd 3rd 4th
buttons 2nd 3rd 4th 5th 6th 7th 8th 9th
CheckedListBox 2nd
ComboBox 2nd
customizing 2nd 3rd 4th 5th 6th
dragging and dropping 2nd 3rd 4th 5th 6th
group boxes 2nd 3rd 4th 5th 6th 7th 8th 9th
iterating
labels 2nd 3rd 4th 5th 6th 7th 8th 9th
ListBox 2nd 3rd 4th 5th 6th 7th
ListView 2nd 3rd 4th 5th 6th 7th 8th
panels 2nd 3rd 4th 5th 6th 7th 8th 9th
PictureBox 2nd 3rd
populating
ProgressBar 2nd 3rd 4th
resources 2nd 3rd 4th 5th 6th 7th 8th
StatusStrip 2nd 3rd 4th
TextBox 2nd 3rd
Timer 2nd 3rd 4th
treeView 2nd 3rd 4th 5th 6th 7th
creating
deactivating
dialog boxes 2nd 3rd 4th
formatting 2nd 3rd 4th 5th 6th
Help 2nd 3rd 4th 5th 6th
inheritance 2nd 3rd
interaction 2nd 3rd 4th 5th
life cycles 2nd 3rd
locations 2nd
MDI 2nd 3rd 4th 5th 6th
menus 2nd 3rd
messages 2nd 3rd 4th
opacity
owned 2nd
owner 2nd
sizing 2nd
transparency 2nd 3rd 4th
viewing 2nd
fragments
Framework Class Library [See classes;FCL]
Framework Configuration tool 2nd
frozen columns
FullTrust permission
functions (XPath) 2nd
FxCop tool 2nd 3rd 4th

Previous Section < Day Day Up >


index_G

Index

[SYMBOL] [A] [B] [C] [D] [E] [F] [G] [H] [I] [J] [K] [L] [M] [N] [O] [P] [Q] [R] [S] [T] [U] [V] [W] [X]

GAC (Global Assembly Cache)
deploying 2nd
Garbage Collection (.NET) 2nd 3rd 4th 5th 6th 7th
GDI (Graphical Device Interface)
GDI+ 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th 13th 14th 15th 16th 17th 18th 19th 20th 21st 22nd 23rd 24th 25th 26th 27th 28th 29th 30th 31st 32nd 33rd 34th 35th 36th 37th 38th 39th 40th 41st 42nd 43rd 44th 45th
fonts 2nd 3rd 4th 5th 6th
Graphics class 2nd 3rd 4th 5th
Graphics object 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th 13th 14th 15th 16th 17th 18th 19th 20th
images 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th 13th 14th 15th 16th
Paint events 2nd 3rd 4th 5th
printing 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th 13th 14th 15th 16th 17th 18th
text 2nd 3rd 4th 5th 6th 7th
Windows 2nd
generics
C# classes 2nd 3rd
classes
collection class example 2nd 3rd 4th
GetHashCode method, overriding 2nd
GIF (Graphics Interchange Format)
Global Assembly Cache [See caching;GAC]
gradients, creating
granting permissions 2nd
Graphical Device Interface [See interfaces;GDI]
Graphical User Interface [See interfaces;GUI]
Graphics class
GDI+ 2nd 3rd 4th 5th
methods 2nd
Graphics Interchange Format (GIF)
Graphics object (GDI+) 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th 13th 14th 15th 16th 17th 18th 19th 20th
graphics.
2-D [See GDI+, images]
GDI+, 379 [See GDI+, images, ]
Graphics.DrawString method
GraphicsPath class 2nd
GroupBox class
GUI (Graphical User Interface)
guidelines
code design 2nd 3rd 4th 5th
connection pooling
handling exceptions 2nd

Previous Section < Day Day Up >


index_H

Index

[SYMBOL] [A] [B] [C] [D] [E] [F] [G] [H] [I] [J] [K] [L] [M] [N] [O] [P] [Q] [R] [S] [T] [U] [V] [W] [X]

handling
data errors 2nd
events 2nd 3rd 4th 5th 6th 7th 8th 9th
buttons
on servers
exceptions 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th
guidelines 2nd
SEH
writing code to 2nd 3rd
keyboard events 2nd 3rd
mouse events 2nd 3rd 4th
Hashtable
creating 2nd
interfaces 2nd 3rd 4th
keys 2nd
serialization
values
HatchBrush class 2nd
headers
columns 2nd
heaps 2nd
height
fonts 2nd 3rd
rows in DataGridView 2nd
Help, adding forms 2nd 3rd 4th 5th 6th
HelpProvider component 2nd
hiding IL 2nd
high surrogates
hit testing shapes 2nd 3rd
hosts, assemblies 2nd 3rd
hot keys
HSB (hue/saturation/brightness)
HSL (hue/saturation/luminosity)
HTML (Hypertext Markup Language)
code-behind models (ASP.NET) 2nd 3rd 4th 5th 6th
inner content
tags
XML style sheets 2nd
HTTP (Hypertext Transfer Protocol) 2nd
ASP.NET 2nd 3rd 4th 5th
HttpRequest class objects 2nd 3rd 4th 5th 6th 7th
HttpResponse class
object 2nd
Hypertext Markup Language [See tags;HTML]

Previous Section < Day Day Up >


index_I

Index

[SYMBOL] [A] [B] [C] [D] [E] [F] [G] [H] [I] [J] [K] [L] [M] [N] [O] [P] [Q] [R] [S] [T] [U] [V] [W] [X]

I/O (Input/Output), asynchronous calls 2nd
ICollection interfaces 2nd
IComparable interfaces, sorting 2nd
IComparer interfaces, sorting 2nd
icons, specifying 2nd
IDbCommand interface 2nd 3rd 4th 5th
IDbConnection interface
IDE (Integrated Development Environment) 2nd
identifiers, classes 2nd
identifying updating 2nd
identity permissions 2nd
IDictionary interfaces
IDictionaryEnumerator interfaces
IEnumerable interfaces 2nd 3rd
IEnumerator interfaces 2nd 3rd
if-else statements 2nd
IHashCodeProvider interfaces
IL (Intermediate Language)
hiding 2nd
viewing
Ildasm.exe 2nd 3rd 4th
IList interfaces 2nd
images
buffering 2nd
copying
creating 2nd 3rd 4th 5th 6th 7th 8th
GDI+ 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th 13th 14th 15th 16th
loading 2nd 3rd 4th
menus 2nd 3rd
mirroring 2nd
modifying 2nd 3rd 4th 5th 6th
Ngen
PictureBox class 2nd 3rd
rectangles 2nd 3rd
rotating 2nd
saving 2nd 3rd 4th
servers 2nd 3rd 4th 5th 6th
ToolTip controls
indexers (C# classes) 2nd 3rd
IndexOf method
infrastructure (CLI)
inheritance
classes
C#
defining
constructors
forms 2nd 3rd
methods 2nd
structures
Initialization Vector (IV)
initializers 2nd 3rd
initializing
fields
interfaces 2nd
virtual DataGridView
inline code models (ASP.NET) 2nd 3rd 4th 5th 6th 7th 8th 9th
inner HTML content
Input/Output [See I/O]
inserting [See adding]
instances
class references
constructors 2nd 3rd 4th 5th
int type
Integrated Development Environment [See environments;IDE]
integrated help systems, configuring 2nd 3rd 4th 5th 6th
interaction, forms 2nd 3rd 4th 5th
interfaces
.NET collections 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th 13th 14th 15th 16th 17th 18th 19th 20th 21st
abstract classes
assemblies (SAO) 2nd 3rd
C# classes 2nd 3rd 4th 5th 6th
controls 2nd
customizing 2nd 3rd
GDI [See also interfaces;GDI]
GUI
Hashtable 2nd 3rd 4th
ICollection 2nd
IComparable 2nd
IComparer 2nd
IDbCommand 2nd 3rd 4th 5th
IDbConnection
IDictionary
IDictionaryEnumerator
IEnumerable 2nd 3rd
IEnumerator 2nd 3rd
IHashCodeProvider
IList 2nd
initializing 2nd
MDI 2nd 3rd 4th 5th 6th
members 2nd
methods
OLE DB
simple controls 2nd 3rd 4th 5th 6th
Intermediate Language [See IL]
Internet client-server interaction 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th 13th 14th 15th 16th 17th 18th 19th 20th 21st 22nd 23rd 24th 25th
Internet permission
interning strings 2nd 3rd
Interop
Invalidate method 2nd 3rd
Invoke method
isolation, code
IsPostBack property
items
data sources 2nd
detecting 2nd
iterating
ListView 2nd
selecting
sorting 2nd
iterating
controls on forms
items
nodes
iterators 2nd 3rd
IV (Initialization Vector)

Previous Section < Day Day Up >


index_J

Index

[SYMBOL] [A] [B] [C] [D] [E] [F] [G] [H] [I] [J] [K] [L] [M] [N] [O] [P] [Q] [R] [S] [T] [U] [V] [W] [X]

JavaScript, BMI calculators 2nd 3rd 4th
JIT (Just-in-Time)
JPEG (Joint Photographics Experts Group)
Just-in-Time [See JIT]

Previous Section < Day Day Up >


index_K

Index

[SYMBOL] [A] [B] [C] [D] [E] [F] [G] [H] [I] [J] [K] [L] [M] [N] [O] [P] [Q] [R] [S] [T] [U] [V] [W] [X]

Kernel Profile
keyboard events, handling 2nd 3rd
keys
F1 (help) 2nd
foreign
hashtables 2nd
hot
primary
private 2nd
public 2nd 3rd

Previous Section < Day Day Up >


index_L

Index

[SYMBOL] [A] [B] [C] [D] [E] [F] [G] [H] [I] [J] [K] [L] [M] [N] [O] [P] [Q] [R] [S] [T] [U] [V] [W] [X]

Label class 2nd
leasing 2nd 3rd 4th 5th
objects
libraries
code
FCL 2nd 3rd 4th 5th 6th
forms 2nd
life cycles
forms 2nd 3rd
object management 2nd 3rd 4th 5th 6th 7th
lifetimes of leases 2nd
LinearGradientBrush class 2nd 3rd 4th
lines
DrawLine method
multi-line text 2nd
linking [See also binding]
ASP.NET [See also binding]
ListBox class 2nd 3rd 4th 5th 6th 7th
listing
files 2nd
font families
Hashtable keys and values
lists
binding 2nd
controls 2nd 3rd 4th 5th 6th
navigating 2nd
ListView class 2nd 3rd 4th 5th 6th 7th 8th
literals, strings 2nd 3rd
loading
data
ADO.NET
into DataSet class 2nd 3rd
images 2nd 3rd 4th
LocalIntranet permission
localization
characters 2nd
forms 2nd 3rd 4th
location
Control property
form location 2nd
locking
deadlock 2nd 3rd
objects
login
controls
long type
loops (C#) 2nd 3rd 4th 5th
do
for 2nd
foreach
transferring control within 2nd
while
low surrogates

Previous Section < Day Day Up >


index_M

Index

[SYMBOL] [A] [B] [C] [D] [E] [F] [G] [H] [I] [J] [K] [L] [M] [N] [O] [P] [Q] [R] [S] [T] [U] [V] [W] [X]

Main() method
manifests 2nd
marshaling by reference (MBR) 2nd
marshaling by value (MBV) 2nd
master pages (ASP.NET) 2nd 3rd 4th 5th
master-detail views, configuring 2nd 3rd 4th
Match() method 2nd 3rd
Matches() method 2nd 3rd
matching characters
MBR (marshaling by reference) 2nd
MBV (marshaling by value) 2nd
MDI (Multiple Document Interface) 2nd 3rd 4th 5th 6th
memory
allocating 2nd 3rd
intern pools
releasing
MemoryStream class 2nd
MenuItem class properties
menus
buttons (ASP.NET) 2nd
context 2nd
forms 2nd 3rd
images 2nd 3rd
MDI forms 2nd 3rd
MessageBox class 2nd
MessageName property
messages
request 2nd
metadata 2nd
MethodInvoker delegate 2nd
methods
accessor
anonymous 2nd
asynchronous 2nd 3rd 4th 5th 6th 7th 8th 9th 10th
C# classes 2nd 3rd 4th 5th 6th 7th 8th 9th 10th
ExecuteReader 2nd
inheritance 2nd
interfaces
modifiers 2nd 3rd 4th 5th 6th
multicasting
Parse, tryParse 2nd
ShowHelp
structures 2nd
methods, .NET Framework
ControlBindingsCollection
System.Array 2nd 3rd
System.Char 2nd 3rd
System.Drawing.Graphics 2nd 3rd 4th 5th
System.Enum
System.IO.DirectoryInfo, FileSystemInfo 2nd 3rd 4th
System.Object 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th
System.RegularExpressions.Regex 2nd 3rd 4th 5th
System.String 2nd 3rd 4th 5th 6th 7th 8th 9th
System.Text.StringBuilder 2nd
System.Windows.Forms.Control
System.Xml.XmlWriter 2nd 3rd
System.Xml.Xsl.XslTransform
metrics, sizing fonts 2nd 3rd
Microsoft CIL standards
Microsoft Intermediate Language [See MSIL]
mirroring images 2nd
MMC (Microsoft Management Console)
Framework Configuration tool
modeless forms, life cycles of
modifiers
abstract 2nd 3rd
access 2nd 3rd
fields
methods 2nd 3rd 4th 5th 6th
override 2nd
sealed 2nd 3rd
stack walk 2nd
versioning 2nd
virtual 2nd
Monitor class 2nd
monitoring encapsulation 2nd
mouse events, handling 2nd 3rd 4th
moving controls 2nd 3rd 4th 5th 6th
MSIL (Microsoft Intermediate Language)
multi-color gradients, creating
multi-file assemblies
multi-line text, drawing 2nd
multicasting delegates
Multiple Document Interface [See forms;MDI]
multiple files, compiling
multithreading 2nd 3rd 4th
user interface controls 2nd
Mutex class 2nd

Previous Section < Day Day Up >


index_N

Index

[SYMBOL] [A] [B] [C] [D] [E] [F] [G] [H] [I] [J] [K] [L] [M] [N] [O] [P] [Q] [R] [S] [T] [U] [V] [W] [X]

namespaces
FCL 2nd 3rd
System.Collections class 2nd 3rd 4th 5th
System.Drawing.Drawing2D
System.IO
directories 2nd 3rd 4th 5th 6th 7th 8th
files 2nd 3rd 4th 5th 6th 7th 8th
streams 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th
System.Web.UI.WebControls
using statements
naming
assemblies
channels 2nd
conventions 2nd
identifiers 2nd
paths
permission sets 2nd
strongly named assemblies 2nd 3rd 4th 5th 6th
narrowing conversion
Native Image Generator [See images;Ngen]
navigating
lists 2nd
XPath
Ngen (Native Image Generator)
nodes
adding, deleting, iterating TReeView 2nd
trees
adding on 2nd 3rd
deleting on 2nd 3rd
XmlNodeReader class 2nd 3rd
Nothing permission
NumberFormatInfo class
numeric types, primitives 2nd
numeric values, formatting 2nd 3rd 4th 5th 6th 7th 8th 9th

Previous Section < Day Day Up >


index_O

Index

[SYMBOL] [A] [B] [C] [D] [E] [F] [G] [H] [I] [J] [K] [L] [M] [N] [O] [P] [Q] [R] [S] [T] [U] [V] [W] [X]

obfuscation and Ildasm.exe 2nd
ObjectDataSource control
objects
ArrayList
BinaryFormatter
C#
.NET collection classes and interfaces 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th 13th 14th 15th 16th 17th 18th 19th 20th 21st
creating 2nd 3rd 4th 5th
exception handling 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th
implementing System.Object methods 2nd 3rd 4th 5th 6th 7th 8th
life cycle management 2nd 3rd 4th 5th
serialization 2nd 3rd 4th 5th 6th
CAO 2nd 3rd 4th 5th 6th 7th 8th
cloning
CommandBuilder 2nd
commands 2nd 3rd 4th 5th
finalization 2nd
locking
polling 2nd 3rd 4th
SAO 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th 13th 14th 15th
SOAP
synchronization 2nd 3rd 4th
WKO
OLE DB providers in .NET 2nd
one-way data binding 2nd
opacity of forms
opening files 2nd 3rd
OpenType fonts, 428 [See fonts]
operators
C#
arithmetic 2nd
conditional 2nd
equality (==)
relational 2nd
overloading 2nd 3rd 4th
XPath 2nd 3rd
optimistic concurrency
ordering tabs
overloading
operators 2nd 3rd 4th
strings 2nd
override modifiers 2nd
overriding
events 2nd
GetHashCode method 2nd
owned and owner forms 2nd

Previous Section < Day Day Up >


index_P

Index

[SYMBOL] [A] [B] [C] [D] [E] [F] [G] [H] [I] [J] [K] [L] [M] [N] [O] [P] [Q] [R] [S] [T] [U] [V] [W] [X]

Page class (ASP.NET) 2nd 3rd 4th 5th
Page_Load event 2nd
pages
events [See Web pages]
pages to print, selecting 2nd
PageSettings class 2nd
Paint events (GDI+) 2nd 3rd 4th 5th
Panel class 2nd 3rd
panels
ASP.NET 2nd
images 2nd
parameters
commands without stored procedures
DataGridView class 2nd 3rd
passing 2nd 3rd 4th
types
Parse method 2nd
parsing numbers
partial classes, code-behind with 2nd
Path class 2nd 3rd 4th
paths
naming
patterns
creational
custom date formatting 2nd
regular expressions 2nd
Pen class 2nd 3rd
performance
AppDomain
View state
permissions 2nd 3rd 4th 5th 6th
assembly requests 2nd 3rd 4th
attributes 2nd
applying 2nd
declarative security 2nd
testing 2nd
built-in security 2nd 3rd
creating
granting 2nd
identity 2nd
PictureBox class 2nd 3rd
PNG (Portable Network Graphics)
policies
binding
default configuration 2nd
security 2nd 3rd 4th
polling objects 2nd 3rd 4th
polymorphism
pooling
connections
threads
populating controls in forms
positioning
controls 2nd
tab stops 2nd
postbacks
posting, cross-page
precompiling
assemblies [See compiling]
preprocessing directives (C#) 2nd 3rd
previewing printing 2nd 3rd 4th 5th 6th
primary keys
primary threads
primitives (C#) 2nd 3rd 4th 5th 6th
bool type
byte type
char type
decimal type
double type
int type
long type
Parse method 2nd
sbyte type
short type
single type
TRyParse method 2nd
PrintDocument class 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th 13th 14th 15th 16th 17th 18th
customizing 2nd 3rd
events 2nd 3rd 4th
printers
resolution
selecting
PrinterSettings object 2nd 3rd
printing
GDI+ 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th 13th 14th 15th 16th 17th 18th
pages 2nd
previewing 2nd 3rd 4th 5th 6th
PrintPage event 2nd
priority threads
private assemblies 2nd
deploying 2nd
private constructors 2nd
private keys
delayed signing
profiles
Compact Profile
Kernel Profile
programmatic security 2nd 3rd 4th 5th 6th 7th 8th
ProgressBar class 2nd 3rd 4th
properties
C# classes 2nd 3rd
controls 2nd 3rd 4th
DataGridView class 2nd 3rd 4th 5th 6th 7th 8th
DataSource controls 2nd 3rd 4th 5th 6th 7th 8th 9th
DataTable.Columns 2nd 3rd
forms
HttpRequest class 2nd 3rd
MenuItem class
Page class
structures 2nd
System.Array class
System.Exception class 2nd
XmlReaderSettings class 2nd
propertyChanged event
ProviderFactory class
providers [See data providers]
proxy classes
remoting 2nd
wsdl.exe utility 2nd 3rd 4th 5th 6th
public keys
tokens 2nd

Previous Section < Day Day Up >


index_Q

Index

[SYMBOL] [A] [B] [C] [D] [E] [F] [G] [H] [I] [J] [K] [L] [M] [N] [O] [P] [Q] [R] [S] [T] [U] [V] [W] [X]

queries (XPath) 2nd 3rd 4th
QueryPageSettingsEvent event 2nd
Queue class 2nd 3rd

Previous Section < Day Day Up >


index_R

Index

[SYMBOL] [A] [B] [C] [D] [E] [F] [G] [H] [I] [J] [K] [L] [M] [N] [O] [P] [Q] [R] [S] [T] [U] [V] [W] [X]

RAD (Rapid Application Development)
RadioButton class 2nd 3rd
RadioCheck property
read-only fields, static
reading
resource files 2nd
text 2nd 3rd
XML 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th 13th 14th 15th
ReadOnly columns
rectangles
drawing 2nd 3rd
DrawRectangle method
red/green/blue (RGB)
references
instances
MBR 2nd
objects
support for CLR 2nd 3rd 4th 5th
types 2nd
reflection
treeView class 2nd 3rd 4th
Regex class 2nd 3rd 4th 5th 6th
IsMatch method
Replace method
Split method
regions, code
registering
methods
types 2nd 3rd
registries (UDDI)
regular expressions 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th
backreferencing groups 2nd
creating regular expressions 2nd
repetition characters
relational operators (C#) 2nd
relationships
constraints 2nd
tables
creating
defining 2nd 3rd
releasing memory
Remote Procedure Call [See RPC]
remoting applications 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th 13th 14th 15th 16th 17th 18th 19th 20th 21st 22nd 23rd 24th 25th 26th 27th 28th 29th
architecture 2nd 3rd 4th 5th
CAO 2nd 3rd 4th 5th 6th 7th
design 2nd
real proxies
registration 2nd 3rd
SAO 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th 13th 14th
types of 2nd 3rd
reports
code for 2nd 3rd 4th
data sources for
previewing printing 2nd 3rd 4th 5th 6th
requests
HTTP 2nd 3rd 4th 5th
permissions 2nd 3rd 4th
UDDI discovery 2nd
resolution, configuring printers
ResourceManager class 2nd
resources
files 2nd 3rd 4th 5th
forms 2nd 3rd 4th 5th 6th 7th 8th
localization
ResourceWriter class
responses
HTTP 2nd 3rd 4th 5th
ResXResourceReader class 2nd
ResXResourceWriter class
reversing text
RGB (red/green/blue)
rolling back updates
rotating images 2nd
rows
DataGridView class 2nd 3rd
DataRow class 2nd 3rd
height 2nd
recognizing 2nd
updating
RPC (Remote Procedure Call)
runtime, localization resources

Previous Section < Day Day Up >


index_S

Index

[SYMBOL] [A] [B] [C] [D] [E] [F] [G] [H] [I] [J] [K] [L] [M] [N] [O] [P] [Q] [R] [S] [T] [U] [V] [W] [X]

SAO (server-activated objects) 2nd 3rd 4th
satellite assemblies 2nd
Visual Studio.NET 2nd
sbyte type
schemas
creating 2nd 3rd
XML 2nd
XSD 2nd 3rd
SDK (Software Development Kit) 2nd 3rd 4th 5th 6th 7th 8th 9th
sealed modifiers 2nd 3rd
searching
strings [See XPath;queries]
substrings [See XPath;queries]
XML with XPath [See XPath;queries]
security 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th 13th 14th 15th 16th 17th 18th 19th 20th 21st 22nd 23rd 24th 25th 26th 27th 28th 29th 30th 31st
.NET Framework Configuration tool 2nd 3rd
AppDomain
configuring 2nd 3rd 4th 5th 6th
CryptoStream class 2nd 3rd 4th
declarative
evidence 2nd 3rd 4th
permissions 2nd 3rd 4th 5th 6th
attributes 2nd
requesting for assemblies 2nd 3rd 4th
policies 2nd 3rd 4th
programmatic 2nd 3rd 4th 5th 6th 7th 8th
stack walk 2nd 3rd 4th 5th 6th
viewing
SEH (structured exception handling)
selecting
ADO.NET access models 2nd
ASP.NET Web controls 2nd
list boxes 2nd
pages to print 2nd
printers
semantics values
Semaphore class 2nd
send and forget
sequences, escape
serialization
attributes 2nd
binary 2nd 3rd 4th 5th
events 2nd
objects 2nd 3rd 4th 5th 6th
XML 2nd 3rd 4th 5th
server-activated objects [See objects;SAO]
server-activated single calls 2nd
server-activated singleton
servers
assemblies 2nd 3rd 4th
clients 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th 13th 14th 15th 16th 17th 18th 19th 20th 21st 22nd 23rd 24th 25th
configuration files 2nd 3rd
event handling
remoting 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th 13th 14th 15th 16th 17th 18th 19th 20th 21st 22nd 23rd 24th 25th 26th 27th 28th 29th
UDDI 2nd 3rd
sessions
Web services
shallow copies, creating 2nd
shapes
drawing [See also GDI+]
DrawLine method
DrawRectangle method
hit testing 2nd 3rd
shared assemblies 2nd
short circuit evaluation
short type
Shortcut property
ShowDialog class 2nd
ShowHelp method
signatures, methods
signing
simple controls 2nd 3rd 4th 5th 6th 7th
single type
sink chains
site evidence
sizing
arrays
controls 2nd
fonts 2nd 3rd
forms 2nd
rows 2nd
SOAP (Simple Object Access Protocol) 2nd 3rd
Software Development Kit [See tools;SDK]
Sort method
sorting
columns
IComparable interfaces 2nd
IComparer interfaces 2nd
items 2nd
source control responsibilities 2nd
special characters, carriage returns 2nd
specifications of CLS
sponsorship 2nd 3rd
SQL (Structured Query Language)
commands 2nd
connection strings 2nd 3rd
stored procedures 2nd 3rd
SqlDataSource control 2nd 3rd 4th
Stack class 2nd 3rd
stacks
runtime
walk 2nd 3rd 4th 5th 6th 7th
standards
.NET Framework 2nd 3rd
CLI 2nd 3rd
ECMA International
StartCap property
state
management of controls 2nd
threads 2nd
View 2nd 3rd
statements
C#
control flow 2nd
if-else 2nd
switch 2nd
using
static constructors 2nd
static method modifiers 2nd
static read-only fields
StatusStrip class 2nd 3rd 4th
stops, tags 2nd
stored procedures, executing 2nd 3rd
Stream class 2nd
streams
members
System.IO namespace 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th
String.Compare method 2nd 3rd
String.CompareOrdinal method
String.Format method
StringBuilder class 2nd 3rd 4th
StringFormat class
StringReader class 2nd
strings
aligning 2nd
C# 2nd 3rd 4th 5th 6th
comparing 2nd
concatenation 2nd
connections 2nd 3rd
creating 2nd 3rd
encoding 2nd
formatting
interning 2nd 3rd
literals 2nd 3rd
methods 2nd
modifying 2nd 3rd 4th
overloading 2nd
resources 2nd
searching 2nd
text 2nd 3rd 4th 5th 6th 7th
transformations 2nd 3rd 4th
trimming 2nd
wrapping 2nd
StringWriter class 2nd
strongly named assemblies 2nd 3rd 4th 5th 6th 7th
structured exception handling [See handling;exceptions;SEH]
Structured Query Language [See commands;SQL]
structures
C# classes 2nd 3rd 4th
comparing 2nd 3rd
defining 2nd
inheritance
methods 2nd
properties 2nd
style sheets (XSLT) 2nd 3rd 4th
subdirectories
creating [See directories]
deleting [See directories]
substrings
extracting [See strings]
searching [See strings]
surrogates
pairs
strings
switch statements 2nd
synchronization
databases 2nd 3rd 4th
objects 2nd 3rd 4th
threads 2nd 3rd 4th 5th 6th 7th
synchronous calls to Web service methods 2nd
System.Array class
System.Collections class
generic namespaces 2nd 3rd 4th
namespaces
System.Drawing.Drawing2D namespace
System.Enum methods
System.Exception class 2nd
System.IO namespace
directories 2nd 3rd 4th 5th 6th 7th 8th
files 2nd 3rd 4th 5th 6th 7th 8th
streams 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th
System.Object class, customizing 2nd 3rd 4th 5th 6th 7th 8th
System.String class 2nd 3rd 4th 5th 6th 7th
System.Web.Services.WebService class 2nd
System.Web.UI.WebControls namespace

Previous Section < Day Day Up >


index_T

Index

[SYMBOL] [A] [B] [C] [D] [E] [F] [G] [H] [I] [J] [K] [L] [M] [N] [O] [P] [Q] [R] [S] [T] [U] [V] [W] [X]

TableLayoutPanel control 2nd
tables
keys
master-detail
relationships
creating
defining 2nd 3rd
tabs, ordering
Tag Image File Format (TIFF)
tags
HTML
XML documentation
target control responsibilities 2nd 3rd
TCP/IP (Transmission Control Protocol/Internet Protocol)
text
boxes
C#
comparing strings 2nd 3rd 4th
formatting DateTime/numeric values 2nd 3rd 4th 5th 6th 7th 8th 9th
modifying strings 2nd 3rd 4th 5th
regular expressions 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th
StringBuilder class 2nd 3rd 4th
System.IO namespace 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th 13th 14th 15th 16th 17th 18th 19th
System.String class 2nd 3rd 4th
Unicode 2nd 3rd 4th 5th 6th
formatting 2nd 3rd
GDI+ 2nd 3rd 4th 5th 6th 7th
patterns 2nd 3rd
printing, 441 [See printing]
reading 2nd 3rd
reversing
strings 2nd 3rd 4th 5th 6th 7th
writing 2nd 3rd
TextBox class 2nd 3rd
Thread class
Thread Local Storage [See TLS]
threads, 590
applying [See asynchronous programming]
creating [See asynchronous programming]
CurrentThread property [See asynchronous programming]
multithreading [See asynchronous programming]
overview of [See asynchronous programming]
pooling [See asynchronous programming]
priority [See asynchronous programming]
synchronization [See asynchronous programming]
throwing exceptions
TIFF (Tag Image File Format)
time slices
time, formatting 2nd 3rd 4th 5th
time-to-live (TTL)
Timer class 2nd 3rd 4th
TLS (Thread Local Storage)
tokens, public keys 2nd
tools
.NET Framework 2nd 3rd
Framework Configuration tool 2nd 3rd 4th 5th
Ildasm.exe 2nd 3rd 4th
wincv.exe
FxCop 2nd 3rd 4th
SDK 2nd 3rd 4th 5th 6th 7th 8th 9th
wsdl.exe utility 2nd 3rd 4th 5th 6th
ToolTip controls 2nd
ToString( ) method 2nd
transactions, rolling back updates
transferring control within loops 2nd
transforming strings 2nd 3rd 4th
transparency of forms 2nd 3rd 4th
transparent proxies
transport sinks
tree nodes
adding on 2nd 3rd
deleting on 2nd 3rd
TReeNode class 2nd 3rd 4th
TReeView class 2nd 3rd 4th 5th 6th 7th
trimming strings 2nd
TrueType fonts, 428 [See fonts]
try block
tryParse method 2nd
TTL (time-to-live)
two-way data binding 2nd
typefaces [See fonts]
types
CLS
CTS 2nd 3rd
enumeration (C#) 2nd 3rd 4th
generics 2nd 3rd
numeric 2nd
references
classes
support for CLR 2nd 3rd 4th 5th
registering 2nd 3rd

Previous Section < Day Day Up >


index_U

Index

[SYMBOL] [A] [B] [C] [D] [E] [F] [G] [H] [I] [J] [K] [L] [M] [N] [O] [P] [Q] [R] [S] [T] [U] [V] [W] [X]

UDDI (Universal Description Discovery and Integration)
unhandled exceptions, 157-159 [See guidelines;handling exceptions]
Unicode standards 2nd 3rd 4th 5th
Uniform Resource Identifier [See URI]
Universal Description Discovery and Integration [See servers;UDDI]
updating
.NET Framework
controls
data sources
values 2nd
databases 2nd 3rd 4th 5th 6th 7th
errors
identifying 2nd
URI (Uniform Resource Identifier)
user controls
applying 2nd
at design time 2nd
building 2nd 3rd
user interfaces
multithreading [See interfaces]
using statements, namespaces
UTF-8, UTF- 2nd

Previous Section < Day Day Up >


index_V

Index

[SYMBOL] [A] [B] [C] [D] [E] [F] [G] [H] [I] [J] [K] [L] [M] [N] [O] [P] [Q] [R] [S] [T] [U] [V] [W] [X]

validation
XML 2nd
XSD 2nd
Validation controls 2nd 3rd 4th 5th
CustomValidator control 2nd
ValidationSummary control 2nd
Value types
support for CLR 2nd 3rd 4th 5th
types
variables
assigning
Char 2nd
verbatim strings
versioning
assemblies 2nd 3rd 4th
modifiers 2nd
serialized objects 2nd
View state 2nd 3rd
virtual mode, DataGridView class 2nd 3rd 4th 5th
virtual modifiers 2nd
Visual Studio.NET
inherited forms
creating
MDI menus 2nd
resource localization
satellite assembles 2nd
Web services 2nd 3rd

Previous Section < Day Day Up >


index_W

Index

[SYMBOL] [A] [B] [C] [D] [E] [F] [G] [H] [I] [J] [K] [L] [M] [N] [O] [P] [Q] [R] [S] [T] [U] [V] [W] [X]

walking the call stack
Web controls, customizing 2nd 3rd 4th 5th 6th 7th 8th 9th
Web Form controls 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th 13th 14th 15th
Web pages
compiling
controls
Web services
access
applying 2nd 3rd 4th
building 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th 13th 14th 15th 16th 17th 18th
extending 2nd 3rd 4th
overview of 2nd 3rd 4th 5th
sessions
testing 2nd
Web.config file
Web Services Description Language [See WSDL]
web.config file
Web services
WebMethod attribute 2nd 3rd 4th 5th
WebService attribute
WebService directive
well known objects [See objects;WKO]
while loop
widening conversion
wincv.exe
Windows
application unhandled exceptions 2nd
Bit Block Transfer 2nd
Forms 2nd 3rd 4th [See also forms]
GDI+ 2nd
Windows.Forms.Form class 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th 13th 14th
WKO (well known objects)
wrappers, HelpProvider 2nd
writing
resource files
XML 2nd 3rd
WSDL (Web Services Description Language)
wsdl.exe utility 2nd 3rd 4th 5th 6th

Previous Section < Day Day Up >


index_X

Index

[SYMBOL] [A] [B] [C] [D] [E] [F] [G] [H] [I] [J] [K] [L] [M] [N] [O] [P] [Q] [R] [S] [T] [U] [V] [W] [X]

XCOPY 2nd
XML (Extended Markup Language)
ADO.NET 2nd 3rd 4th 5th 6th 7th 8th
applying 2nd 3rd 4th 5th 6th 7th 8th 9th 10th
attributes 2nd
DataSet class 2nd 3rd 4th 5th 6th 7th
documentation tags
enumeration
overview of 2nd
reading 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th 13th 14th 15th
resource files 2nd 3rd 4th 5th
schemas 2nd
searching with XPath 2nd 3rd 4th 5th 6th 7th 8th 9th 10th
serialization 2nd 3rd 4th 5th
Web services [See also Web services]
writing 2nd 3rd
XML Schema Definition [See schemas;XSD]
XmlDataDocument class (XPath) 2nd 3rd
XmlDataSource control 2nd 3rd 4th
XmlDocument class (XPath)
XmlNodeReader class 2nd 3rd
XmlReader class 2nd 3rd 4th 5th 6th
XmlSerializer class 2nd 3rd
XmlWriter class 2nd 3rd
XPath
functions 2nd
navigating
operators 2nd 3rd
queries 2nd 3rd 4th
searching XML with 2nd 3rd 4th 5th 6th 7th 8th 9th 10th
XmlDataDocument class 2nd 3rd
XmlDocument class
XPathDocument class
XSD (XML Schema Definition) 2nd 3rd
XSLT (Extended Style Language Transformation) 2nd 3rd 4th
XslTransform class 2nd 3rd

Previous Section < Day Day Up >