VB - Programmer - Guide To The NET FrameWork Class Library
VB - Programmer - Guide To The NET FrameWork Class Library
2 Evolution of VB .NET 29
Design Goals..........................................................................................30
Where We Came From ....................................................................30
Ideals ................................................................................................31
New Language Concepts ......................................................................31
Declaring Variables and Data Types ................................................32
FCL Classes That Replace/Augment VB6 Elements ......................36
Behavioral Changes..........................................................................36
Suggestions for Further Exploration ................................................39
vi
VISUAL BASIC PROGRAMMER’S GUIDE TO THE .NET FRAMEWORK CLASS LIBRARY
13 Messaging 597
Key Classes Related to Messaging ......................................................598
Messaging ............................................................................................600
Messaging Design Pattern ..............................................................601
Practical Applications ....................................................................602
Message Queues ..................................................................................604
Managing Queues ..........................................................................605
Enumerating Queues ......................................................................608
Sending Messages ..........................................................................612
Receiving Messages with MessageQueue........................................614
Asynchronous Messaging ..............................................................618
Suggestions for Further Exploration ..............................................621
Messages ..............................................................................................621
Timeouts and Expirations ..............................................................622
Acknowledgement and Response ..................................................624
Setting Priority................................................................................627
Default Properties ..........................................................................629
Enumerating Messages ..................................................................630
Suggestions for Further Exploration ..............................................632
Serialization ........................................................................................632
Message Body ................................................................................632
XML Format (XmlMessageFormatter) ..........................................636
Binary Messages (BinaryMessageFormatter)................................639
xiv
VISUAL BASIC PROGRAMMER’S GUIDE TO THE .NET FRAMEWORK CLASS LIBRARY
Index 1095
Foreword
It’s been said that change is “the constant, the signal for rebirth, the egg of the phoenix.” In
today’s dynamic operating environment, change and rebirth are not only constants, but also
essential elements to the survival of any successful business.
As individuals, our computing experience has continued to evolve rapidly, most noticeably in
recent years. In fact, if we take a step back in time, we can see how far things have really come
in this short period. In 1990, just over 10 years ago, I owned an NEC Powermate 286—a
machine, that at the time, fulfilled the vast majority of my computing needs quite nicely. After
all, it cranked along at a steady 8MHz, it included not only a 5.25” floppy drive, but also
20MB hard disk that I knew I would never fill, and of course, that 640KB of RAM on board
was all I would ever need. When it came time to “surf” for information, I’d simply fire up my
2400bps modem and do what? No, not dial into my Internet Service Provider—I would make a
direct phone call to my local Bulletin Board System (BBS) in order to view page after page of
flashing, colored ANSI text across my screen. Yes, it all sounds so innocent today, and yet it
was just over 10 years ago.
During the past decade, we’ve undeniably witnessed a revolution in the computing industry.
Moore’s Law, stating that computing power doubles approximately every 18 months, has been
an important agent in the corporate success of many of our companies. In addition, the prolif-
eration of mobile devices, the rapidly decreasing costs and increasing bandwidth of online con-
nectivity, as well as the penetration of standards such as XML and SOAP into the Internet,
have all been major contributing factors to the computing revolution that we continue to expe-
rience today. But what is the net effect of these radical industry changes on us as developers?
The computing revolution of the past decade has not only significantly expanded our range of
programming possibilities, but has also presented us with larger, more complex challenges than
we have ever before faced.
In 1991, a new developer tool emerged that would not only address the development chal-
lenges of the time, but would also fundamentally change the way that we build applications.
Like the computing climate of the past decade, Microsoft Visual Basic 1.0 was revolutionary—
it enabled both professional and casual developers to quickly and effectively build Windows
applications—a task that, at the time, was anything but trivial. Visual Basic introduced the con-
cept of RAD (Rapid Application Development) by providing an entirely graphical environment
to build Windows applications. Not only did this cause an upsurge in the number of Windows
applications being developed, but it also gave birth to a new market of third-party control
vendors. Together, these acted as a major driving force in the rapid adoption of the Windows
platform.
While more and more of us were jumping on the quickly growing RAD VB trend, the comput-
ing landscape and the development needs of our corporations were evolving. At each step of
the way, the Visual Basic language and tool evolved to meet these ever-changing needs.
Sometimes the changes introduced in a new version were relatively minor (Visual Basic 2.0),
sometimes they introduced more radical changes (Visual Basic 4.0), but the overall outcome
remained consistent—a developer tool agile enough to adapt to the changing needs of the
industry and radical enough to play a major part in the adoption of the platform. This not only
applied to Windows-based development with the release of VB 1.0, but also to client-server
programming with VB 2 and 3, 32-programming and the world of Windows 95 with VB 4, and
component development with Visual Basic 5.0.
Yet while each subsequent version of Visual Basic played a crucial role in driving platform
adoption, it has ironically always remained separate from the platform itself. The VB runtime,
while “protecting” us from the underlying complexities of the Win32 APIs and COM program-
ming, also served to obstruct us from new platform features until they were wrapped and made
available to us in our runtime DLL—VBRUNxxx.dll or MSVBVMxx.dll. This somewhat
unnatural separation from the platform has not only caused headaches, but has also been the
source of Visual Basic being disregarded as a second-class language—not suitable for true
enterprise-level development.
Now, as we enter a new era of challenges, highlighted by the need to build highly scalable,
secure enterprise-critical software and Web services powered by XML, a new version of Visual
Basic, Visual Basic .NET, is once again poised to revolutionize the way we build applications.
However, unlike previous versions of Visual Basic, Visual Basic .NET finally achieves that
integration with the underlying platform—the .NET Framework—that will empower VB devel-
opers to accomplish tasks never before possible.
The effects of the new .NET Framework, Visual Basic .NET, and the integration between them
are both exciting and far-reaching. Because Visual Basic .NET was built from the ground-up to
be a first-class player on the .NET Framework, it neither impedes VB developers from directly
accessing the rich feature set available in the Framework libraries nor carries with it antiquated
nonstructured programming constructs of our father’s BASIC. It provides a streamlined, mod-
ernized language that promotes both highly efficient and well-structured programming.
Combine that with the .NET Framework’s provisions for disconnected data access, multi-
threading, code access security, and a full set of object-oriented concepts, and you’ve got a
Visual Basic with unprecedented levels of both power and productivity.
However, with so many new features there comes an inevitable associated cost—that of learn-
ing the new concepts. This is precisely why I’m so excited about Lars’ and Mike’s book,
Visual Basic Programmer’s Guide to the .NET Framework Class Library. In addition to mak-
ing the large, hierarchical structure of the .NET Framework highly digestible, Lars and Mike
do so in a way that enables developers to learn the concepts as they directly relate to common
programming tasks. They cover everything from the organizational structure of the extensive
class library to the various namespaces for building UI, working with XML data, and accom-
plishing common network-based programming tasks. In addition, the book aptly discusses
techniques for accessing existing COM+ services and mapping existing skills from the Win32
APIs to their equivalents in the world of .NET.
As the application development landscape continues to evolve and reinvent itself, it will be
crucial that we as VB developers continue to evolve in concert. Visual Basic .NET and the
.NET Framework provide the springboard we need to take our next big evolutionary step. And
with this book, you’re already well on your way to understanding and applying the new con-
cepts and programming constructs that will propel us into the next 10 years of RAD Visual
Basic application development.
Ari Bixhorn
Product Manager
Visual Basic .NET
Microsoft Corporation
Preface
.NET is not simply a new set of developer tools to be unleashed in late 2001 or early 2002. It
is a multilevel strategy that will be realized over years, not months. Its impact to computing
will be broad and long lasting. Let’s take a look at where .NET came from.
.NET was made public at the Professional Developers Conference (PDC) in Orlando, July
2000. We were both lucky enough to be in attendance. What a shocker. We watched with nod-
ding heads and jaws agape as speaker after speaker illuminated solutions and described new
paradigms in computing. We furiously took notes and couldn’t wait to get our hands on the
.NET bits. We installed Beta 1 in our hotel rooms. We were bitten. It was obvious Microsoft
had done it right and had taken our favorite programming language, Visual Basic, to new
heights. One of the questions that we have since asked ourselves is, “Why did Microsoft
decide to tackle such a vision?” and “Where was .NET (or this vision) before Orlando?”
We start to get a picture of how .NET evolved out of the research laboratory and into
Microsoft’s products. As for the question, “Why .NET and not something else?” we can only
speculate. A quick look at some of Microsoft’s goals for .NET is the basis for our speculation.
• Open standards without compromising intellectual property
• Answer the requests of developers for stronger OOP support
• Answer the requests of businesses to build a scalable, robust platform
• Solve the problems of interoperability
• Integrate all Microsoft products and services
These are some ambitious goals by any standard and are by no means the only goals of
Microsoft’s .NET strategy. However, this subset allows you to see how .NET became a bet-the-
company strategy. In the end, however, Microsoft knew it was the right strategy, saw a chance
to make it happen, and boldly went for it. With the .NET Framework, Microsoft has made a
concerted effort to go back to the drawing boards and fix the fundamental problems that exist
for many developers working with the current crop of Windows DNA/COM/Win32 technolo-
gies. Ground zero for .NET is its prolific set of namespaces.
In this book, we try to answer some very fundamental questions in regard to each unique tech-
nology, such as the following:
• What does the .NET Class Library provide in terms of reusable code?
• Are .NET structures available that I can use to accomplish a specific task?
• How do I go about interfacing with the .NET Framework through my code?
In short, what we hope we have delivered is a definitive guide to the capabilities of the .NET
Framework Class Library. We have tried to provide an appropriate mix of introductory text (to
ease developers into a specific technology that they may not have worked with in the past,
such as XML or Directory Services), combined with task-driven code samples and detailed ref-
erence information.
Our hope is that you get as much joy discovering .NET through our eyes as we did discovering
.NET at the PDC in Orlando, but with a lot less frustration.
About the Authors
Lars Powers ([email protected])
Lars is a Microsoft Certified Solutions Developer (MCSD) with more than 10 years of experi-
ence analyzing business problems and developing software solutions. Most of his experience
centers on leading development teams and writing software in Microsoft development
environments.
Mike Snell ([email protected])
Mike is also a MCSD with more than 10 years of experience writing and designing software.
His experience centers on creating enterprise-level, Web-based systems using the Microsoft
platform.
Lars and Mike
Lars and Mike have been working together at four separate companies for more than six years.
In doing so, they’ve built a wealth of knowledge about executing successful projects and deliv-
ering enterprise-level systems.
Together, they have formed brilliantStorm (http://www.brilliantstorm.com): a partnership
focused on providing developers with .NET productivity tools, information, and training.
Mike
To my wife, Carrie, whose continuous support made this possible (I hope I can return the gesture),
and to my daughter, Allie, and my son, Benjamin, who shouted their encouragement (and let me
know when it was time to eat or sleep) into the ventilation ducts that carried their
messages down to me in the basement…I can come up now.
Acknowledgments
We have been involved with many software projects on many levels, and, without exception,
they have all involved a concerted team effort. This project was no different.
We would like to thank all the hard-working professionals at Sams Publishing for helping us
deliver what you see before you. We would especially like to thank Sondra Scott for believing
in us from the start, Karen Wachs for her editorial determination and patience, and JP and Dan,
our technical editors, who found errors both big and small.
Finally, we would like to thank the hardworking folks at Microsoft for having the courage and
talent to deliver such a monumental piece of technology; it has been many years since we have
been this energized and excited about what the future holds.
Tell Us What You Think!
As the reader of this book, you are our most important critic and commentator. We value your
opinion and want to know what we’re doing right, what we could do better, what areas you’d
like to see us publish in, and any other words of wisdom you’re willing to pass our way.
As an associate publisher for Sams Publishing, I welcome your comments. You can
e-mail or write me directly to let me know what you did or didn’t like about this book—
as well as what we can do to make our books stronger.
Please note that I cannot help you with technical problems related to the topic of this book,
and that because of the high volume of mail I receive, I might not be able to reply to every
message.
When you write, please be sure to include this book’s title and author as well as your name
and phone or fax number. I will carefully review your comments and share them with the
author and editors who worked on the book.
E-mail: [email protected]
Mail:
Associate Publisher
Sams Publishing
800 East 96th Street
Indianapolis, IN 46240 USA
Introduction
It’s not often you get to take over a franchise. Through hard work and dedication, Dan
Appleman has built his Visual Basic Programmer’s Guide series of API books into some of the
most recognized and best selling Visual Basic books of all time. Those books are the inspira-
tion for this one.
When Dan was approached to do a similar book for the .NET Framework, he felt he needed to
pursue other directions and other titles. The paradigm shifts brought on by .NET were also
causing authors and editors to rethink their titles and opportunities. .NET meant a total rewrite
of nearly every title and completely different approaches. Well, as Dan moved in another direc-
tion, we were talking with Sams Publishing about doing a book on the .NET namespaces. The
Programmer’s Guide concept was presented to us; we saw the opportunity and pounced.
We want to thank Dan for the groundwork he has laid for us as Visual Basic developers and
authors. By applying the approach he pioneered with the Win32 API to the world of .NET, we
hope to continue to bring this member of the Sams Programmer’s Guide series honor and dis-
tinction amongst the sea of technical books.
The namespaces themselves are language neutral. All languages in .NET derive from the same
Framework Class Library. Nearly every section in this book can be applied outside of VB. Of
course, all the source code (and there is a lot of source code) is written in VB syntax. However,
we provide liberal comments in the code to ease understanding and help with porting.
After finishing the book, you should have a solid understanding of the functionality available
in the .NET Framework Class Library. You should be able to intelligently use and inherit this
functionality to accomplish a wide variety of common and not-so-common programming tasks.
In addition, you will walk away with a firm grasp of how the different pieces of .NET relate to
one another and work together to provide a fully realized system for software development.
1
IN THIS CHAPTER
• The Composition of .NET 8
• .NET’s Relevance 16
We start this book by describing Microsoft’s .NET strategy and its relevance to the industry.
We first present .NET’s overall timetable and provide a description of the various components
that comprise .NET. We then detail the ramifications and relevance of .NET. Finally, we end
the chapter by defining the parts of the .NET Framework and detailing the benefits they pro-
vide our application development efforts.
After reading this chapter, you should
• Understand Microsoft’s .NET initiative, its components, and its timeline
• See the .NET initiative in the context of its relevance to specific audiences
• Have a solid understanding of the .NET Framework including the Common Language
Runtime (CLR) and Framework Class Library (FCL)
• Understand the benefits of the .NET Framework for you as a developer
• Begin to see the various programming paradigm shifts necessary to transition from
VB6/Win32 development to .NET
The answer? All of the above! Now you can begin to understand the problem with identifying 1
all of the different pieces of .NET. Generally speaking, Microsoft considers the pieces of .NET
to focus on three different areas:
EVOLUTION
OF .NET
1. Tools and languages for .NET-based software development
2. Building block services products for programmers and non-programmers
3. Third-party developed services
Right now, the major pieces to consider fall under the first two categories: software develop-
ment tools and so-called building block services. Third-party services will be in more evidence
after .NET has been broadly adopted.
Microsoft ASP.NET 1
Since the introduction of IIS (Internet Information Server) in late 1997, ASP (Active Server
Pages) has been the principal technology for Microsoft developers to deliver Web content. As
EVOLUTION
OF .NET
we all know, ASP forced developers to embed script and logic inside of UI (HTML) code.
While this was simple enough and relatively easy to use for basic tasks, it quickly became
cumbersome to program against and support as applications grew in scale and complexity.
The .NET version of Microsoft’s ASP finally separates user interface (HTML) from script.
ASP.NET implements a feature that Microsoft calls code-behind. This feature allows you to
write HTML and code into separate files; the HTML file simply maintains a link to its associ-
ated code file. Sounds simple enough, and it is; it is remarkable how much easier it makes life
for developers. The paradigm is equivalent to the win-forms paradigm (in fact, it is called Web
Forms) where developers create a form using drag-and-drop and write code behind the various
controls. Additionally, the code behind Web Forms can be written in any .NET language.
Another problem from which ASP suffered is slow performance due to the scripting code
being processed at runtime. ASP.NET automatically compiles code files when deployed or first
accessed. There is no longer a performance difference between a script class and one written
and compiled into a DLL! However, ASP.NET does not take away your ability to promote
code simply by copying over a file in a directory. Thanks to the JIT (just-in-time) compiler, a
file can be replaced and it will simply be recompiled by the system as need be.
NOTE
ASP developers of old, don’t fret; both the Response and the Request objects are still
available.
ASP.NET allows programmers to quickly infuse Web sites with dynamic content and function-
ality. It represents a serious step forward for the ASP technology.
that make .NET programming easier than ever, it faithfully adheres to its past goals of allow-
ing developers to build quality programs quickly. Chapter 2, “Evolution of VB .NET,” will fur-
ther explore the new Visual Basic language.
Microsoft C#
Microsoft decided to launch .NET with a new language—C#. There were multiple reasons for
creating C#. For one, the language keeps the syntax of a typical C++ application while provid-
ing some of the simplicity of Visual Basic. Microsoft’s hope is that C# will become the lan-
guage of choice for those developers hooked on C++. Secondly, C# provides developers with a
Java-like alternative. It is no secret that the .NET architecture shares some of Java’s design
concepts; C# is the Java of .NET. Of course, .NET will have a Java story (Rational and others
will make sure of that) but Microsoft hopes that C# is compelling enough to lure Java develop-
ers over to the .NET camp. Finally, Microsoft wrote most of the .NET Framework code using
C# thus solidifying it as the language of .NET.
Identity Services 1
These include such things as login and password verification and electronic wallets. These ser-
vices represent a secure and safe way to verify someone’s identity and then act accordingly.
EVOLUTION
OF .NET
Notification and Messaging Services
Building on technology already employed for things like Hotmail (Microsoft’s free e-mail ser-
vice) and Microsoft Instant Messenger (Microsoft’s free messaging software), these services
aim to provide e-mail, fax, and voicemail to and from almost any device from a PC to a hand-
held device.
Personalization Services
Targeted squarely at companies that are interested in catering to the individual, these services
manage the rules and preferences necessary to show people only what they want to see on a
Web site or other computer system.
Calendar Services
Calendar services allow for the integration and management of personal, work, and home cal-
endars. They allow people to track their time and appointments intelligently, and collaborate
with others doing the same.
passing invoices, purchase orders, and inventory information back and forth. Because it is
unrealistic to expect every company involved in such an exchange to change their data format
to some common, central format, the solution is data mapping. Quite simply, data mapping is
the processing of mapping one piece of data to another. What one company calls a SKU,
another company might call an inventory control number; one company may allow alphanu-
meric SKUs, while another allows only numeric control numbers. To help companies map
their data and overcome these obstacles, BizTalk implements a simple drag-and-drop interface.
Microsoft is also customizing BizTalk specifically for certain vertical industries.
the reach of software and data to mobile devices. It enables users to access their personal data 1
(e-mail, faxes, appointments, tasks, and so on) in real-time wherever they happen to be. This is
a key enabler for “wireless” applications in the .NET world.
EVOLUTION
OF .NET
Office .NET
Office .NET is an example of a .NET product that starts to extend services right onto a user’s
personal desktop. Details of this product are still sketchy because it hasn’t been released yet. It
is expected that Microsoft will finally make its popular Office products such as Excel, Word,
and PowerPoint available as a service for a monthly fee instead of as shrink-wrapped software.
It is important to note that Microsoft does not see this method of distributing Office as the only
method; this will become just another option or choice for consumers on how they want to pay
for and use typical functionality like word processing and spreadsheet management.
.NET’s Relevance
.NET intends to change the way we develop, access, and interact with Internet applications.
Given this, it’s easy to see its importance. Of course, .NET changes the way you write soft-
ware, but it is important to know that anyone who accesses information electronically will feel
the effects of .NET. Because .NET’s reach is so great, its relevance is defined differently for
different audiences. This section explores each audience and outlines the ramifications and rel-
evance of .NET to the given audience.
Developers
Of course, as developers, you are our primary audience for this book. We believe you are also
Microsoft’s primary target for .NET. Nothing happens without the code. You are needed to
evangelize, upgrade, learn, design, and develop .NET software. You are in control of .NET’s
future. The nice thing? .NET makes it an easy decision and migration path.
The .NET Framework gives you control. It allows you to choose your language and project
paradigm; even the development environment is completely customizable. We are no longer
forced to compromise or make trade-offs in lieu of productivity. In the past, if you chose an
easy-to-use syntax like Visual Basic you compromised features and speed; if you chose C++,
you compromised ease, manageability, and productivity. No more. In .NET, all languages are
created equal.
.NET allows us to build applications. The vast majority of today’s business application devel-
opment has some Internet component. Currently, to do routine tasks and ensure things like
security and scalability, many programmer cycles are wasted writing repetitive plumbing code.
With .NET, these things are built in. You can construct your application from .NET code
libraries (the focus of this book). You are free to refocus your efforts on solving business prob-
lems (or going home before midnight) and not working on the plumbing of your system.
.NET is done right. As application developers first and authors second, we have first-hand
knowledge of the tools. The .NET tools and environment are a joy. Developers will see
increased productivity and enhanced capabilities. It takes a few hours to get used to, but once
you do, we think you will find that .NET and the new Visual Studio are like a finely refined
cockpit; nearly everything is in its right place and works just the way you think it should.
System Architects
.NET further closes the gap between design and code for the system architects. There is no
longer a need for the architects to have one software license like Visio Enterprise and the
development team another. How many times has a developer wanted to open and change a
model only to be told he or she needed to buy another license? What typically happens is that
Evolution of .NET
17
CHAPTER 1
the models go unupdated and often unexamined past the architecture phase. Developers get 1
heads-down on code and the models become secondary. Visual Stuido.NET has built-in support
for UML. That’s right, now your models can co-exist with your code! Architects can write
EVOLUTION
OF .NET
some code and developers can help keep the models updated. Additionally, Microsoft hopes to
further extend the capabilities of the Visual Studio IDE through its partners in the Visual Studio
Integrator Program (VSIP). Companies like Rational will offer its products as both versions
and extensions of the Visual Studio .NET IDE.
Of course, .NET’s strong OOP capabilities no longer force architects into workarounds for
specific languages. When designing Visual Basic components before, for example, architects
where not free to design with inheritance. The constraints of the Visual Basic language just did
not support this design. With .NET, architects can model the right way and know that any lan-
guage that the developer chooses to implement will work just fine.
Project Managers
We consider project managers to be anyone who has to answer to both users and upper man-
agement on the state, status, or feature set of a piece of software. We know this often includes
a lot of developers. When was the last time you were free to focus on writing code and not sit-
ting in meetings or pushing paper? If you are on the front line of software development, you
are the one who faces a transfer or gets fired when the project goes a year off track and a mil-
lion dollars over budget.
.NET promises to help change this. Applications can be delivered in shorter time frames due to
increased productivity and more focus on business issues. Projects can be delivered at lower
costs. Development teams can again focus on solving real business problems and know, at the
end of the day, things like scalability, reliability, and robustness are baked in by .NET. In short,
those on the front line can once again become the hero. .NET helps ensure successful projects,
time and again.
Companies
The .NET platform fundamentally changes the way companies interact internally, with cus-
tomers, and with partners over the Internet. .NET promises a higher degree of communication,
connectivity, and productivity. It connects employee to employee, employee to partner, and
most importantly, employee to customer. Internet applications evolve from simple user forms
to rich, interactive collaboration. .NET frees the Internet from the PC. It Internet-enables and
connects cell phones, televisions, and other appliances.
.NET allows companies to explore new business models. Just like the Internet created new
markets and sales channels, Microsoft intends software services to evolve existing business
models. For example, think of a company that today gathers auto insurance rate information
An Introduction to .NET
18
PART I
for its customers. It collects this data, and helps its customers make informed decisions. It may
even expose this information on the Internet. With .NET, this company can wrap this informa-
tion into a software service that can be embedded into hundreds of applications. They still col-
lect the data; they in fact change very little. However, they now have new revenue-generating
market opportunity.
.NET opens new partnering opportunities for business. As the prior example illustrated, com-
panies can now draw on each other’s expertise to make a richer offer to potential customers.
For example, if I sell used cars on the Internet, I know my customers will need insurance. I am
not in the business of offering insurance nor do I want to be. With .NET I can find, grab, and
use the insurance service to make a more compelling offer to my customers. If there is a bank
loan rate service I’ll grab that too. In the end, I’ve increased my sales by making it easier for
customers to transact.
.NET allows companies to focus on the future without throwing out the past. Time and again
companies are told that in order to realize their new business model they must rewrite their
legacy systems. .NET is designed to extend and interoperate with those legacy systems.
Companies are encouraged to use .NET to leverage their current investments and at the same
time, plan for the future with built-in standards like XML.
End Users
We are often asked, “Will end users ever actually feel the effect of .NET?” Our answer, “You’d
better believe it.” .NET puts users in control of their information. How frustrating is it that we
have to enter our address and credit card details time and again on site after site. We have to
trust that each site secures our data properly and doesn’t sell it off to list brokers. .NET
promises centralized services. Imagine only one company knowing your private information.
Imagine never re-typing your ship-to or bill-to address. You authorize access to your informa-
tion and a service executes secure transactions on your behalf. Imagine being notified via an
alert on your cell phone that the Father’s Day gift you ordered is out of stock—and never giv-
ing out your cell phone number!
Ultimately, the end user may never hear of .NET. Although knowing the marketing might of
Microsoft, this is probably unlikely. However, it is likely end users will never fully grasp that
when they pick up the TV changer they will be accessing a myriad of .NET services and
servers, or that when they order movie tickets from their cell phone while stuck in traffic, they
are communicating with .NET components. .NET promises to empower users to communicate
on their terms.
Evolution of .NET
19
CHAPTER 1
EVOLUTION
OF .NET
so, we present each topic, describe its importance, and relate applicable or intended application
development benefits.
The .NET platform did not evolve out of Microsoft’s DNA architecture. It is an entirely new
platform and set of technologies. As a result, it requires that you bring an open mind to your
development. The pitfalls of the past are gone and typically, there is a new way to do every-
thing. This book is going to explore and teach you how to accomplish these tasks with .NET.
The term, .NET Framework, refers to Microsoft’s new programming platform, which has been
highly optimized for distributed application development. It encapsulates the runtime, classes,
interfaces, and type system, designed to speed and streamline the development process. There
are two principal pieces to the .NET Framework: the Common Language Runtime (CLR) and
the Framework Class Library (FCL).
The CLR is the foundation of the Framework. It provides the services and code execution envi-
ronment for .NET development. It is made up of a number of additional sub-components, like
the Common Type System (CTS) and the Just-In-Time (JIT) compiler, all of which we will
discuss in detail.
The other piece to the .NET Framework puzzle, the Framework Class Library (FCL), repre-
sents a collection of reusable classes that can be used to execute most common Windows pro-
gramming tasks. Of course, it is also the focus of this book.
These services provide a number of direct benefits to our application development efforts and
code execution. Some of these direct benefits are listed next:
• One of the biggest benefits to .NET development is that the CLR does not restrict the
syntax in which code is written. If a compiler exists for a given language, you can write
.NET code using its syntax. Some of the many languages currently being offered include
Visual Basic, C#, C++, Perl, COBOL, and even Java.
• Real, cross-language development is now possible thanks to the CLS. We can write code
in various languages and ensure that we can inherit from one component to the other,
debug across language boundaries, and even handle exceptions raised from one language
to the next.
• .NET does not force us to throw out the old. There is full support for interoperating with
COM/COM+ services. .NET code can access COM code of old; there is even support for
COM code accessing code written with .NET.
• We are now freed from the registry and all its pitfalls. Code written for .NET includes
metadata, or descriptive information about the code itself, including its dependencies.
With metadata, your code is said to be self-describing, thus rendering the registry, type
libraries, and Interface Definition Language (IDL) obsolete. It also makes the task of
installation and removal much more trivial.
• Our code will execute much faster due to performance gains with the platform, the use
of more compiled code (VB, ASP), and managed services.
• We now have full support for all object-oriented features from within VB, including
implementation inheritance!
• .NET should make memory leaks and reference counting things of the past thanks to its
garbage collector (GC).
• There is now the potential to compile once (to MSIL) and run on any platform! This is
the real Holy Grail of .NET. If a platform supports the runtime, your code will execute
on it.
That said, it is still quite possible (and very likely) for both language and library authors to 1
implement non-CLS–compliant features. A language author may target the CLR, but may also
need to create specific features that are not understood by other .NET languages. The key is
EVOLUTION
OF .NET
that any code you write using a language or library must only use CLS features in the API that
it exposes. If you stay true to this rule, your code is guaranteed to be accessible from all pro-
gramming languages that support the CLS. All non-compliant features are marked as such in
the language definition and usually have a good reason for their non-compliance.
For example, nearly every member of the .NET Framework Class Library is CLS-compliant.
This ensures their access and use from every .NET language. However, some members provide
support for features that are not defined by the CLS. All non-conforming members are identified
in the documentation, and in all cases, a CLS-compliant alternative has been made available.
We are the big benefactors of the CLS. Developers currently can choose from more than 20
different languages when writing .NET code! And the best thing? Every class that you learn to
use by reading this book works the exact same way from all these other languages. We are no
longer constrained by syntax, but instead, are free to choose the language with which we are
most productive—and we only have to learn one API!
It is the responsibility of the Just-In-Time (JIT) compiler to convert the MSIL (using the meta-
data) into native code. It is important to note that MSIL is not interpreted by the runtime. It is,
in fact, compiled natively as a method is requested. Compiling one method at a time saves the
runtime the overhead of compiling the entire library when it only needs one method. Addi-
tionally, methods that are never requested don’t need compiling. Of course, subsequent calls to
the method do not require a recompile. The native code is stored in memory, which is used to
process the additional requests.
NOTE
If you just can’t stand knowing that you are deploying IL code, Microsoft has shipped
a pre-JIT compiler. This allows you to JIT compile your code and store it on disk at
deployment. The pre-JIT compiler is called ngen.exe.
Managed Code
Code written specifically for the CLR is said to be managed code. The term, managed, refers
to the runtime’s services executing against your code. For instance, .NET’s memory manager
and garbage collector (GC) manage the memory used for a class you write. The advantage: You
are no longer responsible for reference counting or controlling memory leaks. Or course, the
runtime offers a number of other managed services in addition to memory management.
All code written with VB .NET is managed code by default. On the other hand, code written
with C++ is unmanaged by default. To write managed code with C++ you must throw a com-
piler switch and mark code as managed with a keyword inside of the managed extensions for
C++. Similarly, C# can mark data as unmanaged by using a keyword.
The benefit of managed code is that it can take advantage of the .NET runtime. Cross language
interoperability, code access security, and garbage collection are available only to managed
code. One drawback is that managed classes created to target the CLR can only inherit from
one base class.
NOTE
The .NET Framework provides the System.Runtime.InteropServices namespace to
facilitate access to native operating system services and other unmanaged code. The
namespace exposes a set of types for working with unmanaged code.
Evolution of .NET
23
CHAPTER 1
Assemblies 1
An assembly represents a group of functionality that is deployed as a single, logical unit.
EVOLUTION
Assemblies represent the code we write and can include other resources such as images or
OF .NET
other binary files associated with our applications. Assemblies are typically the bricks of our
.NET solution. They group functionality, which forms a boundary around code access security,
type, reference scope, version, and deployment unit.
All .NET applications must contain one or more assemblies. Assemblies have what are called
manifests. Assembly manifests represent the metadata that describes the assembly. The mani-
fest contains information on the exposed portions of the assembly, its references to other
assemblies, its version, name, and the files that make up the assembly.
One direct benefit of the .NET assembly model is that it is the end of DLL hell. Assembly ver-
sioning and referencing allows specific reference to versions of a component. This means mul-
tiple versions of a single component can now execute side-by-side.
Another benefit of assemblies is easier installation and deployment. Assemblies will make zero
impact and XCOPY installs possible.
NOTE
To add, remove, or view assemblies in the GAC, Microsoft provides a developer tool
called the Global Assembly Tool (gacutil.exe). Alternatively, you can drag-and-drop
components into the GAC using Windows Explorer since the GAC is also represented
as a directory.
An Introduction to .NET
24
PART I
Assemblies stored in the GAC often exhibit better runtime performance. Thanks to the key-
pairs, the CLR does not have to recheck the assembly’s security, and thus, tends to locate these
bits faster.
NOTE
There is no need to install assemblies into the GAC in order to make them accessible
to COM or unmanaged code.
Namespace
The term namespace is nothing more than a design-time, logical naming scheme for .NET
types. Types are organized in a namespace based on hierarchy indicated by a dot (.). For
example, in the namespace, System.IO, the dot separation indicates that IO is under the hierar-
chy of System. There can only be one System at that level and only one IO under the System
level. Of course, there could be an IO.System or even a System.System.
Evolution of .NET
25
CHAPTER 1
NOTE 1
The System namespace is the root namespace in the .NET Framework. Classes in this
EVOLUTION
OF .NET
namespace represent the data types used in our applications. Types include: Object
(the root of all that is .NET), Byte, Char, Array, String, Int16, and so on. Typically, these
types correspond directly to the data types used in the .NET languages. For instance,
the Integer keyword in VB corresponds (and derives from) .NET’s Int32 type.
As developers, we can create and control our own namespaces. To do so, we simply use the
Namespace keyword. This allows us to organize our types under a hierarchy. It’s important to
note, however, that namespaces are only a design-time convenience. As we’ve discussed, at
runtime, names scope is controlled by the assembly.
NOTE
The Imports keyword allows you to treat the contents of a namespace as part of your
own namespace. It does not actually import anything. It simply provides you the con-
venience of refering to members of the namespace as if they were part and parcel to
your own. For example, the call Dim q as System.Messaging.MessageQueue can be
shortened to Dim q as MessageQueue provided that your application has the line
Imports System.Messaging at the the top.
NOTE
To access the Win32 API, you use P/Invoke. This is explained in detail in Appendix A,
“Calling the Win32 API from Managed Code.”
Interoperability
The CLR provides true language-to-language interoperability, both at design and runtime.
Thanks to the CLS and CTS, you can inherit, debug, and raise exceptions across languages.
COM promised developers interoperation, and it worked, but only at runtime. A component
written with VB could be called from C++ and vice versa, but this only worked at the binary
level. There was no support for true, design-time, cross-language development.
Of course .NET supports calling the raft of existing COM objects; any existing COM compo-
nent can be called from managed code. Microsoft knows you cannot (and should not have to)
re-create all your existing code for .NET. Instead, it provides the Runtime Callable Wrapper
(RCW) inside the Framework. The RCW acts as a proxy that translates COM interfaces into
those of .NET. With the RCW, your managed code thinks it is calling other .NET code.
.NET also supports the calling of COM components directly from managed code. To do so,
you must create a COM Callable Wrapper (CCW). COM does limit the .NET constructs of
which your application can take advantage. Things like parameterized constructors and static
methods are not supported.
One drawback, as you may have guessed, is performance. While it is possible—and often a
very good idea—to communicate between COM and .NET, all of this cross marshaling will
have a performance impact. This is a trade-off you will have to make when deciding when to
rewrite for .NET or interoperate.
Security
.NET provides us with a host of security options. Security in .NET is grouped into the follow-
ing basic set of services:
• ASP.NET Web Application Security is a model that allows you to authenticate users of
a site against the NT file system permissions or an XML file that lists users and their
roles.
• Code Access Security (CAS) defines to what resources your code has access. This
model allows you to build distributed components that can be easily trusted due to their
access permissions.
Evolution of .NET
27
CHAPTER 1
• Role-based security makes decisions on what the user can do or access based on his or 1
her identity and role membership. This is similar to the security model of MTS/COM+.
EVOLUTION
OF .NET
NOTE
For more information on Security in .NET, read Appendix C, “.NET Security Models.”
Summary
In this chapter we presented Microsoft’s .NET strategy and discussed its makeup and impor-
tance relative to various target audiences. We then took a quick look under the hood of .NET.
This information helped position the benefits of .NET to our application development. It will
also serve as a basis for discussion in the coming chapters.
The following is a summary of some of the key points presented in this chapter:
• .NET is Microsoft’s initiative to deliver software as a service.
• .NET is more than a set of developer tools. It includes services, server products, operat-
ing systems, and so on.
• The .NET timeline spans years, not months.
• .NET is important to anybody who accesses, stores, or interacts with data electronically.
• Code in .NET is compiled into MSIL and metadata, stored in PE files, and JIT compiled
natively for a specific platform and hardware.
• Code written in one .NET language can be easily used by any other .NET language
thanks to the CLS and the CTS.
An Introduction to .NET
28
PART I
• The Framework Class Library provides basic programming functions and works the
same from all .NET languages.
• You can call COM objects from .NET and .NET objects from COM.
• Security in .NET is accomplished through Web Application Security, Code Access
Security (CAS), and role-based security.
Evolution of VB .NET CHAPTER
2
IN THIS CHAPTER
• Design Goals 30
The evolution of Visual Basic, over time, has led to inconsistencies and redundancy. VB .NET
did not evolve out of VB6; it is not VB7. VB .NET exists to clean up the language and promote
it to equal footing with other modern languages.
This chapter walks readers through some of the profound changes and productivity enhance-
ments that VB .NET and the new tool, Visual Studio .NET, provide. Developers can expect
nearly every aspect of the way they write code today to be altered in some manner. For the
most part, these changes are intuitive and logical—you should not have trouble picking up and
adapting them.
First, we illustrate the prime drivers and goals behind the new language and tools. We then
walk through a number of key new language concepts. Finally, we present some of the
enhancements the new tools provide.
After reading this chapter you should
• Understand the direction Microsoft has taken the VB language and for what reasons
• Appreciate how traditional VB language constructs have changed
• Begin to use the new VB and .NET language constructs
• Understand and work with some of the key new tools and enhancements VS .NET pro-
vides
Design Goals
VB developers represent a very vocal, loyal, and large Microsoft customer base. These devel-
opers (of which we count ourselves) want to hold on to VB’s ease of use and its hiding of com-
plexities. At the same time, we desire the power and access that other language developers
have. Microsoft had to walk this tightrope when redefining VB for .NET.
To meet these demands, VB clearly had to move from a language of features (or a technology)
to an actual programming language. In this section, we present some of the principles that were
adhered to when VB was extended for .NET. This should help put in perspective the sweeping
changes that the language has undergone.
Ideals
VB .NET had to adhere to some of the ideals of past incarnations of VB. Additionally, it had to
make sure that new features that VB developers have been requesting for years were supported.
Some of the VB ideals include the following:
• The language should be simple and consistent.
• Code written in VB .NET should be easy to read, maintain, and understand.
• Applications should be easy to error proof and debug.
• Developers should be relieved of writing plumbing or redundant code. 2
• The language should allow for rapid application development.
EVOLUTION OF
VB .NET
Some of the new features developers requested and received include the following:
• The capability to execute programming tasks, access servers, write tasks, and so on with-
out leaving the IDE.
• Full support for OO development, including inheritance, constructors, and the like.
• Performance should not be compromised for choosing VB .NET over another language.
When designing .NET, the architects of Visual Basic knew that major changes were in store in
order to support the Common Language Runtime (CLR) and adhere to the Common Language
Specification (CLS). The broad scope of the required changes allowed for a major overhaul
and cleanup of the language. Thankfully, they did not divert and create a crippled .NET
language in favor of ease of use, but instead elevated Visual Basic to a first-class language
for .NET.
NOTE
The largest impact to the language involves VB .NET’s new object-oriented (OO) sup-
port. This includes inheritance, constructors, overriding, delegates, interfaces, and the
like. The changes are so many, in fact, that we devoted Chapter 3, “Object-Oriented
Concepts in .NET,” to the subject. Similar in impact, the topic of multithreading is cov-
ered in Chapter 12, “Working with Threads,” and exception handling is covered in
Chapter 20, “Profiling, Debugging, and Exception Handling.” For a complete list of
data type changes, see Appendix D, “.NET Framework Base Data Types.” A number of
the items presented next are also explained in further detail throughout the book.
Syntax
A number of syntactical changes affect dimensioning variables. Variables declared on one line
separated by a comma, for instance, all result in the same data type. In VB6, a declaration such
as Dim x, y, z as Integer resulted in x and y being declared as Variants and z as an
Integer. With .NET, all three variables would be of type Integer. You can still, of course,
declare two variable types on the same line, such as Dim x as Integer, y as String.
The syntax for creating objects at the time of declaration is also slightly altered by VB .NET.
You can now use the following to create new instances of objects during a Dim statement:
‘not syntactically dissimilar to VB6
Dim myObject As New SomeObject()
Note that the caveats that applied to dimensioning objects As New in VB6 no longer apply with
VB .NET. In previous versions of Visual Basic, if a variable was declared As New, it was often
impossible to destroy; simply accessing it added a reference count. This practice often led to
lost reference counts, and Dim As New was considered taboo. In VB .NET, however, no implic-
it object creation exists; this, along with garbage collection, should remove the As New con-
struct from the “bad form” list.
Evolution of VB .NET
33
CHAPTER 2
Arrays
A number of changes were made to the way .NET handles array declarations. The first is that
the Option Base statement is gone from VB .NET. Option Base allowed you to set the lower
bound value for all arrays declared in your code. With .NET, all arrays have a lower bound
value of zero (0).
Additionally, the support for creating arrays with a range of boundaries is also gone with .NET.
VB6 developers could set an array with elements 2 through 5 with the following syntax:
Dim myArray(2 to 5)
2
NOTE
EVOLUTION OF
VB .NET
You can still call the keywords UBound and LBound to return the upper and lower lim-
its of a given array. However, arrays created in .NET derive from the Array class. As
such, they expose the methods like GetUpperBound and GetLowerBound. Each takes
a value indicating the dimension of which you want to determine the bound. For
example, you can write code that looks like the following:
For i = 0 To myArray.GetUpperBound(0)
Contrary to early beta releases, the number of elements in a VB .NET array is unchanged from
VB6. The statement, Dim MyArray(5), for instance, still contains six items (0–5).
Finally, .NET offers a couple of new ways to dimension arrays. Dim MyArray() as Integer
= New Integer(5) is equivalent to Dim MyArray(5) as Integer. The first example simply
explicitly creates the Integer object. Additionally, you can now initialize the items in an array
at the time of dimensioning. The following code creates a five-item array, each with an initial
value:
Dim myArray(5) as Integer = {1, 2, 3, 4, 5}
Note that Redim and Redim Preserve are still supported by VB .NET.
Strings
With .NET, you no longer need to dimension a string variable with a given length. For
instance, the VB6 syntax, Dim myString as String * 50, is no longer supported. VB .NET
manages strings differently and allocates memory based on the size of the actual string at the
time of assignment.
In fact, the String data type in .NET is said to be immutable. Immutable refers to the fact that
after it is created, the contents of the string cannot be changed. Consider the following example:
An Introduction to .NET
34
PART I
Scope
For the most part, variable scoping remains the same with VB .NET with the exception of
block scope. Block scope dictates that variables dimensioned within a code block are available
only for the term of the given code block. This enables you to dimension variables that are not
actually allocated unless the code falls into the block. A code block is defined as the section
of code contained in the constructs If ... Then, For ... Next, Select ... Case, and Do
... Loop.
The following code illustrates block scope. Notice that as you nest blocks, you simply narrow
the possible scope. However, variables declared inside a parent block are available within the
child blocks. In this case, this means the z that is declared within the For ... Next loop is
accessible inside the nested If ... Then block, but y is not accessible to the For ... Next
block.
Sub Main()
For x = 1 To 10
If x = 2 Then
z = 3
Evolution of VB .NET
35
CHAPTER 2
y = 5
End If
Next
End Sub
Note that if you exit a code block and re-enter during the same call, the variable’s value is still 2
maintained for the life of the object; it is simply available only inside the block.
EVOLUTION OF
VB .NET
Integers
The Integer data type in VB .NET is compliant with both the CLS and CTS, (and therefore
other .NET languages). Unfortunately, an Integer in VB6 is no longer an Integer in VB
.NET, but rather a Short. The following table describes the changes:
NOTE
One nice effect of this change is that Integers in VB .NET are now the equivalent of
Integers of the SQL data type.
Variant
The Variant data type is gone from .NET. Its replacement is the Object data type.
Currency
The Currency data type is absent from VB .NET. Its replacement is the Decimal type.
An Introduction to .NET
36
PART I
NOTE
The Microsoft.VisualBasic namespace is imported by default with any VB .NET
project created within the IDE.
Behavioral Changes
VB6 developers need to be aware of a number of behavioral changes. For instance, VB .NET
sometimes completely reverses VB6 defaults. All changes were made for a good reason, how-
ever, and developers should be able to easily adapt to them. This section indicates a number of
key changes that the language, and its support for the CLR and CLS, dictates.
Destroying Objects
VB6 COM objects relied on reference counting (and the set myObj = nothing construct) to
destroy objects and free resources. VB .NET, however, uses the CLR’s garbage collection (GC)
service to clean up and destroy objects. The result of this change is that you no longer have to
worry about cleaning up your objects.
In exchange for this feature, however, the Class_Terminate event is no longer supported. In its
place is a Finalize method that can be overridden to provide similar support. This method will
be called by the GC service when freeing your object. Developers should, however, not rely on
Evolution of VB .NET
37
CHAPTER 2
this method to destroy key system resources such as database connections and the like. The
nature of GC is such that its time of execution cannot be reliably predicted. Waiting for the GC
to call Finalize on your object to free these resources can impact and limit the scalability of
your system.
Instead of a Finalize, developers are encouraged to implement a Dispose method on all their
objects that require cleanup code at the time of destruction. Cleanup code should be placed
directly inside this Dispose method. It is important to note, however, that unlike Finalize
(and Class_Terminate), Dispose is not automatically called by the runtime. This standard
construct should be called explicitly by all clients of an object. In fact, .NET provides the 2
interface IDisposable that should be implemented to ensure a standard construct for all
EVOLUTION OF
objects. The following is a simple example:
VB .NET
Public Class SomeClass
Implements System.IDisposable
End Class
Parameter Usage
By default, parameters are now passed by value (ByVal) rather than by reference (ByRef),
which was the default in VB6. We suggest that you still explicitly indicate ByVal and ByRef to
make your code readable and easily understood.
One key change developers will quickly appreciate is the standardization of parentheses inside
parameterized calls to methods, objects, and the like. In VB6, if you expected a return value,
you used parameters around your call:
myVar = MyObject.MyMethod(myOtherVar)
When not expecting a return, you omitted the parameters, unless you used the Call keyword,
in which case you used the parentheses.
VB .NET makes it simple: Parentheses are always required.
Another change to parameters is the use of optional parameters in VB .NET. All optional para-
meters must have an explicitly defined default value. This eliminates the need for the
IsMissing function. VB .NET optional parameters look like the following:
Notice that optional parameters still must be defined at the end of the function signature.
An Introduction to .NET
38
PART I
Property Declarations
VB .NET unifies, and renders obsolete, the Property Get and Property Set statements with
the Property declaration statement. The following is an example of the VB .NET method for
defining a property:
Public Class SomeOtherObject
Dim myLocalValue
Get
Return myLocalValue
End Get
Set(ByVal Value)
myLocalValue = Value
End Set
End Property
End Class
VB6 supported the concept of default properties on objects. This required you to use the Set
statement when executing an object assignment instead of accessing the default property. It
also had the effect of making code difficult to read and developers were therefore encouraged
to be explicit. For instance, code that “assigned” an object to a string variable required a devel-
oper to look up the object and determine its default property before being able to work with it.
VB .NET does not allow for default properties without parameters. Now, developers must be
explicit when executing an object assignment or accessing a property. The new syntax elimi-
nates the need for the Set and Let statements. The compiler is no longer confused by the call
myObject = SomeObject—it is clearly a reference assignment.
.NET does support default properties that take arguments. This seems contradictory at first
glance. However, calls to these properties are not ambiguous and therefore are supported.
Primarily, default properties should be reserved to collection classes. It is apparent to the com-
piler and the developer that a call to myVar = myCollection(1) is accessing a default proper-
ty (most likely, an Item property). To declare a default property, you use the Default keyword.
Short Circuiting
One of the key changes to Visual Basic’s behavior inside of .NET is the new support for short
circuiting. Short circuiting states that items evaluated in an expression are not evaluated unless
accessed.
Evolution of VB .NET
39
CHAPTER 2
To support this new feature and at the same time maintain compatibility with existing code,
VB .NET introduces the short circuiting operators OrElse and AndAlso. When you want to
have your expressions short circuit, you use OrElse as a replacement for the Or operator and
AndAlso for the And operator. For example, when two items in an expression are separated by
an OrElse operator, if the first item in the expression evaluates to True, the second item is
never evaluated by the runtime. Instead, the code short circuits. In VB6 (and with the Or opera-
tor in VB .NET) the entire expression (both conditions) is evaluated before execution—this
often resulted in errors or had the effect of limiting developers. The following code presents an
example.
2
Dim x As Object ‘Variant for VB6
Dim y As Int16
EVOLUTION OF
VB .NET
x = “Hello World”
y = 2
Console.WriteLine(y)
End If
New Tools
The IDE offers a number of new tools as well as improvements on nearly all previous tools. In
the following section, we will walk through some of the key productivity enhancing tools
inside of the VS .NET IDE.
An Introduction to .NET
40
PART I
NOTE
To change the project that starts when you click the Run button inside a project
group, you simply right-click the project file and choose Set as Startup Project.
Similarly, in an ASP.NET project, you can right-click a page and choose Set as Start
Page.
The Solution Explorer is also used to manage the various files that relate to an application. All
VB .NET files have the same extension, .vb. These are equivalent to VB6’s class files (.cls),
forms (.frm), modules (.mod), and so on. Additionally, you can work with and link to related
files directly within the Explorer. Support exists for miscellaneous files, similar to VB6’s relat-
ed documents, and solution items—even projects of different sources can be mixed within the
solution. Figure 2.1 captures a shot of the Solution Explorer.
FIGURE 2.1
VS .NET Solution Explorer.
Evolution of VB .NET
41
CHAPTER 2
Class View
The counterpart to the Solution Explorer is the Class View. In Class View, you work with a
code-centric view of your source files. As with the Object Browser or other object-oriented
(OO) views of your code, each class, method, property, and so on is represented in a hierarchi-
cal view. Additionally, each OO type has its own graphical icon. Each icon is altered slightly
with a padlock when a given member is private. Overall, this view provides fast and easy
access to the key areas within your source files without forcing you to scroll through .vb files
in search of the property or method you want to work with. Figure 2.2 illustrates the Class
View tool.
2
EVOLUTION OF
VB .NET
FIGURE 2.2
VS .NET Class View.
Server Explorer
Perhaps one of the biggest gains in ease of use is the embedded Server Explorer. This tool pro-
vides developers with access to all servers and associated tools and services that a given server
might publish. This simple concept should save you time switching between and launching the
various server-management applications. For instance, if your component logs to the event log
on your development server and you want to check a bug, you do not have to leave the IDE to
view that server’s events. Additionally, when programming against a database, you can view
the tables, procedures, and so on from within your source editor. The Server Explorer allows
you to watch messaging queues and manage server services. The Server Explorer tool also
allows you to view and use XML Web services that a given server might expose. Figure 2.3 is
a screenshot of this tool.
Clipboard Ring
The Clipboard Ring is one of those tools that, after using it for a day or two, you will wonder
how you got along without it. The tool is simple: It keeps track of the items that you cut or
copy. It caches each item in the Clipboard, up to a total of 15 items. The last item copied
becomes the first item in the ring. After 15 items, the old items start dropping off the list.
An Introduction to .NET
42
PART I
You can access items from the ring in two ways: The easiest way is a shortcut key,
Ctrl+Shift+V. Repeatedly pressing the V key when you’re holding down Ctrl and Shift cycles
through the ring within the text editor. Additionally, you can access the items in the ring from
the toolbox; Figure 2.4 illustrates this concept.
FIGURE 2.3
The Server Explorer.
FIGURE 2.4
The Clipboard Ring.
Command Window
The Command Window inside the IDE is a new concept for VB .NET developers. The basic
premise is that it enables you to execute Visual Studio commands by typing them command
style directly into a Command Window embedded inside the IDE. Commands range from
those that enable you to manage files, projects, and source code to those that execute builds,
list threads, and view the stack contents. Figure 2.5 captures a shot of the Command Window.
Notice the Intellisense and AutoCompletion support for the commands.
Evolution of VB .NET
43
CHAPTER 2
FIGURE 2.5 2
The Command Window.
EVOLUTION OF
Incremental Search
VB .NET
Incremental Search is another one of those tools that you’ll wonder how you ever did without.
The tool enables you to easily find items within your source code from within the editor. Like
most good tools, it is simple in concept and simple to use. Inside a source file, press Ctrl+I
(Edit, Advanced, Incremental, and Search). Your cursor will change to the binocular icon and
an arrow indicating the direction in which the search is being performed. As you enter text, the
cursor will find and highlight the nearest occurrence of the complete text entered. For instance,
typing c might take you to the first occurrence of a class declaration, but adding the letters an
could take you to your defined property, CanImport. To remove a letter from your search, press
the Backspace key. To find the next occurrence of a search string, press Ctrl+I again. To
change search directions, press Ctrl+Shift+I.
Printing Code
Finally, developers have control over the printing of their source code! From VS .NET, you
can print all source code, in color. VB developers have been clamoring for this for a long time,
usually resorting to some third-party application or tool to allow the printing of their code.
This may seem basic, but it is another example of IDE enhancements to your productivity.
Figure 2.6 captures the Page Setup dialog box for managing how your code is output; note the
check box for line numbers.
Code Editor
The code editor is typically where developers live, breath, and create. VS .NET incorporates
a number of new features that make editing source code less painful, without being overly
intrusive.
An Introduction to .NET
44
PART I
FIGURE 2.6
The Page Setup dialog box.
Task List
The Task List embedded in the IDE is both a new tool for VB developers and a code-editing
enhancement. The tool provides task-based management that directly relates to and tightly
integrates with your source code. Developers can add tasks directly within the task pane; no
more going out to other applications such as Outlook to manage your development tasks. But
the biggest benefit is the automated tasks. These are tasks that the IDE writes directly into the
list when an error occurs while you’re editing code or building a project. These tasks are
tracked by the IDE. You can double-click one and be taken directly to the hot spot in your
code that pertains to the task. As a problem is fixed, the task automatically disappears.
Additionally, using special comment tokens, you type your own tasks within your code as
comments. The default tokens are TODO, UNDONE, and HACK. VB .NET developers sim-
ply type a standard comment followed by the token and a colon. The IDE automatically picks
up the token and creates a task in the list with the appropriate category, source file, and line
number reference. You can even create your own custom tokens. To do so, from within the
IDE, choose Tools, Options, Environment, and Task List. Figure 2.7 shows the Task List in
action.
FIGURE 2.7
Task List.
Evolution of VB .NET
45
CHAPTER 2
Dynamic Help
Another tool that works with the text editor is the context-based help system. Again, keeping
with the theme of staying in the IDE to execute development tasks, VS .NET embeds a help
system that links directly to the code you are in the process of writing. In past incarnations, if
you had a question about a particular object or method that Intellisense (what did we do before
Intellisense?) can’t answer, you had to launch MSDN and start searching the index or browsing
the content tree. Now, as you code, the help system updates itself with relevant information
based on your keystrokes. For example, take a look at Figure 2.8. Here the cursor is positioned
on a line that dimensions a variable of the type EventLog. Notice the Dynamic Help pane. We
have access to the Dim statement, the EventLog class, its members, and other associated topics. 2
EVOLUTION OF
VB .NET
FIGURE 2.8
Dynamic Help.
Outlining Code
The text editor with VS .NET supports code outlining. Code outlining automatically arranges
your code in hierarchical, tree-like blocks. A block might be a class or property definition.
Code inside a block can be hidden and expanded per your preferences. This makes it easier to
work with long source files. You can hide the blocks that you have finished with or are not
working on and view them only as needed. To turn outlining off and on, you use either a short-
cut key or access the associated menu from Edit, Outlining.
Additionally, VB developers have the #Region directive available. This enables you to define
outlined blocks (or regions) of code independent of the editor. To do so, you simply time
#Region “RegionName” and end the region with #End Region, where RegionName is a string
used to name your region. Figure 2.9 shows a number of outlined blocks. Notice the collapsed
block “Properties.” This is a custom-defined region we created to block all our property state-
ments together.
An Introduction to .NET
46
PART I
FIGURE 2.9
Outlined code.
Line Numbering
As you’ve undoubtedly noticed from some of the previous screenshots, VS .NET supports line
numbering. Developers can quickly scan and find code based on error reports that indicate line
numbers. To turn line numbering on and off, you navigate to Tools, Options, Text Editor.
Hyperlinked Comments
Hyperlinked comments represent another small but important enhancement. Developers can
embed hyperlinks to more information directly within their source code. One use for this
would be to embed author details within the source code. This way, if the original developer
changes jobs, moves, and so on, she can update her credentials on a Web site, and all those
maintaining her code can have up-to-date access information for her. To create a URL inside a
comment, simply type http. Figure 2.10 illustrates a URL embedded in a comment.
FIGURE 2.10
Hyperlinked code comment.
Brace Matching
Brace matching is another small, elegant productivity enhancement. When you’re typing code
using braces { [ (, the editor highlights the opening and closing active set of braces. This is
very useful when writing long statements that contain braces embedded inside of braces; how
many times in VB6 have you sat counting and matching closing and ending braces?
Evolution of VB .NET
47
CHAPTER 2
Because VS .NET represents such a sweeping overhaul of the tools, languages, and environ-
ments, a number of additional new tools and enhancements are available for you to explore.
These include but are not limited to the following:
• Start page
• Application templates
• Object Browser
• XML editor
• HTML editor 2
• Style Builder for cascading style sheets
EVOLUTION OF
VB .NET
Summary
VB .NET takes the language that we all know and love to its destined height. The transition,
although not easy, should be intuitive—and worth the effort.
The following are key points presented in this chapter:
• VB .NET is not VB7. Instead, it moves the language away from being a technology
toward being an actual language.
• VB .NET changes a number of programming constructs such as scope, array declaration,
integer, data type, and the like.
• The .NET FCL replaces a number of VB6 functions with classes and associated methods.
• VS .NET intends to embed all necessary tools to realize the goal of allowing developers
to stay within the IDE to execute all programming-related tasks.
Object-Oriented Concepts CHAPTER
3
in .NET
IN THIS CHAPTER
• Classes—Wrapping Data and Behavior
Together 51
Starting with the release of Visual Basic 4.0, the capability to create classes has been intrinsic
to the Visual Basic language. Some might say that Microsoft’s move to support this was the
true beginning of VB’s evolution into an object-oriented language. Whenever it started, and
whatever you thought of Visual Basic’s prior ability (or inability) to support object-oriented
(OO) concepts, .NET brings Visual Basic up to speed with all of the basic properties of an
object-oriented programming language. The deep object support in Visual Basic .NET, and the
.NET Framework in general, is certainly one of the most compelling changes offered in this
new environment.
This chapter will focus on defining the concepts of object orientation as they relate to software
development in general. In Chapter 4, “Introduction to the .NET Framework Class Library,” we
will also examine their specific manifestations in the .NET Framework.
There have been more than a few books written on object-oriented programming, so this chap-
ter will not attempt to deliver a full treatise on a subject well deserving of hundreds of pages.
Instead, we will cover only the ground that we need to cover so that programmers new to
object-oriented programming and programmers with no OO experience at all will have a good
backdrop of knowledge for exploring the .NET Framework Class Library.
We’ll start by reviewing all of the pertinent characteristics of object-oriented languages—an
obvious first step when you consider that the classes and other pieces of the Framework Class
Library are all object-oriented in nature. Then we’ll examine how these concepts have been
brought to life inside of .NET and Visual Basic .NET, hopefully arming you with a solid-
enough understanding of these concepts to make your programming experiences with the
Framework Class Library more productive.
In years past, many developers have debated whether Visual Basic was an object-oriented lan-
guage. Instead of investigating any of these prior claims, arguments, or discussions, let’s focus
instead on the here and now. Visual Basic .NET supports the major traits of an object-oriented
language, including the capability to:
• Wrap data and behavior together into packages called classes (this is a trait known as
encapsulation)
• Define classes in terms of other classes (a trait known as inheritance)
• Override the behavior of a class with a substitute behavior (a trait known as
polymorphism)
We’ll examine each one of these traits in detail. We’ll also examine ways in which you will see
these concepts at work inside of the .NET Framework. Chapter 4 will continue this thread by
specifically examining the nature of the Framework Class Library and attempting to relate
these object-oriented concepts directly to the Framework Class Library.
Object-Oriented Concepts in .NET
51
CHAPTER 3
OBJECT-ORIENTED
CONCEPTS IN
Classes as Approximations
.NET
If we discuss classes in the context of programming, we say that they establish a template for
objects by defining a common set of possible procedures and data. Procedures are used to
imbue the class with a set of behaviors; when implemented in a class they are called methods.
Classes maintain data inside of properties (which may or may not be visible to other classes).
Behaviors are the verbs of classes, and properties are the nouns. A car, for instance, will accel-
erate in a prescribed fashion. This would be a behavior. A car will also have a specific weight,
color, length, and so on. These are properties. From a technical, implementation point of view,
there is actually no difference between the way that methods and properties are implemented.
They both have function signatures, and both execute some body of code. In addition, both of
them can accept parameters and return values.
NOTE
There are some general guidelines for when to use properties versus methods (and
vice versa), but probably the best advice is to just be consistent. Most of the time,
these rules will help steer you to the correct decision:
continues
An Introduction to .NET
52
PART I
• Use a method if you are going to be passing in more than a few parameters.
• If you find yourself writing a method called GetXXX or SetYYY, chances are good
this should be a property instead.
• Methods are more appropriate than properties if there will be many object
instantiations or inter-object communication inside of the function.
• Properties, when implemented, should be stateless with respect to one another.
In other words, a property should not require that another property be set
before or after it is set. If you find this kind of dependency inside of a property,
it should probably be a method instead.
NOTE
The difference between the system that we are programming and the real-world
process that we are modeling is often referred to as the semantic gap. You could
summarize some of what we have been talking about here by saying that object-
oriented programming aims to reduce the semantic gap between programming and
the real world.
Object-Oriented Concepts in .NET
53
CHAPTER 3
Of course, just because the basic premises of objected-oriented programming are simple to
understand doesn’t mean that the actual programming of object-oriented systems is trivial.
Once you can work your way through the syntax and condition yourself to think in an object-
like fashion while actually designing your applications, some of the perceived complexity
associated with software development will begin to fade.
OBJECT-ORIENTED
plexity of an application. In other words, if Class A doesn’t have to implement code to under-
CONCEPTS IN
stand how Class B operates, we have just reduced the complexity of the code.
.NET
As programmers, we initiate a message from one class to another by calling a method or prop-
erty on the target class. Part of this message that we send encapsulates any parameters or data
needed by the receiving class to execute the action.
Thus, we have classes in an object-oriented programming environment. A physical manifesta-
tion of a class in the programming world consists of code that defines these attributes and
behaviors through property and method routines.
In this book, our focus on the Framework Class Library will introduce you to new classes in
each chapter. They will exhibit all of the traits and characteristics of the classes that we have
just defined.
Now, let’s move on and discuss the next OO trait of Visual Basic .NET—inheritance.
An Introduction to .NET
54
PART I
Detail that is
publicly visible to
other objects
Class DataSet
method: InsertRecord()
method: DeleteRecord()
method: UpdateRecord()
Actual execution
details are Detail that is
hidden… publicly visible to
other objects
Class DataFile
InsertRecord method: InsertRecord()
Source method: DeleteRecord()
Object method: UpdateRecord()
Class DataFile
method:InsertRecord()
dlmrstas Recordset
Actual execution buffArray = ret.Serialize()
details are While Not(buffArray.EOF
hidden… .
.
.
FIGURE 3.1
Messaging and information hiding.
FIGURE 3.2
Three classes—no parent class.
By looking at them, it quickly becomes clear that we could hierarchically structure these
classes by abstracting their common traits into a parent class. These three classes would then
be child classes of that one parent class. Figure 3.3 shows how this results in a tree structure
for our classes. Another way to think about this is called sub-typing. Children classes can often
be thought of as different “types” of the parent class (an HR employee is a type of Employee,
and so on).
3
Employee
OBJECT-ORIENTED
CONCEPTS IN
Attributes
Age
.NET
DateofHire
Behaviors
Promote
Terminate
FIGURE 3.3
Introduction of a parent class.
NOTE
If you examine Figure 3.3, you will see that our arrows point from the child classes to
the parent class. This may not seem intuitive to you; after all, aren’t we creating a
child class from a parent class? This notation is advocated because it shows that the
child knows about the parent, but the parent does not (necessarily) know about
the child.
NOTE
There are many different ways to express the inheritance relationship: parent to
child, super-class to sub-class, ancestor class to descendant class, generalized class to
specialized class, and so on. In this book, anytime we use these terms you should
know that we are just referring back to this basic concept of inheritance relationship.
Figure 3.4 shows class nomenclature, in ancestor/descendant terms, against a class tree.
Object-Oriented Concepts in .NET
57
CHAPTER 3
Class A
Generalized Ancestor
Class B
Class C Class D
Specialized Descendant
FIGURE 3.4 3
Inheritance nomenclature examples.
OBJECT-ORIENTED
CONCEPTS IN
We now know that identifying logical relationships between objects will help us out in the area
.NET
of code reuse. But the examples we have talked about so far have been based on relationships
between objects—an appraisal of one object being a type of another object. If, however, you
approach inheritance by first looking at its end result, you’ll find that you can end up with an
entirely different perspective. Let’s look at an example: Let’s say that we have a class that
defines operations for a specific type of printer. We’ll call this class InkJet. Intuitively, we
sense a parent class that would most likely be called Printer. Introducing a Printer parent
class produces the inheritance that we see in Figure 3.5.
Inheriting from the Printer class is a good solution for us because it already defines some
basic operations (line feed, paper out, and so on) that we can use as building blocks for our
InkJet class. At the same time, we will add some of our own behaviors that are specific to
inkjet printers. But what if we had the requirement for some low-level communication code
that would send an error signal across a parallel port? Also, what if that code was already
available to us in yet another class?
Figure 3.6 shows how we could inherit from a fictional ParallelPort object to leverage the
SendErrorSignal code that we need.
An Introduction to .NET
58
PART I
Printer
Printer
Attributes
CommMethod
ISPaperOut
Behaviors
LineFeed
FormFeed
On
Off
InkJet
*denotes inherited members
Attributes
IsOpen
IsInkinstalled
Behaviors
FormFeed*
On*
Off*
AlignCartridges
FIGURE 3.5
Inheritance based on relationship.
ParallelPort
Attributes
IsOpen
LineSpeed
Behaviors
ReadBuffer
WriteBuffer
SendErrSignal
FIGURE 3.6
Inheritance strictly for code reuse.
Object-Oriented Concepts in .NET
59
CHAPTER 3
This is subtly different from what we were doing before because it is very difficult to envision
a logical relationship between a parallel port object and an inkjet object. After all, an inkjet
printer is not a type of a parallel port—there is no obvious hierarchical relationship to draw
between the two. In this case, we would be implementing inheritance to get at raw code reuse.
This doesn’t do anything for us in terms of making our code easier to understand—it does not
reinforce a relationship between abstract classes and real-world objects.
NOTE
Inheriting for pure code reuse in the absence of a sub-type relationship is certainly
something that you can do with classes, but isn’t always the best approach. You gain
code reuse at the expense of increased complexity in your system (and therefore, a
corresponding increase in the effort required to understand your system). In .NET, we
advocate implementing an interface instead of using class inheritance to represent
this relationship; you still get the desired code reuse without complicating your class
relationships (more on interfaces in Chapter 4).
What if you decided to proceed ahead with class inheritance, and decided to inherit from both 3
OBJECT-ORIENTED
the Printer and the ParallelPort class? This is called multiple inheritance (see Figure 3.7).
CONCEPTS IN
Multiple inheritance is not supported by the .NET runtime.
.NET
Printer ParallelPort
Attributes Attributes
CommMethod IsOpen
Is PaperOut LineSpeed
Behaviors Behaviors
LineFeed ReadBuffer
FormFeed WriteBuffer
On SendErrSignal
Off
Attributes
IsPaperOut*
IsInkinstalled
Behaviors
FormFeed*
On*
Off*
AlignCartridges
SendErrSignal*
FIGURE 3.7
An example of multiple inheritance.
An Introduction to .NET
60
PART I
Overriding
One of the common examples used to demonstrate this concept involves a class library that
describes geometric shapes. One of the behaviors that we would like to imbue into our shape
classes is the capability to draw themselves. Using our basic knowledge of geometry, we know
that each shape will require different parameters and use different operations to actually
accomplish the draw operations (pi may be used when drawing circles, squares will need to
know a side length, and so on). Because a procedural programming language doesn’t allow us
to reuse behavior names (think methods), we would end up with a different routine for each
shape type such as DrawCircle, DrawTriangle, and so on. Because we can reuse method
names with polymorphism, we can simplify the programming model considerably by reusing
one method called Draw; each shape class would implement this in a slightly different fashion.
This is called overriding and specific manifestations of this in the Framework Class Library
are discussed in Chapter 4.
Overriding further promotes the concept of information hiding that we talked about earlier:
Each class knows internally how to implement its behaviors, but calling classes don’t know
and don’t care. We just send a message saying, “Draw,” and the target class worries about how
to carry it out. You will often see overriding with inheritance. A child class may override a par-
ent class’s methods to implement specific functionality not relevant to the parent class.
Overloading
A class may also override its own methods based on parameter lists. Consider the class shown
in Figure 3.8.
Square
Behaviors
Draw(length, drawUnits)
Draw(point,point)
FIGURE 3.8
Method overloading.
Object-Oriented Concepts in .NET
61
CHAPTER 3
It represents a class, Square, and its draw methods. The implementation of the draw behavior
differs based on the information that is passed into the Draw method. This is a special case of
overriding called overloading. In our example here, we want to avoid implementing methods
called DrawFromLength and DrawFromCoords; we simplify our class architecture by implement-
ing just one method, Draw, and let it determine which implementation of Draw to use based on
the function signature. In this way, both of the following would be valid method calls:
mySquare.Draw(10,”inches”)
mySquare.Draw(topLeftPoint, bottomRightPoint)
Polymorphism is really all about keeping interfaces between classes the same while allowing
actual implementations to differ. This encourages loosely coupled object designs and hopefully
clarifies system architecture and reduces complexity.
Summary
In this chapter, we introduced the major object-oriented concepts that you will need to under-
stand to get the most out of programming against the Framework Class Library.
We have seen how:
3
• Classes are used as templates for objects, defining how they behave and the types of
OBJECT-ORIENTED
information they need to store.
CONCEPTS IN
.NET
• Inheritance is used to implement class hierarchies and reuse code from a parent class in
its children classes.
• Polymorphism allows classes to take on many different forms of behaviors, depending
on the particular circumstances.
As we explore the class library in Part II of this book, you will see ample evidence of these
concepts in action. In the next chapter, we will show how many of these OO concepts physi-
cally manifest themselves in the .NET Class Library.
Introduction to the .NET CHAPTER
4
Framework Class Library
IN THIS CHAPTER
• Introducing the Framework Class Library 64
The Framework Class Library is a vast tapestry; it will take a while before you are familiar
enough with it to immediately identify its possible uses in a given situation. This chapter will
introduce you to the class library that ships with the .NET Framework. We will examine its
organization, and look at exactly how the classes operate as a reusable layer of functional
building blocks for .NET developers. You should walk away from this chapter understanding,
in principle, what position in the larger .NET initiative that the Framework Class Library holds,
and how it is really designed with the end developer in mind. You should also be primed and
ready to look at actual code examples in the chapters that follow in Part II, “Working with the
.NET Namespaces,” of this book.
This chapter will specifically
• Describe the organization of the class library namespaces.
• Illustrate how the class library implements the core concepts of classes, enumerations,
delegates, interfaces, and structures.
• Identify the key productivity enhancements offered by programming against the class
library.
So, before getting in to the actual makeup of the .NET base classes, we will examine a few
basic aspects of the namespaces that constitute the class library.
NOTE
Referencing pre-existing libraries of code has traditionally been pretty easy with
Visual Basic. One of the main issues, however, was that many times these code
libraries weren’t initially consumable by Visual Basic developers. If you were a C++
programmer, the world was open to you in terms of functionality. VB programmers
had to wait for Microsoft or some other entity to make a wrapper or interface avail-
able that we could use from VB. Documentation also tended to be a problem.
Introduction to the .NET Framework Class Library
65
CHAPTER 4
INTRODUCTION
CLASS LIBRARY
FRAMEWORK
compilation).
THE .NET
Collections Collections of objects including arrays, hash tables, and
dictionaries.
ComponentModel Runtime and design-time behavior of components and controls.
TO
Configuration Configuration settings management for the Framework.
Data Data access and management (essentially defines the ADO.NET
technology).
Diagnostics Application debugging and execution tracing. Also included in
this namespace are classes related to event log manipulation and
performance monitoring.
DirectoryServices Access to the Active Directory.
Drawing Graphics and drawing, including printing.
An Introduction to .NET
66
PART I
You should know that the Framework Class Library can be extended, but that the system root
namespace will always contain classes that are universally useful to applications. Companies
may introduce their own libraries that will co-exist with the System namespace and that will,
in fact, operate under their own root namespace. Microsoft, for instance, has already shown us
an example of this by including several language-focused namespaces under a root Microsoft
namespace. Thus, we have Microsoft.Csharp, Microsoft.VisualBasic, and so on.
object-oriented way. That is to say that, while the Framework Class Library does introduce
some new features, much of the functionality exposed by the namespaces was previously avail-
able to us through the Win32 API, ActiveX controls, COM-based DLLs, and so on. Now, how-
ever, it is wrapped in a way that allows us to program at an abstracted level where we don’t
have to deal with the complexities and granular pieces of data required by the Win32 API. As a
direct result, we only have to deal with one over-arching design pattern in our applications: one
that uses and promotes components in an object-oriented environment. Moreover, the function-
ality is available across all of the CLR-targeted languages!
NOTE
In case you were wondering, the Framework Class Library does not replace the Win32
API. The class library still relies on the actual Win32 API classes to talk directly to the
Win32 API to execute code through something called a P/invoke (platform invoke).
We’ll talk more about this process when we dig into the details of calling the Win32
API from your code in Appendix A, “Calling the Win32 API from Managed Code.” Of
course, if the .NET Framework is ported to another environment, the P/invoke code
will be calling into that environment’s native API.
INTRODUCTION
CLASS LIBRARY
FRAMEWORK
namespace exposes classes for basic input/output operations, and so on. And inside the
THE .NET
System.Drawing namespace, we find objects that would be familiar to any Windows graphics
programmer: the Pen object, the Brush object, and so on.
With any sufficiently sized API, the task of actually locating the code or function that you need TO
for a given task is not an inconsiderable issue. Contrast the organization of the namespaces that
we have talked about thus far with the organization of the flat, monolithic namespace offered
up by the Win32 API. The Win32 API has given us such gems as
FindClosePrinterChangeNotification (which doesn’t find anything; it closes a resource). The
problem is that, as the Win32 API has grown at its core, its developers have had to be more and
more creative with their function names. The ability to look at a function name and know with-
out some research what its purpose is has started to deteriorate. To be fair, it is possible to be
productive using the Win32 API from Visual Basic. It just takes some determination and a lot
An Introduction to .NET
68
PART I
of reference information. The Framework Class Library is a more approachable API: Things
are where you expect them to be. It appeals to the OO programmer in all of us who, after all,
is just interested in simplifying software by using objects.
NOTE
We call any code that runs under the control of the .NET runtime managed code.
Unmanaged code is any code that runs outside of the .NET runtime, such as the
Win32 API, COM components, and ActiveX controls. Currently, the only development
tool available from Microsoft for writing unmanaged code is Visual C++.
Because the Framework Class Library is written in managed code that runs on top of the .NET
Common Language Runtime, it reaps all of the benefits of any other piece of managed code,
such as:
• Freedom from GUIDS, hResults, and so on
• Automatic garbage collection of unused resources and memory
• Support for structured exception handling
Code Reuse
The holy grail of all object-oriented development, code reuse is a large part of the value of the
Framework Class Library. As all code libraries are intended to do, the intent with the class
library is to provide developers with a foundation for application development. If you recall the
concepts of inheritance that we talked about in Chapter 3, “Object-Oriented Concepts in
.NET,” VB .NET programmers are free to derive any of their classes from a base class defined
in one of the system namespaces (assuming, of course, that the class has not been marked as
sealed—see our note on sealed classes later). These classes don’t behave any differently than a
class that you would write in Visual Basic .NET. In VB .NET, the Inherits keyword is all that
is needed. The following code snippet shows a program inheriting from the XMLDocument class
in the System.Xml namespace:
Public Class MyDOM
Inherits System.Xml.XmlDocument
.
.
.
End Class
In addition to using the Framework classes for inheritance, you can also simply instantiate an
4
object directly from one of these classes, and use it—you simply reference the namespace that
INTRODUCTION
CLASS LIBRARY
you need, and then dimension and instantiate an object from one of the classes in that name-
FRAMEWORK
THE .NET
space. Object instantiation will look familiar to VB developers, because this is similar to what
you have done with type libraries and COM DLLs.
This code snippet shows how you can use the Imports keyword to reference a specific name-
TO
space in the class library. The DNS class contained in the System.Net.Sockets namespace is
then used to instantiate an object. The code is clear and simple to read.
Imports System.Net.Sockets
.
.
.
End Sub
End Class
NOTE
You should know that there are classes that don’t allow you to inherit from them.
Conversely, there are also classes that require you to inherit from them. These classes
are called sealed and abstract, respectively, and are discussed in more detail in the
following section.
The end result is a universal, free, logical, and component-based API that can be easily con-
sumed by any of the .NET languages without loss of functionality.
Classes
As we discussed in Chapter 3, a class is a template or blueprint for an object. It defines how an
object should look and behave. It does this principally through methods, properties, and events.
If you have read on from the last chapter or have some experience with object-oriented pro-
gramming, you should be familiar by now with the concepts of classes. You should note, how-
ever, that the .NET runtime supports some class attributes that you may not be familiar with.
These attributes are summarized in Table 4.2.
Classes typically define a constructor; this is a specialized behavior of a class that is called
whenever a new instance is created from that class. In Visual Basic .NET, you will define your
constructors using the Sub New routine. Class_Initialize and Class_Terminate events are
no longer supported. Classes may also implement destructors that will be called when the
object is being destroyed.
Object destruction in .NET works a little bit differently than you have come to expect. .NET
4
INTRODUCTION
implements something called a garbage collector. The garbage collector continually examines
CLASS LIBRARY
FRAMEWORK
THE .NET
a “reference tree;” if it finds an object that doesn’t have any more branches on the reference
tree, it calls the destructor of that object. It is no longer necessary to explicitly destroy man-
aged objects; the garbage collector will take care of it for you.
TO
NOTE
If your class uses resources that are unmanaged in nature, you should implement a
Finalize method. This method, which is not public, will be called by the garbage
collector as it destroys unneeded objects. If you need to provide a public method for
freeing resources (such as a Close method for your class), you should implement the
IDisposable interface.
An Introduction to .NET
72
PART I
All objects in the .NET Framework inherit from the System.Object class. System.Object is
the ultimate superclass in the Framework Class Library.
Structures
Structures are the .NET version of user-defined types from prior versions of Visual Basic.
User Defined Type (or UDT) syntax in Visual Basic, versions 6 and earlier, looked something
like this:
Type InventoryItem
SKU As String * 50
Price As Single
Name As String * 25
Count As Integer
End Type
The metamorphosis into structures means the Type...End Type syntax is no longer supported.
It has been replaced with Structure...End Structure:
Structure InventoryItem
Public SKU As String
Private Price As Single
Public Name As String
Private Count As Integer
End Structure
Note that with structures, each structure element can have its own scope modifier (for exam-
ple, can be public, private, and so on). This was not previously possible with UDTs.
And that’s not all that has changed. Structures in .NET behave a lot like classes. They support
most of the constructs of a class including properties, methods, and events. Table 4.3 summa-
rizes some of the key similarities and differences between classes and structures.
In the class library, structures are used to represent value types such as integers (Int16, Int32,
Int64). In fact, the actual structure object inherits directly from the class ValueType in the root
System namespace.
Introduction to the .NET Framework Class Library
73
CHAPTER 4
Delegates
Delegates may be an unfamiliar concept to the average Visual Basic developer. In essence, del-
egates are function pointers: They encapsulate method calls to other objects. Delegates are
described by the parameters they accept and the value type they return. In other words, you can
declare a delegate that will map to a method call that takes an integer and a single and returns
a string:
public delegate myDelegate(value1 As Integer, value2 As Single) As String
To use the delegate, you would declare a variable that references the delegate like this:
Dim myCallback As myDelegate = New myDelegate(obj.SomeMethod)
If the SomeMethod method that you reference has a function signature that matches the defini-
tion of myDelegate, you are home free, otherwise an error will be raised.
One of the powerful capabilities of a delegate is its ability to perform its function pointing in a
late bound fashion. You can use the preceding delegate to map to any method that follows the
parameter and return type signature; you don’t need to explicitly make a tie between the dele-
gate and a specific function at design time.
Delegates are defined throughout the Framework Class Library namespaces and fulfill a variety
of different tasks, most associated with implementing designs regarding callbacks and event
processing.
Interfaces
An interface is most commonly described as a contract. If you implement an interface, you are
essentially entering into a contract with all other components that exist that says, “I agree to
provide the following functionality in the following manner forever and ever….” Interfaces can 4
have methods and properties just like classes.
INTRODUCTION
CLASS LIBRARY
FRAMEWORK
THE .NET
You can implement interfaces defined in the Framework Class Library by using the
Implements keyword:
Back when we talked about inheritance and code reuse in Chapter 3, we said that interfaces
provide a good structure for implementing code belonging to a class that isn’t necessarily “log-
ically” related to our core class.
This kind of code reuse keeps our class hierarchies from becoming cluttered with seemingly
random inheritance relationships.
NOTE
There are some design considerations to think about when deciding between class
inheritance and interface implementation. For instance, because interfaces are meant
to be binding and perpetual contracts, they can’t (or shouldn’t) be changed. Imple-
menting code in an interface that is likely to change, such as business rules, leads to a
very brittle architecture. You don’t have this concern with class inheritance because
methods and properties can always be added to a base class without “breaking” its
descendant classes.
Enumerations
An enumeration is essentially a named constant—it is an aid to developer productivity because
it allows you to reference values using a recognizable name. Using enumerations greatly
improves code readability and speeds up coding because they provide a way to name or refer-
ence a value that maps to one of the underlying data types defined by the CTS. Visual Basic
developers should be familiar with the concept of “enums”—the syntax has not changed mov-
ing into Visual Basic .NET. The .NET runtime allows enumerations to evaluate to any of the
signed or unsigned integer data types that are defined (such as Int32, Int64, and so on).
As far as the Framework class library is concerned, enumerations are in many of the name-
spaces. One example of an enumeration in the class library is Appearance, contained in the
System.Windows.Forms namespace.
To use an enumeration from your code, simply reference the enumeration’s name and the value
name that you want to use:
myButton.Appearance=Appearance.Button
In the preceding code, we use the Appearance enumeration to specify that the button control
we are using should take on the “Button” appearance.
Enumerations are derived from the System.Enum class, which means you can reference enu-
merations in some very cool ways. For instance, you can call the GetValues() method to get
Introduction to the .NET Framework Class Library
75
CHAPTER 4
an array of all values defined by an enumeration. You can also call the GetNames() method to
get an array of all names for the values defined by an enumeration.
INTRODUCTION
CLASS LIBRARY
FRAMEWORK
THE .NET
NOTE
Not all classes in the Framework class library allow you to create instances just by
using the New operator—some force you to go through a class factory method to get
TO
your initial instance of the class. The WebRequest class is one example: In order to
create a new WebRequest object, you have to use the WebRequest.Create method.
You should also be aware that some classes have static methods and properties. Static
methods apply to classes and not instances. That means that you don’t have to create
an instance of the class in order to use the method. The WebRequest.Create method
is an example of a static method: We simply call it using the class reference without
an actual instance having been created.
An Introduction to .NET
76
PART I
This example shows the method prototype for the GetYear method on the
JulianCalendarClass. It shows us that this function is overridden from its base class (in this
case, from the Calendar class).
Overloading is a form of overriding by providing multiple method instances that differ only in
their parameter list.
As with overriding, when you explore the Framework Class Library you will notice plenty of
examples of overloading. Many class constructors are overloaded to give developers the maxi-
mum choice of instantiation based on the available data.
Consider the following constructors for the TCPClient class:
Overloads Public Sub New()
Overloads Public Sub New(ByVal localEP As IPEndPoint)
Overloads Public Sub New(ByVal hostname As String, port As Integer)
These constructors give you the choice of how you want to instantiate a TCPClient object.
NOTE
If you have been paying attention, you will have noticed by now that both Visual
Basic .NET and the Framework Class Library define base data types. In other words,
you have the Int32 data type defined in the System root namespace, and the Integer
data type defined in Visual Basic. From a best-practice perspective, you may be asking
yourself, which is the preferred method: declaring things using VB .NET intrinsics, or
their actual System types?
To illustrate, both of the following lines of code are valid:
Dim SomeVar As Integer
Dim AnotherVar As System.Int32
We suggest you pick whatever comes more naturally to you, and use it consistently.
Using the actual Framework data types has some attraction if you are programming
Introduction to the .NET Framework Class Library
77
CHAPTER 4
across multiple languages; you don’t have to shift gears. On the other hand, dimen-
sioning a variable as Integer will come much more naturally to Visual Basic develop-
ers. And of course, you don’t have to worry about the repercussions of your choice:
The CLR ensures that all of your code is mapped to the correct underlying data type.
Exception Handling
Exception Handling is the process of managing errors that may be encountered during the exe-
cution of your code. The .NET runtime, and the .NET languages, supports the concept of
Structured Exception Handling (SEH). These exception handlers follow a standard format that
defines a Try block, a Catch block, and a Finally block.
Writing an exception handler requires you to place the code that could possibly generate an
error into the Try block. In the Catch block, you place your code that deals with the error. The
Finally block is where you place operations that should be performed regardless of whether
an error was raised or not. The following code snippet shows a simple routine that implements
its code inside of an exception handler.
Sub DoCalc(ByVal num1 As Integer, ByVal num2 As Integer) As Integer
‘The exception handler is initiated with the ‘try’ block
Try
DoCalc = num1 / num2
Catch appError As Exception
‘handle the error; here, we just alert the user
‘through a message box. To get more detailed
‘debugging level info, we could use the
‘Exception.StackTrace property...
MsgBox(“Error:” & appError.Message)
4
INTRODUCTION
CLASS LIBRARY
FRAMEWORK
Finally
THE .NET
Beep()
End Try TO
End Sub
Notice that the Catch statement syntax allows you to deal with the exception as an object. The
class library defines an actual Exception class that allows the runtime to treat pass exceptions
through as instances of the Exception class. The Exception base class is, in turn, used to
derive more specialized exception classes such as the ApplicationException and
SystemException classes (both defined in the System root namespace) and the WebException
class (defined in the System.Net namespace). In fact, there is a fairly deep class hierarchy built
from the Exception class base (see Figure 4.1).
An Introduction to .NET
78
PART I
Exception
ApplicationException
CookieException
SystemException
ArgumentException
ArithmeticException
ExternalException
Win32Exception
…
FIGURE 4.1
Partial snapshot of the Exception Class Hierarchy.
As you examine the Exception class descendants, you will see that they offer methods and
properties specific to a given coding scenario. They often, for instance, override the ErrorCode
property to provide specific error codes for their particular scope. An exception handler with
multiple catch blocks looks like this:
‘The exception handler is initiated with the ‘try’ block
Try
‘code that could raise an exception goes here
Catch appError As Exception
‘handle the generic error
Finally
Introduction to the .NET Framework Class Library
79
CHAPTER 4
End Try
You can use the appropriate level (generalized or specialized) of exception object that is appro-
priate to your specific piece of code. You will often find yourself using multiple catch blocks
in your code to deal with exceptions raised across different levels of the exception class
hierarchy.
As we talk in-depth about the Framework classes in the chapters to come, we’ll devote time to
talking about these specialized exception classes in the namespaces, and the additional proper-
ties that they offer.
Summary
In this chapter, we have seen how the .NET Framework Class Library employs the basic OO
concepts of classes, inheritance, and polymorphism to provide developers with a rich API for
managed code.
We also investigated:
• The various pieces of a namespace including classes, delegates, interfaces, structures,
and so on
• How the Framework Class Library is designed to make it easy for developers to find and
then consume functionality exposed by its classes
• How to use the class library as an API and as a bed for class inheritance
Now that the anatomy of the class library has been exposed, the next part of this book will
concentrate on visiting, in-depth, some of these namespaces. We’ll explore their classes and
figure out how to write code against them.
4
INTRODUCTION
CLASS LIBRARY
FRAMEWORK
THE .NET
TO
PART
Working with the .NET
Namespaces
II
Part II of this book focuses on an in-depth examination of select
Framework Class Library namespaces. The structure of Part II is
developer task–based. It presents you with an overview of how
to accomplish a given task, leads you through detailed examples
(written in VB .NET), and finally, provides detailed reference
information on select namespace classes, delegates, structures,
and interfaces.
Part II explores
• Which classes and namespace elements to use for a given
programming task
• Design patterns and best practices for using a namespace
• Sample applications to cement the concepts covered in
each chapter
• Reference information on specific namespace elements to
help you become immediately productive with the class
library
This section can be read in sequence, but most readers will want
to skip around as needed. It is organized around common pro-
gramming tasks and designed to get you producing quality code
with a given namespace.
Forms, Menus, and Controls CHAPTER
5
IN THIS CHAPTER
• Key Classes Related to Windows Forms 84
• Creating Forms 86
Visual Basic has always excelled at forms-based application development. It has made the art
of crafting user interfaces with graphical windows a quick and easy job, involving drag-and-
drop controls and simple event coding. Visual Basic .NET continues that tradition, aligning
itself squarely with rapid forms development. Today, Visual Studio .NET and the .NET
Framework itself have become key enablers for graphical user interface (GUI) development.
With the .NET Framework, access to Windows Forms constructs is provided through the
System.Windows.Forms namespace (and its descendants). This means that all languages that
leverage the .NET runtime and its Framework can take advantage of these powerful classes to
provide forms capabilities.
In this chapter, we will start our exploration of the Forms namespace by investigating how it
allows you to create and manipulate Windows Forms. Then, we’ll cover adding menus to forms
and working with menu events. We’ll wrap up with a discussion of controls and how to create
custom controls from the UserControl class. We will not cover the design of Windows Forms
using Visual Studio itself; instead, we will focus explicitly on those items strictly from a class
library perspective. We will also not examine the multitude of controls available with Visual
Studio.
After reading this chapter, you should be able to
• Create forms and alter their appearance programmatically
• Control the interaction of multiple-document interface (MDI) parent and child forms
• Add menus to forms and respond to their events
• Add controls to a form programmatically and create your own controls
dialog box.
PageSetupDialog This class is a representation of the page setup dialog box,
which is common to forms that allow printing.
Working with the .NET Namespaces
86
PART II
Creating Forms
A form is a design-time version of a window. That is, when executed, a form appears as a win-
dow. In the design-time environment, however, a form functions as a canvas with which pro-
grammers can interact. Programmers can place code behind a form, and they can add controls,
menus, and other items to the form; they can customize the way the form looks and behaves.
Users and programmers alike are familiar with forms and their properties for a good reason:
They are the graphical, visual manifestations of programs. They are how consumers of pro-
gram functionality interact with programs to balance bank accounts, manage inventory, calcu-
late taxes, write books, and complete many other tasks.
FIGURE 5.1
Creating a new project.
5
FORMS, MENUS,
AND CONTROLS
FIGURE 5.2
The New Project dialog box.
Working with the .NET Namespaces
88
PART II
Note that because forms are objects in the .NET Framework, there is no magic that happens
inside the Visual Studio .NET IDE to make them work. The IDE is a convenient shell for
forms development because it allows for drag-and-drop form building through the Windows
Form Designer, which takes much of the grunt work out of creating forms and controls. You
could write these windows applications by using Notepad or any other simple text editor, but it
would be an arduous undertaking—there would be no syntax checking, auto-completion, or
even common formatting.
If you double-click on a form, you see the code behind the form. By default, the code you see
should look like this:
Public Class Form1
Inherits System.Windows.Forms.Form
End Sub
End Class
Let’s examine this line by line. First, a class declaration defines a class called Form1:
Public Class Form1
Inherits System.Windows.Forms.Form
This declaration shows that this class will inherit from System.Windows.Forms.Form; that is, it
will inherit from the Form class in the System.Windows.Forms namespace. This simple declara-
tion is where all the action takes place. In one fell swoop, the IDE has created some code that
is poised to take advantage of all the inherent functionality of the Framework-defined Form
class, and as you will see, there is quite a lot of functionality there to be consumed.
The next line might look peculiar to someone who is new to the Visual Studio .NET IDE
because the concept of code regions is new to Visual Studio .NET:
#Region “ Windows Form Designer generated code “
The #Region directive is a new element in the Visual Basic language. It tells the IDE to col-
lapse and hide sections of code. In the IDE, as shown in Figure 5.3, this directive is preceded
by a plus or minus box that you can click on to selectively reveal or hide the code bracketed by
the #Region directive.
Forms, Menus, and Controls
89
CHAPTER 5
End Sub
End Class
FIGURE 5.3
A collapsed code region in the IDE.
The collapsed code region in Figure 5.3 is given a name—Windows Form Designer generated
code—to let users know who put it there and what it contains. By default, Visual Studio
assumes that you won’t want to be bothered by all the code, so it is hidden. However, this is
where all the meat of the form creation code sits. If you expanding the collapsed code region,
the following code is displayed:
#Region “ Windows Form Designer generated code “
End Sub
End Sub
#End Region
As you can see, most of this code is commented with warnings such as “Do not modify it
using the code editor” and “Required by the Windows Form Designer.” In other words, if you
use the Form Designer, as a rule it is not a good idea to change the code it has written. To be
on the safe side, if you need to change something, you should do it through the Form Designer
itself instead of through brute-force code editing; this is yet another reason this region is hid-
den by default.
property (that is, the size of the client area of the form), AutoScaleBaseSize property (that is,
a value used to determine how much to scale the form if auto-scaling is used), Name property
(that is, the name used to reference the form in code), and Text property (that is, the text dis-
played on the title bar of the form).
The Dispose routine is the polar opposite of the New constructor: It is where you place code to
dereference objects, close files, zero out variables, and basically take care of anything impor-
tant that you opened or initialized in the constructor. The Form Designer uses the Dispose rou-
tine to clean up any components that it has used in conjunction with the form.
As you can see, this is no different from instantiating other objects from classes. It gives you a
base form to work with, and from here, you need to decide what style of form should actually
manifest itself when it is shown. We’ll cover these form characteristics when we continue our
discussion about the properties and methods of the Form class later in this chapter. Before we
do that, though, let’s take some time to examine the inheritance hierarchy that culminates in
the custom Form1 class.
Component System.ComponentModel
Control System.Windows.Forms
ScrollableControl
ContainerControl
Form
UserControl 5
FORMS, MENUS,
AND CONTROLS
FIGURE 5.4
An inheritance tree for the Form and UserControl classes.
The Control class inherits from the Component class in the System.ComponentModel name-
space. This is why you see references to components in Form1’s constructor.
Working with the .NET Namespaces
92
PART II
Minimize Box
Title Bar
Resize Handle
Restore/Maximize Box
FIGURE 5.5
The parts of a form.
With prior versions of Visual Basic, forms were modeless by default. However, you could dis-
play a modal form by using an enumeration passed into the Show method of the form object,
like this:
frmAbout.Show vbModal
The Visual Basic–defined constant vbModal tells the runtime to display the form modally. The
Form class in the .NET Framework provides two different types of Show methods: ShowDialog
and Show. You display a form modally by calling the ShowDialog method, as in the following
example:
aboutBox.ShowDialog()
You can pass an owner to the ShowDialog method in the following manner:
aboutBox.ShowDialog(mainForm)
This tells the runtime that the dialog box is a child form to the window, represented by an
instance called mainForm. The owner parameter is optional; if you don’t provide it in the call to
ShowDialog, the resulting dialog box is automatically assigned to the currently active window
as its parent/owner.
You display a modeless form by using the Show method:
mainForm.Show()
A typical design consideration involves the concepts of single-document interface (SDI) versus
MDI applications. An SDI application supports one document at a time, whereas an MDI
application can support multiple open documents at a time. As shown in Table 5.2, a few
properties that are exposed on the Form class can be used to manage MDI applications.
Working with the .NET Namespaces
94
PART II
To initially configure an MDI application, you set the IsMdiContainer property to true for the
parent form. Then, when you create each child form, you set its IsMdiChild property to true.
Listing 5.1 illustrates this concept by setting up Form1 as the MDI container (or parent form)
and then creating three child forms.
‘Indicate that this form will function as the MDI parent form
Me.IsMdiContainer = True
‘Create three new forms; each one will have its MdiParent
‘property set to the current form. This means they will
‘be contained inside of Form1 as children forms
childForm1 = New Form()
Forms, Menus, and Controls
95
CHAPTER 5
End Sub
Me.Controls.AddRange(New System.Windows.Forms.Control() _
AND CONTROLS
{New System.Windows.Forms.MdiClient()})
Me.IsMdiContainer = True
Me.Name = “Form1”
Me.Text = “MDI Form Demo”
Working with the .NET Namespaces
96
PART II
#End Region
End Class
Figure 5.6 shows how the application in Listing 5.1 looks when it is run.
FIGURE 5.6
An example of an MDI application.
You can visually organize MDI child forms by using the Form.LayoutMdi method. The
Form.LayoutMdi method accepts an MdiLayout enumeration value (see Table 5.3) and can cas-
cade the forms (as shown in Figure 5.6), tile them vertically, tile them horizontally, or arrange
their icons if they are minimized. Typically, MDI applications allow users access to these orga-
nizational functions through a Windows menu on the parent MDI container form.
Name Description
CenterParent Centers the form within the parent form.
CenterScreen Centers the form on the screen.
Manual Positions the form based on its current location and size.
WindowsDefaultBounds Positions the form in accordance with the Windows
defaults for location and bounds.
WindowsDefaultLocation Positions the form in accordance with the Windows
default location.
When you create a form, you can also specify the state of the form. By default, all forms are
created in the Normal state. A form in a Normal state displays with its default size (as specified
in the Form object’s Height, Left, Top, and Width properties). You can also cause a form to
maximize itself (that is, consume all available screen real estate) or minimize itself (that is,
shrink down to an icon). The form’s state is controlled through the Form.WindowState prop-
erty, which is used in conjunction with the FormWindowState enumeration. The
FormWindowState enumeration is described in Table 5.5.
5
Changing Border Style
FORMS, MENUS,
AND CONTROLS
The Form class exposes its border style through the property FormBorderStyle. This property
is used in conjunction with the FormBorderStyle enumeration to customize the way a form’s
borders are displayed. For example, the following code line:
myForm.FormBorderStyle = System.WinForms.FormBorderStyle.Sizable
Working with the .NET Namespaces
98
PART II
shows all the members of the FormBorderStyle enumeration (see Table 5.6). Note that chang-
ing the border style also affects the visual aspects of the title bar.
NOTE
The control box shows up as an icon in the left-hand side of the form’s title bar.
Right- or left-clicking on the icon displays the Control Box menu, which usually fea-
tures the commands Restore, Move, Size, Minimize, Maximize, and Close.
Forms, Menus, and Controls
99
CHAPTER 5
Controlling Opacity
One of the intriguing visual changes you can make to a form is to alter its opacity by using the
Opacity property. This property has an effect only on windows that are displayed on machines
running Windows 2000 or higher, and it can be used to achieve some interesting effects. For
instance, you could slowly alter this property so that you slowly phase a form into view when
launched and then slowly fade it out of view when closed. Setting Opacity to 1.0 (that is,
100%) displays an entirely opaque form, and setting it to 0 displays an entirely transparent
form. Note that these settings also affect any controls on the form.
To test the Opacity property, start a new project and add a form to it. Drag a TrackBar control
onto the form. The TrackBar control should show up in your control toolbox in Visual Studio
.NET. It looks like this:
Set the TrackBar control’s Maximum property to 100 and its Minimum property to 0. Double-
click on the control to get to the TrackBar1 change event, and set the form’s opacity to the
trackbar’s current value multiplied by .01. Run the application, and play around with the way
different opacities look by moving the slider on the trackbar. Here’s the code to alter the
Opacity value based on trackbar scroll movements:
form. Changing to a larger font might result in titles not fitting on the title bar or other
examples of overflow.
Working with the .NET Namespaces
100
PART II
‘SetDataObject is overloaded:
‘version 1
Overloads Public Shared Sub SetDataObject(ByVal data As Object)
‘version 2
Overloads Public Shared Sub SetDataObject(ByVal data As Object, _
ByVal copy As Boolean)
This section discusses how to interact directly with the Clipboard. As you start to develop
applications that use Clipboard functionality, you should keep in mind that many controls sup-
port Clipboard commands directly. For example, to copy selected text from a textbox, you
could either use the Clipboard.SetDataObject method or the TextBox.Copy method.
to talk to many different applications, each potentially with its own format and understanding
of data.
You need to first check to see if the data on the Clipboard can be massaged into a format that
is appropriate for a particular situation. For example, if you wanted to take data from the
Clipboard and set a text box’s Text property equal to that data, you would first determine
whether the data could be expressed as a string. The class DataFormats comes to the rescue
here. The DataFormats class provides a number of shared properties that are used to identify
the format of data stored in an IDataObject object. In this respect, its use resembles that of an
enumeration. Table 5.7 shows the different shared properties exposed by DataFormats.
The following code references one of the GetDataPresent methods of the IDataObject inter-
face; it inspects the data in the Clipboard and tells whether it matches the specifications:
Dim clipData As IDataObject = Clipboard.GetDataObject()
The GetDataPresent method accepts one of the fields from the DataFormats class as a para-
meter, and it returns a true or false value, indicating whether the data in the IDataObject
object is amenable to conversion to that format. If the method indicates that the data is in
fact in text form, you have one more step to go through: You need to get the data out of
IDataObject and assign it to the TextBox.Text property. IDataObject.GetData does this.
You again have to pass a field from the DataFormats class to tell the GetData method how to
receive the data. From there, it is a straight assignment to the TextBox.Text property.
This might seem like a lot of work, but notice that only four lines of code have allowed you to
use a very powerful, generic object that can share and store heterogeneous data across different
applications.
NOTE
The real beauty of the DataFormats class is that it allows you to create new data for-
mats and add them to the list of items that it understands. In this way, the Clipboard
and other objects that rely on the DataFormats class are infinitely extensible and
applicable to items that the .NET Framework runtime might not know about up
front.
Creating Menus
Like forms, menus are objects in their own right in the .NET Framework. When you set out to
create menus for an application, you are either creating a main menu that appears below the
title bar, or you are creating a context menu that is displayed when the user right-clicks over
an area of the form. (Context menus are often called pop-up menus.) Menus have become an
established part of user interface design. This section covers the menu-related classes in the
.NET Framework and shows you how to imbue your forms with menu-based functionality.
The MainMenu class is the menu that you will see directly on the form. Some standard items
that would appear on a main menu include File, Edit, View, Window, and Help, among others,
but you can place whatever you want on your main menu. Each item on the main menu is, in
.NET Framework terms, a menu item represented by the MenuItem class. To add a File element
to a main menu, you could write code like the following:
Dim fileMenu As MenuItem = appMenu.MenuItems.Add(“&File”)
From this code snippet, you can see that each MainMenu instance has a collection, called
MenuItems, that can contain instances of the MenuItem class. Membership in this collection
determines what the main menu actually contains. To add something to the main menu, you
simply add a menu item to this collection.
NOTE
There is a Menu class in the forms library. For the most part, you will never need to
deal with it directly; your primary interaction with menus is likely to be through the
MainMenu, ContextMenu, and MenuItem classes. However, it is important to know that
all these classes—and many others that are associated with menus—inherit directly
from the Menu class. For more information, see the .NET Framework documentation
on the Menu class.
5
FORMS, MENUS,
AND CONTROLS
A menu item can be thought of as any selectable item that appears on any menu, not just on
the main menu. This means that in order to add a selection to the File menu—such as an Open
Working with the .NET Namespaces
104
PART II
item—you would access the collection of MenuItems, only this time you would reference the
fileMenu object instead of the appMenu object:
fileMenu.MenuItems.Add(“&Open”)
NOTE
A particular instance of a MenuItem class can be contained in only a single menu at
a time. It also cannot be added more than once to the same menu. If you need to
duplicate a menu item from one menu to another, you use the MenuItem.CloneMenu
method, as follows:
menu2.MenuItems.Add(myMenuItem.CloneMenu())
In this way, you can essentially copy an existing menu item into a new menu.
You now have all the base code elements you need to create a simple main menu and use it on
a form. At this point, however, all you have done is created a MainMenu instance and added
some menu items to it. If you were to run a form project with just that code, you still wouldn’t
see a menu on the form because you now need to associate the MainMenu instance to the form.
This is not done automatically for you; you must set the reference in your code by using the
Form.Menu property:
Form1.Menu = appMenu
Forms, Menus, and Controls
105
CHAPTER 5
Listing 5.2 shows a standard main menu implemented on a form. Figure 5.7 shows what the
generated menu and form look like.
With appMenu
fileMenu = .MenuItems.Add(“&File”)
editMenu = .MenuItems.Add(“&Edit”)
windowMenu = .MenuItems.Add(“&Window”)
helpMenu = .MenuItems.Add(“&Help”)
End With
Me.Menu = appMenu
End Sub
MyBase.Dispose(disposing)
End Sub
End Sub
#End Region
End Sub
End Class
FIGURE 5.7
A form and its main menu.
Listing 5.2 expands the Windows Form Designer region because that is where all the initializa-
tion code for the menu appears. You first declare the menu objects that you need—one
MainMenu and three MenuItem instances—and then in the constructor you actually create the
main menu object and add items to it.
Forms, Menus, and Controls
107
CHAPTER 5
Similarly to the way you assign a main menu to a form, you assign the context menu to a form
by using the ContextMenu property. The following snippet assumes that Me refers to a form:
Me.ContextMenu = formContextMenu
Unlike the MainMenu property, the ContextMenu property is first implemented way back in the
class hierarchy with the Control class. This is because context menus are useful across any
type of control, not just forms. To set up a context menu for a text box control, for instance,
you can use this code:
Dim ctrlContextMenu As New ContextMenu()
myTextBox.ContextMenu = ctrlContextMenu
a Boolean property, and setting it to true causes that particular menu item to automatically
display a list of all child forms that are declared within the application.
Working with the .NET Namespaces
108
PART II
NOTE
If an application has more than nine child forms, the window list itemizes only the
first nine and then displays the More Windows item. Selecting the More Items item
launches a dialog box that has a complete list of all the child windows.
Another common menu feature of MDI applications is the consolidated menu. The parent MDI
window’s menus may contain items that are specific to the currently displayed child window.
Users can select functionality that may be specific or relevant only to the child window from
the parent window’s menus, allowing more flexibility and ease of use. The .NET Framework
runtime automatically merges a child form’s menu onto the parent form’s menus in an MDI
application. You can use the MergeMenu method in code to make this happen. We’ll investigate
using this method in the next section.
Adding Submenus
To demonstrate how to add submenus, in this section we will expand on the File menu from
the previous section. To add the contents of the File menu, you need to add a few new items to
the MenuItems collection of the file menu:
With appMenu
fileMenu = .MenuItems.Add(“&File”)
fileMenu.MenuItems.Add(“&Open...”)
fileMenu.MenuItems.Add(“&Save”)
fileMenu.MenuItems.Add(“Save As...”)
fileMenu.MenuItems.Add(“View As Web Pa&ge”)
fileMenu.MenuItems.Add(“&Close”)
fileMenu.MenuItems.Add(“E&xit”)
editMenu = .MenuItems.Add(“&Edit”)
windowMenu = .MenuItems.Add(“&Window”)
helpMenu = .MenuItems.Add(“&Help”)
End With
The File menu constructed in this code example is shown in Figure 5.8. The main menu con-
sists of several menu items, and each of these menu items is itself a submenu that contains
Forms, Menus, and Controls
109
CHAPTER 5
items that the user can select. This nested relationship might seem confusing, but it is actually
quite simple. After creating the main menu, you have the choice of implementing submenus
(which show lists of commands) or items (which are commands).
FIGURE 5.8
File menu items.
If you were to nest even more items under one of the File menu items, you would end up with
a cascading, or fly-out, menu. For example, you can add a menu item to the existing Close
item, which means Close becomes a way of showing another menu:
Dim fileCloseMenu As MenuItem
With appMenu
fileMenu = .MenuItems.Add(“&File”)
fileMenu.MenuItems.Add(“&Open...”)
fileMenu.MenuItems.Add(“&Save”)
fileMenu.MenuItems.Add(“Save As...”)
fileCloseMenu = fileMenu.MenuItems.Add(“&Close”)
fileCloseMenu.MenuItems.Add(“Now”)
fileMenu.MenuItems.Add(“E&xit”)
editMenu = .MenuItems.Add(“&Edit”)
windowMenu = .MenuItems.Add(“&Window”)
helpMenu = .MenuItems.Add(“&Help”)
End With
To indicate on or off menu selections, the Windows menu system supports the ability to place
checkmarks next to menu items. You can also add shortcut keys and include separator bars to
help organize menus. Let’s add some of these features to the File menu we’ve been working
Working with the .NET Namespaces
110
PART II
with in this chapter. First, let’s add some separator bars to organize the menu items. You add
separator bars the same way you add menu items; you just set the menu item name to a dash
(“-”), like this:
fileMenu.MenuItems.Add(“-”)
FIGURE 5.9
A File menu with Close menu.
You want to group the Open, Save, and Save As items together. The View As Web Page item
should sit in a group by itself, and the Close and Exit items should sit together in the last
group. The code to make this happen looks like this:
With appMenu
fileMenu = .MenuItems.Add(“&File”)
fileMenu.MenuItems.Add(“&Open...”)
fileMenu.MenuItems.Add(“&Save”)
fileMenu.MenuItems.Add(“Save As...”)
fileMenu.MenuItems.Add(“-”)
fileMenu.MenuItems.Add(“View As Web Pa&ge”)
fileMenu.MenuItems.Add(“-”)
fileMenu.MenuItems.Add(“&Close”)
fileMenu.MenuItems.Add(“E&xit”)
editMenu = .MenuItems.Add(“&Edit”)
windowMenu = .MenuItems.Add(“&Window”)
helpMenu = .MenuItems.Add(“&Help”)
End With
You add shortcut keys and check (or uncheck) menu items through properties in the MenuItem
class. To check a menu item, you use the Checked property, which is a Boolean property that
you can set to true or false. To add a shortcut key to one of the menu items, you use the
Shortcut property, which is an enumeration of type System.Windows.Forms.Shortcut. You
use the enumeration Shortcut to specify the key that you want to assign as the shortcut key.
Table 5.8 lists the values that are available in the Shortcut enumeration. The menu now looks
like the one in Figure 5.10.
Forms, Menus, and Controls
111
CHAPTER 5
Name Description
Alt0 (…Alt9) Creates a shortcut with the key combination Alt+0.
Values range from Alt+0 through Alt+9.
AltBksp Creates a shortcut with the key combination
Alt+Backspace.
AltF1 (…AltF12) Creates a shortcut with the key combination Alt+F1.
Values range from Alt+F1 through Alt+F12.
Ctrl0 (…Ctrl9) Creates a shortcut with the key combination Ctrl+0.
Values range from Ctrl+0 through Ctrl+9.
CtrlA (…CtrlZ) Creates a shortcut with the key combination Ctrl+A.
Values range from Ctrl+A through Ctrl+Z.
CtrlDel Creates a shortcut with the key combination
Ctrl+Delete.
CtrlF1 (…CtrlF12) Creates a shortcut with the key combination Ctrl+F1.
Values range from Ctrl+F1 through Ctrl+F12.
CtrlIns Creates a shortcut with the key combination Ctrl+Insert.
CtrlShift0 (…CtrlShift9) Creates a shortcut with the key combination
Ctrl+Shift+0. Values range from Ctrl+Shift+0 through
Ctrl+Shift+9.
CtrlShiftA (…CtrlShiftZ) Creates a shortcut with the key combination
Ctrl+Shift+A. Values range from Ctrl+Shift+A through
Ctrl+Shift+Z.
CtrlShiftF1 (…CtrlShiftF12) Creates a shortcut with the key combination
Ctrl+Shift+F1. Values range from Ctrl+Shift+F1
through Ctrl+Shift+F12.
Del Creates a shortcut with the key Delete.
F1 (…F12) Creates a shortcut with the key F1. Values range from
F1 through F12.
Ins Creates a shortcut with the key Insert.
None No shortcut key is associated with the menu item.
ShiftDel Creates a shortcut with the key combination
Shift+Delete. 5
ShiftF1 (…Shift12) Creates a shortcut with the key combination Shift+F1.
FORMS, MENUS,
AND CONTROLS
FIGURE 5.10
Setting shortcut keys and checking menu items.
NOTE
You can show or hide the shortcut keys that you have defined for a menu item.
Shortcut keys are displayed by default, but you can modify them by using the prop-
erty MenuItem.ShowShortcut. This can be useful if you want to control (or let users
control) the verbosity of the menus. Not showing shortcuts can help reduce the over-
all image footprint of menus, perhaps for advanced users who don’t need or want to
be reminded of the shortcuts all the time.
editMenu = .MenuItems.Add(“&Edit”)
windowMenu = .MenuItems.Add(“&Window”)
helpMenu = .MenuItems.Add(“&Help”)
End With
The resulting radio button style, which takes care of visually displaying the radio button check-
mark, is shown in Figure 5.11. You still have to write the code to enforce the mutually exclu-
sive nature of the selection by turning the checkmark off for one menu item while turning it on
for another.
FIGURE 5.11
Checking mutually exclusive menu items.
fileMenu.MenuItems.Add(“&Close”)
fileMenu.MenuItems.Add(“E&xit”)
editMenu = .MenuItems.Add(“&Edit”)
windowMenu = .MenuItems.Add(“&Window”)
helpMenu = .MenuItems.Add(“&Help”)
End With
Working with the .NET Namespaces
114
PART II
FIGURE 5.12
Setting a default menu item.
Merging Menus
The menu items contained in one menu can be programmatically merged with the menu items
in another menu. Context menus, for instance, may have items in them that are mere duplicates
of items already defined inside the form’s main menu. Rather than recode all those items, you
can use the MenuItem class to simply merge the items from the main menu into the context
menu, via the MenuItem.MergeMenu method.
There are a few types of possible merges. The MenuMerge enumeration (see Table 5.9) classi-
fies these merges; this enumeration is used with the MenuItem.MergeType property to specify,
for each menu item if needed, exactly how it should be merged when the MergeMenu method is
called. To merge menus, you need to first set the MergeType property appropriately for each
menu item that is supposed to take part in the merge. You can even control in what order the
items are merged, through the MenuItem.MergeOrder property (an integer value). The lower
the MergeOrder value, the higher up in the merge menu the item will appear.
The sender object is not new; it is a standard object used with events, and it represents an
instance of the object that fired the event. DrawItemEventArgs is the key here. It is a class,
contained in the Windows.Forms namespace, that essentially encapsulates a bunch of proper-
ties that can be set to alter how the menu is drawn. Table 5.10 shows the properties exposed by
the DrawItemEventArgs class and their description.
Specifies the state of the menu item being drawn. State is defined with the
AND CONTROLS
State
DrawItemState enumeration.
Working with the .NET Namespaces
116
PART II
If you wanted to draw your own menu items, you would first write a handler for the DrawItem
event for the particular MenuItem object. It would look something like this:
Private Sub OwnerDraw_DrawItem(ByVal sender As System.Object, _
ByVal e As DrawItemEventArgs) Handles MenuItem2.DrawItem
Dim rect As New Rectangle(0, 0, 16, 16)
Dim i As New Icon(“sampleicon.ico”)
e.Graphics.DrawIcon(i, rect)
End Sub
In this example, the drawing of MenuItem2 is handled through raw code. To draw an icon onto
the menu, you use an instance of the Rectangle class and the Icon class, in conjunction with
the DrawItemEventArgs.Graphics class instance that is passed into the event.
NOTE
If you plan to create owner-drawn menus, remember that it is an all-or-nothing
proposition. If you set the OwnerDraw property to true, you are telling the compiler
and the runtime that you will handle all the drawing for that menu: background,
text, colors, positioning, and so on. You do not have the ability to paint some of the
menu and then let the runtime do the rest.
Now, you can stub out a routine that will fire whenever the click event fires. Here is an
example:
Protected Sub fileMenuClose_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles fileMenuClose.Click
End Sub
The key to this routine is the Handles fileMenuClose.Click syntax, which informs the com-
piler that the routine will be the “sink”, or recipient, of the click event for the particular object
that is referenced—in this case, the fileMenuClose object.
The second way to react to the selection of a menu item is by supplying your own event han-
dler. An event handler is a delegate or function pointer that acts as a sink for an event. In this
case, the event is the selection, either by direct selection of a menu item with the keyboard or
mouse or through a shortcut key or through a mnemonic.
Remember the MenuItems.Add method? In this case, you overload it to accept an event handler
as well as the name of the new menu item. Where you previously used this:
fileMenu.MenuItems.Add(“&Open...”)
Assigning event handlers to menu items has the same effect as using the click event; you are
simply instructing the compiler to execute a specific block of code whenever a menu item is
Working with the .NET Namespaces
118
PART II
selected. There are, of course, syntactical differences between the two. We have already seen
one, with the new use of the Add method on MenuItems. The routine that handles the event has
a slightly different signature as well. Because you are not shadowing a system-defined event,
there is no need to use the Handles keyword. Continuing from the preceding snippet, if you
wrote a routine called myEvent to handle the File, Open selection, it would look like this:
Protected Sub myEvent(ByVal sender As System.Object, _
ByVal e As System.EventArgs)
MsgBox(“File->Open was selected!”)
End Sub
For the most part, using the click event works just fine. There are a few situations, however, in
which you should use your own event handler. If you need to react to a parent menu item being
selected, you have to use your own event handler because the click event does not fire for
those items. Also, if you want to write one routine that multiple menu items use, you can do
this quite easily with the event handler by just pointing different menu items at the same block
of code.
Next, you can write a simple stub routine that executes when the Popup event is fired:
Protected Sub PopupEventHandler(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles appContextMenu.Popup
End Sub
You use the sender object to determine which control fired the event, and then you can react
accordingly.
Forms, Menus, and Controls
119
CHAPTER 5
Say you have one text box control and one rich text box control on a form. You would like to
implement a context menu with each. It makes sense to implement Cut, Copy, and Paste com-
mands for both. For the rich text box, you want to add an additional item to the context menu
that exploits the ability of RichTextBox objects to load files. In this case, you can implement
some branching logic inside the Popup event to determine whether the control that launches the
menu is the rich text box and, if it is, to add the special-case Load File item. Listing 5.3 shows
a sample application with a text box (TextBox1) and a rich text box (RichTextBox1). In the
Popup event handler, you determine which control causes the context menu to display. If it is
the text box, you show a standard Edit menu, with Cut, Copy, and Paste commands. If it is the
rich text box, in addition to the standard Edit menu commands, you add a Load File item.
(Notice that this example leverages the Clipboard class that we talked about earlier in this
chapter, to implement the code behind the Cut, Copy, and Paste menu items.)
LISTING 5.3 Using the Popup Event and the ContextMenu Class
Public Class Form2
Inherits System.Windows.Forms.Form
End Sub
End If
MyBase.Dispose(disposing)
End Sub
Friend WithEvents TextBox1 As System.Windows.Forms.TextBox
Friend WithEvents RichTextBox1 As System.Windows.Forms.RichTextBox
Working with the .NET Namespaces
120
PART II
End Sub
#End Region
TextBox1.ContextMenu = appContextMenu
RichTextBox1.ContextMenu = appContextMenu
End Sub
End Sub
With appContextMenu
currItem = .MenuItems.Add(“Cu&t”, New EventHandler(AddressOf _
menuCut_Click))
currItem.Shortcut = Shortcut.CtrlX
currItem = .MenuItems.Add(“&Copy”, New EventHandler(AddressOf _
menuCopy_Click))
currItem.Shortcut = Shortcut.CtrlC
currItem = .MenuItems.Add(“&Paste”, New EventHandler(AddressOf _
menuPaste_Click))
currItem.Shortcut = Shortcut.CtrlV
End With
End If
End Sub
‘When closed, the dialog will have the selected file name
‘and path in its FileName property
Dim filePath As String = openFile.FileName()
‘If the FileName prop was empty (as can happen if the cancel
‘button is selected instead of the OK button), we just ignore
‘it; otherwise, the file name and path are passed into the
‘LoadFile method on the RichTextBox1 control. This should
‘load the file into the rich text box.
If Trim(filePath) <> “” Then
RichTextBox1.LoadFile(openFile.FileName())
End If
End Sub
End Class
5
FORMS, MENUS,
AND CONTROLS
FIGURE 5.13
A dynamic context menu.
FIGURE 5.14
A selected menu item.
NOTE
One popular use of the Select event is to display help text in a form’s status bar to
indicate the exact use of the menu item that is highlighted.
An Introduction to Controls
As mentioned earlier in the chapter, we will not dig into the details of the myriad controls that
ship with the .NET Framework. However, it is useful to talk about some generic control con-
cepts that apply to a variety of programming tasks in the .NET Framework.
Forms, Menus, and Controls
125
CHAPTER 5
When working with the .NET Framework controls, such as the listbox, treeview, and button
controls, programmers appreciate that they all expose a common and standard set of properties.
Gone are the days of setting a label control’s Caption property; if you need to alter the text on
a control, whether it is a command button or label, you set the Text property.
The .NET Framework also improves on the programmer’s ability to create a custom control.
These user controls can inherit from the rich class tree that supports all the standard controls in
the .NET Framework. This leaves the door wide open for developers to implement the controls
that they want by either implementing a standard control, inheriting and extending a standard
control, or implementing a control that diverges widely from existing functionality. In this sec-
tion, we’ll review some of the basics of programming with controls and talk about how to cre-
ate your own user controls.
Property Description
AllowDrop A Boolean property that indicates whether the control is capable of
receiving context from a drag-and-drop operation.
Anchor Specifies which sides of the control, if any, are anchored to the edges
of its parent container (from the AnchorStyle enumeration).
BackColor A color instance that indicates the background color of the control.
BackgroundImage An image instance that indicates the background image of the
control.
CausesValidation A Boolean that indicates whether the control causes validation.
ContextMenu Specifies a ContextMenu instance associated with the control.
Controls A collection (Control.ControlCollection) that represents all of the
controls contained within the control.
Cursor Displays when the mouse pointer is within the bounds of the control.
5
FORMS, MENUS,
AND CONTROLS
Dock A DockStyle value that indicates which sides of the parent container
the control is docked to.
Enabled A Boolean property that indicates whether the control is enabled.
HasChildren A Boolean property that indicates whether the control has child
controls.
Working with the .NET Namespaces
126
PART II
Likewise, you can get a good picture of a control’s functionality by examining its methods and
events. Table 5.12 shows some of the most important methods available in the Control class,
and Table 5.13 shows a sampling of the supported events. (To see the entire list of possible
members, refer to the .NET Framework documentation on the class libraries.)
Method Description
BringToFront Causes the control’s z-order to change to the front.
CreateControl Manually forces the control to be created.
CreateGraphics Creates the Graphics object associated with the control; encap-
sulates the control’s brush, font, foreground, and background
colors.
DoDragDrop Starts a drag-and-drop process.
FindForm Returns an instance of the form in which the control is contained.
Focus Places the focus on the control.
GetChildAtPoint Given a set of coordinates, returns a control instance that repre-
sents the child control found there (if any).
Forms, Menus, and Controls
127
CHAPTER 5
Event Description
BackColorChanged Fires when the BackColor property has been changed.
BackgroundImageChanged Fires when the BackgroundImage property has been changed.
Click Fires when the control is clicked.
ContextMenuChanged Fires when the control’s ContextMenu property has been
changed.
ControlAdded Fires when a new control is added.
ControlRemoved Fires when a control is removed.
DockChanged Fires when the control’s Dock property has been changed.
DoubleClick Fires when the control is double-clicked.
DragDrop Fires when a drag-and-drop event is completed.
DragEnter Fires when something is dragged into the interior of the con-
trol’s region.
DragLeave Fires when something that has been dragged into a control is
subsequently dragged out of the control’s region.
GotFocus Fires when the control receives the focus. 5
KeyDown Fires when a key is pressed down while the control has focus.
FORMS, MENUS,
AND CONTROLS
KeyPress Fires when a key is pressed while the control has focus.
KeyUp Fires when a key is released while the control has focus.
LostFocus Fires when the focus shifts from this control to another
control.
Working with the .NET Namespaces
128
PART II
Figure 5.15 shows a simple form with two command buttons. The OK button is anchored to
the top and to the left, and the Cancel button is anchored to the right and to the bottom.
FIGURE 5.15
Anchored controls before resizing a form.
After resizing the form, as shown in Figure 5.16, you can see what happens to the buttons.
FIGURE 5.16
Anchored controls after resizing a form.
Anchoring a control doesn’t just cause its relative position to change; it can also change the
size of the control. Continuing the example with the OK and Cancel buttons, if we elect to
anchor the OK button to the bottom as well as to the top and left, the OK button resizes itself
to remain true to its anchors, as shown in Figure 5.17.
FIGURE 5.17
Control resizing based on anchors.
By using the Or operator, you can specify multiple anchors at a time with the AnchorStyles
enumeration.
Working with the .NET Namespaces
130
PART II
NOTE
Some controls won’t resize, regardless of the anchors specified. The TextBox control,
for instance, relies on its font setup to dictate the possible sizes it can change to; for
instance, it does not allow partial lines of text to appear, so it must be resized in mul-
tiples of the font size.
Docking Controls
The Control class provides support for docking controls. Similar in concept to anchoring a
control, docking a control causes it to anchor to a section of the form and then be resized to fit
that section of the parent form. Docking is traditionally used with multipaned applications such
as Windows Explorer. In Windows Explorer, the treeview control of files and objects appears
on the left, with actual folder content on the right. This treeview control is docked to the left of
the window. Toolbars are another item that is commonly docked in an application.
Figures 5.18 and 5.19 show a form with a treeview, first docked to the left and then docked to
the top.
FIGURE 5.18
A treeview docked to the left.
The code to implement this is also similar to the code used with the Anchor property:
Me.TreeView1.Dock = System.Windows.Forms.DockStyle.Top
In the case of docking, you use the DockStyle enumeration, whose values are listed in Table
5.15.
Forms, Menus, and Controls
131
CHAPTER 5
FIGURE 5.19
A treeview docked to the top.
Name Description
Bottom Docks the control to the bottom edge of the form.
Fill Docks the control to all sides of the form; the control expands to always fill the
form’s interior client area upon resizing.
Left Docks the control to the left edge of the form.
None Removes any docking attributes from the control.
Right Docks the control to the right edge of the form.
Top Docks the control to the top edge of the form.
that might be used by an ActiveX control, which allows you to specify such things as the con-
trol’s background color, cursor, font, and tab order. To fully leverage an ActiveX control,
Working with the .NET Namespaces
132
PART II
however, you have to custom-code the properties and methods that you need. This means that
the typical usage for the AxHost class is as a base class from which you inherit to derive some
basic operations.
Form
X ActiveX
Control
AxHost Wrapper
Form
ActiveX
Control
FIGURE 5.20
The AxHost wrapper.
After the wrapper assemblies have been created, you can use the ActiveX control on forms by
referencing its runtime callable wrapper from the project. Microsoft has distributed a tool with
the .NET Framework SDK—called Aximp.exe—that automatically creates a set of wrapper
assemblies for a given ActiveX control. (Full details on the operation of Aximp.exe can be
found in the .NET Framework MSDN documentation.)
If you need a control that cannot benefit directly from any of the existing controls, you should
create a completely custom control. Otherwise, inheriting from the UserControl class allows
you to create a composite control; in that case, you can pick and choose from the entire control
palette and select pieces of control functionality to include in your control.
From this point on, the code you write depends on exactly what you want the control to do.
You implement your custom code by overriding the existing properties and methods of the
control class and adding the appropriate logic to customize your control’s behavior. Of course,
you can also add new properties, methods, and events.
MyBase.OnPaint(pe)
End Sub
Working with the .NET Namespaces
134
PART II
This code snippet simply references the ClientRectangle structure (a property that is defined
on the base Control class) to instruct the paint event to draw the words “Hello, world!” using
the entire available control surface.
.
.
.
Get
Return orientText
End Get
End Property
End Class
Adding a method is just as easy. You simply create a new public method and include logic
inside it to affect the control in some way:
Public Sub RestoreDefault()
orientText = “Horizontal”
End Sub
Adding an event is a little more involved, but it is still quite approachable. The first step is to
declare the event:
Public Event OrientationChanged(ByVal sender As Object, _
ByVal ev As EventArgs)
Then, you need to create the subroutine that handles the event (by convention, such subroutines
start with On, as in OnOrientationChanged):
Forms, Menus, and Controls
135
CHAPTER 5
The final step is to actually write the code that raises the event:
Public Property Orientation() As String
Get
Return orientText
End Get
End Property
Next, the control is initialized and positioned on the form, inside the form’s
InitializeComponents subroutine:
‘TextControl
Me.TestControl.Location = New System.Drawing.Point(44, 80)
Me.TestControl.Name = “TextControl1”
Me.TestControl.Size = New System.Drawing.Size(156, 20)
Me.TestControl.TabIndex = 0
That is all that is necessary to get the base instance of the control created and placed on a
form.
trols. This listing includes the code fragments discussed in the preceding section, and it
includes an enumeration called OrientationMode, to help ease get/set operations with the
Orientation property. When this control, called CustomControl, is included on a form, you
Working with the .NET Namespaces
136
PART II
should be able to change the orientation of the displayed text from a horizontal to a vertical
position and back again. Because you are raising an OnOrientationChange event, which in
turn invalidates the control and forces a repaint, the effect on the text should be immediate.
After being compiled, the custom control is referenced inside the CustomControlLibrary
assembly.
Case OrientationMode.Vertical
Dim textDirection As StringFormat = New StringFormat()
‘Paint the Text property on the control; note the use of the
‘textDirection instance to specify vertical text
pe.Graphics.DrawString(TEXT_STRING, Font, _
New SolidBrush(ForeColor), rect, textDirection)
End Select
End Sub
Get
AND CONTROLS
Return orientText
End Get
Working with the .NET Namespaces
138
PART II
orientText = Value
End Set
End Property
End Class
Listing 5.5 shows the code for one possible form-based consumer of the CustomControl.
End Sub
Me.TextControl.TabIndex = 0
AND CONTROLS
‘
‘RadioButton2
‘
Me.RadioButton2.Location = New System.Drawing.Point(204, 36)
Working with the .NET Namespaces
140
PART II
End Sub
#End Region
End Sub
End Class
FIGURE 5.21
The TextControl custom control.
Then, for each control that will become part of the composite control, you add a reference. The
following example continues with the scenario of creating a login control that is created from
two textbox controls, two label controls, and a button control:
Friend WithEvents TextBox1 As System.Windows.Forms.TextBox
Friend WithEvents Label1 As System.Windows.Forms.Label
Friend WithEvents Label2 As System.Windows.Forms.Label
Friend WithEvents TextBox2 As System.Windows.Forms.TextBox
Friend WithEvents Button1 As System.Windows.Forms.Button
Next, you add these controls to the overall UserControl container by calling the AddRange 5
method:
FORMS, MENUS,
AND CONTROLS
This takes care of adding the constituent controls to the composite control’s control collection.
Composite controls, at this point, start to follow the design pattern for custom controls, with
code being added to handle events, properties, and methods. The sample application shown in
the following section demonstrates creating and consuming a composite control.
FIGURE 5.22
The EventLogControl composite control.
Code Walkthrough
LISTING 5.6 Code for the EventLogControl Object
All composite controls should inherit from UserControl. This gives you automatic support for
all the base Control properties, events, and methods, and ensures that the new control will
function seamlessly in the IDE.
Here you can see where we set up an enumeration, LogType, for specifying the event log to
view. We also publish an event, LogSourceChanged, which is fired whenever the log is changed
on the control.
Public Class EventLogControl
Inherits System.Windows.Forms.UserControl
ByVal ev As EventArgs)
Working with the .NET Namespaces
144
PART II
So that the new control is easy to work with in a drag-and-drop environment such as Visual
Studio .NET, you anchor the listbox, button, and label controls to allow for easy, dynamic
resizing at design time.
‘Anchor the listbox and button controls to enable easy
‘resizing
ListEvents.Anchor = AnchorStyles.Left Or AnchorStyles.Top _
Or AnchorStyles.Right Or AnchorStyles.Bottom
‘ButtonRefresh
‘
Me.ButtonRefresh.Location = New System.Drawing.Point(288, 280)
Me.ButtonRefresh.Name = “ButtonRefresh”
Working with the .NET Namespaces
146
PART II
End Sub
#End Region
This is the class property, which allows users to select a log to view. Note that you raise the
LogSourceChanged event here.
‘Raise an event
OnLogSourceChanged(New EventArgs())
End Set
End Property
ListEntries(EventLog1.Entries)
‘Invalidate()
RaiseEvent LogSourceChanged(Me, e)
End Sub
The ListEntries routine is used internally by the control to actually enumerate the
EventLogEntry objects contained in the EventLogEntryCollection object. All this is first set
up by specifying the log.
Private Sub ListEntries(ByVal entriesCol As EventLogEntryCollection)
Dim entry As EventLogEntry
count = 0
Next
End Sub
Private Sub EventLogControl_Load(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles MyBase.Load
End Sub 5
FORMS, MENUS,
AND CONTROLS
End Sub
‘
‘fileMenuLog
‘
Me.fileMenuLog.Index = 0
Me.fileMenuLog.MenuItems.AddRange(New _
System.Windows.Forms.MenuItem() _
Working with the .NET Namespaces
150
PART II
End Sub
Forms, Menus, and Controls
151
CHAPTER 5
#End Region
End Sub
Summary
In this chapter, we have looked at the specific classes in the System.Windows.Forms name-
space that can be used to quickly and efficiently assemble the user interface pieces of an appli- 5
cation. By now, you should have a firm grasp of the following:
FORMS, MENUS,
AND CONTROLS
• Responding to events
• Working with classes that inherit from the Control class
• Creating your own controls
The Visual Studio .NET environment automates and simplifies a lot of the programming activi-
ties discussed in this chapter. You should investigate those capabilities so that you can avoid
having to write all the code yourself. This chapter should give you a much greater appreciation
and understanding for the underlying classes that make the .NET Framework so useful.
Font, Text, and Printing CHAPTER
6
Operations
IN THIS CHAPTER
• Key Classes Related to Font, Text, and Printing
Operations 154
• Fonts 156
• Printing 179
Font, printing, and text manipulation together represent one of the more complex parts of the
Windows operating system. Fortunately, the .NET Framework Class Library simplifies this for
all of us. The classes presented in this chapter should give you enough insight into this technol-
ogy to become very productive, very quickly.
The chapter focuses primarily on the namespaces System.Drawing.Printing,
System.Drawing.Text, and to some extent on System.Drawing. Fonts are presented first, fol-
lowed by a simple, Notepad-like sample application. We then discuss printing with the library
and extend the font sample application by adding printing capabilities.
After reading this chapter, you will be able to
• Work with individual fonts and font families
• Retrieve all fonts installed on a system
• Modify text as output to the screen using the Font class and its associated members
• Send output to a print device
• Control printing, including paper source, page orientation, and margins
• Execute print preview functionality
OPERATIONS
PRINTING
Working with Fonts and Text
System.Drawing
Font The Font class enables you to define a specific format for
text, including the following: font face, size, and style
attributes.
FontFamily The FontFamily class abstracts a group of typefaces hav-
ing a similar design but a certain variation in style.
System.Drawing.Text
InstalledFontsCollection With the InstalledFontsCollection, you can reference
all the fonts installed on a specific system.
PrivateFontsCollection The PrivateFontsCollection method enables you to
create and add custom fonts into memory for use by your
application.
Printing
System.Drawing.Printing
Margins The Margins class enables you to manipulate the size of
the margins (the space surrounding the text) of a printed
page.
PageSettings The PageSettings class enables you to specify print set-
tings for one specific page.
PaperSize The PaperSize class lets you represent the size of piece
of paper.
PaperSource The PaperSource class enables you to choose the paper
tray from which the printer gets its paper for printing a
given document.
PreviewPageInfo The PreviewPageInfo class enables you to specify print
preview information for a single page.
PreviewPrintController The PreviewPrintController class enables you to dis-
play a document as a series of images prior to printing.
PrintDocument The PrintDocument class enables you to control output to
a given printer.
PrinterSettings The PrinterSettings class enables you to set various
properties of the printer and thus control how documents
are printed.
Working with the .NET Namespaces
156
PART II
Fonts
A font describes the way a string of text appears on a device—in most cases, a monitor or a
printer. Fonts can vary in size, weight, and style. Bold fonts, for instance, are said to have a
heavier weight than normal fonts. Windows automatically installs a number of standard fonts;
literally thousands of fonts are available to users.
Fonts are known by their typeface name and attributes. For example, Courier Bold 12 point is
a common fixed-pitch, or monospaced, font. Typically, nonscalable fonts actually demand a
new font for each attribute change. For example, if you modify the point size of a font or
change its characteristics to bold, italic, or underline, Windows accesses a physically different
font for each version.
Font, Text, and Printing Operations
157
CHAPTER 6
In many cases, however, Windows can synthesize one font from another. For example, 6
Windows can usually do a good job of creating an underlined font from a normal font, so you
OPERATIONS
PRINTING
occurs. For example, when Windows scales a raster font from a small size to a very large one,
the result can be truly ugly because slight imperfections in a letter’s form become pronounced
as the letter increases in size.
A font family describes a general class of font. In Windows, the term family is used to describe
classifications of fonts, and the terms typeface or facename are used to identify a set of fonts
that share a common character set and design but vary in attributes such as size, weight, slant,
and so on. Every font used by Windows falls into one font-family category.
A font is really a tool that takes a character as input and enables you to determine how that
character should be displayed in a given device context. The .NET Font and FontFamily
classes encapsulate and simplify the use of fonts. With a few simple objects, you can write
some very useful code to enable users to interact with text.
Font Attributes
When working with fonts, it is important to first understand some of the basic characteristics
and dimensions of a font and to define some of the terms used in describing these attributes.
To create fonts, Windows uses various font technologies, each with different advantages and
disadvantages. The following describes the three key font technologies in Windows:
• Raster fonts—A raster font is a series of character bitmaps. When a character needs to
be displayed, the bitmap for the character is copied to the device. The advantage is that
fonts can be optimized to look good on the device for which they are created. The disad-
vantage is that the font is not easily scalable.
• Vector (or stroke) fonts—Vector fonts are made up of graphical elements represented by
GDI function calls. Because they are represented mathematically, they can be scaled eas-
ily with good results. For the most part, vector fonts have been replaced with TrueType
fonts.
• Scalable fonts—Scalable fonts describe their characters mathematically using vectors
and curves. These are the TrueType fonts built in to Windows. These fonts can be scaled
to virtually any size without loss in quality. The disadvantages are at small sizes, they do
not look as good as raster fonts, and they are somewhat slower to draw (not a big deal
with today’s horsepower).
A font’s pitch can be either fixed or variable. In a fixed-pitch font, the width of each character
cell is equal. In a variable-pitch font, the spacing varies depending on the character. For exam-
ple, the letter “I” is narrower than the letter “W.” In fact, a simple character cell actually has a
Working with the .NET Namespaces
158
PART II
great many attributes and characteristics. (It is beyond the scope of this chapter to delve much
further into font dimensions, because our focus is on writing productive code.)
Font Classes
In .NET, the Font class encapsulates a font and is found in the System.Drawing namespace. At
first, this sounds strange. Why would a Font class be in a drawing namespace? The answer is
quite simple: Fonts, like everything else in the user interface, have to be drawn to the display.
In fact, the drawing functions are used to render fonts to a device or drawing surface.
Text is defined by font face, size, and style attributes. The following is a simple example of
how to manipulate the various attributes of a font at runtime. The code assumes a label control
(labelExample) has been placed on a form. We then create a Font instance based on a font-
family name, size, and a font style (here we use bold). Then, because .NET uses the same
libraries throughout, we simply set the Font property of the label control to equal our new
Font instance. The results are that the text inside the label control is now displayed using our
new Font instance.
‘create a new instance of the font object
Dim myFont as New Font(familyName:=”Tahoma”, emSize:=18, _
style:=FontStyle.Bold, unit:=GraphicsUnit.Point)
To create an instance of a font, we can choose from a variety of constructors in the .NET tool-
box. Table 6.2 lists these constructors and provides a description of each. You can see that the
table is a little long, but it does provide all the right combinations. Most of the constructors are
variations on a theme.
PRINTING
emSize(Single): The size of the new font.
Note: Use this constructor to create a new Font object directly from a FontFamily name
and a specific size.
New Font (ByVal family as FontFamily, ByVal emSize as Single, Byval style as
FontStyle)
family(FontFamily): A valid FontFamily object.
emSize(Single): The size of the new font.
Style(FontStyle); A valid FontStyle enumeration member (Bold, Italic, and so on).
Note: Use this constructor to create a new Font object from a FontFamily object, a specific
size, and specific style. You can get FontStyle and FontFamily objects in a variety of
ways, including straight from a string (see example code).
New Font (ByVal family as FontFamily, ByVal emSize as Single, ByVal unit as
GraphicsUnit)
family(FontFamily): A valid FontFamily object.
emSize(Single): The size of the new font.
unit(GraphicsUnit): A valid GraphicsUnit enumeration member (Point, Pixel, Inch, and
so on).
Note: Use this constructor to create a new Font object from a FontFamily object, a specific
size, and a GraphicsUnit object. The GraphicsUnit value indicates how the font size is
calculated.
New Font (ByVal familyName as String, ByVal emSize as Single, ByVal style as
FontStyle)
familyName(String): A string that represents a FontFamily. For example, “Tahoma.”
emSize(Single): The size of the new font.
Style(FontStyle): A valid FontStyle enumeration member (Bold, Italic, and so on).
Note: Use this constructor to create a new Font object directly from a FontFamily name, a
specific size, and a specific style (Bold, Italics, and so on).
New Font (ByVal familyName as String, ByVal emSize as Single, ByVal unit as
GraphicsUnit)
familyName(String): A string that represents a FontFamily. For example, “Tahoma.”
emSize(Single): The size of the new font.
unit(GraphicsUnit): A valid GraphicsUnit enumeration member (Point, Pixel, Inch, and
so on).
Note: Use this constructor to create a new Font object directly from a FontFamily name, a
specific size, and a GraphicsUnit object. The GraphicsUnit value indicates how the font
size is calculated.
Working with the .NET Namespaces
160
PART II
PRINTING
specific size, a FontStyle, a GraphicsUnit object, and a gdiCharSet.
New Font (ByVal family as FontFamily, ByVal emSize as Single, ByVal style as
FontStyle, ByVal unit as GraphicsUnit, ByVal gdiCharSet as Byte,
gdiVerticalFont as Boolean)
family(FontFamily): A valid FontFamily object.
emSize(Single): The size of the new font.
Style(FontStyle): A valid FontStyle enumeration member (Bold, Italic, and so on).
unit(GraphicsUnit): A valid GraphicsUnit enumeration member (Point, Pixel, Inch, and
so on).
gdiCharSet(Byte): A GDI character set value found in WinGDI.h.
gdiVerticalFont(Boolean): Indicates if the font is derived from a GDI vertical font.
Note: Use this constructor to create a new font object directly from a FontFamily, a specific
size, a FontStyle, a GraphicsUnit object, a gdiCharSet value, and a indication of
gdiVerticalFont.
New Font (ByVal familyName as String, ByVal emSize as Single, ByVal style as
FontStyle, ByVal unit as GraphicsUnit, ByVal gdiCharSet as Byte,
gdiVerticalFont as Boolean)
familyName(String): A string that represents a FontFamily. For example, “Tahoma.”
family(FontFamily): A valid FontFamily object.
emSize(Single): The size of the new font.
Style(FontStyle): A valid FontStyle enumeration member (Bold, Italic, and so on).
unit(GraphicsUnit): A valid GraphicsUnit enumeration member (Point, Pixel, Inch, and
so on).
gdiCharSet(Byte): A GDI character set value found in WinGDI.h.
gdiVerticalFont(Boolean): Indicates if the font is derived from a GDI vertical font.
Note: Use this constructor to create a new font object directly from a FontFamily name, a
specific size, a FontStyle, a GraphicsUnit object, a gdiCharSet value, and an indication
of gdiVerticalFont.
When creating a font instance, the FontStyle enumeration allows us to indicate a standard for-
mat for the text. Table 6.3 displays the members of the FontStyle enumeration. A text exam-
ple is provided alongside each enumeration member. Note that the font family used in the
examples is Times New Roman.
Working with the .NET Namespaces
162
PART II
Font Collections
We often need to work with fonts as a group, sometimes to display all the installed fonts on a
system to the user for selection or to output a document’s used fonts to a dialog box. To work
with groups of fonts, we use the System.Drawing.Text namespace. This namespace exposes
to us two key classes: InstalledFontCollection and PrivateFontCollection. These classes
enable us to create and use collections of fonts.
The InstalledFontCollection class behaves as its name indicates; it returns a collection of
FontFamily objects that represent the fonts installed on a given system. The following code
creates an instance of the collection, loops through it, and adds the font-family names to a list
box control (listBox1).
‘local scope
Dim myFonts As System.Drawing.Text.InstalledFontCollection
Dim i As Integer
‘loop through the font families and add to the list box
For i = 1 To UBound(myFonts.Families)
listBox1().Items.Add(myFonts.Families(i).Name)
Next
Private Fonts
The PrivateFontCollection allows us to install a private version of an existing font without
replacing the system version of the font. For example, we could create a private version of the
Arial font in addition to the Arial font that the system uses. The PrivateFontCollection can
also be used to install fonts that don’t exist on a system. This is a temporary font installation
that doesn’t affect the system-installed collection. This is great when you want to use custom
fonts in your application but not install them onto users’ machines.
Font, Text, and Printing Operations
163
CHAPTER 6
Listing 6.1 provides an example of how to load a font from a file into the private font’s collec- 6
tion and then use that font.
‘local scope
Dim myPrivateFonts As System.Drawing.Text.PrivateFontCollection
Dim myFont As Font
‘create of font object from the font family in the private collection
myFont = New Font(family:=myPrivateFonts.Families(0), emSize:=12, _
style:=FontStyle.Regular)
End Sub
Font Classifications
Fonts can be classified into what is called generic font families. These families are independent
of the font families we’ve been discussing. In .NET, a generic font family represents a higher-
level (or parent) category to which all fonts must belong. All fonts (or font families) belong to
one generic font family. Table 6.4 lists the GenericFontFamilies enumeration members and
provides a description of each.
Hotkey Prefix
A hotkey prefix enables users to use a keyboard sequence (usually CTRL+HotKey or
ALT+HotKey) to access functionality represented by text displayed on the screen. The
HotKeyPrefix enumeration stores the possible values for indicating how these keys should be
displayed to the user.
The HotKeyPrefix enumeration is used by the StringFormat class. The StringFormat class
specifies the Windows Forms string class format, which Windows Forms uses to store string
objects. Table 6.5 describes the enumeration’s members.
Member Description
Hide Tells the application not to display a specific hotkey.
None Indicates that there is no hotkey for a specific function.
Show Displays the hotkey prefix.
Text Rendering
A number of options are available to you for indicating how you want .NET to draw your text
to the screen. These options can be set using the TextRenderingHint enumeration. Options
range from the fast-performing but low-quality SingleBitPerPixel to the clearer but slower-
performing ClearType. This enumeration is used to set the TextRenderingHint property of a
Graphics instance used to output text to a screen. Table 6.6 presents a visual representation of
each member using a Bold, 18-point Tahoma font.
Font, Text, and Printing Operations
165
CHAPTER 6
AntiAliasGridFit
ClearTypeGridFit
SingleBitPerPixel
SingleBitPerPixelGridFit
SystemDefault
For text editing, we use the RichTextBox control. This control is set to size with the form. This
is done by setting its dock property to Fill.
Figure 6.1 shows an example of FontPad’s main form.
FIGURE 6.1
FontPad main form.
Code Walkthrough
The code behind the form is nearly as straightforward as the form itself. Listing 6.2 walks you
through the code.
The code listing starts by defining the form and its controls.
OPERATIONS
PRINTING
‘Form overrides dispose to clean up the component list.
Protected Overloads Overrides Sub Dispose(ByVal disposing As Boolean)
If disposing Then
If Not (components Is Nothing) Then
components.Dispose()
End If
End If
MyBase.Dispose(disposing)
End Sub
OPERATIONS
Me.Menu = Me.mainMenu1
PRINTING
Me.Text = “FontPad”
End Sub
This procedure gets called both when the form loads and when users apply changes to the
application’s font settings.
Public Sub resetRichTextBox()
‘local scope
Dim font As Font
Dim fontStyle As FontStyle
Dim fontFamily As FontFamily
Dim styleType As System.Type
‘get a font style object (note: must turn off option strict)
fontStyle = fontStyle.Parse(enumType:=styleType, _
value:=g_FontStyle)
End Sub
Working with the .NET Namespaces
170
PART II
End Sub
When users click the Font item on the Format menu, the following code executes to show the
Font Selection dialog box.
Private Sub menuItemFont_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles menuItemFont.Click
‘local scope
Dim dialogFont As FontSettings
End Sub
When users click the Exit item on the File menu, the application ends.
Private Sub menuItemExit_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles menuItemExit.Click
End Sub
Font, Text, and Printing Operations
171
CHAPTER 6
#End Region 6
OPERATIONS
PRINTING
FontPad Font Settings Dialog Box
The Font Settings dialog box is also similar in feel to that of Notepad’s. For example, the set-
tings apply to the application as a whole (this is a text editor and not a word processor). Users
can browse a list of font families, styles, and sizes (see Figure 6.2). As a user selection
changes, an example of the most recent selection is displayed inside the sample group box.
This gives users a visual cue before selecting a new font setting.
FIGURE 6.2
FontPad: Font Settings form (formFontSettings.vb).
Code Walkthrough
Again, .NET makes the code rather simple. Listing 6.3 represents the code behind the form.
The listing starts with the form and control setup code.
End Sub
OPERATIONS
Me.labelSample.TabIndex = 0
PRINTING
Me.labelSample.Text = “AaBbCcDd - WwXxYyZz”
Me.labelSample.TextAlign = System.Drawing.ContentAlignment.MiddleCenter
Me.listBoxSizes.Location = New System.Drawing.Point(320, 28)
Me.listBoxSizes.Size = New System.Drawing.Size(56, 95)
Me.listBoxSizes.TabIndex = 2
Me.buttonOk.Location = New System.Drawing.Point(384, 28)
Me.buttonOk.TabIndex = 3
Me.buttonOk.Text = “OK”
Me.buttonCancel.DialogResult = System.Windows.Forms.DialogResult.Cancel
Me.buttonCancel.Location = New System.Drawing.Point(384, 60)
Me.buttonCancel.TabIndex = 4
Me.buttonCancel.Text = “Cancel”
Me.listBoxFamilies.Location = New System.Drawing.Point(12, 28)
Me.listBoxFamilies.Size = New System.Drawing.Size(196, 95)
Me.listBoxFamilies.TabIndex = 0
Me.listBoxStyles.Location = New System.Drawing.Point(216, 28)
Me.listBoxStyles.Size = New System.Drawing.Size(96, 95)
Me.listBoxStyles.TabIndex = 1
Me.label2.Location = New System.Drawing.Point(216, 12)
Me.label2.Size = New System.Drawing.Size(120, 20)
Me.label2.TabIndex = 7
Me.label2.Text = “Font Style”
Me.label4.Location = New System.Drawing.Point(320, 12)
Me.label4.Size = New System.Drawing.Size(52, 20)
Me.label4.TabIndex = 9
Me.label4.Text = “Size”
Me.groupBox1.Controls.AddRange(New System.Windows.Forms.Control() _
{Me.labelSample})
Me.groupBox1.Location = New System.Drawing.Point(12, 136)
Me.groupBox1.Size = New System.Drawing.Size(364, 112)
Me.groupBox1.TabIndex = 5
Me.groupBox1.TabStop = False
Me.groupBox1.Text = “Sample”
Me.label3.Location = New System.Drawing.Point(12, 12)
Me.label3.Size = New System.Drawing.Size(120, 20)
Me.label3.TabIndex = 8
Me.label3.Text = “Font Family”
Me.AcceptButton = Me.buttonOk
Me.AutoScaleBaseSize = New System.Drawing.Size(5, 13)
Me.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog
Me.CancelButton = Me.buttonCancel
Me.ClientSize = New System.Drawing.Size(473, 262)
Working with the .NET Namespaces
174
PART II
End Sub
The form’s load event initializes all the controls on the form. It uses the
InstalledFontsCollection class to load the font-family list box (listBoxFamilies). The
font styles are loaded from the FontStyle enumeration member names using a method from
the Reflection classes. The size list box values are hard-coded from an array on the form
load.
Last, we set the selected items for each list box to be that of the application’s default values.
Notepad actually stores these user-configurable values between each use. We will leave this
code up to you—perhaps you could create an XML file for these settings. FontPad’s default
settings are stored in global variables.
Private Sub formFontSettings_Load(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles MyBase.Load
‘loop through the font families and add to the list box
For i = 1 To UBound(myFonts.Families)
listBoxFamilies().Items.Add(myFonts.Families(i).Name)
Next
PRINTING
listBoxStyles().Items.Add(myArray(i))
Next
End Sub
The next procedure, SetSampleText, is responsible for keeping the label control on the font
settings form in synch with user selections. Each list box’s index change event calls this sub-
routine. The code itself creates a Font instance from the values selected in the three list boxes
and applies this object to the sample label’s Font property.
Private Sub setSampleText()
‘local scope
Dim font As Font
Dim fontStyle As FontStyle
Dim fontFamily As FontFamily
Dim styleType As System.Type
Dim fontFamilyName As String
Dim fontSize As String
Dim fontStyleName As String
Else
End If
Else
End If
Else
End If
‘get a font style object (note: must turn off option strict)
fontStyle = fontStyle.Parse(enumType:=styleType, value:=fontStyleName)
emSize:=CSng(fontSize), _ 6
style:=fontStyle, _
OPERATIONS
PRINTING
‘reset the font on the label control
labelSample().Font = font
End Sub
When a user clicks the OK button, the global font values are updated and FontPad’s
ResetRichTextBox procedure is called, thus applying the new settings.
End Sub
When a user clicks the Cancel button, the form simply closes without applying any changes.
Private Sub buttonCancel_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles buttonCancel.Click
End Sub
The following are the change events for the various list boxes. Each simply makes a call to
SetSampleText to update the label control used as a visual cue.
End Sub
End Sub
End Sub
#End Region
End Class
FontPad Module
We use a standard module in the FontPad application to store global variables. Listing 6.4 rep-
resents our three font settings and a reference to the main FontPad form.
‘declare variables that have global scope & set the defaults
Public g_FontFamily As String = “Courier New”
Public g_FontStyle As String = “Regular”
Public g_FontSize As String = “10”
End Module
Font, Text, and Printing Operations
179
CHAPTER 6
Printing 6
OPERATIONS
PRINTING
control and/or the Win32 API. The former did not always provide enough flexibility and the
later sapped the productivity level to which VB developers have become accustomed.
System.Drawing.Printing to the rescue! This namespace provides us with a ton of flexibility
while encapsulating the low-level stuff that can often bog down programmers.
2. Set properties of the PrintDocument class to describe how and where to print. Of course,
in an actual application, we would not hard-code the printer’s name but would query the
system for the default printer instead.
‘tell the print document object the name of the printer
printDocument.PrinterSettings.PrinterName = _
“Epson Stylus COLOR 640 ESC/P 2”
3. Set an event handler to intercept calls from a delegate when a page is printed. We do this
by telling PrintDocument that we want to intercept its PrintPage event. This ensures
that the procedure PrintPage_handler will receive the PrintDocument’s PrintPage
event. This event gets fired for every page that needs to be printed.
AddHandler printDocument.PrintPage, AddressOf Me.printPage_handler
4. Call the Print method to print the document and raise the print event.
printDocument.Print()
Working with the .NET Namespaces
180
PART II
5. Define an event handler routine to process the printing. Note: The handler’s
PrintPageEventArgs passes the appropriate Graphics object used to send output to the
printer.
Private Sub printPage_handler(ByVal sender As Object, _
ByVal ev As System.Drawing.Printing.PrintPageEventArgs)
End Sub
Printer Configuration
The Printing namespace provides a number of enumerations that enable us to manage various
printer configuration settings. This section provides an overview of what is available. It is
arranged into a number of tables that describe the enumerations and their members.
Table 6.7 lists the Duplex enumeration members. This enables you to read and write the
printer’s duplex setting. Duplex, in printing, describes how printers print on both sides of the
paper. This enumeration provides values for the PrinterSettings class’s Duplex property. A
PrinterSettings instance is used to control how a printer is configured to send output.
PaperKind refers to the physical type of paper loaded into the printer. The enumeration is used
by the PaperSize class for its Kind property. PaperSize itself is used by the PrinterSettings
class when specifying the PaperSizes property. The PaperSizes property is a collection of
PaperSize objects that indicate the various sizes of paper the given printer supports. Table 6.8
lists some of the key PaperKind members most commonly used in the United States.
Font, Text, and Printing Operations
181
CHAPTER 6
PRINTING
Folio Standard folio paper (8.5 in. by 13 in.)
Ledger Standard ledger paper (17 in. by 11 in.)
Legal Standard legal paper (8.5 in. by 14 in.)
Letter Standard letter paper (8.5 in. by 11 in.)
Tabloid Standard tabloid paper (11 in. by 17 in.)
Table 6.9 documents the PaperSourceKind enumeration members. Paper sources can be
thought of as paper trays and the like on a physical printer. The enumeration is used by the
PaperSource class for its Kind property. This property is used to both return and to set the
source of the paper used when printing. PaperSource itself is used by the PrinterSettings
class when specifying the PaperSources property. The PaperSources property is a collection
of PaperSource objects that indicates the various paper sources a given printer supports. The
most common setting is AutomaticFeed, which tells most printers that they should handle the
source from where the paper comes.
Member Description
AutomaticFeed Indicates that the source of the paper is automatically fed paper.
Cassette Indicates that the source of the paper is a paper cassette.
Custom Indicates that the source of the paper is a printer-specific paper source.
Envelope Indicates that the source of the paper is an envelope.
FormSource Indicates that the source of the paper is the printer’s default input bin.
LargeCapacity Indicates that the source of the paper is the printer’s large-capacity bin.
LargeFormat Indicates that the source of the paper is large-format paper.
Lower Indicates that the source of the paper is the lower bin of a printer.
Note: If the printer has only one bin, it will be used.
Manual Indicates that the source of the paper is manually fed paper.
ManualFeed Indicates that the source of the paper is manually fed envelopes.
Middle Indicates that the source of the paper is the middle bin of a printer.
SmallFormat Indicates that the source of the paper is small-format paper.
TractorFeed Indicates that the source of the paper is a tractor feed.
Upper Indicates that the source of the paper is the upper bin of a printer.
Note: If the printer has only one bin, it will be used.
Working with the .NET Namespaces
182
PART II
Table 6.10 lists the various printer resolution settings that are available to your applications.
The PrinterResolutionKind enumeration members enable you to tell the printer to output
documents based on some standard resolutions. The enumeration is used by the
PrinterResoultion class for its Kind property. PrinterResoultion itself is used by the
PrinterSettings class when specifying the PrinterResolutions property. The
PrinterResolutions property is a collection of PrinterResolution objects that indicate the
various resolutions supported by a given printer. These properties are handy when users want a
quick version, or draft, of their documents for proofing, or a slower, but high-quality version
for release.
Member Description
Custom Specifies a custom printer resolution setting. If this is set to custom, use
the x and y properties of the PrinterResolution class to determine the
printer resolution in the horizontal and vertical directions, respectively.
Draft Specifies the draft printer resolution setting.
High Specifies the high printer resolution setting.
Low Specifies the low printer resolution setting.
Medium Specifies the medium printer resolution setting.
Table 6.11 lists the PrintRange enumeration members. The enumeration is used to represent
the portions of the document that should be output to the printer. This enumeration is used by
the PrinterSettings class to indicate the range that should be printed.
Member Description
AllPages Indicates that all pages in the document should be printed.
Selection Indicates that only the selected pages in the document should be printed.
SomePages Indicates that only some pages (those from x to y) in the document should
be printed. If using this value, reference the PrinterSettings class,
fromPage and toPage properties.
to the screen as a series of images. This class makes extensive use of the PreviewPageInfo 6
class. The PreviewPageInfo class represents the print preview information of one specific
OPERATIONS
PRINTING
application:
1. First, you declare a number of variables to represent the classes you will need.
Dim printDocument As System.Drawing.Printing.PrintDocument
Dim previewController As System.Drawing.Printing.PreviewPrintController
Dim pageInfoArray() As System.Drawing.Printing.PreviewPageInfo
Dim i As Integer
5. Next, indicate an event handler for each page that is output. Note: The event handler can
be identical to the one defined in the basic printing walkthrough. Perhaps one exception
is that you might want to add the line ev.Graphics.ScaleTransform(sx:=0.25,
sy:=0.25) to the printPage_handler routine. This code scales the output of the
Graphics object by 25%, which makes viewing print images a little easier.
AddHandler printDocument.PrintPage, AddressOf Me.printPage_handler
6. Now call the Print method of the PrintDocument instance. This raises a call to the event
handler and forces the printed content into the preview print controller.
printDocument.Print()
7. All that is left is to view the preview images. After the document is finished printing, the
PreviewPrintController instance that you set as the target of the print output is filled
with a collection of PreviewPageInfo objects. There is one item in the collection per
printed page. Return the images by calling the GetPreviewPageInfo method of the
PreviewPrintController object. You can then loop through the array and output each
image into a picture box defined on a form.
‘return an array of PreviewPageInfo objects from the print controller
pageInfoArray = previewController.GetPreviewPageInfo()
Code Walkthrough
The additional code behind each new menu item’s click event is in Listing 6.5.
‘local scope
Dim dialogPageSetup As PageSetup
End Sub
When the user fires the menuItemPrint_Click event, we get the text from the RichTextBox
control (printString = Me.richTextBox.Text). This gives us a text string to load into the
StreamReader class. The StreamReader will be used when we actually send output to the
printer. Of course, we then show the dialog box to the user.
Private Sub menuItemPrint_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles menuItemPrint.Click
‘local scope
Dim dialogPrint As Print
End Sub
Page Setup
The Page Setup dialog box enables users to select the paper size, the printer’s paper source, the
page orientation, and the page margins. Figure 6.3 shows the Page Setup dialog box. Note its
similarities to Notepad’s Page Setup dialog box.
Working with the .NET Namespaces
186
PART II
FIGURE 6.3
FontPad: Page Setup form (formPageSetup.vb).
Code Walkthrough
The code behind the Page Setup form can be read in Listing 6.6.
End Sub
PRINTING
Private WithEvents groupBox1 As System.Windows.Forms.GroupBox
Private WithEvents textBoxBottom As System.Windows.Forms.TextBox
Private WithEvents comboBoxSize As System.Windows.Forms.ComboBox
Private WithEvents buttonCancel As System.Windows.Forms.Button
Private WithEvents label4 As System.Windows.Forms.Label
Private WithEvents label5 As System.Windows.Forms.Label
Private WithEvents label6 As System.Windows.Forms.Label
Private WithEvents label7 As System.Windows.Forms.Label
Private WithEvents textBoxRight As System.Windows.Forms.TextBox
Private WithEvents label1 As System.Windows.Forms.Label
Private WithEvents label2 As System.Windows.Forms.Label
Private WithEvents label3 As System.Windows.Forms.Label
Private WithEvents textBoxTop As System.Windows.Forms.TextBox
Private WithEvents comboBoxSource As System.Windows.Forms.ComboBox
Private WithEvents buttonOk As System.Windows.Forms.Button
Private WithEvents textBoxLeft As System.Windows.Forms.TextBox
Private WithEvents radioButton1 As System.Windows.Forms.RadioButton
Private WithEvents radioButton2 As System.Windows.Forms.RadioButton
Private WithEvents radioButtonLandscape As System.Windows.Forms.RadioButton
Private WithEvents radioButtonPortrait As System.Windows.Forms.RadioButton
PRINTING
Me.label2.Size = New System.Drawing.Size(4, 0)
Me.label2.TabIndex = 1
Me.label2.Text = “label2”
‘
‘label1
‘
Me.label1.Location = New System.Drawing.Point(12, 24)
Me.label1.Name = “label1”
Me.label1.TabIndex = 0
Me.label1.Text = “Size”
‘
‘groupBox2
‘
Me.groupBox2.Controls.AddRange(New System.Windows.Forms.Control() _
{Me.radioButtonLandscape, Me.radioButtonPortrait})
Me.groupBox2.Location = New System.Drawing.Point(12, 108)
Me.groupBox2.Name = “groupBox2”
Me.groupBox2.Size = New System.Drawing.Size(108, 80)
Me.groupBox2.TabIndex = 1
Me.groupBox2.TabStop = False
Me.groupBox2.Text = “Orientation”
‘
‘radioButtonLandscape
‘
Me.radioButtonLandscape.Location = New System.Drawing.Point(12, 48)
Me.radioButtonLandscape.Name = “radioButtonLandscape”
Me.radioButtonLandscape.Size = New System.Drawing.Size(80, 24)
Me.radioButtonLandscape.TabIndex = 1
Me.radioButtonLandscape.Text = “Landscape”
‘
‘radioButtonPortrait
‘
Me.radioButtonPortrait.Location = New System.Drawing.Point(12, 24)
Me.radioButtonPortrait.Name = “radioButtonPortrait”
Me.radioButtonPortrait.Size = New System.Drawing.Size(60, 24)
Me.radioButtonPortrait.TabIndex = 0
Me.radioButtonPortrait.Text = “Portrait”
‘
‘groupBox3
‘
Me.groupBox3.Controls.AddRange(New System.Windows.Forms.Control() _
{Me.textBoxBottom, Me.textBoxRight, Me.textBoxTop, Me.textBoxLeft, _
Me.label7, Me.label6, Me.label5, Me.label4})
Working with the .NET Namespaces
190
PART II
PRINTING
Me.label6.Location = New System.Drawing.Point(112, 24)
Me.label6.Name = “label6”
Me.label6.TabIndex = 2
Me.label6.Text = “Right”
‘
‘label5
‘
Me.label5.Location = New System.Drawing.Point(12, 52)
Me.label5.Name = “label5”
Me.label5.TabIndex = 1
Me.label5.Text = “Top”
‘
‘label4
‘
Me.label4.Location = New System.Drawing.Point(12, 24)
Me.label4.Name = “label4”
Me.label4.TabIndex = 0
Me.label4.Text = “Left”
‘
‘buttonCancel
‘
Me.buttonCancel.DialogResult = System.Windows.Forms.DialogResult.Cancel
Me.buttonCancel.Location = New System.Drawing.Point(280, 200)
Me.buttonCancel.Name = “buttonCancel”
Me.buttonCancel.TabIndex = 4
Me.buttonCancel.Text = “Cancel”
‘
‘buttonOk
‘
Me.buttonOk.Location = New System.Drawing.Point(196, 200)
Me.buttonOk.Name = “buttonOk”
Me.buttonOk.TabIndex = 3
Me.buttonOk.Text = “OK”
‘
‘PageSetup
‘
Me.AcceptButton = Me.buttonOk
Me.AutoScaleBaseSize = New System.Drawing.Size(5, 13)
Me.CancelButton = Me.buttonCancel
Me.ClientSize = New System.Drawing.Size(367, 236)
Me.Controls.AddRange(New System.Windows.Forms.Control() _
{Me.buttonOk, Me.buttonCancel, Me.groupBox3, Me.groupBox2, Me.groupBox1})
Me.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog
Working with the .NET Namespaces
192
PART II
End Sub
#End Region
When users click the Cancel button, the form simply closes and no setup changes are made.
Private Sub buttonCancel_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles buttonCancel.Click
End Sub
Inside the OK button’s click event, we simply store the user-selected form values to our global
variables. This makes sure that these user-defined settings are available to us inside our Print
dialog box.
Private Sub buttonOk_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles buttonOk.Click
‘local scope
Dim isValid As Boolean
If isValid Then 6
OPERATIONS
PRINTING
‘set paper size and paper source
If comboBoxSize().SelectedIndex <> -1 Then
paperSize = comboBoxSize().SelectedItem.ToString
End If
If comboBoxSource().SelectedIndex <> -1 Then
paperSource = comboBoxSource().SelectedItem.ToString
End If
Else
End If
End Sub
Inside the form’s load event, we select the user’s current default settings. These variables are
declared in a global module. Among the settings is the user’s default printer. Inside this event,
we create a PrintDocument instance and set it to this printer. We then enumerate the
PaperSizes and PaperSources collections, adding their values to the associated combo boxes.
This ensures that the paper size and source is relevant to the current printer and allows users to
select appropriate values.
Private Sub PageSetup_Load(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles MyBase.Load
Working with the .NET Namespaces
194
PART II
‘local scope
Dim printDocument As System.Drawing.Printing.PrintDocument
Dim i As Integer
Dim paperSizes As _
System.Drawing.Printing.PrinterSettings.PaperSizeCollection
Dim paperSources As _
System.Drawing.Printing.PrinterSettings.PaperSourceCollection
Case “PORTRAIT”
radioButtonPortrait().Checked = True
Case Else
radioButtonLandscape().Checked = True
End Select
‘tell the print document object the name of the selected printer
printDocument.PrinterSettings.PrinterName = printerDefault
Next
PRINTING
‘select the user settings
comboBoxSize().SelectedItem = paperSize
comboBoxSource().SelectedItem = paperSource
‘close objects
printDocument.Dispose()
End Sub
End Class
FIGURE 6.4
FontPad: Print form (formPrint.vb).
Code Walkthrough
The code behind the print form is similar to our print example earlier in the chapter. Listing 6.7
provides the code for you to reference.
The code starts by setting up the form and its associated controls. Note that we declare an
instance of the PrintDocument class and the StringReader class at the form level. This
enables us to use these objects from within all the procedures in the form module.
Working with the .NET Namespaces
196
PART II
End Sub
PRINTING
Private WithEvents labelTo As System.Windows.Forms.Label
Private WithEvents labelFrom As System.Windows.Forms.Label
Private WithEvents checkBoxCollate As System.Windows.Forms.CheckBox
Private WithEvents labelColor As System.Windows.Forms.Label
PRINTING
Me.buttonCancel.Text = “Cancel”
‘
‘buttonOk
‘
Me.buttonOk.Location = New System.Drawing.Point(212, 220)
Me.buttonOk.Name = “buttonOk”
Me.buttonOk.TabIndex = 1
Me.buttonOk.Text = “OK”
‘
‘checkBoxCollate
‘
Me.checkBoxCollate.Checked = True
Me.checkBoxCollate.CheckState = System.Windows.Forms.CheckState.Checked
Me.checkBoxCollate.Location = New System.Drawing.Point(12, 80)
Me.checkBoxCollate.Name = “checkBoxCollate”
Me.checkBoxCollate.TabIndex = 2
Me.checkBoxCollate.Text = “Collate”
‘
‘groupBox1
‘
Me.groupBox1.Controls.AddRange(New System.Windows.Forms.Control() _
{Me.comboBoxPrinterName, Me.labelColor, Me.label2, Me.label1})
Me.groupBox1.Location = New System.Drawing.Point(8, 8)
Me.groupBox1.Name = “groupBox1”
Me.groupBox1.Size = New System.Drawing.Size(364, 76)
Me.groupBox1.TabIndex = 2
Me.groupBox1.TabStop = False
Me.groupBox1.Text = “Printer”
‘
‘comboBoxPrinterName
‘
Me.comboBoxPrinterName.DropDownWidth = 304
Me.comboBoxPrinterName.Location = New System.Drawing.Point(52, 20)
Me.comboBoxPrinterName.Name = “comboBoxPrinterName”
Me.comboBoxPrinterName.Size = New System.Drawing.Size(304, 21)
Me.comboBoxPrinterName.TabIndex = 7
‘
‘label2
‘
Me.label2.Location = New System.Drawing.Point(12, 48)
Me.label2.Name = “label2”
Me.label2.TabIndex = 1
Me.label2.Text = “Supports Color:”
‘
Working with the .NET Namespaces
200
PART II
PRINTING
Me.groupBox3.Location = New System.Drawing.Point(236, 92)
Me.groupBox3.Name = “groupBox3”
Me.groupBox3.Size = New System.Drawing.Size(136, 120)
Me.groupBox3.TabIndex = 4
Me.groupBox3.TabStop = False
Me.groupBox3.Text = “Copies”
‘
‘numericUpDownCopies
‘
Me.numericUpDownCopies.Location = New System.Drawing.Point(12, 48)
Me.numericUpDownCopies.Maximum = New Decimal(New Integer() {99, 0, 0, 0})
Me.numericUpDownCopies.Minimum = New Decimal(New Integer() {1, 0, 0, 0})
Me.numericUpDownCopies.Name = “numericUpDownCopies”
Me.numericUpDownCopies.Size = New System.Drawing.Size(44, 20)
Me.numericUpDownCopies.TabIndex = 1
Me.numericUpDownCopies.Value = New Decimal(New Integer() {1, 0, 0, 0})
‘
‘Print
‘
Me.AcceptButton = Me.buttonOk
Me.AutoScaleBaseSize = New System.Drawing.Size(5, 13)
Me.CancelButton = Me.buttonCancel
Me.ClientSize = New System.Drawing.Size(389, 258)
Me.Controls.AddRange(New System.Windows.Forms.Control() _
{Me.groupBox3, Me.groupBox2, Me.groupBox1, Me.buttonOk, Me.buttonCancel})
Me.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog
Me.MaximizeBox = False
Me.MinimizeBox = False
Me.Name = “Print”
Me.Text = “FontPad: Print”
Me.groupBox1.ResumeLayout(False)
Me.groupBox2.ResumeLayout(False)
Me.groupBox3.ResumeLayout(False)
CType(Me.numericUpDownCopies, _
System.ComponentModel.ISupportInitialize).EndInit()
Me.ResumeLayout(False)
End Sub
#End Region
‘form-level scope
Private printDocument As New System.Drawing.Printing.printDocument()
Private printStream As System.IO.StringReader
Working with the .NET Namespaces
202
PART II
Inside the load event of the print form, we load a combo box with all the installed printers on
the system. This is done with the PrintDocument.InstalledPrinters collection. We then set
the selected printer in the combo box to the user’s default settings as stored in our application-
wide variable printerDefault.
Private Sub Print_Load(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles MyBase.Load
‘local scope
Dim i As Integer
Next
End Sub
When users click the Cancel button, we simply unload the form and do not send output to the
printer.
Private Sub buttonCancel_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles buttonCancel.Click
End Sub
When a user clicks the form’s OK button, we start the printing process. First, we do some sim-
ple form-field validation. Then we call the printText procedure, passing the form’s values as
parameters. This gives us a slightly more generic print method that could be used elsewhere.
Private Sub buttonOk_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles buttonOk.Click
6
‘local scope
OPERATIONS
Dim fromPage As Integer
PRINTING
Dim toPage As Integer
End If
End If
Me.Close()
Else
Beep()
End If
End Sub
Inside the PrintText procedure, we control the printing process and set printer settings. We
first create a new instance of the StringReader class and set its string constructor value to our
printString value. Then we tell the PrintDocument object that we will handle its PrintPage
event with our own procedure, printPage_handler.
The page ranges to print are then set.
Next, we create an instance of the Margins class based on the user’s defined margin settings.
Note that margin values are indicated in hundredths of an inch.
Then we set the orientation, paper size, paper source, number of copies, and collate properties
of the PrintDocument.PrinterSettings object.
Finally, we call the Print method of the PrintDocument class. This will fire the PrintPage
event (which we intercept) for every page that it needs to print.
Private Sub printText(ByVal fromPage As Integer, _
ByVal toPage As Integer, _
ByVal copiesToPrint As Integer, _
ByVal isCollate As Boolean)
‘purpose: print the contents of the rich text box to the selected printer
‘local scope
Dim margins As System.Drawing.Printing.Margins
Dim count As Integer
OPERATIONS
End If
PRINTING
‘set the page margin values based on user settings (hundredth of an inch)
margins = New System.Drawing.Printing.Margins(Left:=(marginLeft * 100), _
Right:=(marginRight * 100), Top:=(marginTop * 100), _
Bottom:=(marginBottom * 100))
printDocument.DefaultPageSettings.Margins = margins
‘set the paper size to print from by looping through the paper
‘ sizes collection, matching the paper size name that the
‘ user has selected,
‘ and setting the paperSize property of defaultPageSettings =
‘ to the correct paperSize
For count = 0 To printDocument.PrinterSettings.PaperSizes.Count - 1
If printDocument.PrinterSettings.PaperSizes.Item(count).PaperName = _
paperSize Then
printDocument.DefaultPageSettings.PaperSize = _
printDocument.PrinterSettings.PaperSizes.Item(count)
Exit For
End If
Next
printDocument.DefaultPageSettings.PaperSource = _
printDocument.PrinterSettings.PaperSources.Item(count)
Exit For
Working with the .NET Namespaces
206
PART II
End If
Next
End Sub
Within our custom print page event handler (printPage_handler), we first set the print font to
that of the user’s defined font setting for the application. Next, we start drawing lines to the
printer, one at a time. We calculate the number of lines per page and begin looping through our
count (linesPerPage). We use the StringReader instance (printStream) to read each line and
the Graphics class DrawString method to output the string to the printer.
Private Sub printPage_handler(ByVal sender As Object, _
ByVal ev As System.Drawing.Printing.PrintPageEventArgs)
‘local scope
Dim linesPerPage As Single = 0
Dim yPos As Single = 0
Dim count As Integer = 0
Dim leftMargin As Single = ev.MarginBounds.Left
Dim topMargin As Single = ev.MarginBounds.Top
Dim line As String = “”
Dim printFont As Font
Dim fontStyle As FontStyle
Dim fontFamily As FontFamily
Dim styleType As System.Type
PRINTING
fontStyle = fontStyle.Parse(enumType:=styleType, value:=fontStyleSetting)
‘create a new font object for the printer based on user selected font
‘ set it to the user’s selected font setting
printFont = New Font(family:=fontFamily, _
emSize:=CSng(fontSizeSetting), _
style:=fontStyle, _
unit:=System.Drawing.GraphicsUnit.Point)
Loop
End Sub
Working with the .NET Namespaces
208
PART II
The following are simply click events used to control our form operations:
Private Sub toggleRange(ByVal toggleValue As Boolean)
labelFrom().Enabled = toggleValue
labelTo().Enabled = toggleValue
textBoxFrom().Enabled = toggleValue
textBoxTo().Enabled = toggleValue
End Sub
Call toggleRange(True)
End Sub
Call toggleRange(False)
End Sub
‘tell the print document object the name of the selected printer
printDocument.PrinterSettings.PrinterName = _
comboBoxPrinterName().SelectedItem.ToString
End Sub
End Class
Font, Text, and Printing Operations
209
CHAPTER 6
PRINTING
for your reference.
‘purpose: declare variables that have global scope & set the defaults
‘font settings
Public g_FontFamily As String = “Courier New”
Public g_FontStyle As String = “Regular”
Public g_FontSize As String = “10”
Member Description
PrintDialog The PrintDialog class enables programmers to easily create a
form that gives users access to printer and print property selec-
tions.
PrintPreviewControl The PrintPreviewControl class encapsulates the print pre-
viewing process without any dialog boxes.
PrintPreviewDialog The PrintPreviewDialog class allows programmers to easily
display print preview information to users of their application.
The PrintPreviewDialog uses a PrintPreviewControl.
PageSetupDialog The PageSetupDialog class enables you to create a dialog box
that can be manipulated by users to modify page settings, mar-
gins, and paper orientation.
FontDialog The FontDialog class gives you a form to display to users rep-
resenting a list of fonts that are currently installed on a system.
Users can select a font, size, and style.
FileDialog The FileDialog class enables programmers to easily create a
form that will allow users to navigate their machine and net-
work in search of a file.
Font, Text, and Printing Operations
211
CHAPTER 6
Summary 6
OPERATIONS
PRINTING
sending output to the printer with the .NET Class Library. From here you should be able to
easily strengthen this foundation through your own explorations as you extend your knowledge
into your own application development.
The following are key points to writing code for font, text, and printing functionality with the
.NET Class Library:
• A font describes the way text appears on a device. Fonts can vary in size, weight,
and style.
• The Font class encapsulates an individual font inside of .NET.
• The InstalledFontCollection class enables you to return all fonts installed on a given
system.
• The PrivateFontsCollection class enables you to work with custom fonts without
actually installing them on a system.
• The PrintDocument class is used to control output to a printer.
• To set and retrieve specific settings on a given printer, use the PrinterSettings class.
• The PreviewPrintController class provides a print controller that outputs printed pages
as images that can be viewed prior to submission to the printer.
Stream and File Operations CHAPTER
7
IN THIS CHAPTER
• Key Classes Related to File I/O 214
One of the most important issues any programmer faces is how to go about storing and retriev-
ing information on disk. This issue can be surprisingly complex. The .NET Base Class Library
provides a number of classes that simplify the issue somewhat. The library is a substantial
improvement over the file-related operations Visual Basic programmers had available to them
previously.
This chapter focuses on the .NET namespaces related to directories, files, and synchronous and
asynchronous reading from and writing to data streams. First, an overview is presented that
details the key classes within the namespace. Then we get into files, streams, and data types.
And finally, we write a file-monitoring application that demonstrates the use of these classes.
After reading this chapter, you should be able to do the following:
• Manage directories and files including creating, deleting, and accessing their property
information
• Monitor the file system and respond to basic system events
• Read and write files as streams of data both synchronously and asynchronously
• Access file data at the binary level
• Understand some of the basic design considerations for choosing a file I/O strategy
The code for the example is provided in Listing 7.1. It involves code to create the form, a form
load event, and a pair of index change events for the two list boxes.
The code starts with a few global variable declarations to store a path, directory, and filename.
This is followed by the basic form creation code.
OPERATIONS
Me.AcceptButton = Me.buttonClose
Me.AutoScaleBaseSize = New System.Drawing.Size(5, 13)
Me.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog
Me.ClientSize = New System.Drawing.Size(453, 410)
Me.Controls.AddRange(New System.Windows.Forms.Control() _
{Me.buttonClose, Me.labelFileInfo, Me.listBoxFiles, _
Me.listBoxDirectories, Me.labelDirectoryInfo, Me.label2, _
Me.label1})
Me.Text = “Directory And Files”
End Sub
Inside the form’s load event we return a list of directories. To do so, first we instantiate a
DirectoryInfo class with the line
where myPath is a valid path (c:\). Next, we call the GetDirectories method of the
DirectoryInfo class. This returns an array of DirectoryInfo objects that represent the path’s
subdirectories. Finally, we select an item in the directory list box. This fires the index-changed
event of the directory list box.
Private Sub Form1_Load(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles MyBase.Load
‘local scope
Dim myDirectories() As System.IO.DirectoryInfo
Dim i As Integer
‘loop through the directories and add them to the list box
For i = 0 To UBound(myDirectories) - 1
Next
End Sub
Once a user selects a directory, we must return to her a list of files. In our example, we do this
inside the listBoxDirectories index change event
(listBoxDirectories_SelectedIndexChanged). We first create a new DirectoryInfo object
based on our starting path value and the user’s selected directory:
myDirectory = New System.IO.DirectoryInfo(path:=(myPath & myDirName))
Next we return an array of FileInfo objects using the method GetFiles of the DirectoryInfo
class. Finally, we loop through the array and add the filenames to the list box and select the
first file in the list.
Private Sub listBoxDirectories_SelectedIndexChanged(ByVal sender As _
System.Object, ByVal e As System.EventArgs) Handles _
listBoxDirectories.SelectedIndexChanged
‘purpose: synchronize the files list box with the selected directory
‘ and display the selected directory’s information
‘local scope
Dim myDirectory As System.IO.DirectoryInfo
Dim dirInfo As String
Dim myFiles() As System.IO.FileInfo
Dim i As Integer
OPERATIONS
If UBound(myFiles) >= 0 Then
‘select the file in the list, this will trigger the event to change
‘ the file info label control
listBoxFiles().SelectedIndex = 0
End If
End Sub
‘local scope
Dim myFile As System.IO.FileInfo
Dim fileInfo As String
End Sub
End Sub
#End Region
End Class
In the prior example we used the FileInfo class to display file attributes to the user. This class
has a number of properties that are useful when working with files. Some of these that are of
keen interest are listed in Table 7.2.
Directories
For manipulating directories we primarily use five basic methods: Create,
CreateSubdirectory, Delete, MoveTo, and Refresh. These methods are called from the
DirectoryInfo class:
Working with the .NET Namespaces
224
PART II
• The Create method creates the directory to which the DirectoryInfo object refers. The
CreateSubdirectory method accepts the subdirectory’s path as a parameter. It then cre-
ates the subdirectory and returns it as a DirectoryInfo instance.
• The Delete method has two overloaded parameter sets. The first takes no parameters
and simply deletes the directory and its contents to which the DirectoryInfo instance
refers. The second takes a Boolean value that indicates whether the delete call should
also be recursive; that is, whether it should delete all its subdirectories and their contents.
• The MoveTo method takes a string as a parameter (destDirName) that represents the des-
tination path. The destination path, as you may have guessed, defines the directory and
its path to which the current directory (defined by the DirctoryInfo instance) should be
moved.
• The Refresh method refreshes the DirectoryInfo object instance. The method reloads
the directory information. This is useful for long-running objects to maintain the most
up-to-date information on the directory attributes.
Listing 7.2 is a procedure that demonstrates these methods. The procedure can be copied into a
console application and executed.
Sub Main()
‘local scope
Dim myDirectoryParent As System.IO.DirectoryInfo
Dim myDirectory2 As System.IO.DirectoryInfo
OPERATIONS
‘ to delete its sub directories as well
myDirectory2.Delete()
End Sub
End Module
Files
For working with files we use a similar set of methods found in the FileInfo class. The
Create, Delete, MoveTo, and Refresh methods are all present. However, programming with
files requires a few additional methods: AppendText, CopyTo, CreateText, Open, OpenRead,
and OpenWrite (all of which will be defined later in this chapter).
The Create, MoveTo, and Delete methods are very similar to the DirectoryInfo class. The
CopyTo method copies a version of the file to which the FileInfo instance refers. The method
is overloaded with two parameter sets. The first copies an instance of the given file to the new
directory specified in the parameter destFileName. This method raises an exception if a file
with the same name already exists in the destination directory. Upon success, the method
returns an instance of the FileInfo class based on the copied file. The second overloaded
method takes both the destFileName and the parameter overwrite as a Boolean. Set this para-
meter to True if you want the Copy method to overwrite any existing file with the same name
and False if you do not. Sample code that illustrates these methods is provided for you in
Listing 7.3.
Note the return type of the Create method. On file create, we are returned an instance of the
FileStream object. This object is a stream object based on the new file. The stream object pro-
vides us the ability to read and write to the file. Streams, as well as reading and writing to
files, are discussed later in this chapter.
Working with the .NET Namespaces
226
PART II
Module Module1
Sub Main()
‘local scope
Dim myFileStream As FileStream
Dim myFile As FileInfo
Dim myCopiedFile As FileInfo
OPERATIONS
‘delete the files
myFile.Delete()
myCopiedFile.Delete()
End Sub
End Module
The level of access users are granted to a file is controlled by the FileAccess enumeration.
The parameter is specified in many of the constructors of the File, FileInfo, and FileStream
classes. Table 7.3 lists the members of the enumeration and a brief description of each.
Member Description
Read The Read member indicates that data can be read from a file.
ReadWrite The ReadWrite member defines both read and write access to a given file.
Write The Write member provides write access to a file.
The various attribute values for files and directories are controlled using the FileAttributes
enumeration. It should be noted that not all members are applicable to both files and directo-
ries. Table 7.4 lists the members of the FileAttributes enumeration.
Member Description
Archive The Archive member indicates a file’s archive status. The archive
status is often used to mark files for backup or removal.
Compressed The Compressed member indicates that a file is compressed.
Working with the .NET Namespaces
228
PART II
To control whether a file is created, overwritten, opened, or some combination thereof, you use
the FileMode enumeration. It is used in many of the constructors of the FileStream, File,
FileInfo, and IsolatedStorageFileStream classes. Table 7.5 lists the members of the
FileMode enumeration.
Member Description
Append The Append parameter specifies that a file be opened or created, and its
end searched (seek) out. FileMode.Append can only be used in conjunc-
tion with FileAccess.Write. Any attempt to read fails and throws an
ArgumentException.
Stream and File Operations
229
CHAPTER 7
OPERATIONS
This requires FileIOPermissionAccess.Read FileIOPermission.
OpenOrCreate The OpenOrCreate member indicates that Windows should open a file if it
exists; otherwise, a new file should be created. If the file is opened with
FileAccess.Read, FileIOPermissionAccess.Read FileIOPermission is
required. If file access is FileAccess.ReadWrite and the file exists,
FileIOPermissionAccess.Write FileIOPermission is required. If file
access is FileAccess.ReadWrite and the file does not exist,
FileIOPermissionAccess.Append FileIOPermission is required in
addition to Read and Write.
Truncate The truncate member indicates that Windows should open an existing file
and truncate its size to zero bytes. This requires
FileIOPermissionAccess.Write FileIOPermission.
Use the FileShare enumeration to control whether two processes can access a given file
simultaneously. For example, if a user opens a file marked FileShare.Read, other users can
open the file for reading but cannot save or write to it. The FileShare enumeration is used in
some of the constructors for the FileStream, File, FileInfo, and
IsolatedStorageFileStream classes. Table 7.6 lists the FileShare enumeration members.
Member Description
None The None value indicates that the file should not be shared in any way. All
requests to open the file will fail until the file is closed.
Read The Read member allows users to open a given file for reading only.
Attempts to save the file (or write to it) but read-only processes will fail.
ReadWrite The ReadWrite parameter indicates that a file can be opened for both reading
and writing by multiple processes. Obviously this can cause problems
because the last user to save has his changes applied.
Working with the .NET Namespaces
230
PART II
all files in the directory. If you are trying to monitor only Microsoft Excel files, then you’d set
the Filter property to *.xls. Table 7.7 lists additional properties and describes when you would
implement their use.
OPERATIONS
Filter
directory that are of a specific type.
Target Use the Target property to watch for changes on only a file,
only a directory, or both a file and a directory. By default, the
Target property is set to watch for changes to both directory
and file-level items.
IncludeSubDirectories Set the IncludeSubDirectories property to True to monitor
changes made to subdirectories that the root directory con-
tains. The watcher will watch for the same changes in all
directories.
ChangedFilter Use the ChangedFilter property to watch for specific changes
to a file or directory when handling the Changed event.
Changes can apply to Attributes, LastAccess, LastWrite,
Security, or Size.
Once you’ve decided what objects you are monitoring you’ll need to indicate the events or
actions to which you are listening. The changes that the FileSystemWatcher can monitor
include changes in the directory’s or file’s properties, size, last write time, last access time,
and security settings.
You use the NotifyFilters enumeration of the FileSystemWatcher class to specify changes
for which to watch on files and directories. The members of this enumeration can be combined
using BitOr (bitwise or comparisons) to watch for multiple kinds of changes. An event is
raised when any change you are watching for is made. Table 7.8 lists the members of the
NotifyFilters enumeration and provides a brief description of each.
Working with the .NET Namespaces
232
PART II
Member Description
Attributes The Attributes member allows you to watch for changes made to
the attributes of a file or directory.
CreationTime The CreationTime member allows you to watch for changes made
to the time the file or directory was created.
DirectoryName The DirectoryName member allows you to watch for changes
made to the name of the file or directory.
FileName The FileName member allows you to watch for changes made to
the name of a file.
LastAccess The LastAccess member allows you to watch for changes made to
the date the file or directory was last opened.
LastWrite The LastWrite member allows you to watch for changes made to
the date the file or directory had data written to it.
Security The Security member allows you to watch for changes made to
the security settings of the file or directory.
Size The Size member allows you to watch for changes made to the
size of the file or directory.
NOTE
You might notice that some common tasks such as file copy or move do not corre-
spond directly to an event raised by the component. However, upon closer examina-
tion, you will notice that when a file is copied, the system raises a created event to
the directory to which the file was copied. Similarly, when a file is moved, the system
raises both a deleted event in the file’s original directory and a created event in the
file’s new directory. These events serve in place of actual copy and move to events.
This buffer becomes very important in high-volume monitoring applications. It has the poten-
tial to receive a lot of events. Let’s examine this further. Every change to a file in a directory
raises a separate event. This sounds simple enough, but we have to be careful. For example, if
Stream and File Operations
233
CHAPTER 7
we are monitoring a directory that contains 25 files and we reset the security settings on the
directory, we will get 25 separate change events. Now if we write an application that renames
those files and resets their security we’ll get 50 events: one for each file for both change and
rename.
All these events are stored in the FileSystemWatcher’s internal buffer, which has a maximum
size limit and can overflow. If this buffer overflows, the FileSystemWatcher will raise the
InternalBufferOverflow event. Fortunately, the component allows us to increase this
buffer size.
The default buffer size is set to 8KB. Microsoft indicates that this can track changes on 7
OPERATIONS
using the InternalBufferSize property. For best performance, this property should be set in
increments of 4K (4096, 8192, 12288, and so on) because this corresponds to the operating
system’s (Windows 2000) default page size.
Increasing this internal buffer comes at a cost. The buffer uses non-paged memory that cannot
be swapped to disk. Therefore, we need to keep the buffer size as small as possible. Strategies
to limit the buffer’s size include the NotifyFilter and IncludeSubDirectories properties to
filter out those change notifications in which we have no interest. It should be noted that the
Filter property actually has no effect on the buffer size since the filter is applied after the
notifications are written to the buffer.
To actually connect to the FileSystemWatcher’s events we add handlers in our code as we
would with any other event. For example, to hook into the Changed event, we add code similar
to the following:
AddHandler myWatcher.Changed, AddressOf watcher_OnChange
This tells your application to intercept the Changed event and process through a custom event
called watcher_onChange. The custom event need only have the correct function signature.
Table 7.9 lists the events to which you can subscribe and their associated function signatures.
Once inside the event, we have access to a number of properties related to the event and event
type. These properties come from the event arguments that are passed to us when the event is
raised. They include things like the change type and the path and name to the file or directory.
The WatcherChangeTypes enumeration is used by events of the FileSystemWatcher class. The
enumeration’s members indicate to the event the type of change that occurred to a file or direc-
tory. Table 7.10 lists the members of the WatcherChangeTypes enumeration.
Member Description
All The All member indicates that any of the creation, deletion, change, or
renaming of a file or folder actions occurred.
Changed The Changed member indicates that a change action occurred to a file or
event. Changes can include: size, attributes, security, last write, and last
access time.
Created The Created member indicates that a file or folder was created.
Deleted The Deleted member indicates that a file or folder was deleted.
Renamed The Renamed member indicates that a file or folder was renamed.
Listing 7.4 provides a sample FileSystemWatcher application that serves to further illustrate
these concepts. The code can be executed inside a console application (and downloaded from
www.samspublishing.com). The application monitors a directory for changes to text files.
When a change occurs, the user is notified with a simple call to Console.WriteLine from
within the intercepted change event.
Stream and File Operations
235
CHAPTER 7
Imports System.IO
Module Module1
Sub Main()
‘Call directory()
Call watchDirectory(watchPath:=”c:\watch”)
End Sub 7
‘local scope
Dim myWatcher As FileSystemWatcher
Dim stopValue As String
End Sub
End Sub
End Sub
End Module
➲ For additional information on .NET security issues (for user, file, directory, and code
access), start with the System.Security namespace. Of course, you will also want to
read Appendix D, “.NET Framework Base Data Types,” which deals specifically with
security in .NET.
➲ You will want to check out the FileDialog class found in System.Windows.Forms. This
class allows you to easily display a window’s dialog that allows users to select a file. The
class is the replacement of the old common dialog control.
Member Description
AppendText The AppendText method creates an instance of the StreamWriter class
that allows us to append text to a file. The StreamWriter class implements
a TextWriter instance to output the characters in a specific encoding.
CreateText The CreateText method creates an instance of the StreamWriter class
that creates a new text file to which to write.
Open The Open method opens a file and returns it to us as a FileStream object.
The method has three constructors that allow us to specify the open mode
(open, create, append, and so on), the file access (read, write, read and
write), and how we want the file to be shared by other FileStream objects.
OpenText The OpenText method creates a StreamReader object based on the associ-
ated text file.
OpenRead The OpenRead method creates a FileStream object that is read only.
OpenWrite The OpenWrite method creates a FileStream object that is both read and
write.
You can see that the FileInfo class makes extensive use of the FileStream, StreamWriter,
and StreamReader classes. These classes expose the necessary functionality to read and write
to files in .NET. As you might have guessed, these objects are designed to work with persisted
text files. They are based on the TextWriter and TextReader classes.
The FileStream class can be created explicitly. You’ve already seen that FileInfo uses this
class to expose reading and writing to files. Table 7.12 lists the version of the FileStream con-
structors that can be used to create a FileStream object.
OPERATIONS
New FileStream (ByVal path as String, ByVal mode as FileMode, _ByVal access as
FileAccess)
path: A valid path to the file that the FileStream object will represent.
mode: A member of the FileMode enumeration (Append, Create, CreateNew, Open,
OpenOrCreate, Truncate) that specifies how the file should be opened.
access: A member of the FileAccess enumeration (Read, ReadWrite, Write).
Note: Use this constructor when you know the file’s path, wish to specify how the file is
opened, and need to specify the read/write permissions on the file.
New FileStream (ByVal handle as IntPtr, ByVal access as FileAccess, _ByVal
ownsHandle as Boolean, ByVal bufferSize as Integer)
handle: A valid handle to a file.
access: A member of the FileAccess enumeration (Read, ReadWrite, Write).
ownsHandle: Indicates if the file’s handle will be owned by the given instance of the
FileStreamObject.
bufferSize: Indicates the size of the buffer in bytes.
Note: Use this constructor when you have a valid file pointer, need to specify the read/write
permissions, with to own the file’s handle, and need to set the stream’s buffer size.
New FileStream (ByVal path as String, ByVal mode as FileMode, _ByVal access as
FileAccess, ByVal share as FileShare)
path: A valid path to the file that the FileStream object will represent.
mode: A member of the FileMode enumeration (Append, Create, CreateNew, Open,
OpenOrCreate, Truncate) that specifies how the file should be opened.
access: A member of the FileAccess enumeration (Read, ReadWrite, Write).
share: A member of the FileShare enumeration that indicates how the file will be shared.
Working with the .NET Namespaces
240
PART II
OPERATIONS
This example is a simple, console-based application. It creates a new FileStream object based
on a physical file. It then creates a StreamWriter instance based on the FileStream class. It
calls the WritLine method of StreamWriter to output a line of text to the file. After it closes
the StreamWriter instance, it creates a StreamReader instance based on a FileStream object.
Finally, it loops through the lines in the file and outputs them to the console for your viewing.
Imports System.IO
Module Module1
Sub Main()
‘local scope
Dim fileStream As FileStream
Dim streamWriter As StreamWriter
Dim streamReader As StreamReader
End Sub
End Module
the operation is complete. For instance, suppose your application takes orders in the form of
text files written to a queue. When a file is placed in the queue (or directory), your application
reads the contents of the file and processes the order(s) accordingly. Each file can represent
one order, or can contain a batch of orders. If your application is set up to handle each order
from start to finish as it comes in (synchronously), then a long order will block your applica-
tion from continuing to process orders while simply reading the file.
For a more efficient use of your resources, you will want to read orders asynchronously. That
is, as an order comes in, you will tell a version of the Stream object to start reading the file
and to let you know when it is done. This way, once you fire the BeginRead method, you can 7
continue executing other program logic including responding to and processing additional
OPERATIONS
With asynchronous file I/O, the main thread of your application continues to execute code
while the I/O process finishes. In fact, multiple asynchronous IO requests can process simulta-
neously. Generally, an asynchronous design offers your application better performance. The
tradeoff to this performance is that a greater coding effort is required.
The FileStream class provides us the BeginRead method for asynchronous file input and the
BeginWrite method for asynchronous file output. As a parameter to each, we pass the name of
the method we wish to have called when the operation is complete (userCallback as
AsynchCallback). In VB .NET, the syntax looks like this:
Where myCallbackMethod is the name of the method you wish to have intercept and process
the completed operation notification. From within this callback method, you should call
EndRead or EndWrite as the case dictates. These methods end their respective operations.
EndRead returns the number of bytes that were read during the operation. Both methods take a
reference to the pending asynchronous I/O operation (AsynchResult as IAsynchResult).
This object comes to us as a parameter to our custom callback method. The code in Listing 7.6
further illustrates these concepts.
The application’s Sub Main simply controls the calls to the read operation. You can see in
Listing 7.6 that we execute three separate read requests on three different files. The remaining
bits of functionality are nicely encapsulated and thus, should be easy to reuse.
Module Module1
Sub Main()
Working with the .NET Namespaces
244
PART II
‘local scope
Dim myFiles(3) As String
Dim i As Int16
myFiles(0) = “c:\file1.txt”
myFiles(1) = “c:\file2.txt”
myFiles(2) = “c:\file3.txt”
‘NOTE: now that file reads have started, our application can
‘ continue processing other information and await a callback
‘ from the read operation indicating read is complete
End Sub
The procedure asynchRead sets up the asynchronous file input. The class StateObject is a
simple state object that allows us to maintain file input information, in the form of properties,
across read requests.
Notice that when calling BeginRead, in addition to indicating a callback method, we must
specify both a byte array (array() as Byte) and the total number of bytes (numBytes as
Integer) we wish to have read. To store the bytes, we dimension an array of type byte inside
our state object. We pass byteArraySize in the object’s constructor. We get its size by reading
the file size from the FileInfo object’s Length property. This allows us to create an array of
the exact size we need. Similarly, when we set the number of bytes to read, we use
Stream and File Operations
245
CHAPTER 7
‘local scope
Dim fileStream As FileStream
Dim state As StateObject
Dim fileInfo As FileInfo
7
OPERATIONS
If Not File.Exists(path:=filePath) Then
End If
End Sub
The fileRead method is the application’s callback implementation. This method receives noti-
fication when a BeginRead has completed for a given file.
‘local scope
Dim state As StateObject
Dim bytesRead As Integer
‘set the state object = to the one returned by the asynch results
state = asyncResult.AsyncState
End Sub
End Module
‘class-level scope
Private localFilePath As String
Private localByteArray() As Byte
‘public properties
Public FileStream As FileStream
Get
Return localByteArray
End Get
End Property
OPERATIONS
Get
Return localFilePath
End Get
End Property
End Sub
End Class
Figure 7.2 represents the output of the code listing. Notice that in this case, each file was read
in the same order the request was made. However, there is no guarantee of processing order
due to the asynchronous nature of the request and additional factors like file size and processor
availability. Also notice that after the first (and subsequent) read requests were made, our code
did not stop executing. Rather, it made additional requests, and ultimately, waited on user input
to stop the application. Finally, as each read completed, the notification was sent to our
readFile method and the results of the operation were written to the console.
Working with the .NET Namespaces
248
PART II
FIGURE 7.2
Output of asynchronous example.
Similarly, BinaryWriter provides a number of write methods for writing primitive data to a
stream. Unlike BinaryReader, BinaryWriter exposes only one method, WriteByte, for exe-
cuting binary writes. However, this method has a number of overloads that allow us to specify
Stream and File Operations
249
CHAPTER 7
whether we are writing byte data or string, decimal, and so on. Calls to WriteByte write out
the given data to the stream and advance its current position by the length of the data. Again,
WriteByte(value as Byte) will be the most commonly used method.
The FileStream class also exposes the basic binary methods, ReadByte and WriteByte.
ReadByte and WriteByte behave in the exact same manner as BinaryReader.ReadByte and
BinaryWriter.WriteByte(value as Byte). It is often easier to simply use FileStream for all
your basic needs; this is why it exists. Should you need additional functionality, then you will
want to implement one or more of the binary classes.
Listing 7.7 provides an example of the BinaryReader and BinaryWriter classes. In the exam- 7
OPERATIONS
time, we write each byte out to another file using BinaryWriter. The result is two identical
files. Notice that to create both the reader and the writer we must first create a valid
FileStream (or similar Stream derivation) for the instances to use as their backing.
Imports System.IO
Module Module1
Sub Main()
‘local scope
Dim fsRead As FileStream
Dim fsWrite As FileStream
Dim bRead As BinaryReader
Dim b As Byte
Dim bWrite As BinaryWriter
End
Working with the .NET Namespaces
250
PART II
‘delete file
File.Delete(path:=”c:\test2.bmp”)
End If
Try
Catch
Exit Do
End Try
OPERATIONS
‘wait for the user to stop the console application
Console.WriteLine(“Operation complete.”)
Console.WriteLine(“Enter ‘s’ to stop the application.”)
End Sub
End Module
Open Dialog
To open files, we must present users with a method to browse the file system. .NET provides
us with a FileDialog class specifically designed for this purpose. This class is similar to the
common dialogs with which we’re familiar from the VB of old. The .NET team built the dia-
log using the namespace we’ve presented in this chapter.
In our example application we will create our own dialog using the namespace rather than
FileDialog. This makes sense because our objective is to teach the namespace. However, we
suggest you further explore this class if you need fast and easy access to the file system.
Figure 7.3 is a screen capture of our open dialog.
Obviously, this form will not win any usability or user interface design awards; it was built to
illustrate code. There are two list boxes on the form. One maintains a current list of subdirecto-
ries of the given path. The other displays files of type text within the selected directory. Users
navigate down through subdirectories by double-clicking a directory. To navigate back, they
click the Up button. As directories and files are selected, we write related information to a cou-
ple of label controls.
Stream and File Operations
253
CHAPTER 7
Code Walkthrough
The code used by the open dialog should be very familiar to you by now. The code can be
found in Listing 7.8.
The code starts with some form-level declarations for directory name and filename. This is fol-
lowed by the basic code to build the form.
Inherits System.Windows.Forms.Form
End Sub
Working with the .NET Namespaces
254
PART II
OPERATIONS
Me.buttonCancel.DialogResult = System.Windows.Forms.DialogResult.Cancel
Me.buttonCancel.Location = New System.Drawing.Point(352, 336)
Me.buttonCancel.TabIndex = 0
Me.buttonCancel.Text = “Cancel”
Me.buttonUp.Location = New System.Drawing.Point(224, 48)
Me.buttonUp.Size = New System.Drawing.Size(28, 24)
Me.buttonUp.TabIndex = 5
Me.buttonUp.Text = “up”
Me.listBoxDirectories.Location = New System.Drawing.Point(8, 48)
Me.listBoxDirectories.Size = New System.Drawing.Size(212, 121)
Me.listBoxDirectories.TabIndex = 4
Me.listBox1.Location = New System.Drawing.Point(20, 60)
Me.listBox1.Size = New System.Drawing.Size(0, 4)
Me.listBox1.TabIndex = 3
Me.label5.Location = New System.Drawing.Point(224, 180)
Me.label5.Size = New System.Drawing.Size(92, 16)
Me.label5.TabIndex = 2
Me.label5.Text = “Info”
Me.labelFileInfo.BorderStyle = System.Windows.Forms.BorderStyle.Fixed3D
Me.labelFileInfo.Location = New System.Drawing.Point(224, 196)
Me.labelFileInfo.Size = New System.Drawing.Size(204, 132)
Me.labelFileInfo.TabIndex = 7
Me.buttonOk.Location = New System.Drawing.Point(272, 336)
Me.buttonOk.TabIndex = 1
Me.buttonOk.Text = “OK”
Me.label1.Location = New System.Drawing.Point(8, 8)
Me.label1.Size = New System.Drawing.Size(100, 16)
Me.label1.TabIndex = 8
Me.label1.Text = “Directories”
Me.label2.Location = New System.Drawing.Point(224, 80)
Me.label2.Size = New System.Drawing.Size(92, 16)
Me.label2.TabIndex = 2
Me.label2.Text = “Info”
Me.label3.Location = New System.Drawing.Point(6, 180)
Working with the .NET Namespaces
256
PART II
End Sub
On the form load event we simply make a call to the sub procedure that loads the directory
list box.
‘local scope
End Sub
The procedure loadDirListBox refreshes the contents of the directory list box when the form
loads and as users double-click a subdirectory or press the Up button.
‘purpose: load the list box control based on the current path
‘local scope
Dim myDirectory As System.IO.DirectoryInfo
Dim myDirectories() As System.IO.DirectoryInfo
Dim i As Integer
Stream and File Operations
257
CHAPTER 7
‘loop through the directories and add them to the list box
7
OPERATIONS
‘add the directory name to the list
listBoxDirectories().Items.Add(myDirectories(i).Name)
Next
End Sub
Once the user has selected a file to open and has clicked the OK button, the button’s click
event calls a custom procedure we call readFile.
Private Sub buttonOk_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles buttonOk.Click
‘open the text file as a stream and read it into our editor
‘note: this new file will replace the current file without saving
Call readFile()
End If
End Sub
The readFile procedure creates a StreamReader object, sets its read position to the start of the
file, and reads the file line-by-line. Finally, our rich text box control is updated with the con-
tents of the opened file.
Private Sub readFile()
‘local scope
Dim fileStream As IO.FileStream
Dim streamReader As IO.StreamReader
‘handle any errors the occur when loading the file stream
Try
‘create a new instance of the file stream object
‘ based on the current path and selected file
fileStream = New IO.FileStream(path:=myPath & myDirName & _
myFileName, mode:=IO.FileMode.Open, access:=IO.FileAccess.Read)
Catch
7
End Try
End Sub
Inside the listBoxDirectories double-click event we simply reset the path (myPath) and call
load directories sub (loadDirListBox).
Private Sub listBoxDirectories_DoubleClick(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles listBoxDirectories.DoubleClick
‘local scope
End Sub
When users select a directory from the list box, the directory list box’s index change event gets
fired. This event loads the file’s list box based on the user-selected subdirectory. Inside this
event, we trap for file security access issues. For instance, if you call the GetFiles method of
the Directory object and the user is not granted access to the directory’s files, we must raise
this issue to our user.
Working with the .NET Namespaces
260
PART II
‘purpose: update the directory info label and the file’s list box
‘ when users select a directory
‘local scope
Dim myDirectory As System.IO.DirectoryInfo
Dim dirInfo As String
Dim myFiles() As System.IO.FileInfo
Dim i As Integer
‘select the file in the list, this will trigger the event to _
change
Stream and File Operations
261
CHAPTER 7
End If
Catch
MsgBox(prompt:= _
“Sorry, but you do not have access to browse this folder’s _
7
OPERATIONS
Exception”)
End Try
End Sub
When a user selects a file, the SelectedIndexChanged event is fired. This event allows us to
set the selected filename (myFileName) and update the file properties to the user.
Private Sub listBoxFiles_SelectedIndexChanged(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles listBoxFiles.SelectedIndexChanged
‘local scope
Dim myFile As System.IO.FileInfo
Dim fileInfo As String
End Sub
The Up button’s click event simply navigates the user back up one folder.
‘local scope
Dim intPos As Integer
If intPos = 0 Then
‘no more path info
Beep()
Else
‘trim the path back
myPath = Mid(myPath, 1, intPos)
Call loadDirListBox()
End If
End Sub
#End Region
End Class
Save Dialog
Our application needs a way to persist its data to the file system. We create a Save dialog and
associated code to do just that. Figure 7.4 is a screen capture of the Save dialog in action.
Again, this dialog is sure to offend UI designers but it serves to illustrate the use of the classes.
Stream and File Operations
263
CHAPTER 7
The directory list box and associated Up button were stolen from the Open form. This simple
paradigm provides users with access to the file system. A text box is provided for users to type
the name to which they want to save the file.
The directory browsing provided by the form’s code is nearly the same as the Open example
(we might have considered creating a common dialog to be used by both features).
Code Walkthrough
Listing 7.9 presents the code behind the Open form.
Listing 7.9 starts with a form-level declare for storing the directory name. This is followed by
the basic form code. After that, much of the code is similar to the code in the open dialog with
the exception of the saveFile function.
‘note: issues
‘1. cannot save to the root directory
‘2. the return character not coming out right
‘3. files saved cannot be seen by the open
‘4. delete the file if exists??
End Sub
OPERATIONS
Me.buttonOk.Text = “OK”
‘
‘label2
‘
Me.label2.Location = New System.Drawing.Point(4, 172)
Me.label2.Name = “label2”
Me.label2.TabIndex = 2
Me.label2.Text = “File name:”
‘
‘buttonCancel
‘
Me.buttonCancel.DialogResult = System.Windows.Forms.DialogResult.Cancel
Me.buttonCancel.Location = New System.Drawing.Point(216, 220)
Me.buttonCancel.Name = “buttonCancel”
Me.buttonCancel.TabIndex = 0
Me.buttonCancel.Text = “Cancel”
‘
‘listBoxDirectories
‘
Me.listBoxDirectories.Location = New System.Drawing.Point(8, 72)
Me.listBoxDirectories.Name = “listBoxDirectories”
Me.listBoxDirectories.Size = New System.Drawing.Size(244, 95)
Me.listBoxDirectories.TabIndex = 3
‘
‘labelSaveTo
‘
Me.labelSaveTo.BorderStyle = System.Windows.Forms.BorderStyle.Fixed3D
Me.labelSaveTo.Location = New System.Drawing.Point(8, 28)
Me.labelSaveTo.Name = “labelSaveTo”
Me.labelSaveTo.Size = New System.Drawing.Size(280, 36)
Me.labelSaveTo.TabIndex = 6
‘
‘textBoxFileName
‘
Working with the .NET Namespaces
266
PART II
End Sub
‘local scope
End Sub
‘purpose: load the list box control based on the current path
‘ note: this procedure is the same as the one in formOpen
Stream and File Operations
267
CHAPTER 7
‘loop through the directories and add them to the list box
For i = 0 To UBound(myDirectories) - 1
Next
End Sub
Private Sub listBoxDirectories_SelectedIndexChanged( _
ByVal sender As System.Object, ByVal e As System.EventArgs) _
Handles listBoxDirectories.SelectedIndexChanged
‘local scope
End Sub
Working with the .NET Namespaces
268
PART II
‘local scope
Dim intPos As Integer
If intPos = 0 Then
‘no more path info
Beep()
Else
‘trim the path back
myPath = Mid(myPath, 1, intPos)
Call loadDirListBox()
End If
End Sub
‘local scope
End Sub
‘local scope
Dim fileName As String
‘validate filename
7
OPERATIONS
‘raise a message to the user
MsgBox(prompt:=”Please enter a file name.”, _
buttons:=MsgBoxStyle.Exclamation, title:=”FontPad Save”)
‘position cursor
textBoxFileName().Focus()
Else
End If
End Sub
The primary piece of functionality (save) can be found in the saveFile procedure. First, we
use methods of the File class to determine if the file already exists. If a file does exist, we
handle the situation by first deleting it and then saving it as new. This is a shortcut and not the
best solution. A more robust design would tell the user the file already exists and prompt him
to overwrite. Next, we create the FileStream object to write out the file. We open a
StreamWriter instance and set it to start writing at the beginning of our file. After this, we call
the Write method and pass it our contents as a string value. Finally we Flush and Close the
StreamWriter instance. Our new file is now saved to disk.
Working with the .NET Namespaces
270
PART II
‘local scope
Dim fileStream As IO.FileStream
Dim streamWriter As IO.StreamWriter
Dim filePath As String
End If
End Sub
#End Region
End Class
Stream and File Operations
271
CHAPTER 7
Summary
The ability to handle common file I/O tasks is essential to all developers. The .NET
Framework namespace, System.IO, exposes this functionality. By now, you should have a solid
foothold in this namespace that will support you in your further explorations.
The following is a summary of some of the key points regarding common file I/O program-
ming tasks:
• DirectoryInfo and FileInfo classes contain specific instance methods related to a
given directory or file. Whereas, the Directory and File classes pertain to static meth- 7
ods and are thus used in more generic cases.
• The class, FileSystemWatcher, allows us to monitor the file system for new files, files
being renamed, moved, and so on.
• Use the FileStream object for all basic file reading and writing including asynchronous
and binary.
• To read files asynchronously, we call FileStream.BeginRead and pass the name of a
procedure to receive callback notification when the operation is complete.
• StreamWriter, StreamReader, BinaryWriter, and BinaryReader all provide additional,
more advanced file streaming functionality.
• ReadByte and WriteByte are the primary methods used for reading and writing binary
data.
Networking Functions CHAPTER
8
IN THIS CHAPTER
• Key Classes Related to Network
Programming 274
• Sockets 278
• Implementing a Request/Response
Model 292
• An Asynchronous Request/Response
Pattern 302
This chapter begins our in-depth discussion of namespaces by examining the functionality
exposed by the System.Net and System.Net.Sockets namespaces. This chapter will show you
how to accomplish common network-based programming tasks using the components exposed
in these two namespaces. We’ll begin by examining how the network classes in these name-
spaces provide comprehensive coverage of both high and low-level network programming
tasks. Then, by working our way from the low-level classes dealing with protocol-independent
sockets to the high-level classes that operate over HTTP, we will paint a picture of the exact
functionality offered by these base classes. We will also look at how they can serve as a great
foundation for building network-enabled .NET applications.
After reading this chapter, you should be able to:
• Discuss the layered approach to network functionality offered by the classes of the
System.Net and System.Net.Sockets namespaces
NETWORKING
FUNCTIONS
HttpWebResponse This is an implementation of the WebResponse class, specifi-
cally designed to work with the HTTP protocol.
Generic Request/Response Classes
FileWebRequest The FileWebRequest class allows you to issue requests for files
to remote servers. In other words, this class encapsulates
“file://” requests.
FileWebResponse The FileWebResponse class provides access to a file response
stream sent from a remote server.
WebRequest The WebRequest class abstracts Web-based server requests. This
class is protocol agnostic.
WebResponse The WebResponse class is used to work with Web-based server
responses. This class is protocol agnostic.
WebClient The WebClient class represents the highest level of abstraction
in the .NET networking stack. It allows for simple sending and
receiving of data from a resource. This class is protocol
agnostic.
WebException The WebException class signals errors encountered with any of
the pluggable network protocols supported by .NET.
Working with the .NET Namespaces
276
PART II
FIGURE 8.1
The network class layers in .NET.
Working from the most complex, highest level of control to the least complex, least amount of
control, the base classes can be grouped in the following way: 8
• The socket level of functionality allows direct interaction with datagrams and streams.
NETWORKING
FUNCTIONS
Representative classes include the Socket class.
• The transport level of functionality allows us access to TCP- and UDP-based features.
The core classes here are the TCPClient, UDPClient, and TCPListener.
• The application level of functionality deals principally with HTTP-based Web communi-
cations. The HTTPWebRequest and HTTPWebResponse classes represent this level of
control.
• The “Protocol Agnostic” level of functionality allows easy connection and communica-
tion to a URI without worrying about protocol specifics. A URL (for example,
www.microsoft.com) is a common form of a URI. The WebRequest, WebResponse,
and WebClient classes represent this level of functionality.
Each successive layer of the networking classes builds on the layer below it, abstracting func-
tionality and complexity for us until we reach the pinnacle of ease: the WebClient class which
can, with one line of code, open a connection to a URI (Uniform Resource Identifier) and
retrieve a result.
Working with the .NET Namespaces
278
PART II
NOTE
URI stands for Uniform Resource Identifier; it is a string that uniquely identifies a
resource. URLs, or Uniform Resource Locators, are one form of a URI that describe
Web addresses.
The .NET networking classes offer a few distinct advantages to the old Windows DNA/Win32
API style of networking code. For one, the classes are specifically designed to function in
high-load environments. That means that you won’t have to worry about their performance,
whether you are deploying them on the server side, where scalability and speed are critical, or
on the client side, where functionality tends to be more important than scalability or speed. In
addition, they provide a way to implement a simple architecture that can achieve complex
results by exposing a logical, hierarchical component object model for use.
Now that we’ve examined the general concepts of networking and seen how the System.Net
and System.Net.Sockets classes are arranged to provide programmatic access to these net-
working tasks, we can start looking at the tangible code structures that we’ll deal with when
writing our network-level software. We will start at the bottom with the Socket class.
Sockets
A socket can be thought of as a two-way pipe; it connects two end points and allows data to
flow across the pipe from one end point to the other. As a technology, it was first implemented
as a method for exposing the TCP/IP suite to calling applications. Today, most socket APIs are
generic enough to be used for almost any interprocess communication request. From an appli-
cation developer’s point of view, a socket is something that can be “plugged into” to allow data
to be sent from one endpoint to another.
Sockets are a core technology for programming applications that communicate across IP net-
works—if you program against the Winsock API or use the Winsock Control (an OCX) in
Visual Basic 6, you are already familiar with sockets and what they can do. The
System.Net.Sockets namespace provides straightforward access to this general purpose net-
working API. Working with the Socket class in .NET very closely resembles working with the
socket calls in the Win32 API. When run on the Microsoft Windows platform, the Socket class
is merely an abstraction of the Winsock libraries; in many cases it just passes its calls into the
Winsock API for execution.
Networking Functions
279
CHAPTER 8
NOTE
Keep in mind that the .NET Framework is meant to port to platforms other than
Windows. Should that happen, the Socket class would, of course, have to talk to
something other than the WinSock API, which doesn’t exist outside of Windows.
The other System.Net and System.Net.Sockets classes often build directly on top of the
Socket class for their functionality.
Creating Sockets
A common analogy used to describe the process of socket communication is the concept of a
telephone call: Before you start talking, you first have to dial a number. In our world of net-
work classes, this means establishing a connection; and that means first creating an instance of
the Socket class.
When dealing with sockets-based communication, we are primarily concerned with the two
following classes: 8
• Socket class (from the System.Net.Sockets namespace)—This represents an instance
NETWORKING
FUNCTIONS
of a sockets interface. Remember that this is a bidirectional pipeline that is used to send
and receive data across a network.
• IPEndPoint class (from the System.Net namespace)—This class encapsulates an IP
address (and thus, one end of our “pipe”).
Instancing a socket object from the Socket class is straightforward. Its constructor takes three
arguments: the address family that the socket will use, the type of socket to create, and the type
of protocol that the socket will use once created. Let’s examine each one of these in order.
The address family identifies which type of addressing schema or scope the socket will use
.NET provides us with an enumeration constant that we can use to identify our desired address
family. Appropriately, it is named AddressFamily. Table 8.2 shows the different values sup-
ported by this enumeration.
For our purposes here, we will use the InterNetwork address family (for Internet protocol
addressing).
Working with the .NET Namespaces
280
PART II
Name Description
AppleTalk AppleTalk Addressing
Atm Asynchronous Transfer Mode addressing
Banyan Banyan networking addressing
Ccitt CCITT (X.400/X.500 addressing)
Chaos MIT CHAOS addressing
Cluster Microsoft cluster addressing
DataKit DataKit protocol addressing
DataLink DataLink interface addressing
DecNet DECNet addressing
ECMA European Computer Manufacturers Association addressing
FireFox FireFox addressing
HyperChannel NSC Hyperchannel addressing
Ieee12844 IEEE 1284.4 workgroup addressing
ImpLink ARPANET IMP addressing
InterNetwork IP v4 addressing
InterNetworkv6 IP v6 addressing
Ipx Internetwork Packet Exchange addressing
Irda Infrared Data Association addressing
Iso ISO protocol addressing
Lat Local Address Table addressing
Max MAX addressing
NetBios Network Basic Input/Output System addressing
NetworkDesigners NetworkDesigners OSI protocols addressing
NS Xerox NS addressing
Osi OSI protocol addressing
Pup PUP protocol addressing
Sna IBM SNA addressing
Unix Unix local to host addressing
Unknown <unknown address family>
Unspecified <unspecified address family>
VoiceView VoiceView addressing
Networking Functions
281
CHAPTER 8
The next parameter into our socket constructor is the socket type. There are two types available
to us: stream or datagram.
NOTE
In general, stream-based communication protocols are more reliable because they
have the capability to resend packets that have been received with an error.
Datagram-based communication tends to be faster because it doesn’t have any of the
error control functionality as overhead, but of course you pay the price in reliability.
TCP packets are stream oriented, and UDP packets are datagram oriented.
Since we are specifically talking about TCP transmissions here, we will select stream. Just as
with the previous example, the System.Net.Sockets namespace contains an enumeration,
SocketType (see Table 8.3), that we can use to easily specify our stream socket type like this:
8
SocketType.SockStream
NETWORKING
FUNCTIONS
TABLE 8.3 SocketType Enumeration
Name Description
Dgram Datagram socket
Raw Raw socket
Rdm Reliably-Delivered Messages socket
SeqPacket Sequential packet socket
Stream Stream socket
Unknown Unknown socket
The last parameter required by our socket constructor is the protocol type. Once again, an enu-
meration comes to the rescue. The ProtocolType enumeration (see Table 8.4) allows us to eas-
ily specify protocols such as TCP, UDP, or SPX among others:
ProtocolType.Tcp
Working with the .NET Namespaces
282
PART II
Name Description
Ggp Gateway to Gateway Protocol
Icmp Internet Control Message Protocol
Idp Internet Datagram Protocol
Igmp Internet Group Management Protocol
IP Internet Protocol
Ipx Internetwork Packet Exchange
ND Net Disk Protocol
Pup PUP Protocol
Raw RAW UP Protocol
Spx Sequence Packet Exchange Protocol
SpxII Sequence Packet Exchange II Protocol
Tcp Transmission Control Protocol
Udp User Datagram Protocol
Unknown Unknown Protocol
Unspecified Unspecified Protocol
Putting all of this together, we arrive at the following statement that creates an instance of the
Socket class with the appropriate properties set through its constructor:
Dim ourSocket As Socket = New Socket(AddressFamily.InterNetwork, _
SocketType.Stream, ProtocolType.Tcp)
Sending Data
Now that the socket has been created, we need to connect it to its end points. Since we are
talking in terms of TCP/IP in this example, our end points will be IP addresses. The
IPEndPoint class embodies the concept of an IP-based end point, and is available in the
System.Net library. Also in the System.Net namespace is the IPAddress class. We will use an
IPAddress instance to create our IPEndPoint instance. The first order of business is to create
an IPAddress object from a valid IP address. One convenient way of doing this is by using the
IPAddress.Parse method. We can supply this method with an IP address in dotted quad form
and have an IPAddress object returned to us:
Dim localAddress As IPAddress = IPAddress.Parse(“10.0.0.1”)
Networking Functions
283
CHAPTER 8
Now, to create an IPEndPoint instance, we pass our IPAddress into the constructor, along with
a port number, like this:
Dim localEndPoint As IPEndPoint = New IPEndPoint(localAddress, 8080)
We have now specified our local address (that is, the “near” side of the connection). Now we
need to set up the remote address (the far side of the connection). Here we will assume that we
don’t know the actual IP address of the machine to which we want to connect—we just know
its host name. To resolve a host name into an IP address, we turn to the DNS class, also located
in the System.Net library. The DNS class has a Resolve method that will accept a host/server
name:
Dim targetAddress As IPAddress
targetAddress = DNS.Resolve(“www.microsoft.com”).AddressList(0)
Dim endPoint As IPEndPoint = New IPEndPoint(targetAddress, 8080)
The Resolve method returns an IPHostEntry object. This isn’t quite what we are looking for;
we just want an IPAddress object. But the IPHostEntry object exposes an AddressList prop-
erty that will return what we are looking for. In the code example, you can see that we are
going after the first IP address associated with that particular IPHostEntry by typing this:
.AddressList(0). 8
Now, all that is left to do is connect our socket to the two end points. The Socket.Bind method
NETWORKING
FUNCTIONS
will connect us with our local end point:
‘connect the socket to the local end-point
ourSocket.Bind(localEndPoint)
And the Socket.Connect method will connect us with the remote end point:
‘connect the socket using our endpoint object
ourSocket.Connect(endPoint)
Once the socket is fully connected, we can move data across the pipe. What would it take to
send a plain text message across the socket to our end point? Not much. Socket.Send accepts
a byte array for transmission across the socket. If we wanted to send a test message like
“socket programming is easy!” we first have to massage it into the required byte array form.
Again, the .NET namespaces provide us with the answer in the form of another ready to use
class. The Encoding class from the System.Text library and its GetBytes method provide a
quick way to get what we want—an array of bytes.
Dim encoder As Encoding
Dim ourMsg As Byte() = encoder.GetBytes(“socket programming is easy!”)
To send, the string requires a Send method call from our Socket class:
ourSocket.Send(ourMsg)
Working with the .NET Namespaces
284
PART II
Receiving Data
If your application is one that sits on the other end of the pipe and is a data recipient, or if you
are expecting a reply back from data that you have sent out across the socket, you will employ
the Receive method of the Socket class.
The Receive method acts in much the same way as the Send method in that it deals with byte
arrays instead of serialized strings. To get the byte array into a more usable form, we can again
use the Encoding class. This time we will call the GetString method, passing it the byte array.
Examine the code in Listing 8.1. First, we create an empty buffer to hold the data coming in
via the Receive method. The buffer is a byte array, pre-declared at a set size (we could have
used a variable array here as well). The Receive function expects an array to store the data,
and also needs to know the total length of the array and in what position in the array it should
start filling the data. Besides filling the provided buffer with the incoming data, the receive
method also returns the number of bytes received as its return parameter—and starts filling
from the beginning of the array.
After receiving all of the data, we want to shut the socket down. This is supported through the
Socket.Shutdown method. This method takes an instance of the SocketShutdown enumeration
(see Table 8.5). We can tell the socket that we want to shut down its ability to send data,
receive data, or both send and receive data.
Name Description
Both Shut down the socket from sending and receiving data
Receive Shut down the socket for receiving data
Send Shut down the socket for sending data
Module Module1
Sub ReceiveOverSocket()
‘buffer for the incoming data (capped at 256)
Dim buffArray(256) As Byte
NETWORKING
‘IPAddress(objects)
FUNCTIONS
‘A host may have more than one IP attached to it; we just want the
‘first one returned in the AddressList property (zero based)
targetAddress = Dns.Resolve(“www.microsoft.com”).AddressList(0)
Dim endPoint As IPEndPoint = New IPEndPoint(targetAddress, 8080)
‘To serialize, just pass the byte array through the GetString
‘method.
dataString = dataEncoder.GetString(buffArray, 0, numBytes)
ourSocket.Shutdown(SocketShutdown.Both)
End Sub
End Module
Working with the .NET Namespaces
286
PART II
NOTE
If you have written any socket-oriented applications in Visual Basic before, you may
already have a routine ripe for porting to .NET that will map these error numbers to
their more meaningful error descriptions.
So, we can add a catch block to a generic exception handler to deal specifically with socket
exceptions:
Sub Main()
‘The exception handler is initiated with the ‘try’ block
Try
‘this is where we would put some code that deals with
‘sockets
NETWORKING
FUNCTIONS
Catch appError As Exception
‘handle the error; here, we just alert the user
‘through a message box. To get more detailed
‘debugging level info, we could use the
‘Exception.StackTrace property...
MsgBox(“Error:” & appError.Message)
Finally
Beep()
End Try
End Sub
NOTE
Any class that inherits from the Socket class will throw SocketException instances.
This includes the DNS class as well as the TCPClient, TCPListener, and UDPClient
classes.
Working with the .NET Namespaces
288
PART II
The previous instantiation code handles the details of actually obtaining a connection to the
URI that we want. Writing data to the resulting socket is also simple. The GetStream method
defined by the TCPClient class returns an interesting structure that deserves more investiga-
tion—a NetworkStream.
Network Streams
With .NET, Microsoft has generalized the many different applications and uses of data streams
into one super-class called Stream. This powerful class, which is represented in the System.IO
namespace, can be used to represent and interact with any type of stream that you can think of
including file streams, network streams, XML data streams, and data streams from databases,
among others. As you gain more exposure to the .NET base classes, you will realize that
streams are a consistent concept that runs through the entire fabric of .NET. In fact, the
System.Net.Sockets library contains a class called NetworkStream that is a direct subclass of
the Stream class.
NOTE
Although the NetworkStream class is derived from the parent Stream class, there are a 8
few methods and properties that are not supported. The Seek and Position methods
NETWORKING
that are defined in the Stream class will throw an exception if you try to use them.
FUNCTIONS
And NetworkStream objects are not seekable (you can test for this by examining the
CanSeek property—it will always return false).
The Write method available off of the NetworkStream class can be used to send data across the
stream to the target end point. Similar to the example we looked at with the Socket send
method, the NetworkStream Write method accepts a byte array containing the actual data to be
sent across the stream. It also accepts an offset into the byte array (the point at which you want
to start sending data) and an integer representing the total number of bytes to send:
Dim netStream As NetworkStream = Tcp.GetStream()
netStream.Write(bytArray, 0, bytArray.Length)
If we were to rewrite our previous socket code using the TCPClient class, it would look some-
thing like this:
‘create our TCPClient class
Dim tcp As TCPClient = New TCPClient(“www.microsoft.com”,8080)
The third class we need to discuss at this level of networking code is the TCPListener class.
But before moving on, let’s take a minute to recap network operations using the Socket class
and the TCPClient class by looking at their key differentiators.
Socket
• Allows programming and interoperability across a wide range of protocols (including
custom protocols).
• Represents the lowest level of control over network communications in the Framework
Class Library.
TCPClient
• Streamlined and simplified specifically for Internet-based TCP communication.
• Leverages the NetworkStream class for reads and writes.
• Knows only about TCP packet construction.
• Actually built on top of the Socket class; represents a higher level of abstraction.
NOTE
There is no equivalent to the TCPListener class for UDP packets; in other words, the
UDPListener class does not exist.
The TCPListener class has an overloaded constructor. You can create an instance by providing
one of the following: a port to listen to, an IPEndPoint, or an IP address and port. Once instan-
tiated, use the Start method and Accept methods to access any incoming connection through
its socket. The Start method initiates the listener, while the Accept method actually allows a
Networking Functions
291
CHAPTER 8
connection to be made, returning either a Socket instance or a TCPClient instance that you
can then use to talk across the connection. The code in Listing 8.2 shows the specifics of lis-
tening and reacting to TCP connections. The following example creates a listener object that
waits for a TCP connection on port 8080.
Module Module1
NETWORKING
FUNCTIONS
‘If a connection is requested, the next line of code will
‘return a TCPClient object. If you wanted a socket object
‘instead of a TCPClient object, you could do the following
‘instead:
‘Dim objSocket As Socket = objListener.AcceptSocket()
‘Code will block here until a connection is attempted
Dim tcp As TcpClient = listener.AcceptTcpClient()
End Sub
End Module
Working with the .NET Namespaces
292
PART II
Creating Requests
The WebRequest and WebResponse classes form the underpinnings for the request/response
model in .NET. They represent a protocol-agnostic view; the classes themselves contain a class
factory that will manufacture the appropriate protocol-specific class depending on what
Uniform Resource Identifier (URI) you are trying to connect to. These so-called descendant
classes are protocol-specific implementations of the more generic WebRequest/WebResponse
classes. A request to an HTTP URI, for instance, would generate a HTTPWebRequest class, a
request to a file-based URI would generate an FileWebRequest class, and so on. For example,
the following code attempts a connection to a URL through the WebRequest class:
Dim rqst = WebRequest.Create(“http://www.samspublishing.com”)
Retrieving Responses
We can then use the GetResponse method off of the WebResponse class to retrieve the
response generated by our target URI:
Dim resp = rqst.GetResponse()
It is important to note a subtle difference in dealing with these classes: You do not directly cre-
ate instances of these classes. Rather, you must rely on the Create method to generate new
instances of the WebRequest class and the GetResponse method to generate new instances of
the WebResponse class.
After retrieving the response, we can deal with the stream it represents by using the
GetResponseStream method:
From this point, our interaction can follow the same design patterns that we previously saw
when discussing streams created from the TCPClient class.
Networking Functions
293
CHAPTER 8
NOTE
If there is no compelling reason to deal with protocol-specific properties, it is better
to construct your request/response designs by using the WebRequest and WebResponse
classes. In this way, if new protocols are added to .NET, your code will automatically
function appropriately with those protocols with no coding changes necessary.
The resulting HTTPWebResponse object offers a slightly different pattern of properties and
methods than the parent WebResponse class. We now have access to new properties and meth-
ods that were not previously available. In addition, the methods and properties defined by the 8
WebResponse class have been overridden to return or process HTTP information. The
NETWORKING
FUNCTIONS
ProtocolVersion property, for instance, does not exist on the WebResponse base class but is
implemented on the HTTPWebResponse class to return the actual version of the HTTP protocol
with which the response was formatted. This is a good example of a subclass providing a new
member to the base class. An example of a property being overridden is found in the Headers
property: This method now returns HTTP-specific name/value pairs. For more information on
actual properties and methods supported, see the reference located at the end of this chapter.
You can examine the StatusCode property on the HttpWebResponse class to get access to the
HTTP status codes returned as part of a response. It returns an enumeration that evaluates to
the HttpStatusCode enumeration, shown in Table 8.7.
Equivalent HTTP
Name Status Code Description
Accepted 202 The request was accepted.
Ambiguous 300 The server couldn’t decide
what to return (equivalent to
MultipleChoices).
BadGateway 502 An intermediate proxy server
received a bad response.
Working with the .NET Namespaces
294
PART II
NETWORKING
FUNCTIONS
could not be found on the server.
NotImplemented 501 The server does not have the func-
tionality required to fulfill the
request.
NotModified 304 The requested resource has not
been changed from the client’s
cached copy.
OK 200 The request was successful, the
requested data is in the response.
PartialContent 206 In response to a GET command that
included a byte range, the server
answered with a partial response.
PaymentRequired 402 Not currently defined in the HTTP
protocol.
PreconditionFailed 412 The server can not meet one or
more of the conditional request
headers specified.
Working with the .NET Namespaces
296
PART II
NETWORKING
FUNCTIONS
Unauthorized 401 The requested resource requires
proper authentication, which was
not supplied.
UnsupportedMediaType 415 The server has refused the request
because the request itself is an
unsupported type.
Unused 306 Not currently defined in HTTP 1.1.
UseProxy 305 The requested resource needs to be
accessed through the proxy identi-
fied in the Location header.
Listing 8.3 pulls together all the request/response objects we have talked about for creating
requests, receiving responses, and dealing with exceptions. This console application attempts
to open a Web page (by issuing a request), and then displays the results to the console window
(by writing out the response). Any errors encountered along the way are also written out to the
console window.
Module Module1
Sub Main()
Try
Networking Functions
299
CHAPTER 8
‘Read from the stream and write any data to the console.
numBytes = netStream.Read(buffArray, 0, buffArray.Length)
8
While numBytes > 0
NETWORKING
FUNCTIONS
For currIndex = 0 To numBytes - 1
Console.Write(“{0}”, buffArray(currIndex))
Next currIndex
Console.WriteLine()
numBytes = netStream.Read(buffArray, 0, buffArray.Length)
End While
End If
Catch appErr As Exception
‘ non-web error raised...
Console.WriteLine(“An app error was encountered: {0}”, _
appErr.ToString)
Finally
Console.WriteLine(“...”)
Console.WriteLine(“Finished.”)
Console.WriteLine(“Hit <ENTER> to exit.”)
Console.ReadLine()
End Try
End Sub
End Module
Networking Functions
301
CHAPTER 8
To upload data to a server, all we need to know is the URI and a byte array with the data that 8
we want to send. The following code shows how easy this is:
NETWORKING
FUNCTIONS
Dim web As New WebClient()
web.UploadData(myURL, byteArray())
Uploading an actual file is just a slight variation on this syntax. Instead of a byte array of data,
we pass in a fully qualified filename:
web.UploadFile(myURL, myFile)
NOTE
Both the UploadData and UploadFile methods perform their work through HTTP
POST commands.
The DownloadData and DownloadFile methods are mirrors of their upload counterparts that we
just discussed. The DownloadData method takes an address parameter and returns a byte array
of the data that was downloaded. The DownloadFile method takes an address and a fully quali-
fied filename (and doesn’t return anything).
buffArray() = web.DownloadData(myURL)
web.DownloadFile(myURL, localFilename)
Working with the .NET Namespaces
302
PART II
Overview
First, let’s talk through a scenario: You want to issue a request for a Web page from a server.
After the request has been made, your application should continue without waiting for the
response. Once the response comes back, your application should be signaled somehow, so
that it can now deal with the data it has received. This sets the stage for our asynchronous
request/response design pattern.
With this scenario in mind, let’s walk through each step and see how we can use the intrinsic
capabilities of the .NET Framework and the networking classes to handle each piece of this
pattern.
Networking Functions
303
CHAPTER 8
This will create an AsyncCallBack instance with the address of the subroutine that needs to
react to the callback.
The state object, used in the BeginGetResponse method call, is a little tricky to understand 8
until we get farther into the async process. For now, just accept the fact that it is used to persist
NETWORKING
FUNCTIONS
data between asynchronous method calls. One of the things that we are interested in persisting
is the actual WebRequest object used to make the BeginGetResponse call. Other than its
slightly confusing reason for existence at this point, there is really no mystery to this state
object. It is simply an instance of a class that you create to hold state through properties. In
this example, we could define the class like this:
Public Class State
Public httpRqst As HttpWebRequest
Note that we have explicitly defined the request property as an HttpWebRequest instance since
we know we are going after a Web page. To actually issue our async request, two things need
to be done. First, the request object itself must be instantiated and then assigned into an
instance of the State class that we created:
Dim rqst As HttpWebRequest = WebRequest.Create(someURL)
Dim aState As State = New State()
aState.httpRqst = rqst
Working with the .NET Namespaces
304
PART II
End Sub
This IAsyncResult interface is the key here: We will use it to get to the response object.
Remember that the WebResponse object is what is created in response to the GetResponse
method. The same holds true for our BeginGetResponse call—we will need to get a handle to
the resulting WebResponse object in order to examine the response data. The EndGetResponse
method will return us the response object that we are looking for. First, the request object will
need to be pulled back out of the state object’s property, and then the EndGetResponse method
will be called:
Public Sub ReceiveResponse(rslt As IAsyncResult)
Dim retState As State = CType(rslt.AsyncState, State)
Dim httpRqst As HttpWebRequest = retState.httpRqst
NOTE
Just because you have implemented an async pattern with the WebRequest and
WebResponse classes doesn’t mean that you have to do so with the Stream class as
well. After receiving the response instance, you could simply interact with its stream
synchronously. Microsoft, however, strenuously advises against “mixing” synchronous
access with asynchronous in a tightly bound process like this for performance reasons.
It is often easiest to just reuse the state object you have already created; add a few new proper-
ties to the class to hold the read buffer for the stream, and perhaps the concatenated results of
the multiple stream reads, and you are all set.
Dim respStream As Stream = resp.GetResponseStream()
retState.respStream = respStream
NETWORKING
FUNCTIONS
this point, you should recognize the stream read pattern from our previous sections.
Public Sub ReadStream(rslt As IAsyncResult)
Dim retState As RequestState = rslt.AsyncState
Figure 8.2 shows the entire pattern laid out from a process flow perspective. You can see that
Step 1 is indeed a call to BeginGetResponse.
1:BeginGetResponse
9: EndRead
2: (callback)
8: (callback)
4: (instantiate)
7: BeginRead
5: GetResponseStream
WebResponse
Stream
6: (instantiate)
FIGURE 8.2
A basic async flow for the WebRequest/WebResponse classes.
NETWORKING
scape of network resources and servers that blindly accept our requests for connections, file
FUNCTIONS
downloads, resource uploads, and protocol queries. This has been useful to conduct our quick
tour of these networking classes, but the real world acts a bit differently: Conscientious server
administrators and software architects tend to require proper credentials from a client before
handing over the rights to access files or traverse Web directories. In this section, we will talk
about the System.Net structures that allow programmers to provide credentials when querying
resources. We will also talk a bit about using proxies in our programming efforts.
Authentication Methods
Before we look at the actual helper classes for dealing with credentials and authentication,
we’ll take a high-level look at the different types of authentication supported by the .NET
classes. For Internet communication, there are primarily five different types of authentication
supported: Basic, Digest, NTLM, Kerberos, and Negotiate.
You may already be familiar with these concepts if you have been responsible for structuring
security on IIS Web servers before—particularly, with IIS 8.0 and above. For a more in-depth
treatment of security in general, you may want to look at Chapter 14, “Browser/Server
Communications,” where we cover security functions. Because we will be referencing some of
these in our code examples in this section, here is some basic information on these different
authentication methods:
Working with the .NET Namespaces
308
PART II
• Basic Authentication: This is a clear-text method in which the username and password
are encoded (not encrypted) and then sent to the server.
• Digest Authentication: This is an encrypted method of authentication: The server will
issue a nonce—a random data string—that the client then uses to encrypt its credential
information. This information is sent back to the server where it is compared to the
expected values. If the two match, authentication is accomplished.
• Kerberos Authentication: This method of authentication relies on users being authenti-
cated by a Kerberos Authentication Server. Once authenticated, the user uses this
encrypted “ticket” as a pass to access specific services.
• NTLM: Probably more commonly known as Windows NT Challenge/Response, this also
is an encrypted method of sending user credentials. In this case, the encryption is based
on a hash algorithm and it is also uuencoded. NTLM stands for NT LAN Manager.
Now let’s look at the specific classes that will help us to actually use credentials when query-
ing network resources.
Encapsulating Credentials
The NetworkCredential class is used to encapsulate credentials for use when requesting a
resource. Credentials are simply pieces of identifying data that can be associated with a level
of authorization in a given system. When we log into a Windows 2000 Server, we supply a
login name (or username) and password as credentials.
NOTE
Other authentication schemes may make use of other forms of credentials, but the
login name and password pair is arguably the most common form that you will bump
into in your programming efforts, and certainly the prevailing form of credentials on
the Web.
As we saw in the previous paragraphs, these credentials are commonly encoded or encrypted
using standardized methods.
The NetworkCredential class is fairly light in terms of properties and methods. It allows you
to supply password and username pairs through the Password and UserName properties—you
can also specify these items through the class constructor. The class also supports specification
of a domain through the Domain property. In terms of methods, its one unique method is the
GetCredential method that accepts a URI and an authentication type and returns a
NetworkCredential instance.
Networking Functions
309
CHAPTER 8
Probably the most common and easiest use of this class will be through simple instantiation
and the use of its constructor. The following code creates a NetworkCredential instance with
the username “John Doe”, password “fortknox”:
Dim creds As NetworkCredential = new NetworkCredential( “John Doe”, _
“fortknox”)
The NetworkCredential class is to be used in conjunction with some of the other classes that
we have talked about in the previous sections of this chapter. Let’s take a look at how we can
use an instance of the NetworkCredential class in conjunction with the WebClient class and
the WebRequest class. In general, most of the network classes support the specification of cre-
dentials through a Credentials property. This property can be assigned an instance of a
NetworkCredential object. The specified data is then presented for authentication (if needed)
to the resource controller (server). In essence, this is meant to provide evidence of your code’s
capability to perform the particular function or access the particular resource that you are tar-
geting. Thus, to specify credentials for use with a WebClient instance, we can say:
Dim web as WebClient
web.Credentials = New NetworkCredential(“John Doe”, “fortknox”)
NETWORKING
rqst.Credentials = New NetworkCredential(“John Doe”, “fortknox”)
FUNCTIONS
Table 8.9 shows the classes in the System.Net and System.Net.Sockets namespaces that sup-
port the use of the Credentials property.
Using the NetworkCredential class in the ways we have shown is best if you are dealing only
with a small number of URIs that you need to access, or if you will be providing the same set
of credentials for each of the URIs you need to access. If you are dealing with a multitude of
URIs, each with their own credentials that have to be passed, the preferred solution is to use
the System.Net structure for caching credentials: the CredentialCache class.
Working with the .NET Namespaces
310
PART II
Module Module1
Sub Main()
‘the WebClient we will use with the cred cache
Dim myClient As WebClient
End Module
Caching your credentials centralizes maintenance of logins in your code, and helps to make for
a much more robust solution.
Networking Functions
311
CHAPTER 8
NOTE
The WebProxy constructor has many different, overloaded forms. We could, for
instance, have supplied a URI instance instead of a string to specify our URL. Check
with the .NET Framework SDK documentation to see which constructor works best
8
for your particular situation.
NETWORKING
FUNCTIONS
If you do not specify a proxy when using the request classes, the system will use the global
proxy server setting. By default, this will be set to whatever your local Internet Explorer set-
tings are set. You can also change this global setting by using the GlobalProxySelection
class. Let’s look at some code that sets a global proxy server; the proxy settings we implement
will hold for all created instances of WebRequest/HTTPWebRequest objects, unless we choose
to explicitly override them by using the Proxy property that we just discussed.
This code will have the same effect as our previous code example: All requests made through
the request object that we have created will be routed through http://ourproxy:8080.
Dim myProxy As WebProxy = New WebProxy(“http://ourprxy:8080”)
GlobalProxySelection.Select = myProxy
are trying to connect. Refer to the .NET Framework SDK documentation for more infor-
mation on how this class can help you to centralize connection management and speed
up URI requests.
➲ General information on security in the .NET Framework can be found in Appendix C,
“.NET Security Models.”
FIGURE 8.3
The SocketTransmitter application.
8
3. Type in a message/command to send to the remote end point. Confused about what to
NETWORKING
FUNCTIONS
enter for a command? Try researching the different protocol standards to see what
common sets of commands they have defined. To get you started, you could try the
following:
• Connect to a server that you know is a newsgroup server. Then issue NNTP-
specific commands such as “authinfo user xxxx pass yyyy” where xxxx is your
login id and yyyy is your password. Once logged in, try selecting a specific news-
group by sending “GROUP xxxx” where xxxx is the newsgroup name. From there,
you can read different articles by using the article command (“ARTICLE xxxx”,
where xxxx is the article number).
• Connect to an HTTP server and then request a Web page by using the GET com-
mand. “GET / HTTP/1.1\r\nHost: “ + server + “\r\nConnection: Close\r\n”.
• You can get more ideas of command by examining the actual Request for
Comments or Standards available at www.w3.org.
4. The messages sent out, the responses received, and any errors encountered should all
show up in the Activity Log.
Working with the .NET Namespaces
314
PART II
Code Walkthrough
Listing 8.4 walks you through the code of a sample application—the Socket Transmitter.
For the most part, the Windows Forms designer generates this code. We have added code to
initialize the visual state of the form and to actually instantiate a socket instance through a call
to a private subroutine called CreateSocket.
‘Add any initialization after the InitializeComponent() call
SetFormState(“INITIAL”)
CreateSocket()
End Sub
NETWORKING
‘It can be modified using the Windows Form Designer.
FUNCTIONS
‘Do not modify it using the code editor.
<System.Diagnostics.DebuggerStepThrough()> Private Sub _
InitializeComponent()
Me.Label4 = New System.Windows.Forms.Label()
Me.buttonBind = New System.Windows.Forms.Button()
Me.Label1 = New System.Windows.Forms.Label()
Me.Label2 = New System.Windows.Forms.Label()
Me.Label3 = New System.Windows.Forms.Label()
Me.textBoxLocalPort = New System.Windows.Forms.TextBox()
Me.checkResolveRemote = New System.Windows.Forms.CheckBox()
Me.GroupBox2 = New System.Windows.Forms.GroupBox()
Me.buttonSend = New System.Windows.Forms.Button()
Me.textBoxSend = New System.Windows.Forms.TextBox()
Me.GroupBox3 = New System.Windows.Forms.GroupBox()
Me.listBoxActivity = New System.Windows.Forms.ListBox()
Me.buttonClear = New System.Windows.Forms.Button()
Me.checkResolveLocal = New System.Windows.Forms.CheckBox()
Me.buttonConnect = New System.Windows.Forms.Button()
Me.textBoxLocal = New System.Windows.Forms.TextBox()
Me.textBoxRemote = New System.Windows.Forms.TextBox()
Me.StatusBar = New System.Windows.Forms.StatusBar()
Me.GroupBox1 = New System.Windows.Forms.GroupBox()
Me.textBoxRemotePort = New System.Windows.Forms.TextBox()
Me.GroupBox2.SuspendLayout()
Working with the .NET Namespaces
316
PART II
NETWORKING
FUNCTIONS
Me.GroupBox2.Size = New System.Drawing.Size(396, 52)
Me.GroupBox2.TabIndex = 1
Me.GroupBox2.TabStop = False
Me.GroupBox2.Text = “Send a Message”
‘
‘buttonSend
‘
Me.buttonSend.Location = New System.Drawing.Point(316, 20)
Me.buttonSend.Name = “buttonSend”
Me.buttonSend.Size = New System.Drawing.Size(72, 20)
Me.buttonSend.TabIndex = 1
Me.buttonSend.Text = “Send”
‘
‘textBoxSend
‘
Me.textBoxSend.Location = New System.Drawing.Point(8, 20)
Me.textBoxSend.Name = “textBoxSend”
Me.textBoxSend.Size = New System.Drawing.Size(300, 20)
Me.textBoxSend.TabIndex = 0
Me.textBoxSend.Text = “”
‘
‘GroupBox3
‘
Me.GroupBox3.Controls.AddRange(New System.Windows.Forms.Control() _
{Me.listBoxActivity, Me.buttonClear})
Working with the .NET Namespaces
318
PART II
NETWORKING
FUNCTIONS
Me.GroupBox1.Controls.AddRange(New System.Windows.Forms.Control() _
{Me.Label4, Me.textBoxRemotePort, Me.textBoxLocalPort, Me.Label3, _
Me.checkResolveRemote, Me.checkResolveLocal, Me.buttonConnect, _
Me.buttonBind, Me.Label2, Me.textBoxRemote, Me.textBoxLocal, _
Me.Label1})
Me.GroupBox1.Location = New System.Drawing.Point(8, 4)
Me.GroupBox1.Name = “GroupBox1”
Me.GroupBox1.Size = New System.Drawing.Size(400, 172)
Me.GroupBox1.TabIndex = 0
Me.GroupBox1.TabStop = False
Me.GroupBox1.Text = “Specify the Socket End-Points”
‘
‘textBoxRemotePort
‘
Me.textBoxRemotePort.Location = New System.Drawing.Point(76, 116)
Me.textBoxRemotePort.MaxLength = 5
Me.textBoxRemotePort.Name = “textBoxRemotePort”
Me.textBoxRemotePort.Size = New System.Drawing.Size(40, 20)
Me.textBoxRemotePort.TabIndex = 1
Me.textBoxRemotePort.Text = “”
‘
‘SocketTransmitter
‘
Me.AutoScaleBaseSize = New System.Drawing.Size(5, 13)
Me.ClientSize = New System.Drawing.Size(416, 509)
Working with the .NET Namespaces
320
PART II
End Sub
#End Region
End Sub
This routine creates a new socket instance. We use a structured exception handler to catch any
errors at this point and display them to the user.
Private Sub CreateSocket()
‘This creates our socket object; we have hard-coded the address family,
‘socket type, and protocol type. Feel free to lay around with these
‘settings...
Try
netSocket = New Socket(AddressFamily.InterNetwork, _
SocketType.Stream, ProtocolType.Unspecified)
End Sub
Clicking on the Bind button will call this routine. This assumes that CreateSocket has previ-
ously been called, and that we now have a valid socket object (netSocket) to work with. If the
local end point address requires DNS resolution, we indicate this through the useDNS Boolean
parameter. The local parameter identifies the local address for the bind, and the port parame-
ter identifies the local port for the bind.
Networking Functions
321
CHAPTER 8
If useDNS Then
‘User has supplied a server name; we first need to resolve it
‘using the DNS class
localAddress = Dns.Resolve(“local”).AddressList(0)
Else
‘User has supplied an IP address in dotted quad format; just
‘use the IPAddress.Parse method to get at the actual
‘IPAddress instance
localAddress = IPAddress.Parse(local)
End If
NETWORKING
FUNCTIONS
Catch sockErr As SocketException
‘A socket exception was encountered.
End Try
End Sub
Working with the .NET Namespaces
322
PART II
If useDNS Then
‘User has supplied a server name; we first need to resolve it
‘using the DNS class
serverAddress = Dns.Resolve(server).AddressList(0)
Else
‘User has supplied an IP address in dotted quad format; just
‘use the IPAddress.Parse method to get at the actual
serverAddress = IPAddress.Parse(server)
End If
End Try
End Sub
This is where all of the action is. The SendToSocket routine takes the text “command” or mes-
sage typed in and sends it across the socket to the machine sitting at the remote end point.
After sending the message, the ReceiveFromSocket routine is called.
Private Sub SendToSocket(ByVal msg As String)
‘This routine sends data across the socket and then
‘receives the reply.
8
‘Translate the string into a byte array
Dim encoder As Encoding
NETWORKING
FUNCTIONS
Dim sendMsg As Byte() = encoder.GetBytes(msg)
Try
Dim bytesSent As System.Int32
ReceiveFromSocket()
End Try
End Sub
The ReceiveFromSocket subroutine collects data coming back across the socket in response to
the message sent. It will loop through the bytes received until there are no more left; it then
writes the response out to the activity log on the main form.
Private Sub ReceiveFromSocket()
Try
Dim bytesRcvd As System.Int32
Dim rcvBuffer As Byte()
Dim reply As String
End Try
8
NETWORKING
FUNCTIONS
End Sub
This is just a utility routine; it is responsible for doing some basic formatting on the “mes-
sages” we write to the screen by way of the activity log.
Private Sub WriteActivity(ByVal msg As String, ByVal msgType As String)
Dim prefix As String
End Sub
Case “BOUND”
textBoxLocal.Enabled = False
buttonBind.Enabled = False
checkResolveLocal.Enabled = False
textBoxRemote.Enabled = False
checkResolveRemote.Enabled = False
textBoxSend.Enabled = False
buttonSend.Enabled = False
Case “CONNECTED”
textBoxLocal.Enabled = False
buttonBind.Enabled = False
checkResolveLocal.Enabled = False
textBoxRemote.Enabled = False
checkResolveRemote.Enabled = False
textBoxSend.Enabled = True
buttonSend.Enabled = True
End Select
End Sub
This is a helper class that we use to provide more descriptive error descriptions whenever the
application encounters an actual WinSock exception.
Public Class WinSockError
Public Function GetDescription(ByVal errNum As System.Int32) As String
NETWORKING
FUNCTIONS
GetDescription = “Address family not supported by protocol _
family”
Case 10037
GetDescription = “Operation already in progress”
Case 10053
GetDescription = “Software caused connection abort”
Case 10061
GetDescription = “Connection refused”
Case 10054
GetDescription = “Connection reset by peer”
Case 10039
GetDescription = “Destination address required”
Case 10014
GetDescription = “Bad address”
Case 10064
GetDescription = “Host is down”
Case 10065
GetDescription = “No route to host”
Case 10036
GetDescription = “Operation now in progress”
Case 10004
GetDescription = “Interrupted function call”
Case 10022
GetDescription = “Invalid argument”
Working with the .NET Namespaces
328
PART II
End Function
End Class
NETWORKING
the form and by watching the actual price column—it will transition from “waiting” to an
FUNCTIONS
actual dollar amount (that is, if the program was successful in parsing a price out of the HTML
response). Here are a few notes on the application:
• The application will prompt you for an ISBN number (some very basic bounds and pat-
tern checking will be done to see if you actually typed in a valid ISBN from a format
perspective).
• The application will then go out and query a list of Web site resources that function as
ISBN-based interfaces into a bookseller’s Web site.
• The resulting HTML response is parsed in an attempt to find the price of the book. The
application stores a specific string pattern by each site entry; it will look for this pattern
when attempting to discern the actual pricing information. Note that HTML parsing is a
pretty poor way to derive data from a Web page; it would be far better if the data was
delineated in an XML format!
FIGURE 8.4
ISBNCrawler: The main dialog.
This dialog is pretty self-explanatory. Simply select which of the “big three” you want to query
for a particular book, enter the ISBN number in the text box, and press the Go button. You
should see a message that the server is being queried. After being queried, the hourglass should
go away, control should be returned back to the application, and the form will indicate that it is
waiting for a response.
Once the response is received, the raw HTML will be displayed in the Returned HTML text
box. If the application was successful at parsing a price out of the HTML, it will show up to
the right of the Go button.
Code Walkthrough
Listing 8.5 walks you through the code of a sample application—the ISBNCrawler.
We hold the actual URL of the site to be queried in the form-local variable targetSite. We
also hold our State object at this scope as well.
‘Currently targeted site
Dim targetSite As String
Dim aState As State = New State()
These are the constants being used for the ISBN query facility for each site.
Const AMAZON_QRY As String = “/exec/obidos/ASIN/”
Const BARNES_QRY As String = “/isbninquiry.asp?isbn=”
Const BORDERS_QRY As String = “/fcgi-bin/db2www/search/search.d2w/
➥Details?mediaType=Book&searchType=ISBNUPC&code=”
Windows Form Designer Code: nothing special here, although we do “initialize” the targetSite
variable with the URL for Amazon.com.
#Region “ Windows Form Designer generated code “
8
Public Sub New()
MyBase.New()
NETWORKING
FUNCTIONS
‘This call is required by the Windows Form Designer.
InitializeComponent()
NETWORKING
FUNCTIONS
‘
Me.textBoxHTML.Location = New System.Drawing.Point(12, 24)
Me.textBoxHTML.Multiline = True
Me.textBoxHTML.Name = “textBoxHTML”
Me.textBoxHTML.Size = New System.Drawing.Size(424, 180)
Me.textBoxHTML.TabIndex = 4
Me.textBoxHTML.Text = “”
‘
‘GroupBox2
‘
Me.GroupBox2.Controls.AddRange(New System.Windows.Forms.Control() _
{Me.RadioButton3, Me.RadioButton2, Me.RadioButton1})
Me.GroupBox2.Location = New System.Drawing.Point(16, 12)
Me.GroupBox2.Name = “GroupBox2”
Me.GroupBox2.Size = New System.Drawing.Size(208, 112)
Me.GroupBox2.TabIndex = 1
Me.GroupBox2.TabStop = False
Me.GroupBox2.Text = “Target Site”
‘
‘RadioButton3
‘
Me.RadioButton3.Location = New System.Drawing.Point(20, 80)
Me.RadioButton3.Name = “RadioButton3”
Me.RadioButton3.Size = New System.Drawing.Size(172, 16)
Me.RadioButton3.TabIndex = 2
Me.RadioButton3.Text = “Borders.com”
‘
Working with the .NET Namespaces
334
PART II
End Sub
#End Region
8
When you press the Go button, we first check to see if a valid ISBN number was entered (this
is a rudimentary check at best). If everything checks out, we change the cursor, display a mes-
NETWORKING
FUNCTIONS
sage through the form title bar, and then call IssueAsyncRequest.
Private Sub buttonGo_Click(ByVal sender As System.Object, ByVal e As _
System.EventArgs) Handles buttonGo.Click
‘We will first pass the ISBN number through a short validation
‘routine; if everything looks good, we can go ahead and issue our async
‘requests.
If ValidISBN(Trim(textBoxISBN().Text)) Then
buttonGo.Enabled = False
Me.Cursor = System.Windows.Forms.Cursors.WaitCursor
Me.textBoxHTML.Text = “”
Me.Text = “ISBNCrawler - Issuing request”
targetSite = targetSite & Trim(textBoxISBN.Text)
IssueAsyncRequest()
Me.Text = “ISBNCrawler - Waiting for response...”
Me.Cursor = System.Windows.Forms.Cursors.Default
buttonGo.Enabled = True
Else
IssueAsyncRequest launches a request object, thereby starting off the async design pattern.
The WebRequest object is created with the URL specified in targetSite, and then the
BeginGetResponse method is called.
ResetFormCursor()
ResetFormCursor()
End Try
End Sub
This is the subroutine, which should receive the callback once a response is received. After
retrieving the response stream, an asynchronous read is started on the stream by calling
BeginRead.
Networking Functions
337
CHAPTER 8
NETWORKING
FUNCTIONS
Dim respStream As Stream = httpResp.GetResponseStream()
ResetFormCursor()
ResetFormCursor()
End Try
End Sub
The ReadStream subroutine received the call back from the async stream read. If more bytes
remain to be read, it will call itself again until the data is exhausted. Once all of the data has
been read in, its entirety is written out to the HTML results box, and the parsed price (if one
was found) is displayed to the screen as well.
Private Sub ReadStream(ByVal rslt As IAsyncResult)
Try
‘Get the state object from the async result
Dim retState As State = CType(rslt.AsyncState, State)
Else
‘No more data in the stream; parse the price out and
‘write the HTML to the form
Dim price As String = ParsePrice(retState.RqstData.ToString)
Me.textBoxHTML.Text = retState.RqstData.ToString
Me.Cursor = System.Windows.Forms.Cursors.Default
End If
ResetFormCursor()
8
NETWORKING
FUNCTIONS
MsgBox(“An IO error occurred with the stream object->” & _
streamErr.Message & “;” & streamErr.StackTrace)
ResetFormCursor()
End Try
End Sub
ParsePrice attempts to pull the actual book price out of a string by looking for the pattern
“Our Price:”. Again, this is not the best way to do things, but it suffices for the scope of this
demonstration.
Private Function ParsePrice(ByVal resp As String) As String
‘This routine just performs some rudimentary guessing in terms of the
‘price returned to us in the response object
Dim currPos As Integer
Dim priceGuess As String
Working with the .NET Namespaces
340
PART II
End Function
‘now look for evidence that it is not (these are not exhaustive _
obviously...)
If isbn.Length > 10 Or isbn.Length < 10 Then
ValidISBN = False
ElseIf Not IsNumeric(isbn) Then
ValidISBN = False
End If
End Function
End Sub
This class is our state class, responsible for holding onto our stream items and request/response
items between async calls.
8
Imports System
Imports System.Net
NETWORKING
FUNCTIONS
Imports System.Text
Imports System.IO
End Sub
End Class
Working with the .NET Namespaces
342
PART II
Summary
The networking classes exposed in the class library represent a very powerful tool for the
Visual Basic .NET developer. The ease with which developers can perform complex opera-
tions, coupled with the capability to write low-level network functions, represents a large step
forward from Visual Basic’s previous abilities in this arena.
In this chapter, we examined:
• Socket programming using the System.Net.Sockets namespace
• Sending and receiving TCP/IP network traffic using the TCPListener and TCPClient
classes
• Using HTTP-specific, as well as protocol-agnostic, classes to issue requests to Web
servers and react to their responses
• How to employ a typical .NET design pattern with the networking classes to allow appli-
cations to issue and receive data in an asynchronous fashion
• Creating variables that are specific and local to individual threads
Drawing Functions CHAPTER
9
IN THIS CHAPTER
• Key Classes Related to Drawing 344
• Transformations 375
Nothing in Windows gets to the user’s screen without the aid of a drawing function. This
includes images, colors, and even text. The OS must render all things visually by drawing pix-
els to an output device (monitor, printer, and so on). Of course, Windows does a good job of
hiding drawing functions from the average developer. When did you last need to call an API
function to display text to the screen or change the background color of a button? Our controls,
compiler, and operating system serve to limit our need to make direct calls into the drawing
library. However, there is always the case where your application requirements are beyond the
scope of what can be done with controls and so on. Perhaps you must create custom pie charts
for your users on-the-fly. Or maybe you need to allow your users to view a group of fonts or
send output to the printer. Chances are that you will eventually need to write your own custom
visual display code. This is where the .NET drawing library comes into play. It provides you
with a host of classes that make adding drawing capabilities to your application easy and fun.
This chapter illustrates common programming tasks using the namespaces related to drawing
in the .NET Framework Class Library. The chapter starts by illustrating the key classes used to
execute drawing functions with the namespace. Then follows a detailed discussion of these key
classes and related code examples. Lastly, we will create a simple drawing application that
serves to demonstrate how these classes can be used in the context of a larger application and
serves as an experimental ground.
After reading this chapter, you should be able to do the following:
• Understand how Windows manages coordinates
• Draw basic shapes including lines, curves, rectangles, and polygons
• Fill shapes and lines with various colors, patterns, and gradients
• Work with groups of shapes
• Work with bitmaps and icons in your application
• Rotate, stretch, and skew graphics
FUNCTIONS
DRAWING
Pen The Pen class is used to draw the outlines of objects (lines, rectan-
gles, ellipses, and so on). It defines the line weight and color simi-
lar to an actual pen.
Rectangle The Rectangle structure stores information about a rectangle
(location, width, and height). This structure is used to draw rectan-
gles, ellipses, pies, and so on.
CustomLineCap The CustomLineCap class is used to create a custom, user-defined
end cap for a line.
Working with the .NET Namespaces
346
PART II
GDI+
All drawing with .NET-managed code happens through the Graphics Device Interface plus
(GDI+) layer. GDI+ is the new API Windows uses to provide the .NET Framework with graph-
ics, imaging, printing, and typography capabilities. Prior to .NET, VB programmers mostly had
to rely on Win32 API calls into GDI to execute drawing functions. In .NET, GDI+ is wrapped
by the drawing namespace. This provides easy, object-oriented access to drawing functions
from all .NET languages.
GDI+ shields your application from having to deal with the details and particulars of device
drivers. It allows you to send output to the screen or printer without concern for calling into
the driver that manages a given device. For example, your application need not write new code
to support an Epson printer versus an HP; think if you had to write new code for every graph-
ics card your application had to support. Instead, GDI+ makes the calls to the specific device
driver for us, thus insulating our application from the hardware and allowing us to easily create
device-independent software.
Practical Applications
GDI+ provides objects like pens and brushes—objects used by programmers to illustrate ideas
and to create tools for their users to do the same. If you are creating applications with illustrat-
9
ing capabilities, you see the obvious need for drawing functions. For instance, if your applica-
tion allows users to select a color, you’ll most likely use the Color structure or the
FUNCTIONS
DRAWING
ColorPalette class.
Beyond illustration applications, however, you might be surprised by how often drawing func-
tions are required. For instance, word processing applications use lines and curves to render
borders for tables, pages, and around text. A search word game might use the DrawLine func-
tion to cross out words as users find them. Spreadsheet applications and the like could use the
DrawPie method to create pie charts based on user data. CAD (computer-aided design) applica-
tions outline objects and calculate distance between points with lines and curves. Even Web
applications might create graphics on the server based on user-submitted data. These images
could be stored to the file system and displayed out to the user’s browser. You can see that,
before long, you will more than likely need to execute drawing functions with the .NET
Framework Class Library. So, let’s get started learning to draw using the .NET namespaces.
Working with the .NET Namespaces
348
PART II
Drawing Basics
Most computer-based drawing is done on a two-dimensional plane using a basic set of objects.
These objects are like building blocks. In the hands of a competent craftsman, they can be
manipulated to create interesting effects and complex shapes. But before we can build the sky-
scraper, we must first set the basic foundation.
origin
x-axis
(0, 0)
FIGURE 9.1
Windows default coordinate system.
Drawing Functions
349
CHAPTER 9
Pens
The companion to the Graphics class is the Pen class. In fact, in order to draw nearly anything,
you’ll need at least a Graphics and a Pen instance. The Pen class is used to define how the out-
lines of shapes are rendered to the surface. Similar to a real pen, the Pen class defines a width
color that will be used to do the drawing. Additionally, you can create a Pen based on a Brush
instance. This allows you to draw with more stylized lines. Table 9.2 demonstrates the various
constructors that are available to you when creating new Pen instances.
Constructor Description
New Pen(Color, Single) Creates a Pen object based on a color (Color structure) and a
width (Single) in pixels.
New Pen(Color) Creates a Pen object based on a color defined by the Color
structure. Sets the pen’s width to the default of 1 pixel. 9
New Pen(Brush, Single) Creates a Pen object using a valid class derived from the
FUNCTIONS
Brush base class. The pen’s width (Single) is defined in
DRAWING
pixels.
New Pen(Brush) Creates a Pen object based on a valid Brush object. Sets the
pen’s width to the default of 1.0 pixels.
The Color parameter, used in the Pen constructor, is defined by an instance of the Color struc-
ture. The Color structure represents an ARGB (Alpha, Red, Green, and Blue) color. Most col-
ors come predefined as properties of the Color structure for easy use. For example, Color.Red
indicates the ARGB equivalent of red. There are a wide variety of predefined colors, every-
thing from LawnGreen to Tomato to Transparent. Additionally, you can call methods of the
Color structure to create custom colors or return the brightness, saturation, and hue of a given
color.
Working with the .NET Namespaces
350
PART II
Lines
So far, we’ve talked about pens and drawing methods but have yet to render anything to the
screen. Now we’ll use a Pen object to draw a line onto a form. This may not seem exciting, but
it provides a foundation.
A line is a set of pixels linked by a start and end point. Line attributes are defined by the Pen
object with which they are drawn. Of course, as pens can vary in width and color, so to can
lines. To draw a line, we use the DrawLine method of the Graphics class. This method is over-
loaded; it defines a number of ways you can pass it parameters. For instance, you can pass it a
Pen object and two Point structures between which GDI+ will draw the line. A Point struc-
ture stores the x and y coordinates of a point on a 2D plane.
The following code uses the DrawLine method and a Pen instance to draw a blue line onto a
form. You can test this code, create a new form-based application, add a button to it, and add
the code in the listing to the button’s click event.
‘local scope
Dim myGraphics As Graphics
Dim myPen As Pen
Note that before we could draw anything to the screen, we needed to return a valid drawing
surface. To do so, we created an instance of the Graphics class. This provides us an object on
which to draw. The constructor accepts a Windows handle as its parameter. We pass it the
active Windows handle. This sets up the Graphics object to use the active form as its target for
drawing our line.
Next, a Pen instance is created. We pass its constructor a valid color and width. Finally, the
DrawLine method of the Graphics object is called to render the line onto the form. The version
of the DrawLine method we used requires a Pen instance and a set of start and end coordinates.
These coordinates are simply passed in order as two points defined as (x1, y1) and (x2, y2).
The method connects the two coordinate points with a blue line based on our Pen object.
dashed lines and to attach start and end line caps. Line caps can be as simple as an arrowhead
or as complex as a custom-defined cap. Table 9.3 lists properties of the Pen class that are spe-
cific to dashes and caps.
Property Description
CustomStartCap The CustomStartCap property is used to set or get a custom-
defined line cap. The CustomStartCap property defines the cap at
a line’s start. The property is of type CustomLineCap.
CustomEndCap The CustomEndCap property is used to set or get a custom-defined
line cap. The CustomEndCap property defines the cap at a line’s
end. The property is of type CustomLineCap.
DashCap The DashCap property is used to set or get the style used for the
start or end caps of dashed lines.
DashOffset The DashOffset property is used to set or get the distance
between the start of a line and the start of the dash pattern.
DashPattern The DashPattern property sets or gets an array of integers that
indicates the distances between dashes in dash-patterned lines.
DashStyle The DashStyle property sets or gets the style used for dashing a
line. The property is of the type DashStyle enumeration.
DashStyle enumeration members include the following: Dash,
DashDot, DashDotDot, Dot, Solid.
EndCap The EndCap property sets or gets the LineCap object used to define
the end of the line. EndCap is of the type LineCap. The LineCap
enumeration includes the following members: AnchorMask,
ArrowAnchor, Custom, DiamondAnchor, Flat, NoAnchor, Round, 9
RoundAnchor, Square, SquareAnchor, and Triangle.
FUNCTIONS
StartCap The StartCap property sets or gets the LineCap object used to
DRAWING
define the start of the line. StartCap is of the type LineCap. The
LineCap enumeration includes the following members:
AnchorMask, ArrowAnchor, Custom, DiamondAnchor, Flat,
NoAnchor, Round, RoundAnchor, Square, SquareAnchor, and
Triangle.
The following code demonstrates setting the styles and cap properties of a Pen object. The
code first creates a Pen object of the color blue. It then sets the EndCap property to an arrow
using the LineCap enumeration. Last, it indicates the line’s DashStyle to be a dash followed
by a dot (DashDot).
Working with the .NET Namespaces
352
PART II
Joins
Suppose we have multiple lines that are joined to indicate a shape or routing direction through a
diagram. The point at which two lines are joined can be rendered with three distinct styles. The
Pen class defines how lines are joined. To do so, it provides the LineJoin property. This prop-
erty is of the type LineJoin enumeration whose members include those listed in Table 9.4.
To join lines, you must add each line to a Path object (discussed later in the chapter). The path
is drawn to the surface using one Pen instance. Intersecting lines are then joined based on the
LineJoin property of the given Pen instance. The following snippet illustrates this with code.
‘local scope
Dim myPath As New System.Drawing.Drawing2D.GraphicsPath()
Dim myGraphics As Graphics
Dim myPen As New Pen(color:=Color.Blue, Width:=8)
Curves
A curve is an array of points defining the perimeter of a conic section. Curves can be used for
such things as connecting points on a graph or drawing a handlebar mustache.
There are two types of curves in the .NET library: cardinal splines and Bèzier splines. There
are also a number of methods of the Graphics class that can be used to draw curves. Table 9.5
lists these methods. For our discussion, we will focus on the DrawCurve and DrawBezierCurve
methods.
Method Description
DrawCurve The DrawCurve method connects an array of points using a curved
line.
DrawClosedCurve The DrawClosedCurve method draws a closed curve using an array of
points. A closed curve ensures that the shape is closed. For instance,
if you drew a curve between three points, the method would close the
curve by connecting the third point with the first.
DrawBezier The DrawBezier method is used to draw a Bèzier curve.
DrawArc The DrawArc method draws an arc from a specified ellipse.
9
Cardinal Splines
A cardinal spline is an array of points through which a line smoothly passes. The curve or
FUNCTIONS
DRAWING
bend of the line is defined by a tension parameter. The curve’s tension indicates how tightly the
curve bends, the lower the tension on a given curve, the flatter (straighter) the line. A curve
with a tension of zero (0), for instance, is equivalent to drawing a straight line between points.
To create a cardinal spline, you use the DrawCurve method. This method allows us to control
the curve’s tension and define the number of points in a given curve. The method is over-
loaded, and as such, provides a number of ways to display a curve. In the following example,
we create a blue curve that passes through three points. Notice that we did not specify the ten-
sion. When left blank, the method uses the default tension of 0.5.
Working with the .NET Namespaces
354
PART II
Figure 9.2 helps illustrate the concept of curve tension. The innermost line is drawn with a ten-
sion setting of zero. Each successive line increases the tension by .5 until we reach 2.0.
FIGURE 9.2
Curve tension.
Bèzier Splines
Bèzier splines can be used to create a wide variety of shapes. Fonts, for instance, often use
Bèzier splines for outlining characters. Four points define a Bèzier spline: a start and end point
and two control points. The curve is drawn between the start and end point. The control points
influence how the curve flows between the points. As the curve moves from point to point, it is
“pulled” toward the nearest control point.
Consider the following code:
Drawing Functions
355
CHAPTER 9
In the preceding example, we created a Bézier curve using the DrawBezier method. First, we
defined a set of four points. The first point (100, 75) is the starting point. The next two points,
(125, 50) and (150, 75) act as the control points. The curve ends at the point (175, 50).
FUNCTIONS
DRAWING
➲ Use DrawBeziers method to create a series of Bèzier curves.
Rectangles
The Rectangle structure is the backbone of the shapes presented in this section. Classes like
Ellipse and Pie use it to bind their shape. The structure stores the size and location of a rec-
tangular region.
We have two constructors available to create an instance of the Rectangle structure. One cre-
ates the rectangle based on the upper-left x and y coordinate, the width of the rectangle, and its
height. To the other constructor, you pass both location as an instance of the Point structure,
and Size as an instance of the Size structure.
Listing 9.1 illustrates both rectangle constructors. The code is simply fired by a button’s click
event. The rectangles are output to the active form.
End Sub
Drawing Functions
357
CHAPTER 9
The DrawRectangle method of the Graphics object allows us to draw the outline of a rectan-
gle to the drawing surface. The method is overloaded with three different sets of parameters.
The first set allows you to create a rectangle based on a Pen and Rectangle instance. The other
two sets create a rectangle directly from a Pen instance and the rectangle’s x and y coordinates
(width and height). The difference between these two is the data types used to define the coor-
dinates and size. One uses the Int32 data type, and the other uses a Single.
Ellipses
An ellipse is simply a circle or oval bound inside a Rectangle structure. To draw an ellipse,
you can use the DrawEllipse method of the Graphics class. This method requires a Pen object
and some semblance of a rectangle definition (structure instance, coordinates, and so on). The
following code illustrates drawing an ellipse inside of a defined Rectangle instance.
‘dimension variables of local scope
Dim myGraphics As Graphics
Polygons
A polygon is a closed plane, or object, represented by at least three lines (segments). To draw
polygons with the namespace, we simply play connect-the-dots. We first create the dots using
the Point structure. These dots are actually a series of coordinates through which we will draw
lines. Figure 9.3 shows a set of six points using the basic Windows coordinate system. 9
To connect the dots, we use the DrawPolygon method of the Graphics class. We indicate the
FUNCTIONS
DRAWING
order in which to connect the points of the polygons by their order in our Point array. You can
see from code Listing 9.2 that the points can be connected in a variety of ways to produce var-
ied results.
Working with the .NET Namespaces
358
PART II
x-axis
5 15 25 35 45 55 65
5
(25, 15)
15
(35, 20)
(15, 20)
25
35
y-axis
(15, 40)
(35, 40)
45
(25, 45)
55
65
FIGURE 9.3
Polygon coordinates.
‘draw the polygon (connect the dots between the points in the array)
myGraphics.DrawPolygon(pen:=New Pen(Color.Blue, Width:=2), _
points:=myPoints)
‘draw the polygon (connect the dots between the points in the array)
myGraphics.DrawPolygon(pen:=New Pen(Color.Blue, Width:=2), _
points:=myPoints)
End Sub
Notice that the last member of the array in Listing 9.2 always points back to the starting point.
This closes the polygon. If you omit this last element in the array, the method assumes it and
will close the polygon for you.
Pies
A pie is simply a wedge of a circle, similar to a section in a pie chart or a piece of pie. GDI+
defines a pie section by an Ellipse object (which is contained by a Rectangle), and the two
radial lines that intersect with the endpoints of the arc defined by the Ellipse. Figure 9.4 illus-
trates this point.
x-axis
5 15 25 35 45 55 65 75 85 95 9
5
FUNCTIONS
95 85 75 65 55 45 35 25 15
DRAWING
x, y
height
y-axis
width
startAngle
sweepAngle
FIGURE 9.4
DrawPie method.
Working with the .NET Namespaces
360
PART II
From Figure 9.4, you can see that the x and y coordinates actually define the upper-left corner
(or starting point) of the bounding rectangle. The pie piece is drawn from the center of this
rectangle. The width and height of the rectangle define the size of the ellipse, which in turn
defines the size of our wedge.
To create a pie wedge we use, you guessed it, the Graphics class. The DrawPie method has
two key parameters (in addition to the Rectangle that defines the Ellipse and the Pen object
that is used to draw the pie), startAngle and sweepAngle. The startAngle parameter is an
angle that is defined clockwise from the x-axis to the first side of the pie. The parameter,
sweepAngle, is defined clockwise from the first side of the pie to the second side of the pie
section. This is easier to grasp by looking at both the code in Listing 9.3.
End Sub
Drawing Functions
361
CHAPTER 9
From Listing 9.3, you can see that when the first section of the pie is created, we set the start
angle to 0 and the sweep angle to 90° counterclockwise (negative). The next piece starts where
the last one left off and again creates a section of equal value (90°). Note how the last piece
was created with the FillPie method with an offset to add extra highlighting to the piece.
Filling Shapes
So far, we’ve dealt with the outline of a shape. Now we will focus on the interior, or fill area,
of a shape. GDI+ gives us the concept of a brush to indicate how a shape is filled. Brushes are
useful when blending colors for a desired effect like a fade or indicating a shape’s texture like
sand, stone, or brick. Brushes can be a single solid color, a blend of colors, a texture, a bitmap,
or a hatched pattern.
To create brush objects in our code, we use a derivative of the Brush class. Brush is an abstract
base class. Classes that derive from Brush are as follows: SolidBrush, TextureBrush,
RectangleGradientBrush, LinearGradientBrush, and HatchBrush. This section
discusses the various Brush derivatives.
SolidBrush
The SolidBrush class could not be more basic. It works just as it sounds; it provides a single- 9
colored brush with which to fill shapes. It has one constructor and one property, Color. Of
course, this property is of the type Color structure. The following is an example of how to cre-
FUNCTIONS
DRAWING
ate a SolidBrush object:
Dim myBrush as New SolidBrush(color:=Color.Red)
TextureBrush
To create custom fill effects, you use the TextureBrush class. Custom fills are useful when
you want to apply your own design to the interior of a shape. For example, suppose you’ve
created a bar graph and you want to fill each bar with the logo of a different company. The
TextureBrush class would allow you to use a bitmap of each company’s logo to fill each rec-
tangle or bar in the graph. Using the TextureBrush class and bitmap images, you can create
endless fill patterns.
Working with the .NET Namespaces
362
PART II
The following code creates a TextureBrush instance based on a simple bitmap made up of
three 45° lines. The bitmap is defined inside of an ImageList control (imageList1). When we
create the object instance, we set the WrapMode parameter to the TileFlipXY enumeration
member. This reverses the image both vertically and horizontally before it is applied to the
graphic surface.
‘dimension a local variable of type TextureBrush Class
Dim myBrush As TextureBrush
Table 9.6 demonstrates how you can use the WrapMode property to tile an image inside the
TextureBrush object to create a desired effect.
Effect Description
This is the original 16 × 16 bitmap that is used to create the various effects.
A 32 × 32 filled area using the WrapMode.Tile property. The 16 × 16
bitmap is repeated four times in a tiled fashion.
A 32 × 32 filled area using the WrapMode.TileFlipX property. The 16 × 16
bitmap is reversed horizontally and then repeated four times (tiled).
A 32 × 32 filled area using the WrapMode.TileFlipY property. The 16 × 16
bitmap is reversed vertically and then repeated four times (tiled).
A 32 × 32 filled area using the WrapMode.TileFlipXY property. The 16 ×
16 bitmap is reversed vertically and horizontally before being tiled.
LinearGradientBrush
In Windows 9x and above, you’ve undoubtedly seen how you can blend two colors across
the title bar of a window from within the “Display Settings” control panel. Well, the
LinearGradientBrush class allows us to do just that; we can blend two colors across a
given shape.
To do so, we first create an instance of the class based on two colors and a blend style. Blend
styles are defined by the LinearGradientMode enumeration. We then use a fill method of the
Graphics object to paint our shape with the blended style. Listing 9.4 illustrates this by creat-
ing a Rectangle object and then using the blended LinearGradientBrush to fill its interior by
calling FillRectangle.
Drawing Functions
363
CHAPTER 9
‘local scope
Dim myGraphics As Graphics
Dim myBrush As System.Drawing.Drawing2D.LinearGradientBrush
Dim myRectangle As Rectangle
End Sub
9
Notice that when we created the brush, we set the LinearGradientMode parameter to indicate
FUNCTIONS
DRAWING
a blend from the top of the shape to its bottom (Vertical). You can get the four effects defined
in Table 9.7 by using this enumeration.
HatchBrush
Remember the first paint programs? Remember showing your friends a wall built out of red
brick that you drew with a rectangle and filled with the brick pattern? Well, the HatchBrush
class allows us to create numerous predefined fill patterns, including brick.
We create a HatchBrush by passing in a hatch style, using the HatchStyle enumeration, and a
foreground and background color to be used by the hatch style. Listing 9.5 fills an ellipse
using a checkerboard HatchBrush instance.
‘local scope
Dim myGraphics As Graphics
Dim myBrush As System.Drawing.Drawing2D.HatchBrush
Dim myRectangle As Rectangle
End Sub
Table 9.8 provides a visual representation of the various patterns possible using the
HatchBrush class. Hopefully, this will serve as a handy reference when you need to pick
the perfect pattern. A description of each member is not necessary; the associated graphic
tells the whole story. Remember, the table uses black and white, but you can use any color
for the foreColor and backColor parameters for nearly unlimited effects.
Horizontal Vertical
LightHorizontal LightVertical
DarkHorizontal DarkVertical
DashedHorizontal DashedVertical
NarrowHorizontal NarrowVertical
DiagonalBrick Cross 9
FUNCTIONS
HorizontalBrick DiagonalCross
DRAWING
SmallGrid SolidDiamond
LargeGrid DottedDiamond
DottedGrid OutlinedDiamond
SmallCheckerBoard SmallConfetti
LargeCheckerBoard LargeConfetti
Working with the .NET Namespaces
366
PART II
Percent05 Percent10
Percent20 Percent25
Percent30 Percent40
Percent50 Percent60
Percent70 Percent75
Percent80 Percent90
Plaid Sphere
Trellis Shingle
Wave Weave
BackwardDiagonal ForwardDiagonal
DarkDownwardDiagonal LightDownwardDiagonal
DarkUpwardDiagonal LightUpwardDiagonal
DashedDownwardDiagonal DashedUpwardDiagonal
WideDownwardDiagonal WideUpwardDiagonal
Divot
Drawing Functions
367
CHAPTER 9
Collections of Shapes
It is often useful to collect various “building block” shapes into a single unit. Rather than man-
aging each rectangle in a bar graph, for instance, it is often easier to group these objects into a
single, manageable unit. If the objects need to be moved or redrawn, you can simply make one
method call. Similarly, if you are transforming the objects, maybe rotating them all 45°, it is
much easier to transform a group than transform each item independently. The
System.Drawing.Drawing2D namespace provides us the Path class for grouping shapes.
Additionally, once you’ve defined your various object groups, it is often necessary to indicate
how those groups interact with one another. If you’ve ever used a drawing application, you are
undoubtedly familiar with the concepts of bring-to-front and send-to-back. These are features
that allow an artist to indicate how shapes (or groups of shapes) relate to one another in layers.
The System.Drawing namespace gives us the Region class for indicating object interaction
and layers.
Paths
Paths enable more advanced drawing techniques in .NET. A path is made of one or more geo-
metric shapes (rectangle, line, curve, and so on). By grouping shapes together in a path, we are
able to manage and manipulate the group as one object. We add shapes to a path for storage in
what is called the world coordinate space. This coordinate system is essentially virtual. It is
the place where the shapes logically exist in memory relative to one another. The graphic can
then be manipulated as a whole. It can be drawn to the screen over and over. In addition, it can
be transformed (rotated, sheared, reflected, scaled) when moving from this logical world space
to the physical device space (form). For example, you might have a 10 × 20 rectangle stored
inside a path. When you place it on the form, you can rotate it 20° and sheer the rectangle. The
key is that the rectangle still exists as a 10 × 20 rectangle (not rotated, not sheared) in the 9
world space.
FUNCTIONS
DRAWING
To create a path, we use the GraphicsPath class. This class provides methods like AddLine,
AddRectangle, AddArc, and so on; each adds their shape to the path. Paths can contain multi-
ple figures or groups of shapes that represent one object. When adding a shape to a path, it is
best to indicate to which figure the shape belongs. We do this by calling the StartFigure
method. Each subsequent call to an add function adds the shape to the figure. If we call
StartFigure again, a new figure is started and all following shapes are added to the new fig-
ure. We call the CloseFigure method prior to starting a new figure if we wish the figure to be
closed off, or connected from start point to end point.
Listing 9.6 creates a GraphicsPath instance. We add a few shapes to the GraphicsPath class
and then display the path to the form.
Working with the .NET Namespaces
368
PART II
End Sub
Drawing Functions
369
CHAPTER 9
If you use paths a lot, you will want to check out the Flatten method of the GraphicsPath
class. This method allows you to change how items are stored within the object instance. By
default, state is maintained for each item added to the path. This means that if a curve and an
ellipse, for instance, are stored in a GraphicsPath, then data for the curve’s points and control
points as well as data that defines the ellipse is stored in the object. By flattening the path, you
allow the object to manage the shape as a series of line segments, thus reducing overhead. In a
completely flattened path, all points are stored as points to be connected by line segments.
FUNCTIONS
You still draw with the Graphics class, but the Region class allows you to set parameters for
DRAWING
drawing. For example, you set the region parameter of the SetClip method of the Graphics
object to your instance of Region. This tells the graphics object that your Region further
defines the graphics area on the given drawing surface.
Listing 9.7 presents a clipping example. We first draw a rectangle and add it to a Path object.
We then define a Region instance based on the Path object. After that, we call the SetClip
method of our Graphics container and pass in the Region object. Finally, we draw a number of
strings to the graphic surface; notice how our defined region clips them.
Working with the .NET Namespaces
370
PART II
‘local scope
Dim myGraphics As Graphics
Dim myPen As New Pen(color:=Color.Blue, Width:=2)
Dim myPath As New System.Drawing.Drawing2D.GraphicsPath()
Dim myPoints(2) As Point
Dim myRegion As Region
Dim i As Short
‘define a triangle
myPath.StartFigure()
myPoints(0) = New Point(x:=100, y:=20)
myPoints(1) = New Point(x:=50, y:=100)
myPoints(2) = New Point(x:=150, y:=100)
Next
End Sub
Drawing Functions
371
CHAPTER 9
Did you notice that when we called SetClip we also set something called the combineMode?
This indicates how the two regions or shapes should be combined. In our case, we had a trian-
gular Region object and a few strings that we drew. We set the combineMode enumeration to
Replace to indicate that the string information should replace the region inside the triangle. Of
course, there are a number of additional members to this enumeration. Table 9.9 lists them.
Also, note that each enumeration member has a corresponding method on the Region class
(Region.Replace for example).
FUNCTIONS
Union
DRAWING
should be joined. The result is an area that is
defined by all points in both regions.
Xor The Xor member is the opposite of the
Intersect member. It contains all points that
are not common to either region.
The following code was used to create the example graphics in the previous table. Note that we
created two regions and combined them using the Region.[Method] syntax.
Working with the .NET Namespaces
372
PART II
‘local scope
Dim myGraphics As Graphics
Dim myPath As New System.Drawing.Drawing2D.GraphicsPath()
Dim myPath2 As New System.Drawing.Drawing2D.GraphicsPath()
Dim myRegion As Region
Dim myRegion2 As Region
Images
The namespace library gives us three classes for working with images: Image, Bitmap and
Icon. Image is simply the base class from which the others inherit. Bitmap allows us to convert
a graphics file into the native GDI+ format (bitmap). This class can be used to define images
as fill patterns, transform images for display, define the look of a button—its uses are many.
Although the bitmap format is used to manipulate images at the pixel level, GDI+ can actually
work with the following image types:
• Bitmaps (BMP)
• Graphics Interchange Format (GIF)
• Joint Photographic Experts Group (JPEG)
• Exchangeable Image File (EXIF)
• Portable Network Graphics (PNG)
• Tag Image File Format (TIFF)
Creating an instance of Bitmap requires a filename, stream, or another valid Image instance.
For example, the following line of code will instantiate a Bitmap object based on a JPEG file:
Dim myBitmap As New System.Drawing.Bitmap(fileName:=”Sample.jpg”)
Once instantiated, we can do a number of things with the image. For instance, we can change
its resolution with the SetResolution method or make part of the image transparent with
MakeTransparent. Of course, we will also want to draw our image to the form. We use the
DrawImage method of the Graphics class to output the image to the screen. The DrawImage
method has over 30 overloaded parameter sets. In its simplest form, we pass the method an
instance of Bitmap and the upper-left coordinate of where we want the method to begin draw-
ing. For example: 9
myGraphics.DrawImage(image:=myBitmap, point:=New Point(x:=5, y:=5))
FUNCTIONS
DRAWING
Scaling and Cropping
It is often helpful to be able to scale or crop an image to a different size. Suppose you need a
100 × 100 image to fit in a 20 × 20 space, or you want to give your users the ability to zoom in
on a portion of an image. You use a variation of the DrawImage method to scale images. This
overloaded method takes a Rectangle instance as the destination for drawing your image.
However, if the rectangle is smaller or larger than your image, the method will automatically
scale the image to match the bounds of the rectangle.
Another version of the DrawImage method takes both a source rectangle and a destination rec-
tangle. The source rectangle defines the portion of the original image to be drawn into the des-
tination rectangle. This, effectively, is cropping. The source rectangle defines how the image
Working with the .NET Namespaces
374
PART II
gets cropped when applied to the destination. Of course, you can crop to the original size or
scale the cropped portion to a new size. Listing 9.8 provides a detailed code example of both
scaling and cropping an image.
‘local scope
Dim myBitmap As System.Drawing.Bitmap
Dim myGraphics As Graphics
Dim mySource As Rectangle
Dim myDestination As Rectangle
End Sub
Notice that we actually drew the image to the form three times. The first time, we drew the
image into a rectangle (mySource) based on its original size. The second time, we scaled the
image to two times its original size (myDestination) by creating a larger rectangle and out-
putting the image accordingly. Finally, we cropped a portion of the original output and put it in
a new, larger rectangle. Figure 9.5 shows the code’s output to the form.
Drawing Functions
375
CHAPTER 9
FIGURE 9.5
Scale and crop output.
Icons
An icon in Windows is a small bitmap image that represents an object. You cannot go far in
without seeing and working with icons. For example, the File Explorer uses icons to represent
folders and files; your desktop contains icons for My Computer, Recycle Bin, and My Network
Places.
We use the Icon class to work with icons in .NET. We can instantiate an Icon instance in
much the same way we created Bitmap objects. The following code creates an icon based on a
file name:
Dim myIcon as New Icon(fileName:=”myIcon.ico”)
The DrawIcon method of the Graphics class is used to draw the icon to the form. To it, you
can pass the icon and either just the upper-left x and y coordinates or a bounding rectangle. If
you pass a Rectangle instance, the icon will be scaled based on the bounding rectangle. The
following line of code draws an icon object into a bounding rectangle.
myGraphics.DrawIcon(icon:=myIcon, _
rectangle:=New Rectangle(x:=5, y:=5, width:=32, height:=32))
The Graphics class also gives us the DrawIconUnstretched method that allows us to specify a 9
bounding rectangle without actually scaling the icon. In fact, if the icon is larger than the
bounding rectangle, it will be cropped to fit from the left corner down and to the right.
FUNCTIONS
DRAWING
Suggestions for Further Exploration
➲ To animate images, take a look at the ImageAnimator class.
➲ Check out the SmoothingMode property of the Graphics class and the SmoothingMode
enumeration members. This method allows you to set things like antialiasing to make
your graphics look “smoother.”
Transformations
In the previous section, we saw how we could enlarge an image based on the dimensions of a
rectangle. This was a kind of a transformation. Transformations not only allow us to scale
Working with the .NET Namespaces
376
PART II
images, but also rotate, flip, and skew them. In drawing applications, you oftentimes need to
enlarge an image to fill an area. Or you may have to rotate an arrow in a flow chart to point at
a given process. This is all done through transformation.
Origins
As we stated earlier, by default GDI+ sets the upper-left corner of our drawing surface as the
origin. But suppose that we want items stored in the world coordinate space to be output to a
different section of the screen. For instance, suppose you have three line segments stored in a
GraphicsPath object and want the DrawPath to interpret the origin of the form as 50, 0 instead
of 0, 0. To do so, you would use the TranslateTransform method of the Graphics class. In its
simplest form you pass the amount of pixels to transform in the x direction and the y direction.
In our example, the code would look as follows:
MyGraphics.TranslateTransform(dx:=50, dy:=0)
This is an example of a global transformation. That is, it applies to the Graphics instance as a
whole. Now all items drawn with this instance will have an origin of 50, 0. To reset the origin
back to the default state, you can simply call the ResetTransform method. There is also a
local transformation. Local transformations apply to a single object or collected set of shapes.
For example, we could have achieved the same results by calling the Transform method
of the GraphicsPath class. Like the Tranform property of the Graphics class, the
GraphicsPath.Transform method requires a matrix object to set its value.
Unit of Measurement
Tired of working with pixels? Are you more comfortable with inches or centimeters? You can
reset the graphics unit used by the Graphics class by setting the PageUnit property. This
property takes a valid member of the GraphicsUnit enumeration. Members include Inch,
Millimeter, Pixel, Point, and so on. After setting the PageUnit property, subsequent calls to
the Graphics object will interpret your values as the new unit of measurement. For instance,
the following code sets the unit to inches and creates a rectangle one inch wide and a half
inch high:
myGraphics.PageUnit = GraphicsUnit.Inch
myGraphics.DrawRectangle(pen:=New Pen(Color.Red, Width:=0.1F), _
x:=0, y:=0, Width:=1, Height:=0.5F)
Why do we care? Well, through matrix math, we can transform objects. For instance, suppose
you have a point at (2, 3) and you wish to rotate that point 90°. Well, you would multiply the
matrix [2 3] by the matrix [0 1 | -1 0]. The result would be the rotated point at (-2, 3). The
math is: (2*0)+(2*-1) = x and (3*1) + (3*0) = y.
The namespace library provides the Matrix class for working with these transformations. It
exposes the methods Multiply, Rotate, Scale, and Shear. Each of these applies its intended
effect on the Matrix instance. The Graphics class also has similar methods like
RotateTransform, ScaleTransform, and MultiplyTransform. Table 9.10 demonstrates
transformations and their effects.
Transformation Code
myGraphics.ScaleTransform(sx:=2, sy:=0.5F)
Effect
Description This example uses the ScaleTransform method to stretch the image out to
twice its width but compress it to half of its height.
Transformation Code 9
Dim myMatrix As New System.Drawing.Drawing2D.Matrix( _
FUNCTIONS
DRAWING
m11:=1.5F, m12:=0, m21:=1.5F, m22:=0.75F, dx:=0, dy:=0)
myGraphics.MultiplyTransform(matrix:=myMatrix)
Effect
Description This example creates a Matrix instance and multiplies the output by the
supplied matrix. The result is an image that is stretched along the x axis
(m11:=1.5) and condensed along the y axis (m22:=.75) and skewed along the
y axis (m21:=1.5).
Working with the .NET Namespaces
378
PART II
Description This example uses the RotateFlip method of the Bitmap class to flip the
image on its x axis. Notice that we use the FlipType enumeration. There are
a number of additional members to this enumeration to produce more flip-
ping and rotating results.
Transformation Code
Dim myMatrix As New System.Drawing.Drawing2D.Matrix( _
m11:=1, m12:=0, m21:=0, m22:=1, dx:=0, dy:=0)
myMatrix.Shear(shearX:=-0.75F, shearY:=0)
myGraphics.MultiplyTransform(matrix:=myMatrix)
Effect
Description In this example we create a basic Matrix object that has no effect on the
object. We then call the Shear method to indicate that the x-axis should be
sheared by -.75. The result is a “faster .NET.”
9
FIGURE 9.6
FUNCTIONS
DRAWING
System.Drawing MDI form.
Code Walkthrough
The code for the MDI form (see Listing 9.9) is rather basic. There is code to control the menu
events, to load the form, and to reset the form when users click on the “New” menu item.
The code starts with form-level declarations and basic form building code.
Working with the .NET Namespaces
380
PART II
Inherits System.Windows.Forms.Form
End Sub
End Sub
The form load event, formMDI_Load, is where we initialize the application and set a global
reference to both the child form (m_myChild) and a Graphics object that references it
(m_myGraphics). This allows us to maintain a reference to the drawing surface at all times.
9
Private Sub frmMDI_Load(ByVal sender As System.Object, _
FUNCTIONS
DRAWING
ByVal e As System.EventArgs) Handles MyBase.Load
End Sub
The resetChildForm procedure allows us to create new forms based on the default values for
the application.
Private Sub resetChildForm()
End Sub
The following sub routines are the click events for the various menu items:
• menuItemNew_Click creates a new drawing surface
• menuItemExit_Click exits the application
• menuSuface_Click loads the surface dialog
• menuItemDraw_Click loads the drawing form
Private Sub menuItemNew_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles menuItemNew.Click
‘purpose: user clicks the NEW item on the menu bar to create a new
‘ drawing surface
‘call the method used to reset the child form to its defaults
Call resetChildForm()
End Sub
‘purpose: close the app. when the user clicks the EXIT menu item
End Sub
‘purpose: show the surface dialog when the user clicks the
‘ SURFACE menu item
‘local scope
Dim mySurface As frmSuface
‘set the start position of the modal dialog to the center position
‘ of its parent
mySurface.StartPosition = FormStartPosition.CenterParent
9
‘show the form as modal
mySurface.ShowDialog(Me)
FUNCTIONS
DRAWING
End Sub
‘local scope
Dim myDraw As frmDraw
Working with the .NET Namespaces
384
PART II
End Sub
#End Region
End Class
Even though the parent form is of the type MDI, our code restricts the number of child win-
dows to just one. We do not allow users to create more than one child form. This simplifies the
example. Microsoft Paint has a similar design pattern. With very little additional effort, you
can modify the code to manage multiple drawing surfaces.
The child form itself contains no additional code. Listing 9.10 shows the default code gener-
ated by Visual Studio .NET. The only items of interest are the form’s property settings. The
form’s BorderStyle is set to None and the ShowInTaskBar property is set to False. This pro-
vides users with the illusion of working on a document, when in reality, all documents in
Windows are simply versions of forms.
End Sub
Drawing Functions
385
CHAPTER 9
‘the following are key properties of the child form that were
‘ changed to make the form look more like a painting surface
Me.AutoScaleBaseSize = New System.Drawing.Size(5, 13)
Me.BackColor = System.Drawing.Color.White
Me.BorderStyle = System.Windows.Forms.FormBorderStyle.None
Me.MaximizeBox = False
Me.MinimizeBox = False
Me.ShowInTaskbar = False
Me.Text = “formChild”
End Sub
#End Region
9
End Class
FUNCTIONS
DRAWING
Surface Form
The surface form demonstrates the Color structure. It allows users to manage properties of the
drawing surface. Users can change the background color of the child form and its height and
width. Figure 9.7 is a screen shot of the surface dialog.
Code Walkthrough
Listing 9.11 is long for such a simple form, but much of the code is simply default property
overrides for the form and its controls. The key procedures in this listing are the form load
event, the Defaults button event, and the OK button event.
Working with the .NET Namespaces
386
PART II
FIGURE 9.7
System.Drawing drawing surface form.
End Sub
FUNCTIONS
Me.groupBox2.TabIndex = 0
DRAWING
Me.groupBox2.TabStop = False
Me.groupBox2.Text = “Background Color”
Me.comboBoxColors.DropDownStyle = _
System.Windows.Forms.ComboBoxStyle.DropDownList
Me.comboBoxColors.DropDownWidth = 121
Me.comboBoxColors.Location = New System.Drawing.Point(12, 28)
Me.comboBoxColors.MaxDropDownItems = 10
Me.comboBoxColors.Size = New System.Drawing.Size(156, 21)
Me.comboBoxColors.Sorted = True
Me.comboBoxColors.TabIndex = 1
Me.buttonCancel.DialogResult = System.Windows.Forms.DialogResult.Cancel
Me.buttonCancel.Location = New System.Drawing.Point(196, 44)
Me.buttonCancel.TabIndex = 5
Working with the .NET Namespaces
388
PART II
End Sub
The form load event (formSurface_Load) uses the Reflection namespace to load a drop-
down box with the names of the properties of the Color structure. The load event then initial-
izes the remaining fields on the form to match the current state of the child form.
NOTE
The System.Reflection namespace is a very powerful set of classes. While they are
beyond the scope of this book, you are encouraged to browse the MSDN reference to
see what can be accomplished with this namespace.
FUNCTIONS
DRAWING
Dim myColor As System.Drawing.Color
Dim myProps() As System.Reflection.PropertyInfo
Dim myType As System.Type
Dim count As Integer
End If
Next
End Sub
The Cancel button click event closes the form without applying any updates.
Private Sub buttonCancel_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles buttonCancel.Click
‘purpose: respond the the cancel button’s click event and close the
‘ form without the applying the changes
End Sub
The Defaults button event simply loads the form fields with the application’s default values.
Private Sub buttonDefaults_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles buttonDefaults.Click
End Sub
When users click the OK button, the properties of the child form are set to these new values.
The key piece here is that after changing properties of the child form, we have to get a new
reference to it for the Graphics object to function properly. If we do not rereference the form,
the graphics object is unaware of the changes to the drawing surface, which results in shapes
getting cut off at the window’s old size and other undesirable behavior.
Private Sub buttonOk_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles buttonOk.Click
Else
FUNCTIONS
DRAWING
m_myChild.Refresh()
End If
End Sub
#End Region
End Class
Working with the .NET Namespaces
392
PART II
Draw Form
The draw form allows users to draw shapes onto the child form. Obviously, this is not the
ideal way to create graphics; mouse or pen-based input is much easier. Nevertheless, for the
clarity of this example and in the interest of simplicity, we’ll define shapes by text boxes and
drop-downs.
The features of the draw form allow users to create basic shapes (line, rectangle, and ellipse).
They can set the color and width of the shape outline. Shapes can be filled with a solid color, a
blend, or a pattern. The form also allows users to rotate the shape prior to drawing it to the
surface. The Apply and Clear buttons were added so that you could create multiple shapes onto
the child form without leaving the draw dialog. Figure 9.8 is a screen capture of the draw form.
FIGURE 9.8
System.Drawing draw form.
Code Walkthrough
Listing 9.12 represents the code behind the draw form. Again, much of the code is form and
control property settings.
End Sub
FUNCTIONS
DRAWING
Private WithEvents groupBox5 As System.Windows.Forms.GroupBox
Private WithEvents label6 As System.Windows.Forms.Label
Private WithEvents label7 As System.Windows.Forms.Label
Private WithEvents textBoxHeight As System.Windows.Forms.TextBox
Private WithEvents textBoxWidth As System.Windows.Forms.TextBox
Private WithEvents textBoxX As System.Windows.Forms.TextBox
Private WithEvents textBoxY As System.Windows.Forms.TextBox
Private WithEvents label8 As System.Windows.Forms.Label
Private WithEvents buttonCancel As System.Windows.Forms.Button
Private WithEvents buttonOk As System.Windows.Forms.Button
Private WithEvents label9 As System.Windows.Forms.Label
Private WithEvents label10 As System.Windows.Forms.Label
Private WithEvents comboBoxBlendTo As System.Windows.Forms.ComboBox
Private WithEvents comboBoxBlendFrom As System.Windows.Forms.ComboBox
Working with the .NET Namespaces
394
PART II
FUNCTIONS
CType(Me.numericUpDownWeight, _
DRAWING
System.ComponentModel.ISupportInitialize).BeginInit()
Me.buttonApply.Location = New System.Drawing.Point(336, 432)
Me.buttonApply.TabIndex = 5
Me.buttonApply.Text = “Apply”
Me.radioButtonNone.Checked = True
Me.radioButtonNone.Location = New System.Drawing.Point(12, 24)
Me.radioButtonNone.TabIndex = 0
Me.radioButtonNone.TabStop = True
Me.radioButtonNone.Text = “No Fill”
Me.numericUpDownRotate.Location = New System.Drawing.Point(56, 24)
Me.numericUpDownRotate.Maximum = New Decimal(New Integer() _
{360, 0, 0, 0})
Working with the .NET Namespaces
396
PART II
FUNCTIONS
Me.comboBoxOutlineColor.DropDownStyle = _
DRAWING
System.Windows.Forms.ComboBoxStyle.DropDownList
Me.comboBoxOutlineColor.DropDownWidth = 121
Me.comboBoxOutlineColor.Location = New System.Drawing.Point(12, 80)
Me.comboBoxOutlineColor.Size = New System.Drawing.Size(148, 21)
Me.comboBoxOutlineColor.TabIndex = 2
Me.buttonOk.Location = New System.Drawing.Point(252, 432)
Me.buttonOk.TabIndex = 4
Me.buttonOk.Text = “Ok”
Me.label8.Location = New System.Drawing.Point(28, 112)
Me.label8.Size = New System.Drawing.Size(48, 23)
Me.label8.TabIndex = 5
Me.label8.Text = “From”
Me.label9.Location = New System.Drawing.Point(12, 32)
Working with the .NET Namespaces
398
PART II
FUNCTIONS
Me.groupBox3.Controls.AddRange(New System.Windows.Forms.Control() _
DRAWING
{Me.numericUpDownWeight, Me.comboBoxOutlineColor, Me.label10, _
Me.label9})
Me.groupBox3.Location = New System.Drawing.Point(328, 12)
Me.groupBox3.Size = New System.Drawing.Size(168, 124)
Me.groupBox3.TabIndex = 1
Me.groupBox3.TabStop = False
Me.groupBox3.Text = “Outline”
Me.groupBox4.Controls.AddRange(New System.Windows.Forms.Control() _
{Me.label15, Me.numericUpDownRotate, Me.label14})
Me.groupBox4.Location = New System.Drawing.Point(12, 332)
Me.groupBox4.Size = New System.Drawing.Size(484, 88)
Me.groupBox4.TabIndex = 3
Me.groupBox4.TabStop = False
Working with the .NET Namespaces
400
PART II
End Sub
The Cancel button click event simply closes the form without applying the current form val-
ues. Of course, if you’ve already pressed the Apply button, then the Cancel button just closes
the form.
Private Sub buttonCancel_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles buttonCancel.Click
End Sub
The formDraw_Load procedure initializes the controls on the form. In it, we fill the various
combo boxes directly from enumeration and structure members using the Reflection
namespace.
Note that at the end of the form load event, we set the AcceptButton and CancelButton prop-
erties of the form to the OK and Cancel buttons, respectively. The AcceptButton property indi-
cates what button on the form should respond to the Enter key being pressed (default button).
The CancelButton property indicates the button that is fired when users click the Escape key.
This is new in .NET. In past versions of VB, in order to implement a button that responds to
the Enter or Cancel keys you would set properties of the button; now, you set properties of
the form.
Private Sub frmDraw_Load(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles MyBase.Load
‘local scope
Dim myColor As System.Drawing.Color
Dim myProps() As System.Reflection.PropertyInfo
Dim myType As System.Type
Dim count As Integer
Dim colorName As String
Dim myHatchStyle As Drawing.Drawing2D.HatchStyle
Dim myArray() As String
Dim myLinGradMode As Drawing2D.LinearGradientMode 9
‘return the type of the color structure
FUNCTIONS
DRAWING
myType = myColor.GetType()
End If
Next
Next
Next
‘set the cancel button on the form to respond to the escape key
Me.CancelButton = buttonCancel()
End Sub
dialog
The Apply button’s click event simply validates the form by calling validateForm. If no vali-
dation rules were broken, it calls the submitForm method to apply the shape to the child form.
Private Sub buttonApply_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles buttonApply.Click
‘purpose: allow users to draw items to the form without closing the
‘dialog before drawing can happen the form fields must be validated
If Not validateForm() Then
FUNCTIONS
DRAWING
Beep()
Else
End If
End Sub
The validateForm function checks for valid entries in our text boxes and returns either True to
indicate all fields are valid or False to indicate one or more are invalid.
Working with the .NET Namespaces
404
PART II
Return False
Else
Return True
End If
End Function
The submitForm method simply calls the draw method contained in our module. Of course, it
passes all the form values as parameters.
Private Sub submitForm()
‘local scope
Dim strFillType As String
fillType:=strFillType, _
outlineWeight:=CSng(numericUpDownWeight().Value), _
outlineColor:=comboBoxOutlineColor().SelectedItem.ToString, _
solidColor:=comboBoxSolidColor().SelectedItem.ToString, _
blendFrom:=comboBoxBlendFrom().SelectedItem.ToString, _
blendTo:=comboBoxBlendTo().SelectedItem.ToString, _
pattern:=comboBoxPattern().SelectedItem.ToString, _
patternForeColor:=comboBoxPattFore().SelectedItem.ToString, _
patternBackColor:=comboBoxPattBack().SelectedItem.ToString, _
blendStyle:=comboBoxBlendStyle().SelectedItem.ToString, _
rotateAngle:=numericUpDownRotate().Value)
End Sub
The OK button’s click event checks the field entries for validity using validateForm. It then
calls submitForm to apply the shape to the child form. Finally, it closes the dialog.
Private Sub buttonOk_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles buttonOk.Click
Else
FUNCTIONS
DRAWING
‘close the form
Me.Close()
End If
End Sub
End Sub
#End Region
End Class
Drawing Module
The drawing module contains the application’s global variables and the actual draw procedure.
Code Walkthrough
We first set the application’s default values including the drawing surface settings. We then
declare the global objects that reference the child form and the associated graphics objects.
The drawing module is presented in Listing 9.13.
Module modDrawing
The draw method takes all of the necessary values from the draw form as parameters. It uses
the Graphics class to render shapes onto the surface. You can see that most of the code is just
simple logic to determine the type of shape to draw, the shape’s outline, and its fill type. One
interesting method call is myState = m_myGraphics.Save(), where myState is declared as
Drawing Functions
407
CHAPTER 9
FUNCTIONS
DRAWING
‘purpose: draw a shape to the drawing surface (frmChild)
‘local scope
Dim myPen As Pen
Dim myRectangle As Rectangle
Dim myBrush As Brush
Dim myHatchStyle As Drawing2D.HatchStyle
Dim myType As System.Type
Dim myBlendStyle As Drawing2D.LinearGradientMode
Dim myState As Drawing2D.GraphicsState
Else
‘draw a rectangle
m_myGraphics.DrawRectangle(pen:=myPen, _
rect:=myRectangle)
End If
Case “PATTERN”
Case “BLEND”
FUNCTIONS
DRAWING
color1:=color.FromName(name:=blendFrom), _
color2:=color.FromName(name:=blendTo), _
LinearGradientMode:=myBlendStyle.Parse( _
enumType:=myType, value:=blendStyle))
End Select
Else
End If
End If
End Select
End Sub
End Module
Summary
In this chapter, we walked through the key classes related to drawing using managed code. The
following is a summary of some of the key points related to executing common programming
tasks with the .NET Framework Class Library:
• GDI+ is the underlying technology used by the managed code to execute drawing tasks.
• By default, a graphic’s surface has its origin in the upper-left corner.
• The Graphics class is used to execute nearly all drawing tasks. You use its methods like
DrawLine, DrawEllipse, and FillRectangle to render shapes and colors to the drawing
surface.
• Classes derived from Brush can be used to fill the interior of shapes. Brush classes
include SolidBrush, TextureBrush, and HatchBrush.
• The GraphicsPath class is used to group shapes. This allows you to manipulate various
shapes as a whole.
• The Region class allows you to define areas of your drawing surface for clipping and hit
testing.
• The Bitmap class encapsulates an image. You can use the DrawImage method of the
Graphics class to render it to the surface.
• The Matrix class provides a number of ways to transform images and shapes.
• The following methods of the Graphics class can be used to transform shapes:
RotateTransform, ScaleTransform, MultiplyTransform.
Reading and Writing XML CHAPTER
10
IN THIS CHAPTER
• Key Classes Related to XML 412
• Learning by Example:
The Hotel Reservations Desk 467
Working with the .NET Namespaces
412
PART II
If you think of the .NET Framework as a multilayered technology, then the eXtensible Markup
Language (XML) is the glue that binds those layers together. The .NET Framework provides
unprecedented functionality for the VB programmer to natively interact with and use XML
inside applications. As a storage mechanism, XML is the common denominator for storing
data and persisting objects. As a communications mechanism, XML forms the underpinnings
for how the .NET classes talk to one another. It is the default way that objects in .NET express
and exchange data. For these reasons alone, an understanding of XML is necessary to be pro-
ductive in the .NET runtime. But the real story is not how the Framework uses XML, but how
the Framework enables you, the developer, to speak the universal XML language.
This chapter introduces the XML class libraries, System.XML and System.Xml.Schema, that
.NET uses as an API for parsing, validating, and manipulating XML. First, we’ll establish
some baseline definitions and summaries for XML concepts. Then we will talk about the
XML-related industry standards that the .NET Framework supports. Finally, we get to the meat
of the chapter: an in-depth look at the classes that constitute the System.Xml and
System.Xml.Schema namespaces.
Markup Languages
XML is a language that is used to describe data. This stands in contrast to a language such as
the Hypertext Markup Language (HTML), which is a language used to display data. To fully
appreciate what XML is and what it does, it is useful to have a baseline understanding of
markup languages in general.
10
What Is a Markup Language?
WRITING XML
READING AND
Markup languages exist to add meaning or formatting to documents. Rich Text Format (RTF)
is one example of a markup language. It consists of a defined set of tags or tokens that lend
instruction on how to display a piece of a document. Figure 10.1 shows a formatted sentence
typed into WordPad.
Working with the .NET Namespaces
414
PART II
FIGURE 10.1
An RTF document in WordPad.
When the document is saved with the RTF format, the following code results:
{\rtf1\ansi\ansicpg1252\deff0\deflang1033
{\fonttbl{\f0\fswiss\fcharset0 Arial;}}
\viewkind4\uc1\pard\f0\fs20\par
Markup languages add \i meaning \i0 or \b formatting \b0 to documents.
\par}
In this example, you can see that the \i tag indicates that the piece of the document between
the \i and \i0 tags should be italicized. The \b and \b0 tags indicate that the text between
them should be displayed in bold text. The RTF format and others like it do their job well, but
they are ill suited to the Web. For one thing, they don’t generate the most readable of docu-
ments; their syntax can be confusing and awkward to the human eye. For another, there is little
to the document that actually looks like it has structure. For programmers, who rely on struc-
ture, this is anathema.
FIGURE 10.2
Formatted HTML inside Internet Explorer.
This is what the HTML code needed to generate the previous document looks like:
<html>
<head>
<title>Markup Languages</title>
</head>
<body>
<p>
<font face=”Arial”>Markup languages add <i>meaning</i> or
<b>formatting</b> to documents.</font>
</p>
</body>
</html>
HTML and other markup languages follow a set of rules, which usually are explained inside a
specification. For XML (and HTML), this specification is maintained by the World Wide Web
Consortium (W3C). The language specification defines the actual syntax rules of the docu-
ment. The HTML specification, for instance, defines a rule that says that each HTML docu-
ment must begin with an <html> tag and end with an </html> tag. Typically, these language
specifications define general syntax rules, the order in which the various tags can appear,
whether different tags are dependent upon other tags, and so on.
10
In the next section, we look at the structure of XML documents specifically.
WRITING XML
READING AND
Working with the .NET Namespaces
416
PART II
Nodes
An XML document consists of various discrete chunks of information called nodes. Nodes are
the lowest level of informational unit contained in an XML document. When you read an
Reading and Writing XML
417
CHAPTER 10
XML document using some of the XML reader classes in System.XML, they will read those
documents one node at a time. If you started listing the nodes of the XML document in Listing
10.1, this is what the list would look like:
• Node 1: <?xml version=”1.0” encoding=”utf-8” ?>
• Node 2: <guests>
• Node 3: <guest id=”jlk0910211”>
• Node 4: <firstname>
• Node 5: Jim
• Node 6: </firstname>
…and so on. Nodes are useful for low-level parsing, but they don’t carry enough context with
them to be ultimately useful for understanding the data in an XML document. Consider the
fifth node containing the text Jim. What does this node mean? We can guess, based on the
nodes before (<firstname>) and after (</firstname>), that this is the first name of a hotel
guest, but, taken by itself, the node doesn’t offer up a whole lot of meaning. When people look
at or create an XML document, they typically think in terms of elements, not nodes.
Elements
An element is used to describe and contain data. As such, it is the real power behind an XML
document’s structure. Elements are named and can encapsulate data through values or attrib-
utes. The syntax for an element is as follows:
<elementname attrib1=”value1” attrib2=”value2” ...>Data</elementname>
Attributes and data are all optional with elements, but the starting tags and ending tags are
required. For instance, this is also a valid element:
<MyElement></MyElement>
If the element in question does not have any data associated with it, the ending tag can be
short-circuited by closing out the starting tag with a / character, like this:
<MyElement someAttrib=”Yes”/>
or like this:
<MyElement/>
10
Elements are very useful because, unlike nodes, they provide you with enough contextual
WRITING XML
READING AND
information to evaluate the data that they contain. The following is an element from the hotel
register XML document:
<firstname>Jim</firstname>
Working with the .NET Namespaces
418
PART II
Because elements are said to include the starting tag, the ending tag, and everything in
between, the following is also an element from our hotel example:
<guests>
<guest id=”jlk0910211”>
<firstname>Jim</firstname>
<middlename>L</middlename>
<lastname>Kelley</lastname>
<roomnbr>295</roomnbr>
<checkindate>6/17/2001</checkindate>
<numnights>4</numnights>
<preferred>No</preferred>
</guest>
</guests>
The <guests> element, in this case, happens to be the outermost element in the XML docu-
ment, and it is referred to as the document element or the root element. You can see that nested
elements set up a parent-child relationship between different pieces of data. The <guest> ele-
ment is a child of the <guests> element and has several child elements of its own, such as
<firstname>, <roomnbr>, and <preferred>. Because of their capability to contain other ele-
ments, elements form the basis for data relationships inside an XML document.
Now let’s take a further step back and examine the different sections to an XML document.
Each XML document has two major sections, the prolog and the document elements.
The Prolog
The prolog section of an XML document is used to specify document-wide settings or attrib-
utes. Typically, this includes the version of XML that the document adheres to, the character
set that it was encoded with, and any external resource references, such as style sheets or
schemas (more on these later in the chapter). The tags and structure of this section are con-
trolled by the actual XML specification. This stands in sharp contrast to the document ele-
ments section, whose structure and tag content are entirely up to the XML document author.
The prolog consists of the XML declaration, processing instructions, and comments.
The XML declaration must be the very first line in the document. The XML declaration con-
sists of three parts: the XML version number, the encoding type, and a “standalone” declara-
tion. The XML version number is required. This references which version of the XML
Reading and Writing XML
419
CHAPTER 10
specification was used to construct the document. The encoding type is optional; if specified, it
identifies which character set was used to encode the document.
NOTE
Utf-8 encoding is the most common type of encoding supported by XML parsers and
writers. If the XML document is encoded with Utf-8 or Utf-16, the parser should be
capable of figuring this out automatically, without it being specified in the XML dec-
laration. If the encoding is not Utf-8 or Utf-16, definitely be sure to include it in the
encoding type. Other encoding types that you might run into include ISO-8859-1, Big-
5, and Shift-JIS. For a good description of XML encoding, see the article “Character
Encodings in XML and Perl,” by Michel Rodriguez, currently available at http://www.
xml.com/pub/a/2000/04/26/encodings/index.htm.
The standalone declaration is also optional. If we had used one in the hotel register XML file,
it would appear like this:
<?xml version=”1.0” encoding=”utf-8” standalone=”no”?>
The standalone value can be set to either Yes or No. A value of Yes indicates that the document
will not reference any external files. That is, no external files will be needed to correctly parse
or understand the document’s content. If an external resource is indicated (such as a style
sheet) and the value is set to Yes, the parser will throw an error. A standalone value of No indi-
cates that external resources may be used to parse and understand the document. No is the
default value if no standalone declaration is made.
Processing Instructions
Processing instructions are a very loosely defined set of statements, typically used for refer-
encing style sheets from within an XML document. Processing instructions really are meant to
be instructions that are passed directly through to applications; it is up to the application how
to handle the instruction. While these processing instructions typically are contained in the
document prolog, they also may appear at the end of the document or even inside the docu-
ment. Here is an example of a processing instruction:
<?xml-stylesheet type=”text/xsl” href=”register_display.xsl”?>
tify the root element of the XML document, and it allows a way for you to relate the XML
document to a Document Type Definition file (DTD). (DTDs are covered in much greater detail
in the section titled “XML Schemas.”) DOCTYPE declarations are not required; when used, the
Working with the .NET Namespaces
420
PART II
root element parameter is the only one required (DTD references are optional). The following
is an example of a DOCTYPE declaration:
<!DOCTYPE guests SYSTEM “URIToDTD”>
The DTD reference can be of type SYSTEM (as shown previously) or of type PUBLIC. Again, we
will cover this topic in more detail during our actual discussion of DTDs later in this chapter
(again, in the “XML Schemas” section).
Comments
Comments also are allowed in the document prolog. Comments look like this:
<!--This is a test-->
Comments also may appear inside the document proper or at the end of the document,
although they cannot be contained within element tags.
Document Elements
All the nodes following the prolog are document elements. This is the actual meat of the docu-
ment. The tag definitions here can be completely customized to structure data exactly the way
that it needs to be structured for the particular application or process that will consume or
write it. Document elements must follow the syntax for well-formed XML, something we will
talk about in detail in the section, “Validating XML Documents.” Essentially, this means that
the elements must follow certain basic rules: They must have a starting tag and an ending tag,
and parent elements must wholly contain their child elements. That is, this is correct:
<parent>
<child></child>
</parent>
This is not:
<parent>
<child>
</parent>
</child>
XmlNodeReader
XmlReader
XmlTextReader
XmlValidatingReader
XmlTextWriter
FIGURE 10.3
The XML reader and writer classes.
The XMLReader class implements functions that can read in an XML text file or stream and
then navigate through the different attributes contained in that file. It also implements proper-
ties that enable you to programmatically determine the current node that is being processed 10
and the content of that node. The XMLWriter class provides functionality to output XML as a
WRITING XML
READING AND
stream. It enables you to generate well-formed XML and manage the progress and status of
the output.
Working with the .NET Namespaces
422
PART II
You can choose from two major design patterns when parsing an XML document. One
involves processing XML text in a forward-only manner, node by node. The other way
involves building a “tree” of an XML document in memory and then hopping from node to
node (or element to element) on the tree. Both approaches are supported by different classes in
the System.XML namespace, and both have their advantages and disadvantages. We’ll start by
examining the first approach: forward-only parsing of nodes.
Quite a few different overloaded constructors are available with the XmlTextReader class.
Supplying a string (as we previously did with fileName) enables you to load a file either from
a local file path or from a URL, as you see in the following two lines of code:
‘read from a local file-path
reader = New XmlTextReader(“C:\xml documents\transcript.xml”)
You also can provide a stream object, instead of a string, to the constructor to parse in an XML
document sitting inside of a stream object. Likewise, you can specify a TextReader object. For
now, let’s concentrate on parsing XML from a local file.
The XmlTextReader exposes a Read method that is used to actually start the parsing. When you
use the Read method, you actually are moving a virtual “viewport” across the document. This
viewport can look at one node at a time; calling the Read method advances it to the next node,
enabling you to examine that node’s properties. Listing 10.2 shows a simple console applica-
tion that reads in an XML file (hotel_register.xml) and then prints its content to the console
window.
Module dataReader
Reading and Writing XML
423
CHAPTER 10
Try
‘Read in an XML file (location stored in fileName)
reader = New XmlTextReader(fileName)
reader.WhitespaceHandling = WhitespaceHandling.None
Finally
If Not (reader Is Nothing) Then
reader.Close()
End If
End Try
End Sub
READING AND
End Module
Working with the .NET Namespaces
424
PART II
In this example, we are setting up a while loop based on the value returned from the Read
method. The Read method has a few different overloaded definitions. The one that we are
using here takes no parameters and simply tells the Reader object to read in the next node
from the target file. If there are no nodes left in the file, it will return a value of false.
Inside the while loop, we execute a second method XmlTextReader.ReadOuterXML. The
ReadOuterXML method returns all the XML content of the current node and all its children as a
string. Because we have just issued the Read command once at this point, the current node
would be the very outermost node (<hotel>). That means that we would expect this method to
return the entire file, with the exception of the XML version spec. In other words, it should
return all the document elements. In fact, this is just what it does (see Listing 10.3).
Compare the actual XML source, Listing 10.3, to the output in Figure 10.4.
FIGURE 10.4
XML file output to the console.
The XmlTextReader.ReadState property is a handy way to tell exactly what state the reader is
currently in. You can use it to tell whether the reader has reached the end of the file, to see
whether it is currently reading a node, or even whether it has been closed. The ReadState
10
property returns an instance of a ReadState enumeration (see Table 10.2).
WRITING XML
READING AND
Working with the .NET Namespaces
426
PART II
This example gets us started. You have seen how to reference an XML document into our
XmlTextReader through its class constructor. You also have seen how to use the Read method
to advance through the document one node at a time, and how to use the ReadState property
to interpret exactly what the reader is currently doing. Now, let’s see what type of information
we can retrieve about the specific node that we have read in.
Let’s make a few modifications to the code.
Instead of using the ReadOuterXml method, let’s truly parse this document node by node, print-
ing out the pertinent node properties to the console along the way. Because the XmlTextReader
class is constructed entirely around node-by-node access, many of its properties return infor-
mation about the current node that has been read in through the Read method. Table 10.3
shows all the properties of the XmlTextReader class that are specific to the current node.
Name Description
AttributeCount Returns the number of attributes defined on the current node
BaseURI Returns the base URI string for the current node
Depth Returns the depth of the current node in the overall XML docu-
ment structure
Encoding Returns the encoding attribute for the current node
HasAttributes Returns a Boolean value indicating whether the current node has
any attributes
HasValue Returns a Boolean value indicating whether the current node has
a value
IsDefault Returns a Boolean value indicating whether the current node’s
value was derived as the result of using a default value specified
in a DTD or XSD file
Reading and Writing XML
427
CHAPTER 10
By including code inside our while loop (which is based on the Read method), we can exam-
ine the value of each of these properties and then write that value to the console. The new,
revised code appears in Listing 10.4.
Module dataReader
Sub Main()
Const fileName = “hotel_register.xml”
Try
‘Read in an XML file (location stored in fileName)
reader = New XmlTextReader(fileName)
Dim nodeType As XmlNodeType
reader.WhitespaceHandling = WhitespaceHandling.None
‘false
While reader.Read()
End While
MsgBox(“An XML error has occurred (“ & xmlErr.Message & “).” & _
vbCrLf & vbCrLf & “ Xml Source Line Nbr: “ & _
reader.LineNumber & vbCrLf & “ Xml Source Position: “ & _
reader.LinePosition)
MsgBox(“An error occurred (“ & appErr.Message & “). Stack info:” & _
appErr.StackTrace)
End Try
End Module
When you run this code, you will get output similar to that shown in Figure 10.5. In this
screenshot, you can see the start of a <guest> node.
In the code, you will notice that we use an enumeration to determine the element type. The
XmlNodeType enumeration (documented for you in Table 10.4) is often a useful branching indi-
cator when parsing a document. For instance, our parser might not care about comments, XML
declarations, or processing instructions; we could choose to just ignore those types of nodes
Reading and Writing XML
429
CHAPTER 10
and process only nodes of type Element or CDATA. It also comes in handy in understanding
how nodes are pieced together to form elements and other constructs. Peruse the output gener-
ated by the code in Listing 10.4—it will give you a much better understanding of how parsers
view and treat each node.
FIGURE 10.5
Node properties output.
• Processing instructions
• XML declarations
• DOCTYPE declarations
Working with the .NET Namespaces
432
PART II
Because these items are rarely needed by your actual parsing algorithm, ignoring them auto-
matically by using the MoveToContent method is an efficient way to get at core data in the
XML document.
The second way that you can efficiently ignore irrelevant XML markups is through the use of
XmlReader.Skip. The Skip method enables you to jump from element to element instead of
from node to node. Let’s examine a snippet from our hotel_register.xml file:
<guest id=”jlk0910211”>
<firstname>Jim</firstname>
<middlename>L</middlename>
<lastname>Kelley</lastname>
<roomnbr>295</roomnbr>
<checkindate>6/17/2001</checkindate>
<numnights>4</numnights>
<preferred>No</preferred>
</guest>
<guest id=”nlt0000704”>
<firstname>Nadia</firstname>
<middlename>L</middlename>
<lastname>Tatonovich</lastname>
<roomnbr>615</roomnbr>
<checkindate>6/17/2001</checkindate>
<numnights>4</numnights>
<preferred>No</preferred>
</guest>
If we were reading in this file using XmlTextReader.Read, we would visit each node in turn
until the end of the document. If we wanted only a certain guest, however, it would be easier to
skip over the entire <guest> element to get to the next one if the guest doesn’t have the ID we
are looking for. If we were specifically interested only in the guest record with the ID of
nlt0000704 and we had just read in the node <guest id=”jlk0910211”>, we could immedi-
ately skip to the next guest record by calling Skip. Listing 10.5 shows a revision to our previ-
ous code that iterated through each node. By using the Skip method, we jump over entire
elements at a time until we arrive at the specific node that we want, which is then printed to
the console (see Figure 10.6).
Module dataReader
Sub Main()
Const fileName = “hotel_register.xml”
Reading and Writing XML
433
CHAPTER 10
Try
‘Read in an XML file (location stored in fileName)
reader = New XmlTextReader(fileName)
Dim nodeType As XmlNodeType
reader.WhitespaceHandling = WhitespaceHandling.None
Exit While
READING AND
Else
reader.Skip()
Working with the .NET Namespaces
434
PART II
MsgBox(“An XML error has occurred (“ & xmlErr.Message & “).” & _
vbCrLf & vbCrLf & “ Xml Source Line Nbr: “ & _
reader.LineNumber & vbCrLf & “ Xml Source Position: “ & _
reader.LinePosition)
MsgBox(“An error occurred (“ & appErr.Message & “). Stack info:” & _
appErr.StackTrace)
Finally
If Not (reader Is Nothing) Then
reader.Close()
End If
End Try
End Module
Using the XmlTextReader is a great way to quickly run through an XML document because it
is specifically optimized for speed. Its disadvantage lies in its forward-only nature. It doesn’t
facilitate hopping around an XML document, forward and backward. An alternative approach
to parsing XML documents revolves around the XmlDocument class.
Reading and Writing XML
435
CHAPTER 10
FIGURE 10.6
The Skip output.
When the XML file is loaded into the XmlDocument object, you can traverse the nodes by using
instances of the XmlNode class. The XmlNode class represents individual nodes in a document
and has methods available to move from node to node. If we wanted to process the tree from
the top down, we would first set our current node to the document’s root. This is called the
document element, and a reference to it is returned through the XmlDocument.DocumentElement
property (we touched on the concept of a document element earlier in the chapter in our dis-
cussion of elements).
Dim startNode As XmlNode
startNode = document.DocumentElement
Now, we can set up a loop to run through all the children of the document root. If we are care-
ful in the way we approach this, we can even set up a recursive routine to parse through the 10
nodes like this:
WRITING XML
READING AND
In this code, we accept an instance of the XmlNode class and first determine whether it has any
children nodes. We then assign the current node instance to be the first child of itself. Now the
recursive part comes into play: We then pass this new node reference into the RecurseNodes
routine again. Finally, we move on to the next sibling node by using the XmlNode.NextSibling
method call.
NOTE
A node is said to be a sibling to another node if they are both immediate children of
the same parent node in the tree structure of the document. In our hotel_register.xml
file, all the <guest> nodes are siblings to one another.
By using the FirstChild, ParentNode, and NextSibling properties on the XmlNode class, we
can navigate through the document using the parent and child relationships inherent in its
structure. See Listing 10.6 for an example of navigating through nodes. The following revision
of the previous console application displays the parent-child relationships in the XML
document.
Module dataReader
Sub Main()
‘Path and name of the XML file to parse
Const fileName = “..\hotel_register.xml”
Try
‘Read in an XML file (location stored in fileName)
reader = New XmlTextReader(fileName)
Reading and Writing XML
437
CHAPTER 10
MsgBox(“An XML error has occurred (“ & xmlErr.Message & “).” & _
vbCrLf & vbCrLf & “ Xml Source Line Nbr: “ & _
reader.LineNumber & vbCrLf & “ Xml Source Position: “ & _
reader.LinePosition)
MsgBox(“An error occurred (“ & appErr.Message & “). Stack info:” & _
appErr.StackTrace)
Finally
If Not (reader Is Nothing) Then
reader.Close()
End If
Console.ReadLine()
End Try 10
WRITING XML
READING AND
End Sub
Working with the .NET Namespaces
438
PART II
End While
End If
Reading and Writing XML
439
CHAPTER 10
Figure 10.7 shows the output from this applet. Each node in the document is displayed along
with its ancestry (a list of all the parent nodes).
FIGURE 10.7
Parent-child lineage from the XmlDocument class.
Just like the XmlTextReader class, the XmlNode class exposes a NodeType property that can tell
you which type of node you are dealing with (in conjunction with the XmlNodeType enumera-
tion—refer back to Table 10.4).
There is also a way to return nodes by name by using the GetElementsByTagName method. The
XmlDocument.GetElementsByTagName method is handy because it returns an instance of an
XmlNodeList class, which has a list of node entities matching a tag name (which you pass to
the method). The following code returns a node list of all nodes matching the tag name
product. It then processes each of the nodes contained in the XmlNodeList object and writes
out their XML content:
10
WRITING XML
READING AND
If we had a requirement to extract all the <guest> elements from the hotel_register.xml docu-
ment, this would be quite easy to implement using the XmlNodeList returned by this method.
In Listing 10.7, we first load our familiar hotel_register.xml file into an XmlDocument instance,
and then we place a call to GetElementsByTagName, passing in the tag name of guest. The
application then writes out the XML contained by each of the guest elements to the console.
Module dataReader
Sub Main()
Const fileName = “hotel_register.xml”
Dim xmlDoc As New XmlDocument()
Try
‘simple var for our loop into the node list
Dim i As Integer
‘get the node list of all nodes matching the tag name “guest”
Dim nodes As XmlNodeList = xmlDoc.GetElementsByTagName(“guest”)
End Module
FIGURE 10.8
Parsing elements by their tag name.
developer’s job easier). These two categories are referred to as fundamental and extended,
respectively. The .NET Framework Class Library has support for both fundamental structures
and extended structures. Table 10.5 lists the Framework classes considered to be fundamental;
Working with the .NET Namespaces
442
PART II
Table 10.6 lists those considered to be extended. Developers already familiar with program-
ming against the W3C DOM should be very comfortable with using these classes.
There is a fairly even match between the properties and methods supported on both the
XmlTextReader class and the XmlNodeReader class—so why would you use XmlNodeReader
objects? Remember that instances of XmlDocument are in-memory representations of XML
documents. If you have an extremely large XML document, this can prove to be problematic in
terms of resource usage. In those cases, the XmlTextReader is the optimal solution because the
XML document is not represented in memory at all; operations are conducted as straight read
operations from a file or a stream.
tation on the actual XmlReader class to see what it provides you in terms of base func-
tionality; you might want to implement your own class to deal with XML that is not
coming from a source that the .NET classes recognize.
Working with the .NET Namespaces
444
PART II
➲ We have talked about parsing discrete pieces from an XML document, but if you need to
actually query an XML document, you will need to dig into the System.Xml.Xpath
namespace. It supports the XPath standards for querying XML documents and returning
nodes as data.
Now that the stage is set, let’s see how to write a simple XML file.
You can see that, in addition to the file and stream outputs, you have the option to specify a
TextWriter instance. This turns out to be a very powerful feature. The TextWriter class, in
the System.IO namespace, is an abstract class designed to allow for any possible text output.
The Framework already implements a few customized writers for HTTP and HTML; others
can be created from the TextWriter base class. In this way, the options for XML output are
bound only by what can be implemented using the TextWriter class. See Chapter 6, “Font,
Text, and Printing Operations,” for more information.
After you have specified what form the output will take, you can begin to write into that output
channel. As expected, XmlTextWriter exposes a vast spectrum of write methods to handle all
the different node types. In fact, out of the 30 different methods implemented in the class
(whether inherited from XmlWriter or newly implemented), all but three of them are actual
methods that write XML. Most of these write methods are specialized to write specific XML
nodes or elements. In addition to these dedicated tag writers, one method enables you to write
raw text into the XML document (WriteRaw), and one enables you to “copy” a node from an
instance of an XML reader (WriteNode). Table 10.8 presents all the pertinent methods and
their descriptions.
Name Description
WriteAttributes Writes out any attributes located at the current node of an
associated reader object
WriteAttributeString Writes out an attribute and value
WriteBase64 Encodes bytes in Base64 and then writes out the result
WriteBinHex Encodes bytes as binhex and then writes out the result
WriteCData Writes out a CDATA block with the supplied text
WriteCharEntity Writes out a character entity with the supplied text
WriteChars Writes out characters to the output channel
WriteComment Writes out a comment containing the supplied text
WriteDocType Writes out a DOCTYPE declaration with the supplied name
and attributes 10
Writes out an element with the supplied text
WRITING XML
WriteElementString
READING AND
To see this in action, let’s write a simple (but slightly meaningless) program that reads in an
XML file using XmlTextReader and then writes it back out, a node at a time, using
XmlTextWriter (see Listing 10.8).
The first step is to set up a loop that runs through the entire source XML file. For each node
encountered, the program will examine the type of node and its various properties and will
execute the appropriate write method. After having processed the file, it should be easy to tell
whether we have gotten the XmlTextWriter code right: The files should be visibly identical to
one another.
Reading and Writing XML
447
CHAPTER 10
Module dataReaderWriter
Sub Main()
Const FILE_NAME_IN = “..\hotel_register.xml”
Const FILE_NAME_OUT = “..\hotel_register_out.xml”
Try
‘Read in an XML file (location stored in fileName)
reader = New XmlTextReader(FILE_NAME_IN)
‘Ignore whitespaces
reader.WhitespaceHandling = WhitespaceHandling.None
‘Loop through the source file; for each different node type
‘encountered, call the corresponding write method off the writer
‘object. The goal here is a carbon copy of the source file. If a
‘node type is encountered, the user will be notified via the
‘console. Try experimenting on your own XML documents. Where the
‘code encounters a node type that isn’t handled in the Select Case,
‘try adding it in with the appropriate writer method.
10
While reader.Read()
WRITING XML
READING AND
Working with the .NET Namespaces
448
PART II
Case XmlNodeType.Comment
writer.WriteComment(reader.Value)
Case XmlNodeType.Element
Console.WriteLine(“Writing: start of element -> ‘“ _
& reader.Name & “‘“)
writer.WriteStartElement(reader.Name)
If reader.HasAttributes Then
For i = 0 To reader.AttributeCount - 1
reader.MoveToAttribute(i)
writer.WriteAttributeString(reader.Name, _
reader.Value)
Next
End If
Case XmlNodeType.Text
Console.WriteLine(“Writing: element content -> ‘“ _
& reader.Value & “‘“)
writer.WriteString(reader.Value)
Case XmlNodeType.EndElement
Console.WriteLine(“Writing: end of element -> ‘“ & _
reader.Name _
& “‘“)
writer.WriteEndElement()
Case Else
Console.WriteLine(“!!!An un-handled node type was _
encountered:” & reader.NodeType)
End Select
End While
MsgBox(“An XML error has occurred (“ & xmlErr.Message & “).” & _
vbCrLf & vbCrLf & “ Xml Source Line Nbr: “ & _
reader.LineNumber & vbCrLf & “ Xml Source Position: “ & _
reader.LinePosition)
MsgBox(“An error occurred (“ & appErr.Message & “). Stack info:” & _
appErr.StackTrace)
End Try
End Module
Most of this code should be self-explanatory. However, a few pieces deserve a closer look.
In the Select Case statement, if the node is a start element node, we also have to deal with
the possibility that the start element tag will have attributes that we must write out. There are
two ways of doing this. In Listing 10.8, we have used the WriteAttribute method. This writes
out a textual name value pair to the specified document. An alternative, and arguably easier,
way is to use the WriteAttributes method. The WriteAttributes method is designed to
work in conjunction with a reader object, and it behaves in different ways depending on where
the reader is currently positioned. If the reader is positioned at a start element tag, it will write
out any and all attributes and then close the tag. In other words, we would have replaced that
Select Case element with this:
Case XmlNodeType.Element 10
Console.WriteLine(“Writing: start of element -> ‘“ & reader.Name _
WRITING XML
READING AND
& “‘“)
writer.WriteStartElement(reader.Name)
Working with the .NET Namespaces
450
PART II
If reader.HasAttributes Then
writer.WriteAttributes(reader, False)
End If
.
.
.
The second parameter supplied to the reader is a Boolean value that indicates whether to write
any default attributes that might be attached to the XML document.
The second item that can’t be discerned from a simple examination of the code is how the
XmlTextWriter.Close method works. This method closes out the document (and thus the file,
stream, and so on) that the writer was working on. But you should know that it also automati-
cally closes out any open-ended tags by writing out their end element tags for you. That means
that if you left the document in a state like this
<parent>
<child>
and then called Close, the document would actually look like this:
<parent>
<child>
</child>
</parent>
Now that we have a grasp of the mechanics for reading and writing XML documents, we can
move on to examining how XML documents can be validated. Validation implies that a
schema exists that will describe the content of a particular XML document, so that is
where we will start our discussion.
XML Schemas
Because XML documents adhere to a specific schema, or layout, it is often desirable to verify
that a particular XML document remains true to the schema that it is supposed to follow.
Having the capability to define a schema externally to an XML document allows XML as a
technology and a language to continue further than other markup languages such as HTML.
What do we mean when we say that schemas can be defined externally? Well, one of the
things that makes XML so well suited to data descriptions is this: The actual tags for XML are
not defined in the XML specification. This means that XML can behave as a metalanguage.
Metalanguages can be used to define other languages. This neatly side-steps the problem of
being locked into a specific tag set. Regardless of how large a predefined tag set is, it will
never satisfy everyone’s needs regarding structuring data.
Reading and Writing XML
451
CHAPTER 10
The capability for XML documents to define their own multiple tag sets is an important differ-
entiator from the other markup languages that we have discussed. XML documents can define
their tags through yet another document—this second document contains the schema to which
the first document must adhere. A few different standards define what this schema document
does and what its syntax looks like.
The XML classes in the .NET Framework support Document Type Definitions (DTD), XML
Schema Documents (XSDs), and XML-Data Reduced Language schemas (XDRs). This sec-
tion examines each of these in turn and then walks through the Framework support for validat-
ing XML documents against each.
DTD Documents
We’ll start our look at XML schema descriptions with the DTD. Here, we have assembled a
short DTD file:
<!ELEMENT books (book)*>
<!ELEMENT book (title, chapters, summary, price)>
<!ELEMENT title (#PCDATA)>
<!ELEMENT chapters (chapter)*>
<!ELEMENT chapter (#PCDATA)>
<!ELEMENT summary (#PCDATA)>
<!ELEMENT price (#PCDATA)>
This is a DTD that a publishing company might use. It defines the structure for an XML docu-
ment that will describe books. The syntax might look a little strange, but it should be easy to
see what the intent of the different pieces is. Of course, the DTD—which is just a plain text
file—follows rules of its own. The DTD rules are part of the actual W3C XML specification.
An XML document that adheres to the previous DTD (let’s call it books.dtd) would look
something like this:
<!DOCTYPE books SYSTEM “book.dtd”>
<books>
<book>
<title>VB.NET Unleashed</title>
<chapters>
<chapter>Introduction</chapter>
<chapter>The New IDE</chapter>
<chapter>...</chapter>
</chapters> 10
<summary> blah blah blah </summary>
WRITING XML
READING AND
<price>59.99</price>
</book>
</books>
Working with the .NET Namespaces
452
PART II
If you closely examine this XML document, you can start to make a tie back to the concepts of
specifications versus tag usage rules. The XML specification says that we must enclose our
tags in < and > characters (as you can see with <books>); the DTD tells us that the <name>,
<chapters>, <summary>, and <price> tags are contained within the <book> tag.
Because the DTD used in a particular XML document can vary from document to document,
the XML specification provides syntax for indicating which DTD defines the structure of the
XML document. This is the DOCTYPE declaration that we visited earlier in this chapter when we
discussed the anatomy of an XML document—and that we now see as the first line in our
XML document snippet.
</xsd:complexType>
READING AND
</xsd:element>
</xsd:schema>
The XSD specification is a more sophisticated descendant of the XDR specification. The XDR
format started life at Microsoft. It was this format that was later adopted and extensively
extended by the W3C to create the XSD format standard. In principle, the XDR format most
likely will be phased out in favor of the XSD format, but the XML classes are “aware” of both.
As a reference point, Listing 10.10 again shows our book DTD document. In this incarnation,
it follows the XDR format.
NOTE
Although we can’t recommend using XDR as your schema format, you might have
some significant work already invested in that precursor to the XSD format. Microsoft
provides a utility—distributed along with the .NET Framework SDK—called XSD.exe.
It accepts an XDR file as input and writes out an equivalent XSD file:
xsd.exe <schema.xdr> [/outputdir:]
This handy utility also can generate XSD schemas from .NET assemblies and XML
documents.
In an XML file, you can reference an XDR or XSD schema by using the following syntax:
<HeadCount xmlns=’x-schema:books.xdr’>
Reading and Writing XML
455
CHAPTER 10
This directs any schema-aware parser to the source schema document used to structure the
XML document.
Both XSD files and XDR files enable you to define new attribute types in addition to just spec-
ifying the order and relationship between elements of an XML file. Consider this revised XML
file that describes a book. Note that, in the line where we begin the actual book node, we have
added an attribute to the node called ISBN.
<books>
<book ISBN=”xxxxxxxx”>
<title>VB.NET Unleashed</name>
<chapter>Introduction</chapter>
<chapter>The New IDE</chapter>
<chapter>...</chapter>
<abstract> blah blah blah </summary>
<price>59.99</price>
</book>
</books>
We now can define this attribute, which lives inside the book tag, by doing this inside
our XSD:
<AttributeType name=”ISBN” dt:type=”string” required=”yes”/>
We have just created a brand new attribute type; this new attribute type is of the string data
type, and must be included everywhere that it is referenced in association with an element.
Integrating this into our XSD results in this:
<Schema name=”BOOK” xmlns=”urn:schemas-microsoft-com:xml-data”>
<AttributeType name=”ISBN” dt:type=”string” required=”yes”/>
<ElementType name=”name” content=”textOnly”/>
<ElementType name=”chapter” content=”textOnly”/>
<ElementType name=”chapters” content=”eltOnly” model=”closed”>
<element type=”chapter” maxOccurs=”*”/>
</ElementType>
<ElementType name=”summary” content=”textOnly”/>
<ElementType name=”price” content=”textOnly”/>
<ElementType name=”book” content=”eltOnly” model=”closed”/>
<attribute type=”ISBN”/>
<element type=”title”/>
<element type=”chapters”/>
<element type=”summary”/> 10
<element type=”price”/>
WRITING XML
</ElementType>
READING AND
NOTE
The System.Xml.Schema namespace implements .NET’s XML Schema Object Model
(SOM). The SOM is essentially a Document Object Model specifically designed to
implement XML Schemas. The classes in the SOM directly correspond to the specifica-
tions laid out in the W3C’s XML Schema Recommendation (visit http://www.w3.org
for more information).
To explore the capabilities of the schema classes that the .NET Framework Class Library pro-
vides, let’s revisit our hotel registration scenario. An XSD that would describe the hotel_regis-
ter.xml document is presented in Listing 10.11. To refresh your memory on what the hotel
registration XML document looks like, refer back to Listing 10.3.
LISTING 10.11 An XSD Schema for the Hotel Register XML Document
<xsd:schema xmlns:xsd=”http://www.w3.org/2001/XMLSchema” _
attributeFormDefault=”qualified” elementFormDefault=”qualified”>
<xsd:element name=”hotel”>
<xsd:complexType>
<xsd:sequence>
<xsd:element name=”guests” minOccurs=”0” maxOccurs=”unbounded”>
<xsd:complexType>
<xsd:sequence>
<xsd:element name=”guest” minOccurs=”0” maxOccurs=”unbounded”>
<xsd:complexType>
<xsd:sequence>
<xsd:element name=”firstname” type=”xsd:string”
minOccurs=”0” />
<xsd:element name=”middlename” _
type=”xsd:string”
minOccurs=”0” />
<xsd:element name=”lastname” _
type=”xsd:string”
minOccurs=”0” />
<xsd:element name=”roomnbr” type=”xsd:string”
Reading and Writing XML
457
CHAPTER 10
This XSD seems dense and complicated, but it really isn’t. Let’s walk through the pieces. First,
what do we know about the hotel_register.xml document itself?
• It contains a container element for the hotel itself.
• The hotel element contains a guests element, which, in turn, contains all the guest ele-
ments.
• Each guest element contains a firstname, middlename, lastname, roomnbr,
checkindate, numnights, and preferred element.
• There may be many guest elements and many hotel elements (although we have shown
examples with only one hotel instance, to keep things simple).
The XSD file should be a straightforward replay of this information. The first thing that the
XSD does is set up a bunch of header information, such as the name of the XSD, the name- 10
space it belongs to, and so on. Then it identifies the root document element, hotel, like this:
WRITING XML
READING AND
<xsd:element name=”hotel”>
The hotel element is considered a complex type because its subnodes are nontextual and
because it has attributes. Simple types can hold only values and may not have element or
Working with the .NET Namespaces
458
PART II
attribute subnodes. Everything inside this complex type is described by a sequence schema ele-
ment. A sequence element simply defines an order to the child elements. After this, the docu-
ment matches up to our next XML element: the guests element.
<xsd:complexType>
<xsd:sequence>
<xsd:element name=”guests” minOccurs=”0” maxOccurs=”unbounded”>
Just as with the hotel element, the guests element is also a complex type, with a sequence of
subnodes below it. Our next element up for description is the guest element. This element is
contained inside the guests element and thus appears nested inside it. As usual, this element is
described using a complex type.
<xsd:element name=”guest” minOccurs=”0” maxOccurs=”unbounded”>
<xsd:complexType>
<xsd:sequence>
<xsd:element name=”firstname” type=”xsd:string” minOccurs=”0” />
<xsd:element name=”middlename” type=”xsd:string” minOccurs=”0” />
<xsd:element name=”lastname” type=”xsd:string” minOccurs=”0” />
<xsd:element name=”roomnbr” type=”xsd:string” minOccurs=”0” />
<xsd:element name=”checkindate” type=”xsd:string” minOccurs=”0” />
<xsd:element name=”numnights” type=”xsd:string” minOccurs=”0” />
<xsd:element name=”preferred” type=”xsd:string” minOccurs=”0” />
</xsd:sequence>
<xsd:attribute name=”id” form=”unqualified” type=”xsd:string” />
</xsd:complexType>
</xsd:element>
We also can see that the guest ID attribute is defined inside the guest element schema. The
rest of the document consists of just the closing tags for all these elements that we have dis-
cussed. So, you can see that the XSD might be confusing at first glance, but it is very easy to
read and understand when looked at from the vantage point of the XML file that it is supposed
to define.
To add the schema elements into the overall schema document, we use the XmlSchemaElement
class. This class encapsulates XSD elements. To create our hotel “root” element, the code
would look like this:
Reading and Writing XML
459
CHAPTER 10
To create the complex type and sequence groupings, we need to use the
XmlSchemaComplexType class and the XmlSchemaSequence class:
Setting the SchemaType property of the elementHotel object to the previously created
complex type object tells the schema generator that the hotel element is a complex type. The
XmlSchemaComplexType.Particle property then is used to identify whether this complex type
element contains a choice (represented by XmlSchemaChoice), a sequence (represented by
XmlSchemaSequence), or a nonsequenced grouping of child nodes (XmlSchemaAll).
We now have created the opening pieces of the hotel element. To add it to the schema, we sim-
ply write this:
‘Add the element to the schema
mySchema.Items.Add(elementHotel)
This adds the elementHotel object to the root schema document. To add more child nodes to
the hotel element itself, we can just execute the same Add method. Keep in mind, however,
that these child elements are being added to the sequence grouping and not directly to the
elementHotel node:
Listing 10.12 shows a console application that builds the XSD file that we witnessed in
Listing 10.11.
Sub Main()
Try
Working with the .NET Namespaces
460
PART II
mySchema.Write(Console.Out)
Finally
Console.WriteLine()
Console.WriteLine(“Hit ‘Enter’ to exit.”)
Console.ReadLine()
End Try
End Sub
Reading and Writing XML
463
CHAPTER 10
If we were targeting XSD validation specifically, we would set the property like this:
validator.ValidationType = ValidationType.Schema
NOTE
You must set the ValidationType property before the first call to
XmlValidatingReader.Read.
The source XML document should specify its attendant schema document, but you also have
the option of building up a collection of schemas and then validating XML documents against
that collection. The XmlSchemaCollection class can contain both XSD and XDR schemas
for this purpose. An XmlSchemaCollection instance then can be assigned through the
XmlValidatingReader.Schemas property. When the document is parsed by calling the validat-
ing reader’s Read, ReadInnerXml, ReadOuterXml, or Skip methods, the document is compared
and analyzed against the schema defined inside the XmlSchemaCollection instance:
‘create a new schema collection object
Dim schemaCollection as XmlSchemaCollection = new XmlSchemaCollection()
‘now, add the schema collection through the validating reader’s schemas
‘property
reader.ValidationType = ValidationType.Schema
reader.Schemas.Add(schemaCollection)
If you are validating multiple XML documents against the same XML schema, using an XML
schema collection object will help improve application performance because the actual schema
is loaded only once and then is cached for further references.
Now that we have indicated the type of schema validation that we want performed and exactly
what schema we want to validate against, we can start parsing through the XML document
using the validating reader. As the validating reader encounters XML elements that do not con-
form to the specified schema and schema type, it flags a validation issue. It may do so by rais-
ing either exceptions or events.
There are a few things to note about the event handler. For one, its signature needs to match
that of the ValidationEventHandler delegate (this means that we need the sender object and
the ValidationEventArgs object as parameters). The second thing to note is how the actual
validation issue is carried into the event handler: through the ValidationEventArgs instance.
The ValidationEventArgs, also defined in System.Xml.Schema, carries three properties useful
for specifying the exact validation failure that occurred. The Message property returns the
actual description of the failed validation, the Exception property returns an instance of the
XmlSchemaException class associated with the validation failure, and the Severity property 10
returns a value from the XmlSeverityType enumeration (documented in Table 10.10).
WRITING XML
READING AND
Working with the .NET Namespaces
466
PART II
Because the actual validation error event is defined on the XmlValidatingReader class (it is
called ValidationEventHandler), the next step is easy: Use the AddHandler command to tell
the runtime that our HandleValidationError sub should handle any
ValidationEventHandler events:
With these two pieces in place, we can start the read of the XML document. This happens in
an identical way to the read design pattern of the XmlTextReader class.
While validator.Read()
.
.
.
End While
Reading and Writing XML
467
CHAPTER 10
Finally
‘Close the reader.
If Not (validator Is Nothing) Then
validator.Close()
End If
End Try
In general, it is preferable to handle validation issues using events instead of exceptions. Using
the event handler gives you access to more information about the error, allows the validator to
continue or to be restarted, and enables you to differentiate between actual errors and valida-
tion discrepancies.
FIGURE 10.9
The main window.
Code Walkthrough
Listing 10.13 shows a sample XML file that you can use for testing the ReservationsDesk
application.
<guest id=”cnh0002652”>
READING AND
<firstname>Casper</firstname>
<middlename>N</middlename>
<lastname>Houston</lastname>
<roomnbr>109</roomnbr>
Working with the .NET Namespaces
470
PART II
Only two items have been added into the global class scope: resFile (a string pointing to the
currently selected XML document) and xmlDoc (an XmlDocument instance holding the XML
file pointed at by resFile). Listing 10.14 shows the ReservationsDesk application source
code.
Me.TextBoxMiddleName.Name = “TextBoxMiddleName”
Me.TextBoxMiddleName.ReadOnly = True
Me.TextBoxMiddleName.Size = New System.Drawing.Size(168, 20)
Me.TextBoxMiddleName.TabIndex = 3
Working with the .NET Namespaces
474
PART II
‘
READING AND
‘ComboHotel
‘
Me.ComboHotel.DropDownStyle = _
System.Windows.Forms.ComboBoxStyle.DropDownList
Working with the .NET Namespaces
476
PART II
Me.GroupBox1.TabIndex = 0
READING AND
Me.GroupBox1.TabStop = False
‘
‘Main
‘
Me.AutoScaleBaseSize = New System.Drawing.Size(5, 13)
Working with the .NET Namespaces
478
PART II
End Sub
#End Region
This menu event handler launched the Open File dialog box to allow users to “load” an XML
document (this needs to be a document that specifically follows the correct schema). After
loading the XML document, it parses the hotel nodes to get a list of unique hotel IDs.
Private Sub MenuItem2_Click(ByVal sender As System.Object, ByVal e As _
System.EventArgs) Handles MenuItem2.Click
OpenFileDialog1.DefaultExt = “.xml”
OpenFileDialog1.Filter = “Reservation files (*.xml)|*.xml”
OpenFileDialog1.InitialDirectory = _
System.Reflection.Assembly.GetExecutingAssembly.Location
OpenFileDialog1.ShowDialog()
resFile = OpenFileDialog1.FileName
If resFile = “” Then
Me.Text = “ReservationsDesk (no file loaded)”
Else
Me.Text = “ReservationsDesk (file: “ & resFile & “)”
LoadResFile(resFile)
ListHotels()
Reading and Writing XML
479
CHAPTER 10
End Sub
This is the routine that performs the actual load of the XML document from a file:
Public Sub LoadResFile(ByVal file As String)
Dim str As String
Dim i As Integer
Dim reader As XmlTextReader
Dim validator As XmlValidatingReader
Try
‘If the file didn’t validate, this handler should catch that...
Catch validationErr As XmlException
MsgBox(“The reservation file ‘“ & resFile & “‘ has failed to _
validate against the corporate data schema standard. The file _
may be corrupt, or may be from an invalid source. Please _
select a new file.”)
‘“ & resFile & “‘. The file may be corrupt, or may be from an _
READING AND
End Try
End Sub
The ExecuteQuery routine takes the supplied search criteria and processes each node in the
document, one by one, until it finds the correct match. These matches are returned in the form
of guest IDs that show up in the list box to the lower left of the screen.
Private Sub ExecuteQuery()
Dim nodeReader As XmlNodeReader
Dim currID As String
Dim fullName() As String
Dim firstName As String
Dim lastName As String
Dim nodeLastName As String
Dim nodeFirstName As String
Try
‘We use an XmlNodeReader here to navigate the document
nodeReader = New XmlNodeReader(xmlDoc)
ListBoxGuests.Items.Clear()
lastName = Trim(fullName(0))
firstName = Trim(fullName(1))
‘This outer while loop keeps the cursor moving until it finds
‘a “hotel”
Reading and Writing XML
481
CHAPTER 10
Exit While
End If
End If
End While
Else
‘This branch handles searching by check-in date
‘This outer while loop keeps the cursor moving until it finds
‘a “hotel”
While nodeReader.Read()
If nodeReader.NodeType = XmlNodeType.Element And _
nodeReader.Name = “hotel” Then
End While
Exit While
End If
End If
End While
10
WRITING XML
End If
READING AND
Working with the .NET Namespaces
484
PART II
End Sub
ListHotels is responsible for parsing the hotel node information out of the document and into
the hotel drop-down control.
Public Sub ListHotels()
Try
Dim reader As XmlNodeReader = New XmlNodeReader(xmlDoc)
End While
End Sub
Finally, this is the routine that goes after a specific guest node, displaying its information on
the screen. The selection of the guest node is based on the selected guest ID in the list box.
Public Sub DisplayGuestInfo(ByVal id As String)
Dim nodeReader As XmlNodeReader
Try
nodeReader = New XmlNodeReader(xmlDoc)
End Sub
Summary
In this chapter, we discussed the importance of XML and how its characteristics as a data-
description language can be put to good use with the .NET Framework.
Specifically, you learned how to leverage the System.Xml and System.Xml.Schema classes to
do the following:
• Read and parse XML documents from files and streams using the XmlTextReader class
• Write well-formed XML documents using the XmlTextWriter class
• Validate XML documents with the XmlValidatingReader class
• Create well-formed schemas, element by element, using the XmlSchema class and its
attendant element classes, such as XmlComplexType and XmlSequence
After reading this chapter, you should have a good understanding of not only how to interact
with XML using the Framework Class Library, but also how to best deploy XML as a technol-
ogy in your applications.
10
WRITING XML
READING AND
XSLT and XPath CHAPTER
11
IN THIS CHAPTER
• Key Classes Related to XSLT and XPath 490
• Learning by Example:
ReservationsDesk 2 524
Working with the .NET Namespaces
490
PART II
This chapter continues our discussion of the System.Xml namespaces by delving into the .NET
Framework support for XSL transformations (XSLT) and XPath queries.
Although the System.Xml namespace exposes objects uniquely suited to navigating and read-
ing XML documents, the power offered by XPath expressions in that realm is unparalleled.
XSL style sheets and their attendant transformation capabilities also bring much to the XML
developer’s bag of tricks.
We’ll start off this chapter by giving a beginner’s introduction to XSLT and XPath. Then we’ll
explore the namespace support for style sheet transformations, navigation of XML documents
using XPath, and finally, the process of querying XML documents for matching node sets
using XPath expressions.
By the end of this chapter, you should be able to:
• Transform an XML document into another structured document format (including
HTML) using the System.Xsl classes
• Navigate XML documents, forward or backward, by using the features of the
XPathNavigator class and its “helper” classes
• Construct complicated XPath expressions, issue them against an XML data store, and
then process the results
XSLT
XSLT Transformations
AND
IXsltContextFunction Interface This is an interface to an XSLT function in an XSLT
style sheet.
XPATH
IXsltContextVariable Interface This is an interface to an XSLT variable in an XSLT
style sheet.
XsltArgumentList Class The XsltArgumentList Class encapsulates a collec-
tion of XSLT-defined parameters.
XsltCompileException Class This class encapsulates a collection of XSLT com-
piler exceptions.
XsltContext Class This class represents the current context of the XSLT
processor. It is used primarily to allow resolution of
functions, parameters, and namespaces.
XsltException Class This class is used to represent any errors thrown dur-
ing an XSLT transformation.
XslTransform Class This class is the primary class for performing XSL
transformations.
XPath Functions
IXPathNavigable Interface This interface provides the capability to create
XPathNavigator objects.
XPathDocument Class This class is used to cache XML documents in an
XPath model.
XPathExpression Class This class holds compiled XPath expressions.
XPathNavigator Class The XPathNavigator class implements a cursor over
a data store for XPath parsing.
XPathNodeIterator Class This class allows enumeration over a specific set of
XML nodes.
XSLT—Document Transformation
XSLT, or eXtensible Stylesheet Language Transformations, is a W3C-defined mechanism for
taking an XML document and transforming it into another structured document. The resulting
document could be XML, HTML, PDF, and so on. As we have mentioned in the previous
chapter, XML is an extensible language that is used to structure information inside of a docu-
ment. XSLT documents are actually written with XML syntax; if you understand XML, XSLT
documents should look familiar to you. Let’s look at one right now:
Working with the .NET Namespaces
492
PART II
<xsl:template match=”product”>
<p>
<xsl:apply-templates />
</p>
</xsl:template>
You can see that we are still dealing with nodes, elements, and the like. Some syntax may look
foreign (such as <xsl:template match=”product”> and <xsl:apply-templates />), but the
structure of the document should be easy to understand because it is really a well-formed
XML document. Let’s first look at why you would want to transform XML documents to
begin with, and then study the exact process for doing so. Next, we will focus on the actual
XSLT-specific commands that are available.
Source
Document
Result
XSLT Processor
Document
XSLT
Stylesheet
FIGURE 11.1
The XSLT transformation process.
XSLT and XPath
493
CHAPTER 11
Let’s look at a simple example of how we can affect an XML document. The three listings that 11
follow demonstrate how a real transformation might work (we’ll explain the specifics of the
XSLT
XSL syntax in the next section).
AND
The Source Document
First up, in Listing 11.1, is our source XML document. It is a brief and very simple XML doc-
XPATH
ument that describes two products.
</product>
<product>
<commonname>Garden Caddy</commonname>
<price>$19.99</price>
</product>
<product>
<commonname>Mosquito Trap</commonname>
<price>$229.00</price>
</product>
<product>
<commonname>Hammock (Cotton)</commonname>
<price>$129.95</price>
</product>
<product>
<commonname>Hammock (Polyester)</commonname>
<price>$179.95</price>
</product>
<product>
<commonname>Hammock Stand</commonname>
<price>$125.00</price>
</product>
</products>
Working with the .NET Namespaces
494
PART II
XSLT
</td>
</tr>
<tr>
AND
<td>
XPATH
Garden Caddy
</td>
<td>
$19.99
</td>
</tr>
<tr>
<td>
Mosquito Trap
</td>
<td>
$229.00
</td>
</tr>
<tr>
<td>
Hammock (Cotton)
</td>
<td>
$129.95
</td>
</tr>
<tr>
<td>
Hammock (Polyester)
</td>
<td>
$179.95
</td>
</tr>
<tr>
<td>
Hammock (Polyester)
</td>
<td>
$125.00
</td>
</tr>
</table>
Working with the .NET Namespaces
496
PART II
The HTML in Listing 11.3 doesn’t represent the ideally structured HTML document. It is
missing header information and has only rudimentary formatting instructions, but you can see
that the HTML works just well enough for most browsers to read it. Figure 11.2 shows the
result document displayed inside a browser:
FIGURE 11.2
The result document in a browser.
This process of taking an XML and transforming it into HTML is probably one of the more
common uses of XSLT transformations. But as we have said, our result document can really be
any sort of structured document.
Now that we know what XSLT transformations are, it’s time to dig into the meat of XSLT style
sheets.
This is the root document element for an XSLT style sheet. It is this element that contains all
the other XSL elements in the document; it is the ultimate parent to all other XSL nodes. It can
specify, with its attributes, a version, an ID, and namespaces.
XSLT and XPath
497
CHAPTER 11
NOTE 11
XSLT
XSLT documents can also use the <xsl:transform> element as their root document
AND
element. For all practical intents and purposes, xsl:stylesheet and xsl:transform are
identical in everything but their name.
XPATH
The other XSLT elements concern themselves with one of the following:
• Formatting and setting up attributes and directives
• Selecting a portion of the source document to process
• Processing and logical comparison of document elements
The first set of elements encountered inside the document element will be the formatting and
attribute elements. These elements are responsible for specifying miscellaneous instructions to
the XSLT processor. For example, the <xsl:output> element indicates whether the result doc-
ument will be in a format other than XML. In our previous example, we set this value to
HTML to indicate that we were outputting an HTML document.
At the heart of XSLT functionality is the <xsl:template> directive. This selects portions of
the source document by using pattern matching. If you again refer back to our previous exam-
ple in which we transformed an XML document into an HTML document, you will see that
we used the <xsl:template> element to specify which nodes of our source document we
wanted to transform.
You also have the capability to execute simple logic in an XSLT document by using if-then
statements, looping statements, and formatting statements.
Table 11.2 lists all the currently defined XSLT elements.
XSLT
xsl:param Supplies a way to create named parameters for use with an
AND
xsl:stylesheet element.
xsl:preserve-space Allows text nodes that contain only whitespace.
XPATH
xsl:processing-instruction Writes a processing instruction element to the result
document.
xsl:sort Provides the sort criteria for sorting nodes selected by an
xsl:for-each or xsl:apply-templates element.
xsl:strip-space Tells the processor to remove any nodes that only contain
whitespace.
xsl:stylesheet The root document element; contains all other XSLT ele-
ments in a style sheet.
xsl:template Used to define a series of commands and the nodes that they
should apply to.
xsl:text Writes raw text to the result document.
xsl:transform See xsl:stylesheet.
xsl:value-of Creates a string expression for evaluation by the processor;
primarily used to return the text value of nodes returned by
some other function.
xsl:variable Creates a variable (and value) that can be referenced in the
style sheet.
xsl:when Used with the xsl:choose and xsl:otherwise elements to test
for multiple conditions.
xsl:with-param Used to override parameters supplied to a template.
• Everything starts with the <xsl:template> tag. The processor will look at this element’s
match attribute and locate all the nodes that correspond to the identified match. In our
example, the first template tag tries to match against “products.” This means that the
XSLT processor will be looking for nodes with the name “products.” Each node matched
to the match attribute is passed into the template.
• The processor now processes each element in the template. A template is considered to be
all the content located inside of the xsl:template element. As each part of the template is
processed, the processor will start to build the result document. The template will essen-
tially hold two types of content: XSLT instructions and literals. XSLT instructions are the
elements that we have documented in Table 11.2. Literals are non-XSLT instructions that
are passed verbatim to the result document. This occurs in fragments, which will all be
pulled together at the end of the process to create the whole result document. If we again
refer back to Listing 11.3, the first template processed would include the following (note
the <table> </table> literals, and the <xsl:apply-templates> instruction):
<table>
<xsl:apply-templates />
</table>
• While the processor executes the XSLT instructions, it generates actual content for the
result document. These fragments are inserted into their correct location in the working
result document (really just an in-memory representation of the final result document).
• The final step occurs when the XSLT processor actually processes the template for the
source documents root. This causes all the previously generated result fragments to be
assembled in the correct order. Typically, at this point, the result document is complete
and is streamed out to its final destination.
This whole process is best understood as one of substitution: The XML document content (ele-
ment content) is substituted in for xsl:apply-templates tags, matching specifically on the
element identified by the xsl:template match attribute.
The preceding is a quick tour of what XSLT is and what it does. At this point, your basic
understanding of XML document transforms is almost complete. However, we have to touch
on one more set of concepts before diving into the .NET XSLT namespace particulars: XPath.
NOTE
Any element in a style sheet that does not have the xsl: prefix is considered to be a
literal result element (LRE). LREs are simply shoved into the result document as is. This
is what lets us get away with embedding HTML tags into a style sheet. The style sheet
is not HTML aware; it simply writes the tags into the result document (which, in our
examples, is an HTML document).
XSLT and XPath
501
CHAPTER 11
XSLT
ment (including all their possible attributes): the Microsoft XML SDK documentation
AND
and the actual W3C XSLT specification at http://www.w3.org/TR/xslt.
➲ We’ve mentioned this Web site in Chapter 10, “Reading and Writing XML,” but it
XPATH
deserves to be repeated here: http://www.xml.com is a great site for gathering informa-
tion about all things XML, including XSLT and XPath.
XPath Basics
A discussion of XSLT is not complete without a discussion of XPath. They are part and parcel
of the larger transformation features provided for XML documents.
XPath, XML Path Language, is a specific XML-based vocabulary designed to uniquely specify
portions of an XML document. That is, XPath enables you to select certain pieces of an XML
document that are of interest to you. It does this through a standard syntax similar to a query
language.
What if your assignment as a programmer was to take that XML document and transform it,
using XSLT, into an HTML document that would display the name of the third part in the sec-
ond assembly?
You could loop through the document using some sort of XSLT-specified loop, perhaps setting
up a counter variable so that you would know how many “assembly” nodes deep you were and
how many “part” nodes deep you were, but the solution really cries out for some way to spec-
ify a path through the XML document to the exact node or nodes that you want. XPath makes
this task trivial. Listing 11.5 shows one possible solution that uses an xsl-template element
that matches XML nodes using something called an XPath expression. An XPath expression is
a series of one or more functions that resolve to a specific location in an XML document.
Transforming the XML document in Listing 11.4 with the style sheet in Listing 11.5 would 11
result in the HTML code in Listing 11.6:
XSLT
LISTING 11.6 HTML Result Document
AND
<table>
XPATH
<tr>
<td>
connecting flange
</td>
</tr>
</table>
XPath Expressions
XPath expressions are meant to verbalize a path into an XML document, reaching down to a
specific node or set of nodes. This is a successful method of navigating through an XML docu-
ment because of XML’s hierarchical nature. Just as you would navigate to a specific file in a
file store using a path like this:
c:\my documents\work stuff\financials\cash flow.xls
Each XPath expression can consist of a variety of operators, each with its own specific rele-
vance to the path. In addition, XPath expressions can contain functions, such as the
Position() function we used in the preceding example.
Table 11.3 shows some of the more common operators (also known as tokens) in an XPath
expression.
Table 11.4 shows all the currently defined XPath functions with their descriptions. Each func-
tion is grouped within one of five categories. The node-set functions provide the capability to
query or return nodes, the string functions provide string manipulation and formatting func-
tionality, the Boolean functions provide true/false condition testing, the number functions pro-
vide numerical and arithmetic operations, and the XSLT functions provide information about
nodes encapsulated in a collection. Like the XSLT elements, the best place for detailed docu-
mentation on each of these functions is the Microsoft XML SDK documentation or the W3C
at http://www.w3.org/TR/xpath.
XSLT
String Functions
AND
string Converts the supplied parameter to a string and returns it
XPATH
string-length Returns the number of characters in the reference string
substring Returns a substring of the references string, as indicated by the sup-
plied offset position value and the supplied length value
substring-after Returns a substring of the first referenced string that follows the
first occurrence of the second referenced string
substring-before Returns a substring of the first referenced string that occurs before
the first occurrence of the second referenced string
translate Returns a version of a string with the characters replaced by any
matching characters supplied in the arguments to the function
Boolean Functions
boolean Converts the supplied parameter to a Boolean value
false Returns the value false
lang Returns a Boolean indicating whether the xml:lang attribute
matches the supplied parameter
not Returns the opposite of the supplied Boolean parameter
true Returns the value true
Number Functions
ceiling Returns the integer that is closest in value and larger than the sup-
plied integer
floor Returns the integer that is closest in value and smaller than the sup-
plied integer
number Returns the supplied parameter as a number
round Returns the integer closest in value to the supplied parameter
sum Returns the sum of all nodes in the referenced node set
XSLT Functions
current Returns the current node that is being processed
element-available Returns a Boolean value indicating whether the processor supports
the instruction element specified in the parameter
format-number Returns a string representation of the supplied number parameter
function-available Returns a Boolean value indicating whether the processor supports
the function specified in the parameter
generate-id Generates a unique ID for the first node in the identified node set
Working with the .NET Namespaces
506
PART II
XPath expressions themselves can contain a mixture of path identifiers and functions. Our
example interspersed both:
/assemblies/assembly[position()=2]/part[position()=3]
When XPath expressions are evaluated by the XSLT processor, they can result in one of the
following:
• A set of nodes
• A Boolean value
• A numeric value
• A string value
With a summary of both XSLT and XPath concepts under our belt, we are now prepared to
examine the System.Xml.Xslt and System.Xml.XPath namespaces.
Then we need to specify the style sheet we want to use for our transform. This is done through 11
the XslTransform.Load method. We have a variety of options on how to actually use the Load
XSLT
method; it is overloaded in several forms because the XslTransform class supports many types
of sources for the style sheet. We’ll talk about the different varieties of inputs in a moment. For
AND
now, let’s assume we want to specify a local file as the style sheet:
XPATH
xslt.Load(“xform.xsl”)
We now need an instance of the source document. Let’s assume our source XML document is
the “assemblies.xml” document that we constructed in Listing 11.4 and that it is available to us
as a local file:
sourceDoc = “assemblies.xml”
resultDoc = “out.html”
xslt.Transform(sourceDoc, resultDoc)
Let’s look at a simple console application that brings all this together. We will reuse the gen-
eral structure of the files that we detailed at the beginning of this chapter in Listings 11.1 and
11.2. This time through, we have added some more data fields to the XML source document
(which we call garden_catalog.xml; see Listing 11.7).
In Listing 11.8 we have added some more HTML formatting elements in the style sheet
(xform.xsl), but everything is still basic in content.
XSLT
</b>
</td>
</tr>
AND
</xsl:template>
XPATH
<xsl:template match=”price”>
<tr>
<td>
Price:
</td>
<td>
<xsl:apply-templates />
</td>
</tr>
</xsl:template>
<xsl:template match=”SKU”>
<tr>
<td>
SKU:
</td>
<td>
<xsl:apply-templates />
</td>
</tr>
</xsl:template>
<xsl:template match=”instock”>
<tr>
<td>
In Stock?
</td>
<td>
<xsl:apply-templates />
</td>
</tr>
</xsl:template>
<xsl:template match=”mfr”>
<tr>
<td>
Made By:
</td>
<td>
<xsl:apply-templates />
</td>
</tr>
</xsl:template>
</xsl:stylesheet>
Working with the .NET Namespaces
510
PART II
Finally, the console app that brings everything together is in Listing 11.9.
Module Module1
Sub Main()
Try
Dim xslt As New XslTransform()
Dim sourceDoc As String
Dim resultDoc As String
Dim xformDoc As String
sourceDoc = “garden_catalog.xml”
resultDoc = “display_catalog.html”
xformDoc = “xform.xsl”
Console.WriteLine()
Console.WriteLine(“Loading style sheet...”)
xslt.Load(xformDoc)
Console.WriteLine(“Processing transformation...”)
xslt.Transform(sourceDoc, resultDoc)
Console.WriteLine(“Processing complete.”)
Console.WriteLine(“Hit ENTER to exit.”)
Console.ReadLine()
End Sub
End Module
If everything goes well, after running the console application you should end up with an
HTML document (display_catalog.html) in the applications execution directory. Figure 11.3
shows what the resulting HTML looks like in a browser.
XSLT and XPath
511
CHAPTER 11
NOTE 11
XSLT
Don’t forget that for the program to work as written, these XML and XSL documents
AND
need to be in the same directory as the application executable.
XPATH
FIGURE 11.3
Display_Catalog.html in a browser.
Sources of Input
The capability of the XslTransform class to support many varieties of input and output is its
key strength. The possibilities are represented through the different overloaded versions of both
the Load method and the Transform method. Each version supports a different design pattern
for XSLT transforms.
Here are the different definitions of the Load method:
We can see that the class supports URLs for referencing style sheets on the Internet,
XmlReader instances, and XPathNavigator instances. It also enables you to reference an
IXPathNavigable interface—and that interface is implemented by the XmlNode class, the
XmlDocument class, and the XmlDataDocument class. In essence, we can implement a transform
over nearly any type of data store by using one of these four sources.
As an example of the flexibility, consider some of the following code snippets:
‘Load the style sheet from a web adddress...
xslt.Load(“http://someserver/xform.xsl”)
Figure 11.4 shows us the various supported inputs to the transform process.
The XPathNavigator is an interesting option to use because it has been specifically designed
and optimized for style sheet parsing. Before we get into the specifics of the XPathNavigator
and related classes, let’s study the different types of outputs possible with the
XslTransform.Transform method.
XSLT and XPath
513
CHAPTER 11
XPathNavigator XPathNavigator
11
XSLT
AND
XmlTextReader
XmlReader
XPATH
XmlValidatingReader
XmlNode
XslTransform
IXPathNavigable
XmlDocument
XmlUrlResolver
implict
Http:// or file://
explict XmlResolver
Http:// or file://
FIGURE 11.4
Supported inputs to the transform process.
Types of Output
Like the Load method, the XslTransform.Transform method is supported in a multitude of
overloaded versions. Following are their function signatures:
Overloads Public Function Transform(ByVal input As IXPathNavigable, _
ByVal args As XsltArgumentList) As XmlReader
You can see that some of the Transform methods specify the result document instance as a para-
meter, and others will actually return an instance by defining Transform as a function call. For
instance, we can do the following to get an XmlReader instance as the result of the transform:
Dim reader As XmlTextReader = xslt.Transform(xpathNavigable, argList)
Figure 11.5 represents a combined picture of the supported inputs and outputs of the
XslTransform class.
XmlTextReader
XmlReader Stream
XmlValidatingReader
XmlNode
XslTransform
IXPathNavigable TextWriter
XmlDocument
XmlUrlResolver
implict
Http:// or file:// XmlWriter
XmlResolver
explict
Http:// or file:// Http:// or file://
FIGURE 11.5
Supported inputs to the transform process.
XSLT and XPath
515
CHAPTER 11
Handling Exceptions 11
XSLT
An XsltException instance will be thrown if an error is encountered while applying a style
sheet through the XslTransform class. The XsltException class provides three specialized
AND
properties to help you diagnose and deal with the error:
XPATH
• LineNumber Property—The line number in which the processing error occurred
• LinePosition Property—The exact position in the document line where the error
occurred
• SourceUri Property—Returns the URI pointing to the loaded style sheet
All this works as expected in a Try-Catch block:
Try
Dim xslt As New XslTransform()
.
.
.
xslt.Load(xformDoc)
xslt.Transform(sourceDoc, resultDoc)
.
.
.
Catch xslErr As XsltException
MsgBox(“An error was encountered (“ & xslErr.Message & _
“)in the XSLT document at line “ & xslErr.LineNumber & _
“, position “ & xslErr.LinePosition)
Catch appErr As Exception
MsgBox(“A general application error was encountered.” & vbCrLf & _
vbCrLf & appErr.Message)
End Try
xmlDoc.Load(fileName)
There are really two approaches supported by the XpathNavigator class to document naviga-
tion. The first is a simple, node-based series of “move to” methods. By using these methods in
conjunction with the HasChildren property, it is relatively easy to hop around nodes by navi-
gating up and down the XML element tree.
For instance, we can position the XPath cursor to the root node and then, using this pattern,
recursively examine all the child nodes.
Dim xmlNav As XPathNavigator
xmlDoc.Load(fileName)
xmlNav.MoveToRoot()
RecurseNodes(xmlNav)
If xmlNav.HasChildren Then
‘child nodes detected
‘move to the first child...
xmlNav.MoveToFirstChild()
XSLT and XPath
517
CHAPTER 11
XSLT
RecurseNodes(xmlNav)
AND
‘and finally, position the navigator back node
‘we started at
XPATH
xmlNav.MoveToParent()
End If
‘We can now loop through the other nodes at the same
‘level as the current node
While xmlNav.MoveToNext()
‘(do some processing)
RecurseNodes(xmlNav)
End While
End Sub
You will note that this is very similar to the code that we wrote in the previous chapter to
recursively process nodes in an XML document using the XmlDocument class.
By querying the NodeType property (which returns an XpathNodeType instance, as shown in
Table 11.5), we can figure out the type of element on which the cursor is currently positioned.
If you need to examine an element’s attributes, you can first determine if the current element
has attributes and, if it does, walk through those attributes the same as you walk node sets. To
Working with the .NET Namespaces
518
PART II
do this, you would query the HasAttributes property, using the MoveToNextAttribute to
process each one individually:
If xmlNav.HasAttributes Then
While xmlNav.MoveToNextAttribute()
‘(do some processing)
Method Description
MoveToAttribute Moves the XPathNavigator to the attribute with the passed-
in name
MoveToFirstAttribute Moves the XPathNavigator to the first attribute of the
current node
MoveToFirstChild Moves the XPathNavigator to the first child node of the cur-
rent node
MoveToFirstNamespace Moves the XPathNavigator to the first namespace of the cur-
rent node
MoveToId Moves the XPathNavigator to the first attribute with the
specified ID value
MoveToNamespace Moves the XPathNavigator to the namespace node with the
specified name
MoveToNext Moves the XPathNavigator to the next sibling of the
current node
MoveToNextAttribute Moves the XPathNavigator to the next attribute of the cur-
rent node
MoveToNextNamespace Moves the XPathNavigator to the next namespace of the cur-
rent node
MoveToParent Moves the XPathNavigator to the parent of the current node
MoveToPrevious Moves the XPathNavigator to the previous sibling of the cur-
rent node
MoveToRoot Moves the XPathNavigator to the root node
Of course, the real power of XML document addressing lies with XPath expressions and
queries.
XSLT and XPath
519
CHAPTER 11
XSLT
The basic functionality of XPath queries begins with the XPathNavigator.Select method.
This method takes in an XPath expression as either a string or as an actual XPathExpression
AND
instance. To illustrate programmatically specifying an XPath expression, let’s revisit the sce-
nario we set up in Listing 11.5. Specifically, we have an XML source document that contains
XPATH
assembly elements. Each assembly will have one or more part elements assigned to it.
To start off, let’s write a simple console application that duplicates a solution to our prior
requirement of selecting the third part from the second assembly. We have already presented
the XPath expression necessary to retrieve that portion of the XML document:
“/assemblies/assembly[position()=2]/part[position()=3]”
Recall that this expression consists of both path information and function calls. Issuing this as
a query command should be as simple as pumping this expression in as a string to the
XPathNavigator.Select method:
xmlNav.Select(“/assemblies/assembly[position()=2]/part[position()=3]”)
The last piece of the puzzle falls into place when the Select method returns an
XPathNodeIterator instance. We will have to deal with this iterator instance when
we call the Select method:
Dim nodeIterator As XPathNodeIterator
nodeIterator = _
xmlNav.Select(“/assemblies/assembly[position()=2]/part[position()=3]”
The XPathNodeIterator, as its name suggests, is used to provide an iteration mechanism over
a specific set of nodes. In this case, it is used to iterate through the node set returned as a result
of our Select method call. As an iterator object, it provides the expected methods and proper-
ties necessary to count the nodes in the node set (through the Count property), retrieve the cur-
rent node as an XPathNavigator instance (through the Current property), and move to the
next node (by using the MoveNext method).
To see this in action, let’s modify our initial XPath expression slightly:
“/assemblies/assembly[position()=2]/part”
Instead of selecting the third part from the second assembly, this revised expression should
give us all the parts contained in the second assembly. Because we are expecting multiple
nodes returned to us, this will allow us to see the iteration object visiting multiple nodes.
Listing 11.10 presents a console application that displays the name of each part contained in
assembly #2 using the preceding XPath expression, and Figure 11.6 shows the output:
Working with the .NET Namespaces
520
PART II
Module Module1
Sub Main()
Try
Dim xmlNav As XPathNavigator
Dim xmlDoc As XmlDocument = New XmlDocument()
Dim expression As String
Console.WriteLine()
Console.WriteLine(“Hit Enter to Exit...”)
Console.ReadLine()
End Try
End Sub
End Module
XSLT and XPath
521
CHAPTER 11
11
XSLT
AND
XPATH
FIGURE 11.6
Results of the XPath query.
You can see that all the part elements inside the targeted assembly have been returned with one
concise XPath expression. To demonstrate some more of the power of XPath, we’ll tweak the
console application a little bit to let you interactively specify an expression (see Listing 11.11):
Module Module1
Dim boolContinue As Boolean
Sub Main()
Try
Dim xmlNav As XPathNavigator
Dim xmlDoc As XmlDocument = New XmlDocument()
Dim expression As String
xmlDoc.Load(fileName)
boolContinue = True
expression = PromptForQuery()
Console.WriteLine()
Console.WriteLine()
End While
End Try
End Sub
Public Function PromptForQuery() As String
Dim expr As String
PromptForQuery = Trim(expr)
End Function
End Module
XSLT and XPath
523
CHAPTER 11
Figure 11.7 shows the results of a few queries made against the same assemblies.xml file. 11
XSLT
AND
XPATH
FIGURE 11.7
More XPath queries.
There is also the option of encapsulating XPath expressions in an XPathExpression class. The
XPathExpression class is actually a precompiled representation of an XPath expression and, as
such, may offer performance benefits for static queries.
XPathExpression objects are generated by calling the XpathNavigator.Compile method; after
retrieving an XPathExpression instance, this can be passed into the XPathNavigator.Select
method:
Dim xpathExpr As XPathExpression
xpathExpr = xmlNav.Compile(“assemblies.xml”)
xmlNav.Select(xpathExpr)
We had previously mentioned that an XPath expression could evaluate to a node set, a
Boolean, a number, or a string. The XPathExpression class lets you query its ReturnType
property to determine exactly what type of value was returned as a result of the query. Table
11.7 shows the possible XpathResultType enumerations.
FIGURE 11.8
ReservationsDesk 2 main form.
XSLT and XPath
525
CHAPTER 11
XSLT
Key concepts covered by the OrderQuery application are the following:
• Use of the XPathNavigator and XPathNodeIterator classes to “Walk” nodes in an
AND
XML document
XPATH
• Dynamically querying an XML document using XPath expressions
Listing 11.12 shows all of the usual form setup for the user interface. We have added two
declarations that are global to the Main class: resFile contains the full file path and name for
the source XML document, and xmlDoc is our global XmlDocument instance that represents
that file.
XSLT
Me.TextBoxPreferred = New System.Windows.Forms.TextBox()
Me.Label7 = New System.Windows.Forms.Label()
Me.Label6 = New System.Windows.Forms.Label()
AND
Me.TextBoxRoomNbr = New System.Windows.Forms.TextBox()
XPATH
Me.Label5 = New System.Windows.Forms.Label()
Me.TextBoxLastName = New System.Windows.Forms.TextBox()
Me.Label4 = New System.Windows.Forms.Label()
Me.TextBoxMiddleName = New System.Windows.Forms.TextBox()
Me.Label3 = New System.Windows.Forms.Label()
Me.Label2 = New System.Windows.Forms.Label()
Me.TextBoxFirstName = New System.Windows.Forms.TextBox()
Me.ListBoxGuests = New System.Windows.Forms.ListBox()
Me.MainMenu1 = New System.Windows.Forms.MainMenu()
Me.MenuItem1 = New System.Windows.Forms.MenuItem()
Me.MenuItem2 = New System.Windows.Forms.MenuItem()
Me.MenuItem3 = New System.Windows.Forms.MenuItem()
Me.OpenFileDialog1 = New System.Windows.Forms.OpenFileDialog()
Me.DateTimeCheckin = New System.Windows.Forms.DateTimePicker()
Me.RadioButtonByName = New System.Windows.Forms.RadioButton()
Me.TextBoxName = New System.Windows.Forms.TextBox()
Me.Label1 = New System.Windows.Forms.Label()
Me.buttonSearch = New System.Windows.Forms.Button()
Me.ComboHotel = New System.Windows.Forms.ComboBox()
Me.GroupBox1 = New System.Windows.Forms.GroupBox()
Me.GroupBox2.SuspendLayout()
Me.GroupBox1.SuspendLayout()
Me.SuspendLayout()
‘
‘TextBoxCheckinDate
‘
Me.TextBoxCheckinDate.Location = New System.Drawing.Point(264, 120)
Me.TextBoxCheckinDate.Name = “TextBoxCheckinDate”
Me.TextBoxCheckinDate.ReadOnly = True
Me.TextBoxCheckinDate.Size = New System.Drawing.Size(168, 20)
Me.TextBoxCheckinDate.TabIndex = 3
Me.TextBoxCheckinDate.Text = “”
‘
‘TextBoxNumNights
‘
Me.TextBoxNumNights.Location = New System.Drawing.Point(264, 144)
Me.TextBoxNumNights.Name = “TextBoxNumNights”
Me.TextBoxNumNights.ReadOnly = True
Me.TextBoxNumNights.Size = New System.Drawing.Size(44, 20)
Me.TextBoxNumNights.TabIndex = 3
Me.TextBoxNumNights.Text = “”
Working with the .NET Namespaces
528
PART II
XSLT
Me.Label7.Location = New System.Drawing.Point(184, 148)
Me.Label7.Name = “Label7”
Me.Label7.Size = New System.Drawing.Size(76, 16)
AND
Me.Label7.TabIndex = 4
XPATH
Me.Label7.Text = “# Nights:”
‘
‘Label6
‘
Me.Label6.Location = New System.Drawing.Point(184, 124)
Me.Label6.Name = “Label6”
Me.Label6.Size = New System.Drawing.Size(76, 16)
Me.Label6.TabIndex = 4
Me.Label6.Text = “Check-In:”
‘
‘TextBoxRoomNbr
‘
Me.TextBoxRoomNbr.Location = New System.Drawing.Point(264, 96)
Me.TextBoxRoomNbr.Name = “TextBoxRoomNbr”
Me.TextBoxRoomNbr.ReadOnly = True
Me.TextBoxRoomNbr.Size = New System.Drawing.Size(44, 20)
Me.TextBoxRoomNbr.TabIndex = 3
Me.TextBoxRoomNbr.Text = “”
‘
‘Label5
‘
Me.Label5.Location = New System.Drawing.Point(184, 100)
Me.Label5.Name = “Label5”
Me.Label5.Size = New System.Drawing.Size(76, 16)
Me.Label5.TabIndex = 4
Me.Label5.Text = “Room Nbr:”
‘
‘TextBoxLastName
‘
Me.TextBoxLastName.Location = New System.Drawing.Point(264, 72)
Me.TextBoxLastName.Name = “TextBoxLastName”
Me.TextBoxLastName.ReadOnly = True
Me.TextBoxLastName.Size = New System.Drawing.Size(168, 20)
Me.TextBoxLastName.TabIndex = 3
Me.TextBoxLastName.Text = “”
‘
‘Label4
‘
Me.Label4.Location = New System.Drawing.Point(184, 76)
Me.Label4.Name = “Label4”
Me.Label4.Size = New System.Drawing.Size(76, 16)
Working with the .NET Namespaces
530
PART II
XSLT
‘MainMenu1
‘
Me.MainMenu1.MenuItems.AddRange(New System.Windows.Forms.MenuItem()_
AND
{Me.MenuItem1})
XPATH
‘
‘MenuItem1
‘
Me.MenuItem1.Index = 0
Me.MenuItem1.MenuItems.AddRange(New System.Windows.Forms.MenuItem()_
{Me.MenuItem2, Me.MenuItem3})
Me.MenuItem1.Text = “&File”
‘
‘MenuItem2
‘
Me.MenuItem2.Index = 0
Me.MenuItem2.Text = “&Open”
‘
‘MenuItem3
‘
Me.MenuItem3.Index = 1
Me.MenuItem3.Text = “E&xit”
‘
‘DateTimeCheckin
‘
Me.DateTimeCheckin.CustomFormat = “”
Me.DateTimeCheckin.Format = _
System.Windows.Forms.DateTimePickerFormat.Custom
Me.DateTimeCheckin.Location = New System.Drawing.Point(168, 76)
Me.DateTimeCheckin.Name = “DateTimeCheckin”
Me.DateTimeCheckin.Size = New System.Drawing.Size(192, 20)
Me.DateTimeCheckin.TabIndex = 4
Me.DateTimeCheckin.Value = New Date(2000, 2, 12, 0, 0, 0, 0)
‘
‘RadioButtonByName
‘
Me.RadioButtonByName.Location = New System.Drawing.Point(32, 52)
Me.RadioButtonByName.Name = “RadioButtonByName”
Me.RadioButtonByName.RightToLeft = System.Windows.Forms.RightToLeft.Yes
Me.RadioButtonByName.Size = New System.Drawing.Size(128, 16)
Me.RadioButtonByName.TabIndex = 2
Me.RadioButtonByName.Text = “Search By Name”
‘
‘TextBoxName
‘
Me.TextBoxName.Location = New System.Drawing.Point(168, 48)
Working with the .NET Namespaces
532
PART II
XSLT
Me.AutoScaleBaseSize = New System.Drawing.Size(5, 13)
Me.ClientSize = New System.Drawing.Size(454, 355)
Me.Controls.AddRange(New System.Windows.Forms.Control() _
AND
{Me.GroupBox2, Me.GroupBox1})
XPATH
Me.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog
Me.Icon = CType(resources.GetObject(“$this.Icon”), System.Drawing.Icon)
Me.MaximizeBox = False
Me.Menu = Me.MainMenu1
Me.Name = “Main”
Me.Text = “ReservationsDesk 2 (no file loaded)”
Me.GroupBox2.ResumeLayout(False)
Me.GroupBox1.ResumeLayout(False)
Me.ResumeLayout(False)
End Sub
#End Region
This click event handler is responsible for displaying the File Open box, which lets the user
select the input XML file.
Private Sub MenuItem2_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles MenuItem2.Click
OpenFileDialog1.DefaultExt = “.xml”
OpenFileDialog1.Filter = “Reservation files (*.xml)|*.xml”
OpenFileDialog1.InitialDirectory = _
System.Reflection.Assembly.GetExecutingAssembly.Location
OpenFileDialog1.ShowDialog()
resFile = OpenFileDialog1.FileName
If resFile = “” Then
Me.Text = “ReservationsDesk 2 (no file loaded)”
Else
Me.Text = “ReservationsDesk 2 (file: “ & resFile & “)”
LoadResFile(resFile)
ListHotels()
buttonSearch.Enabled = True
ComboHotel.Enabled = True
Working with the .NET Namespaces
534
PART II
End Sub
The LoadResFile routine takes care of loading up our global xmlDoc object. All our XPath
queries will be made against that object.
Public Sub LoadResFile(ByVal file As String)
Try
xmlDoc.Load(file)
End Try
End Sub
This event handler kicks off the whole query process by calling the ExecuteQuery routine.
Private Sub buttonSearch_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles buttonSearch.Click
ExecuteQuery()
End Sub
Two major queries happen in this application. The first one pulls back all the guest nodes that
match the criteria. ExecuteQuery is responsible for that, adding the guest IDs to the list box as
it iterates through the node set returned by the XPath expression.
Private Sub ExecuteQuery()
Try
Dim expression As String
Dim xmlHotel As XmlDocumentFragment
Dim xpathNavGlobal As XPathNavigator
Dim xpathIterator As XPathNodeIterator
Dim firstName As String
Dim lastName As String
Dim fullName As String()
Dim checkinDate As String
XSLT and XPath
535
CHAPTER 11
XSLT
Dim guestID As String
AND
xpathNavGlobal = xmlDoc.CreateNavigator
XPATH
‘ If we are searching by name, build the appropriate
‘ XPath expression...
If RadioButtonByName.Checked Then
‘split the first/last name out
fullName = Split(TextBoxName.Text, “,”)
lastName = Trim(fullName(0))
firstName = Trim(fullName(1))
ListBoxGuests.Items.Clear()
If xpathIterator.Count = 0 Then
MsgBox(“No guest records were found.”, _
, “No Records Found”)
Else
Working with the .NET Namespaces
536
PART II
ReturnGuestID is a helper function; it takes the current XPathNodeIterator instance and uses
it to retrieve the ID attribute for the current node. This is then used to fill the guest ID list box.
Public Function ReturnGuestID(ByVal iterator As XPathNodeIterator) _
As String
Try
iterator.Current.MoveToFirstAttribute()
ReturnGuestID = iterator.Current.Value
iterator.Current.MoveToPrevious()
End Function
ListHotels is another helper function. It preloads the hotel drop-down list as soon as the
XML source document is loaded into our global xmlDoc object.
Public Sub ListHotels()
Try
Dim xpathNavGlobal As XPathNavigator
Dim xpathIterator As XPathNodeIterator
xpathNavGlobal = xmlDoc.CreateNavigator
xpathIterator = xpathNavGlobal.Select(“/register/hotel/@id”)
XSLT and XPath
537
CHAPTER 11
XSLT
While xpathIterator.MoveNext()
ComboHotel.Items.Add(xpathIterator.Current.Value)
End While
AND
XPATH
If ComboHotel.Items.Count <> 0 Then
ComboHotel.Select(1, 1)
End If
End Sub
This is where our second major query takes place. Operating from the guest ID attribute,
DisplayGuestInfo pulls back a pointer to that entire guest element and then walks the child
elements to retrieve the guest record data (such as first name, last name, check-in date, and so
on). This is all then displayed out to the screen.
Public Sub DisplayGuestInfo(ByVal id As String)
Try
Dim iterator As XPathNodeIterator
Dim nav As XPathNavigator
Dim childNav As XPathNavigator
Dim childIterator As XPathNodeIterator
nav = xmlDoc.CreateNavigator()
iterator.MoveNext()
iterator.Current.MoveToFirstChild()
TextBoxFirstName.Text = iterator.Current.Value
iterator.Current.MoveToNext()
TextBoxMiddleName.Text = iterator.Current.Value
iterator.Current.MoveToNext()
TextBoxLastName.Text = iterator.Current.Value
iterator.Current.MoveToNext()
TextBoxRoomNbr.Text = iterator.Current.Value
Working with the .NET Namespaces
538
PART II
iterator.Current.MoveToNext()
TextBoxNumNights.Text = iterator.Current.Value
iterator.Current.MoveToNext()
TextBoxPreferred.Text = iterator.Current.Value
Summary
XPath and XSLT are two powerful technologies that can be put to some fantastic use for
querying XML documents and transforming XML documents. We have seen the powerful sup-
port offered in .NET for XML document reading, writing, and validating: the System.Xml.Xsl
and System.Xml.XPath namespaces further enrich that functionality.
In this chapter, we investigated the following:
• The concepts of XSLT and style sheets
• XPath expressions
• Transforming XML documents into HTML documents by using the XslTransform class
and an XSL style sheet
• Selecting nodes out of an XML document using XPath expressions
• Navigating XML documents using the cursor-based XpathNavigator class
Working with Threads CHAPTER
12
IN THIS CHAPTER
• Key Classes Related to Threading 540
In this chapter, we’ll discuss how to leverage the Framework Class Library’s support for threads.
Due to the previous lack of support in Visual Basic for easily generating and managing threads,
the topic of threading and multithreaded application design is not one that Visual Basic develop-
ers are typically familiar with. So, first on tap is a brief discussion of why you would want to
develop multithreaded applications in the first place. Multithreaded applications are not a cure-
all, but they are certainly one more tool that you can use to solve application design problems.
Then, we’ll introduce some of the basic concepts of threading that will enable you to digest
and understand how Microsoft has structured the threading classes in the .NET Framework.
There is a very rich threading library exposed to you, and Visual Basic .NET also has some
intrinsic language support for threads.
Finally, we will dive into programming with threads at both an introductory level and a more
advanced level.
After reading this chapter, you should be able to:
• Apply threading concepts in your application design
• Create and use thread objects to execute code
• Understand how and why threads transition from state to state
• Discuss ways to prevent contention between threads
• Store and retrieve data within a thread and across threads
WORKING WITH
a common, framework-maintained pool of threads.
THREADS
Understanding and Applying Threads
Before we can examine how to use threads in our code, we need a crash-course in what a
thread is and how threading is supported by the Windows operating system.
resources. Processes can create other processes: Launching MS Word from its executable
inside of Windows Explorer causes Windows Explorer (a process) to launch MS Word (also a
process). So where do threads come in? Threads are the basic unit of work for a multitasking
operating system. The task scheduler doesn’t schedule processes to run, it schedules threads
to run.
Threads are actually a very simple construct. There are just a few things that you need to know
to get started:
1. A process is a discrete body of code that operates inside of a context (think program).
2. A thread is a path of execution through a body of code. Every process has at least one
thread, created by default.
3. Microsoft Windows, as a multitasking operating system, allocates processor time by
threads (thus the need for number two).
4. There may be certain technical challenges that are best solved by creating new threads
inside of a process (thus creating a multithreaded program).
Let’s move on and try to answer the question of why you, as a developer, should care about
threads.
Increasing Performance
Multithreaded applications do not necessarily lead to performance increases. It is false to think
that you can increase your application’s performance in any meaningful way if the application
is running on a single processor machine. The reason for this should be obvious: The processor
still has the same amount of work to do, regardless of how you have designed your threads. In
a multiprocessor machine, however, the ability to increase performance is a very real and
intended result of multithreaded application design. Because the scheduler can place threads
Working with Threads
543
CHAPTER 12
onto any one of the CPUs for execution, you could have two threads—thus, two pieces of
work—executing simultaneously. Figure 12.1 shows an approximation of how one such multi-
threaded program might run on a single processor machine versus a dual processor machine.
12
Execution Time
Thread A...........
WORKING WITH
THREADS
Thread A...... Thread B......
Execution Time
Thread B....................
Thread A...........
Thread B....................
FIGURE 12.1
Single versus dual CPU processing.
NOTE
An interesting question is raised about the use of processes versus threads. If the
operating system allocates time to threads, and each process by definition has at least
one thread, couldn’t we just create multiple processes to achieve our goal of multi-
tasked, concurrently executing code? The short answer is yes. So why turn to multiple
threads instead of multiple processes? Threads represent a halfway point between
having no concurrency at all in our application designs and using processes to achieve
concurrency. Threads offer a few unique benefits over processes. For one, their over-
all context is much smaller, making it faster for the OS to switch between threads
instead of between processes. It is also much easier to share data between threads
than it is between processes. And lastly, threads have a much lower resource over-
head than processes.
Working with the .NET Namespaces
544
PART II
One common design pattern you will see with multithreaded applications is the “divide and
conquer” pattern. If a program has a long and complex series of computations that it must per-
form, and if those calculations do not have to be performed in serial, it should be possible to
break the atomic sections of that computation apart and place their execution on their own
thread. Again, if the application is run on a multiprocessor system, this design pattern should
lead to quantifiable performance increases; simply put, it should reach the end of the computa-
tion faster.
Just because measurable performance increases won’t occur on a single-processor machine is
not a reason to abandon the concept of multithreaded applications.
There is always the illusion of speed—we can increase an application’s responsiveness lending
the appearance of speed.
Increasing Responsiveness
Many times, it is sufficient to create the illusion of speed in your applications by simply
increasing how fast your user interface responds to user input. This leads us to another com-
mon design pattern found in multithreaded applications: the UI versus worker threads pattern.
Applications designed around this pattern place their computational work onto one or more
“background” threads that execute at the same time that another thread monitors the user inter-
face for events. An excellent example of this approach can be found in the way that many
applications handle printing to a physical printer. In MS Word, for instance, the print job is
spooled and then handled by a completely separate worker thread. This means that control is
returned back to the application almost as soon as you click the Print button; you are free to
continue typing your document at the same time that it is running off your printer.
Another good example of this is found with the way that Windows Explorer allows you to
search for files on your hard drive. Take a look at Figure 12.2, which shows the familiar
Windows 2000 Search for Files and Folders window. In this figure, we have just entered a file-
name pattern to search for, and pressed the Search button.
Figure 12.3 shows that, while the application is busy performing the search, we can still inter-
act with the dialog. In this case, we have entered a new filename pattern. We can also quite
easily stop the search because of this worker thread approach. The Stop Search button is avail-
able and ready.
Being able to respond to user input quickly, without forcing users to wait for the application to
finish whatever it is doing, leads to a snappy program that users tend to like. Having to stare at
an hourglass pointer is often frustrating for users. The hourglass is a sign to users that the soft-
ware can’t keep up with their ability to interact with it—a definite problem that can be easily
rectified in many cases.
Working with Threads
545
CHAPTER 12
12
WORKING WITH
THREADS
FIGURE 12.2
Searching for a file (*.*).
FIGURE 12.3
Still searching.
Application Scalability
Threads can help applications reach higher levels of scalability. By scalability, we mean the
capability of an application to efficiently use operating system and hardware resources to han-
dle increased demand or load. A scalable application allows you to add software or hardware
resources in order to accommodate increasing demands. There are many different ways to
implement threads to enhance scalability. One approach would be to implement a thread pool
(or use the class library’s ThreadPool class) to handle user computations. As users (or load)
are added to the system, threads are spooled up to handle the workload. As users leave the sys-
tem, the threads are returned back to the thread pool. This is an example of front-end scaling.
We can also scale applications on the back-end. For instance, we might choose to spawn a
Working with the .NET Namespaces
546
PART II
thread for each network connection available on the system to listen for specific requests com-
ing across the network. In other words, our software intelligently recognizes when resources
are added or subtracted from the system and adjusts accordingly through thread instantiation or
destruction.
The Thread class is the primary worker class in the System.Threading namespace. It is a
sealed, non-inheritable class. To create a thread and start executing some code on it, you have
to understand the Thread class constructor and the Start method. First, a quick glance at the
constructor. It is defined like this:
Public Sub New(ByVal start As ThreadStart)
The ThreadStart object required in this constructor is actually a delegate. It is responsible for
pointing at the block of code that the thread will run. The Start method, which will actually
start the thread on its code path, is a simple, parameterless method, defined like this:
Public Sub Start()
12
WORKING WITH
To try to flesh these items out a bit, let’s look at some self-contained code. In the following
THREADS
example, we’ll kick a new thread off to deal with some computations (contained in the
SomeCode routine).
Module Module1
Now, we’ll write a short dummy routine that will represent the code that we want to run on our
new thread.
Sub SomeCode()
Dim x As Integer
Dim loopIndex As Integer
For loopIndex = 1 To 10
x = x * x + 2
Next
End Sub
Our Sub Main represents the core, primary thread of our application. It’s here that we will cre-
ate our new thread object and then start it running the SomeCode routine. As we have already
seen, the thread constructor accepts an instance of a ThreadStart object, which is simply a
delegate to the piece of code that we want to run on our new thread (remember that a delegate
is just a pointer to a subroutine). After creating the thread object, we need to “start” it. Calling
the Start method simply tells the thread to begin running the code pointed to by the
ThreadStart delegate that was used when we created the thread object.
Sub Main()
Dim ourThread As Thread = New Thread(New ThreadStart(AddressOf SomeCode))
‘do some stuff
At this point in our code, we have created and started our new thread. The operating system
Working with the .NET Namespaces
548
PART II
has taken over management of the thread’s execution and has returned control back to our
application—in other words, our path of execution in our application has now been split. We
have one thread (the intrinsic thread) that was automatically created along with our applica-
tion, running parallel to the thread that we explicitly created.
‘continue on with our main code
End Sub
End Module
That’s it! We have now looked at a process that spawned two threads. Both of these threads
will run to completion (in other words, execute until they reach the end of their code path) and
then die. When all of the threads in a process have completed, the process itself will complete.
NOTE
The Application.ThreadExit event will be fired just prior to a thread completing. In
the case of the preceding code snippet, we would expect this event to fire twice:
once for the explicit thread we created and once for the main thread of the applica-
tion. When the main thread of the application finishes, the ThreadExit event will be
followed immediately by the ApplicationExit event. Of course, any code in the
ThreadExit event that actually requires more computation by the thread will keep
the thread alive—the thread may never die!
Prioritizing Threads
All threads are not created equal. When writing multithreaded applications, it is often useful
to indicate to the system that more attention should be spent on one thread over another. The
Windows operating system uses a priority ranking system to decide how often it will give
processor attention to a particular thread. This whole scheduling process works something
like this:
1. Each thread is allocated a certain time slice in which it will run; at the end of that time
slice, the system will wrest control back from the thread.
2. The scheduler now needs to figure out which of the waiting threads will get the next time
slice.
3. The waiting threads are all categorized by their priority; those with a higher priority rela-
tive to another will receive their time slice first.
4. Where multiple waiting threads exist with the same priority level, the scheduler will deal
with each of them in the order that they were entered into the waiting thread queue.
Working with Threads
549
CHAPTER 12
5. After a thread has had its time slice expired it is placed in the back of the waiting thread
queue for its particular priority level.
Thread priorities range across five different levels: lowest, below normal, normal, above nor-
mal, and highest. Naturally, there is an enumeration (ThreadPriority) that can be used to rep-
resent these values (see Table 12.2).
Name Description
AboveNormal A thread somewhere between normal and highest priority.
12
WORKING WITH
BelowNormal A thread somewhere between normal and lowest priority.
THREADS
Highest A thread of the highest priority; use sparingly if at all.
Lowest A thread of the lowest possible priority. Do not use this for timing
dependent threads.
Normal A thread of normal priority; this is the default value for a thread.
Of course, you can also use the Priority property to query the thread for its currently indi-
cated priority level:
If workerThread.Priority = ThreadPriority.Lowest Then …
Using the Thread.Priority property, you can explicitly set or view the priority level of the
thread that you have created. Setting thread priorities is useful when you need to leverage
process from one thread relative to another. For instance, a thread that is running code that
needs to be responsive and snappy should be running at a higher priority level than a thread
that only needs to crunch some numbers during the system’s idle time. In fact, this is a fairly
typical pattern used by multithreaded applications: One thread is used to react to user interface
operations (leading to a snappy and responsive program, something users love), while another
worker thread is used to conduct its non–time-critical business in the background (during
whatever lulls may occur on the user interface side of things).
NOTE
In addition to a priority, a thread is also assigned as either a background thread or a
foreground thread. Surprisingly, these have nothing to do with the actual priority or
urgency of a thread. The difference is in the way that they affect the current running
Working with the .NET Namespaces
550
PART II
instance of the .NET runtime. The .NET runtime will shut itself down when the last
foreground thread expires (for example, reaches the aborted or stopped state). Any
background threads still running will be issued ThreadAbortException exceptions. You
can set or query this attribute by using the IsBackground property (a boolean prop-
erty). As long as there is a foreground thread running, the runtime will stay alive.
When playing around with thread priority levels, you need to exercise a certain amount of
restraint and act responsibly. For one thing, your code needs to share the processor with other
programs. It isn’t fair or responsible to users to simply assume that your application can or
should supercede the processing of everything else in the system. You also need to remember
that these priorities are only useful if there is a relative difference between threads. Simply
setting all of your threads to the highest priority defeats the purpose of having a priority scale
in the first place. If you need to bump up the responsiveness of some code, try using the
AboveNormal priority level. Reserve the use of Highest for those threads that have to respond
to critical events or timings (perhaps these are code routines that need to react to hardware
events such as disc changes, keyboard input, and so on). Likewise, setting thread priority
to Lowest should be reserved for those operations that can truly be executed whenever
time permits.
NOTE
Actually, the whole issue of thread priority is slightly more complicated than this. The
scheduler, in addition to looking at the priority level of the thread, also considers the
priority level of the process that the thread is running in. In addition, the scheduler
allows for dynamic prioritization of threads. A thread, for instance, may have its pri-
ority boosted for a time slice because of a system event, such as a user typing on the
keyboard. For the most part, however, you won’t need to concern yourself with these
intricacies.
Pausing a Thread
From time to time, you may need to tell a thread to stop what it is doing and wait. There are
actually two ways that you can accomplish this: You can sleep a thread or you can suspend a
thread. Both are similar in effect with some key differences that we should touch on. Let’s first
examine putting a thread to sleep.
When you issue a Sleep message to a thread, you are basically telling it to stop executing for a
fixed period of time—in this case, milliseconds. Execution will be frozen (or blocked) until the
Working with Threads
551
CHAPTER 12
specified timeout has transpired. Why would you want to do this? If your thread is processing
data being returned from another thread, you may want to put it to sleep for brief chunks of
time to allow its data buffer to fill up. Or, you may have a polling operation working in a
thread that needs to wake up, examine some data, and then go back to sleep if there hasn’t
been a change. Whatever the reason, issuing the Sleep method call requires only one parame-
ter that specifies how long the thread should sleep (in milliseconds). The following code would
sleep the referenced thread for approximately 5 seconds:
workerThread.Sleep(5000)
Specifying zero milliseconds invokes a special case: The thread scheduler will take away any 12
remaining time from the thread on its current time slice. In effect, you are telling the scheduler
WORKING WITH
to cancel the thread out of its current slot and place it back in the queue for execution.
THREADS
We can also tell the thread to sleep indefinitely, waking up only when another thread interrupts
it. You can interrupt a sleeping thread, causing it to continue execution, by calling the
Interrupt method:
workerThread.Interrupt()
The other way that we can pause a thread is through the Suspend method. Instead of pausing a
thread for a specific period of time, the Suspend method will pause the thread indefinitely until
a Resume call is made to the thread. The Suspend method does not take any parameters. There
is also another key difference: Suspending a thread will not cause the thread to freeze immedi-
ately. Instead, the thread will keep running until it reaches something called a safe point. A
safe point is a runtime term; it refers to a point in time when the garbage collector can safely
interrupt a thread.
Stopping a Thread
So far, we have seen how to start a thread and then pause, or block, a thread while running.
The last basic thread operation that we will pursue is the simple concept of stopping a thread.
Of course, once your thread runs out of code to execute, it will die on its own. To explicitly
stop a thread, you have to turn to the Abort method. Like the Suspend method, the Abort
method doesn’t accept any parameters:
workerThread.Abort()
In addition, the Abort method does not immediately stop a thread in its tracks. Again, the
runtime will wait for the thread to reach a safe point before actually terminating it. As
straightforward as its use seems, there are a few peculiar twists to using the Abort method
that you need to be aware of. For one, calling the Abort method causes an exception—a
ThreadAbortException—to be thrown. This exception is unique from any of the other excep-
tions that you will deal with in .NET—it can’t be caught! In other words, if you have a
Working with the .NET Namespaces
552
PART II
structured exception handler, the Catch block will never fire even in the face of a thrown
ThreadAbortException. Any Finally blocks you have written, however, will be fired.
TIP
If for some reason a thread has its Abort method called twice, the second abort call
and any subsequent abort calls will cause a DuplicateThreadAbort exception to be
thrown. The first abort will still throw the ThreadAbortException, which will be the
call ultimately responsible for its death.
If you abort a thread that is in a state where it can’t be aborted, the general rule of thumb is
that the abort message is delayed until the thread reaches a state where it can be stopped. This
causes a few situations that might run contrary to your expectations on how the thread should
behave. For instance, you might expect that aborting a thread that hasn’t been started would
throw an exception. After all, how can you stop something that isn’t started? The other plausi-
ble expectation would be for the method to just do nothing. Unfortunately, it does neither.
Aborting a thread that hasn’t started will actually have the effect of queuing an abort call to the
thread. If the thread ever does get started, the abort will be immediately applied to it.
In some cases, the runtime will actually push the thread to a state where it can be stopped. If
you abort a suspended thread, the thread will first be resumed and then stopped. Aborting a
thread that is sleeping will cause the thread to be interrupted before being stopped.
With the ThreadPool, instead of using a ThreadStart delegate to point at the code we want to
run, we use a WaitCallBack delegate. The effect is the same, but the WaitCallBack delegate
accepts an object parameter to carry state between method calls. If you don’t need to use it,
you don’t have to, but the subroutine that is pointed at by the WaitCallBack delegate must
match its function prototype. The WaitCallBack delegate definition looks like this:
Public Delegate Sub WaitCallBack(ByVal state As Object)
Therefore, our SomeCode subroutine from our example has to be changed to look like this: 12
WORKING WITH
Public Sub SomeCode(ByVal state As Object)
THREADS
Note that we aren’t actually using the state object for anything (at least in this example); it is
just there to synchronize the function definitions, allowing the WaitCallBack delegate to work
correctly.
If we revisit our prior code sample for starting a new thread, we can see the changes made to
work with the ThreadPool class:
Imports System.Threading
Module Module1
Sub SomeCode()
Dim x As Integer
Dim loopIndex As Integer
For loopIndex = 1 To 5
x = x * x + 2
Next
End Sub
Sub Main()
‘do some stuff
End Sub
End Module
Working with the .NET Namespaces
554
PART II
Working with the ThreadPool is a particularly useful and quick way to delegate a work item
(function or subroutine) onto its own thread, without worrying about the inherent housekeep-
ing and overhead yourself.
• To temporarily stop a thread, use the Thread.Sleep method. You can specify the number
of milliseconds that the thread should block until continuing with its execution.
• To stop a thread until you tell it to start up again, use the Thread.Suspend and
Thread.Resume methods, respectively.
WORKING WITH
SuspendRequested Thread is waiting until a safe point has been reached before tran-
THREADS
sitioning to Suspended.
Unstarted Thread has been created, but not started.
WaitSleepJoin Thread is waiting.
You can’t affect a thread’s state by simply assigning it through this property; it is read-only.
The only way that you can affect a thread’s state is through an action method call (such as the
Start and Stop methods that we just went over). Figure 12.4 shows a state diagram represent-
ing all of the possible thread states, and the actual transitions that are responsible for moving a
thread from one state to another.
You can see by looking at the graphic that there is only one initial state for a thread,
Unstarted, and two possible “ending” states or final states: Aborted and Stopped. These last
final states are final in the sense that once a thread enters either of those two states, it cannot
leave the state. Likewise, there is no way to place a thread back into the Unstarted states.
Most of these states are self-explanatory, but there are a few that deserve some attention.
The WaitSleepJoin state may be a little confusing to you at first glance. It doesn’t sound like
a state that we can place with any degree of certainty—what does it mean to wait-sleep-join?
The property enumeration itself is really just a concatenation of the three different states that it
represents. In other words, a thread could be waiting, sleeping, or joined. Any of these states
will be indicated as WaitSleepJoin. We now know what a sleeping thread is all about, but
waiting threads and joined threads require some more background.
Joining Threads
If you recall our discussion about the Abort method, you will remember that the thread isn’t
actually aborted until it reaches a safe point. This could be a problem if you have some code
that depended on the thread actually reaching the end state that you want it to be in. You
might, for instance, have some global cleanup code that needs to be executed as soon as the
Working with the .NET Namespaces
556
PART II
thread has died. It isn’t good enough to know that it will eventually abort; you need to know
the second that it has aborted. This is where the concept of joining a thread comes in.
Essentially, one thread may join another as a form of software eavesdropping. The thread that
has called the Join method (the calling thread) on another thread (the joined thread) will block
its execution until the targeted thread has been actually aborted. Then, its execution will
continue.
(suspend honored)
Suspend
Requested
Suspended
Suspend Resume
Notify
Interrupt
(time exp.)
Wait
Unstarted Start() Running Join
Sleep
Sleep
Join
(thread expiration) Wait
Stop Abort
Requested Requested Aborted
(stop honored)
Stopped
FIGURE 12.4
Thread state transitions.
Just so we don’t get confused, let’s examine a specific scenario. Suppose that you had a simple
forms-based application that, when activated, would spool a thread to perform some back-
ground operations. When the form is deactivated (by, say, the user minimizing it to the task
bar), we want to abort the thread. The trick here is that we don’t want the form to become
Working with Threads
557
CHAPTER 12
deactivated until we are guaranteed that the thread has aborted. We could handle this scenario
by hooking the Activated and Deactivated events, and popping in our threading code. It might
look something like this:
Private Sub Form1_Activate(ByVal sender As System.Object, ByVal e As _
System.EventArgs) Handles MyBase.Activated workerThread.Start()
End Sub
WORKING WITH
End Sub
THREADS
In the deactivate event, we first issue the Abort command, and then we join our worker thread.
Our code will block on this line of code—workerThread.Join()—preventing the form from
deactivating until the workerThread object has actually transitioned into the aborted state.
happening, Thread A will continue on its path of execution, performing some heavy-duty com-
putations. (Maybe its computing a re-entry trajectory or amortizing a 1,000 year loan!) Once
Thread A is at the end of its code, it will open the file that was created by Thread B and read
the last value in. This value should, at this point, hold the average of the numbers that we
started with.
Just so we can visually picture this flow, here is a “timeline” of how things are supposed
to work:
Thread A Thread B
(Program started)
Read int vals into array (9,73,11,99,1024)
Create Thread B (state: unstarted)
Start Thread B (state: running)
>Perform some additional calcs< Create file
. Write value into file (9)
. Write value into file (73)
. Write value into file (11)
. Write value into file (99)
. Write value into file (1024)
. Compute avg. of the values
. Write avg. into file (243.2)
>Calcs done< (state: stopped)
Open file and read in last value (243.2)
The problem here is that we don’t know how long either thread will take to run. You may be
lulled into a false sense of safety, knowing that Thread A has some pretty intensive calcula-
tions to do, while Thread B should be fairly speedy with its stuff. Working with asynchronous
concepts like threads can be dangerous work; you need to start challenging these assumptions.
If Thread A finishes up before Thread B has had a chance to write the average value into the
file, Thread A will be picking up what it thinks is the average value but really isn’t. This could
happen if Thread A is abnormally speedy with its calculations, or if the file system is busy
with other things and is taking a while to honor Thread B’s file requests, thus slowing Thread
B down.
Here is our ideal world turned upside down. Thread A reads in the final average before Thread
B has finished:
Working with Threads
559
CHAPTER 12
Thread A Thread B
(Program started)
Read int vals into array (9,73,11,99,1024)
Create Thread B (state: unstarted)
Start Thread B (state: running)
>Perform some additional calcs< Create file
. Write value into file (9)
. Write value into file (73)
. Write value into file (11)
12
WORKING WITH
. Write value into file (99)
THREADS
>Calcs done< Write value into file (1024)
Open file and read in last value (1024) Compute avg. of the values
. Write avg. into file (243.2)
. (state: stopped)
This problem is called a race condition. In a race condition, one of the threads is actually rac-
ing the other to the finish line—a real problem if your code assumes who the winner will be.
Running multiple threads also introduces the specter of deadlocks. A deadlock exists when two
or more threads are each waiting for something to happen before they will continue with their
execution. Let’s look at another scenario. We’ll use a hardware resource backdrop for this
example. Consider an application that writes a file out via the modem port. The application is
designed such that, while a thread is using the modem port, it will lock it for its use only—
likewise with accessing the file. If you have one thread that locked the modem port and then
tried to lock the file, while another thread had locked the file and was trying to lock the
modem port, you would have a deadlock or stalemate between the two threads. Each thread is
waiting on the other to release its lock before processing. See Figure 12.5.
(lock) thread
blocked
modem
FIGURE 12.5
A deadlock between threads.
Working with the .NET Namespaces
560
PART II
Deadlocks and race conditions are notoriously difficult to diagnose and debug, and the prob-
lem gets exponentially worse with each thread that you create.
Hopefully, as you were reading through and thinking about these simple examples of thread
contention, you realized that if you could just force the threads to communicate their current
intentions and actions to one another, you could eliminate or reduce the likelihood of these
contentions occurring. This act of coordinating the actions of two or more threads is what we
call synchronization. In fact, we have already talked about one primitive form of synchroniza-
tion when we discussed the use of the Join method. By joining one thread to another, we were
able to effectively coordinate their actions. In the following sections, we’ll tackle the issue of
synchronizing threads head on. We’ll learn how to write well-behaved threads that leverage the
synchronization objects exposed by the System.Threading namespace to avoid contentions.
If poor thread coordination is the problem, synchronization is the cure.
NOTE
The “cure” can introduce problems of its own. That is, it is better to not get sick in
the first place! Try to design your applications so that issues like deadlocks and race
conditions are not possible instead of relying on synchronization to take care of
them. Solving a race condition might cause a deadlock, and each synchronization
object you use consumes application resources and can decrease application
performance.
thread
mutex
guarded resource
Blocked and waiting
threads
thread 12
WORKING WITH
THREADS
FIGURE 12.6
Serializing access through a mutex.
To see this in action, read through the following sample code in Listing 12.1. You can run the
code by simply creating a new console application project in Visual Studio .NET and then
pasting the code listing into the Module1 module. In this console application, we are protect-
ing a simulated file from thread contention by serializing write access to the file. The simplest
way to achieve this is to write a wrapper class to represent the write operation. This class
(appropriately called FileWrapper) will be responsible for implementing a mutex to guard its
concurrent use. Because we write out to the console when we enter our threaded code and
leave it, you can see quite easily that the threads are serialized: You will never see multiple
“Thread started” messages in a row. Each thread must finish before the next one can begin. To
prove this, try removing all of the mutex code and then run the program. You should see
instances of thread overlap as evidenced by two or more “Threads started” messages in a row.
Module Module1
Dim numThreads As Integer
Public threadsAreDone As New AutoResetEvent(False)
Public targetFile As New FileWrapper()
Sub Main()
Dim loopIndex As Integer
numThreads = 5
threadsAreDone.WaitOne()
End Module
Class FileWrapper
‘Instance our mutex
Private lockMutex As New Mutex()
Try
‘this is where our code would go to do the actual file I/O
Catch
‘We would want to catch any system or file i/o specific exceptions
‘here
Working with Threads
563
CHAPTER 12
End Sub
End Class
12
WORKING WITH
You can see that we only used two methods from the Mutex class: WaitOne and ReleaseMutex.
THREADS
The WaitOne method signals the mutex that a thread is waiting to use it. The ReleaseMutex
signals that a thread has completed its interaction and is through with the mutex. The runtime
can now pass the mutex on to the next thread that needs it.
NOTE
In the correct threading vernacular, a mutex is said to be signaled when it is free, and
non-signaled when it belongs to a thread.
If our thread dies before calling ReleaseMutex, the mutex will still be released.
In our FileWrappper class, we deal exclusively with file write operations through our
WriteToFile subroutine. What if we intended to also wrap read operations? A ReadFromFile
would be easy enough to write:
Public Sub ReadFromFile()
Try
‘this is where our code would go to do the actual file I/O
Catch
‘We would want to catch any system or file i/o specific
‘exceptions here
Finally
‘When all is done, we need to release the mutex so the next
‘queued thread can use it
lockMutex.ReleaseMutex()
End Try
End Sub
Our original premise for the use of a mutex was serializing write access to a file. But, is this
really something we would need to do with a read operation? Two threads reading from a file
at the same time wouldn’t be in contention; this is a fairly safe operation to conduct in parallel.
A mutex in this scenario is overkill. In the next section, we will discuss another synchroniza-
tion object: the ReaderWriterLock class. The ReaderWriteLock class serializes write access
while allowing multiple read threads inside of the code at once.
Try
‘this is where our code would go to do the actual file I/O
Catch
‘We would want to catch any system or file i/o specific
‘exceptions here
Finally
‘When all is done, we need to release the mutex so the next
‘queued thread
‘can use it
lockRW.ReleaseWriterLock()
End Try
12
End Sub
WORKING WITH
THREADS
Public Sub ReadFromFile()
Try
‘this is where our code would go to do the actual file I/O
Catch
‘We would want to catch any system or file i/o specific
‘exceptions here
Finally
‘When all is done, we need to release the mutex so the next
‘queued thread can use it
lockRW.ReleaseReaderLock()
End Try
End Sub
End Class
The overall design pattern is the same as the one we used with the Mutex class. Inside of each
block of code, where we access the resource, we first request a lock for our thread. Then, when
we are done, we release the lock.
Let’s see what our previous mutex console application looks like using the ReaderWriterLock
class. In Listing 12.2, we have implemented two separate methods in our FileWrapper class:
Working with the .NET Namespaces
566
PART II
One simulates reading from the file, and the other simulates writing to the file. We spool
threads just as before. This time, however, every fourth thread will be assigned to run the
WriteToFile code. The other threads will run the ReadFromFile method.
Module Module1
Public numThreads As Integer = 10
Public threadsAreDone As New AutoResetEvent(False)
Public targetFile As New FileWrapper()
threadsAreDone.WaitOne()
Class FileWrapper
‘Instance our ReaderWriterLock
Private lockRW As New ReaderWriterLock()
12
WORKING WITH
Public Sub ReadFromFile(ByVal threadNum As Integer)
THREADS
‘This code actually requests ownership of the lock
lockRW.AcquireReaderLock(Timeout.Infinite)
Try
‘this is where our code would go to do the actual file I/O
Catch
‘We would want to catch any system or file i/o specific exceptions
‘here
Finally
‘When all is done, we need to release the mutex so the next queued
‘thread can use it
lockRW.ReleaseReaderLock()
End Try
End Sub
Catch
‘We would want to catch any system or file i/o specific exceptions
‘here
Finally
‘When all is done, we need to release the mutex so the next queued
‘thread can use it
lockRW.ReleaseWriterLock()
End Try
End Sub
End Class
You are free to use the Monitor class just like the other synchronization primitives that we
have discussed. If we revisit our FileWrapper example, we can see again that our design pat-
tern for synchronization remains essentially unchanged.
Public Sub WriteToFile()
Monitor.Exit()
End Sub
This is where Visual Basic .NET provides us with some intrinsic locking support. Consider the
following code, and compare it carefully to our previous Monitor code:
12
Public Sub WriteToFile()
WORKING WITH
THREADS
‘This code actually requests ownership of the mutex
SyncLock(Me)
Thread.Sleep(2000)
End SyncLock
End Sub
Visual Basic .NET’s one contribution to thread synchronization is the SyncLock statement.
It allows you to block out some code that then requires exclusive locking by threads or
processes; a thread, for instance, will block on the SyncLock(Me) until the lock has been
released.
The SyncLock statement simply calls out to the Monitor class in much the same fashion that
we have shown previously: The beginning of the statement, SyncLock(Me) is the equivalent to
calling Monitor.Enter, and the End Synclock statement causes Monitor.Exit to be called.
What if the preceding statement is run in a block of code that multiple threads are running? An
unseen problem lurks here: To the runtime, our one line of code is actually two operations—an
increment and an assignment. First, the value of someVar is incremented, and then the result is
stored into someVar. What happens if, between the increment and the assignment operation,
another thread executes this line of code? The variable someVar will have actually been incre-
mented twice instead of the expected once.
There is a specialized synchronization object that specifically handles incrementing and decre-
menting variables like this: the Interlocked class.
‘Interlocked class definition
NotInheritable Public Class Interlocked
It exposes an Increment and a Decrement method that guard against this type of contention by
forcing the code to behave as if the increment and assignment operations occur in one single,
atomic operation. Here are their definitions:
Overloads Public Shared Function Increment(ByRef location As Integer) As _
Integer
The Interlocked class only exposes static, shared methods so there is no need to instance an
object of its class. It is used like this:
newValue = Interlocked.Increment(someVar)
The location parameter is the variable that you want to increment or decrement. These meth-
ods are really targeted at variable operations that involve keeping a count of items. You will
notice that you cannot pass the increment value into these methods; they always add or sub-
tract 1 to the integer specified by location, making these items fairly useless for computational
arithmetic on variables. Some design patterns require your application to maintain a count of
the number of threads that are currently executing. In the case of an application that delegates
some processing to a number of worker threads, the application will want to know when all of
the threads have completed their work. We can do this easily enough by having each thread
decrement a global “thread count” variable when it is done, like this:
Class ThreadOp
Public Sub DoSomething()
‘Perform some calculations…
Thread.Sleep(500)
numRunningThreads = Interlocked.Decrement(numRunningThreads)
End Sub
End Class
Working with Threads
571
CHAPTER 12
WORKING WITH
The last topic that we will tackle with regards to threads is that of variable access and scope.
THREADS
What happens when more than one thread is executing a body of code where a variable is
defined? Is that variable’s value unique across the instances of the thread, or is it shared? In
this section, we will look at how variables interoperate between threads. We will then look at
ways that we can explicitly tell VB .NET to create variables that are private to a specific
thread.
Sub ourMethod()
Dim d As Integer
End Sub
End Class
How would you expect these different variables to react given multiple threads? The answer is
this: Threads, because they share the same virtual address space as any other thread in their
process, also share any global variables that have been defined. For VB .NET developers, that
means that any variable you have defined as static or instance members will persist across any
and all threads. Any local variables that are marked as such will remain local to each thread.
So, looking back to the code snippet above, we should expect variables a, b, and c to exist
globally across all threads that, say, run the ourMethod method. Variable d, because it is local
in scope, would be unique to each thread.
Working with the .NET Namespaces
572
PART II
Thread-Private Variables
From time to time, you may run across the need to explicitly tell the compiler that a specific
variable should remain private and specific to a given thread. To declare a variable in such a
fashion, we can use a declarative attribute class. A declarative attribute is a special modifier to
a variable declaration. The .NET runtime defines a few declarative attribute classes; you can
also define your own. To use a declarative attribute class, you simply specify the class and its
constructor like this:
<SomeDeclAttrib()> Private someVar As Integer
The one we are interested in at the moment is the ThreadStaticAttribute class. Specifically,
to use ThreadStaticAttribute, we would write
<ThreadStaticAttribute()> Shared someVar as Integer
By including this syntax, we are telling the compiler that the someVar variable should be main-
tained on each thread, not globally across threads. That’s really all there is to it. If two threads
started to run a body of code where we had someVar defined previously, they would not over-
write each other’s copy of the someVar variable.
So, to store something in a data slot, we would first allocate a new data slot, store something
in it, and then retrieve it like this:
Working with Threads
573
CHAPTER 12
‘store something in it
Thread.SetData(tls, “test”)
‘retrieve it
Dim var As String = Thread.GetData(tls)
To try and illustrate further the concept of thread-private variables, see Listing 12.3. This
simple console application declares a few different types of variables inside a class, and then
spawns a few threads to run a method off that class. Each variable is randomly assigned a
12
WORKING WITH
value. As the threads run, they will write the content of each variable out to the console
THREADS
window.
Module Module1
Sub Main()
‘Our startup routine
Console.ReadLine()
Main()
End Sub
End Module
Class VarTester
‘Dim a variety of data types for us to use
End Sub
Sub SomeMethod()
‘This bogus method just writes out variable values out to a
‘console window. We have wrapped everything in a SyncLock
12
‘statement so it is easier to read the output (in other
WORKING WITH
‘words, thread 2 won’t write its values out until thread 1
THREADS
‘is done, etc.)
Dim e As Integer
SyncLock GetType(VarTester)
Randomize()
Thread.SetData(myTLS, Int((100 - 1 + 1) * Rnd() + 1))
e = Int((100 - 1 + 1) * Rnd() + 1)
Notice the output in Figure 12.7. We see that variables a, e, and something we call “TLS slot”
change between each thread. The other variables remain the same. Variables a and e should
both be thread specific, variable a because of our use of the ThreadStaticAttribute modifier,
Working with the .NET Namespaces
576
PART II
and variable e because it is a locally declared variable. The “TLS slot” is a data slot created on
the thread (again, thread specific). Therefore, the code behaves as expected: Each of these
should be private and unique to the individual threads.
FIGURE 12.7
Console output—thread local storage.
The other variables are all static or instance member variables. These are shared across all of
the threads. Their values will be the same across each thread instance. Try running the program
a few times (just press the Enter key) to satisfy yourself that things are working as you expect
them to.
Learning by Example—ThreadedTimer
This sample application is simple in nature. It illustrates some of the basic threading concepts
that we have talked about to this point by creating two explicit threads. One thread continu-
ously displays the current system time onto the form. The other thread continuously “moni-
tors” the timer thread, reporting its current state out to a list box. The form also exposes a few
buttons that allow you to play with the timer thread by starting it, sleeping it, suspending it, or
aborting it (see Figure 12.8).
FIGURE 12.8
Window—ThreadedTimer application.
Working with Threads
577
CHAPTER 12
This program is primarily an educational exercise to see how the timer thread progresses from
state to state based on the user clicking one of the method buttons.
With this application, we are also showing a common design pattern for threads: using a
“background” worker thread in conjunction with a user interface thread. In our case, the
threadUI object constantly runs to poll the threadTimer on its current state. The threadTimer,
of course, is subject to your control by using the buttons on the left-hand side. In an actual
business application, our UI thread would be running code to accept user input, repaint the
form, fill list boxes with data, and so on, thus allowing users to continuously interact with the
application. All the while, the background worker thread is working on the problem at hand: 12
calculating payroll, interest payments, and so on. Again, in our case here the worker thread is
WORKING WITH
represented by threadTimer. This thread obviously does nothing useful for us. We could have
THREADS
easily used a timer control to do the same thing, but it shows us the possibility.
End Sub
WORKING WITH
‘
THREADS
‘GroupBox2
‘
Me.GroupBox2.Controls.AddRange(New System.Windows.Forms.Control() _
{Me.Button1, Me.ListBox1})
Me.GroupBox2.Location = New System.Drawing.Point(160, 48)
Me.GroupBox2.Name = “GroupBox2”
Me.GroupBox2.Size = New System.Drawing.Size(156, 176)
Me.GroupBox2.TabIndex = 3
Me.GroupBox2.TabStop = False
Me.GroupBox2.Text = “Thread State History”
‘
‘ListBox1
‘
Me.ListBox1.Location = New System.Drawing.Point(8, 24)
Me.ListBox1.Name = “ListBox1”
Me.ListBox1.Size = New System.Drawing.Size(140, 121)
Me.ListBox1.TabIndex = 0
‘
‘Label5
‘
Me.Label5.Location = New System.Drawing.Point(104, 4)
Me.Label5.Name = “Label5”
Me.Label5.Size = New System.Drawing.Size(124, 36)
Me.Label5.TabIndex = 2
Me.Label5.Text = “00:00:00”
‘
‘StartThread
‘
Me.StartThread.Location = New System.Drawing.Point(24, 28)
Me.StartThread.Name = “StartThread”
Me.StartThread.Size = New System.Drawing.Size(104, 24)
Me.StartThread.TabIndex = 1
Me.StartThread.Text = “Start”
‘
Working with the .NET Namespaces
580
PART II
End Sub
#End Region
WORKING WITH
End Sub
THREADS
Private Sub InitForm()
‘This routine just does some UI housekeeping,
‘disabling buttons that aren’t valid at launch
StopThread.Enabled = False
PauseThread.Enabled = False
SleepThread.Enabled = False
End Sub
Do
If Trim(stateEnum.GetName(enumType:=stateEnum.GetType(), _
value:=threadTimer.ThreadState)) <> Trim(currentState) Then
currentState = _
Trim(stateEnum.GetName(enumType:=stateEnum.GetType(), _
value:=threadTimer.ThreadState))
ListBox1.Items.Add(item:=currentState)
End If
Loop
End Sub
End Sub
End Sub
WORKING WITH
‘experiment with different times by replacing the constant...
THREADS
Const SLEEP_INTERVAL = 2000
threadTimer.Sleep(millisecondsTimeout:=SLEEP_INTERVAL)
End Sub
Code Walkthrough
LISTING 12.5 Divide and Conquer Code Listing
The application starts out with the typical Imports statements. Here, we are using the IO
library and the threading library.
Imports System.IO
Imports System.Threading
WORKING WITH
‘holds the count of values found in the file
THREADS
Private runningCount As Integer
This is all of the form designer code. We haven’t added anything to this; it is included for the
sake of being complete.
#Region “ Windows Form Designer generated code “
End Sub
WORKING WITH
Me.ComputedAverage.Location = New System.Drawing.Point(92, 52)
THREADS
Me.ComputedAverage.Name = “ComputedAverage”
Me.ComputedAverage.Size = New System.Drawing.Size(84, 16)
Me.ComputedAverage.TabIndex = 3
Me.ComputedAverage.Text = “---”
‘
‘ElapsedTime
‘
Me.ElapsedTime.Location = New System.Drawing.Point(92, 32)
Me.ElapsedTime.Name = “ElapsedTime”
Me.ElapsedTime.Size = New System.Drawing.Size(84, 16)
Me.ElapsedTime.TabIndex = 2
Me.ElapsedTime.Text = “---”
‘
‘Label3
‘
Me.Label3.Location = New System.Drawing.Point(8, 52)
Me.Label3.Name = “Label3”
Me.Label3.Size = New System.Drawing.Size(80, 16)
Me.Label3.TabIndex = 1
Me.Label3.Text = “Average:”
‘
‘Label2
‘
Me.Label2.Location = New System.Drawing.Point(8, 32)
Me.Label2.Name = “Label2”
Me.Label2.Size = New System.Drawing.Size(80, 16)
Me.Label2.TabIndex = 0
Me.Label2.Text = “Elapsed Time:”
‘
‘UseThreadPool
‘
Me.UseThreadPool.Location = New System.Drawing.Point(76, 32)
Me.UseThreadPool.Name = “UseThreadPool”
Working with the .NET Namespaces
588
PART II
End Sub
#End Region
Working with Threads
589
CHAPTER 12
WORKING WITH
THREADS
‘Looping variable for our for loops...
Dim loopIndex As Long
If you recall, the specific format of file that this application deals with is a straight-text format
with decimal values space delimited. This allows us to use the Split method off the string to
easily generate an array of the values we need to process.
‘Split the values out into an array for easier
‘processing.
fileStringValues = fileString.Split(“ “)
Here we use a class from the System library: Environment. The Environment class has a use-
ful property, TickCount, which returns the number of milliseconds since the operating system
started. We will use this to compute our elapsed time.
‘set our start time
startTime = Environment.TickCount
‘Loop through our array of values; increment the count and re-sum
‘the total each time through
For loopIndex = 0 To fileStringValues.GetUpperBound(0)
runningTotal = runningTotal + CType(fileStringValues(loopIndex), _
Decimal)
runningCount = runningCount + 1
Next loopIndex
End Sub
The ReadFile function is responsible for our file I/O operations. It opens the file pointed to by
fileName, reads the entire length of the file using a StreamReader object and then assigns it to
a string (fileString). This is then returned through the function.
Private Function ReadFile(ByVal fileName As String) As String
Dim targetFile As FileInfo = New FileInfo(filename:=fileName)
Dim fileContent As StreamReader = targetFile.OpenText()
Dim fileString As String = fileContent.ReadToEnd()
fileContent.Close()
ReadFile = fileString
End Function
WorkFileMulti is the routine that will compute the average of values in the target file by using
two threads requested from the ThreadPool object. Like WorkFileSingle, this subroutine
accepts a single parameter, fileString, which represents the contents of our target file in
string format.
Private Sub WorkFileMulti(ByVal fileString As String)
‘Holds our starting time tick-count
Dim startTime As Integer
WORKING WITH
‘We will use two threads from the ThreadPool
THREADS
numThreads = 2
‘copy the front half of our original array into a new array
fileStringValues.Copy(fileStringValues, currLowerBound, frontValues, 0, _
chunkLength)
‘copy the back half of our original array into a new array
fileStringValues.Copy(fileStringValues, currLowerBound + chunkLength, _
backValues, 0, backValues.Length)
This is where our application execution will start to branch across two separate threads. We are
passing in a delegate for each thread, as well as a state object which, in this case, is the array
of values we want that particular thread to work on.
‘Request two threads from the thread pool; note that we pass in a
‘different half of the array to divide and conquer the average
‘computation
Working with the .NET Namespaces
592
PART II
The program will actually block out this next line of code until the AutoResetEvent object that
we created (threadsAreDone) signals.
‘Wait for threads to complete
threadsAreDone.WaitOne()
‘Threads are done; compute the average and assign into “average”
average = runningTotal / runningCount
End Sub
This subroutine is the element of code that we will run on each of our two threads from the
thread pool. Because this is the one piece of code in the application that can be entered by
more than one thread at a time, we use the Interlocked class and the Synclock statement to
help serialize the threads during access to global variables. This subroutine accepts a string
array; this is a piece of the target file for the thread to work on.
Private Sub ComputeAverage(ByVal valueArray() As String)
‘just a variable to use in our For loop
Dim loopIndex As Long
WORKING WITH
End Sub
THREADS
This is our delegate for our threads; this is required by the QueueUserWorkItem method on the
ThreadPool class. We are using the state parameter to pass around the array that each thread
needs to work on.
Private Sub ThreadCallBack(ByVal state As Object)
After the thread has completed ComputeAverage(), it will fall back into the delegate
ThreadCallBack. We neeed to decrement the global numThreads var; when we reach zero, we
know that both threads are done executing. We can then indicate this to the application by call-
ing the Set method off of our global AutoResetEvent object (called threadsAreDone).
If Interlocked.Decrement(numThreads) = 0 Then
threadsAreDone.Set()
End If
End Sub
Of course, clicking on the Start button sets it all in motion. This is where we hook the click
event and respond accordingly.
Private Sub Start_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles Start.Click
➲ To signal to the application that our threads are done executing, we use the
AutoResetEvent class. Research this class in the .NET documentation: What is the dif-
ference between the WaitOne, WaitAny, and WaitAll methods?
➲ How would you approach this application’s design if the ThreadPool class didn’t exist?
In other words, how might you go about implementing your own ThreadPool class?
Summary
In this chapter, we had a look at how the .NET Framework supports thread concepts. We
saw how: 12
• We can use the Thread class to create and manage our own threads.
WORKING WITH
THREADS
• We can leverage the ThreadPool class to quickly and easily dispatch work onto multiple
threads.
• The various synchronization mechanisms help programmers avoid thread contention.
• Variables can be created that are specific and local to individual threads.
The ability to control threads represents a very large step forward for Visual Basic developers,
and the rich threading support in the .NET Framework makes syntax easy to understand and
clear to implement.
Messaging CHAPTER
13
IN THIS CHAPTER
• Key Classes Related to Messaging 598
• Messaging 600
• Messages 621
• Serialization 632
Most business applications today require some form of distributed processing. At its core,
that’s what .NET is all about—the capability to easily create highly distributed applications.
Architects and developers alike need technologies analogous to message queuing to help
bridge the gap between these distributed environments and dissimilar networks.
This chapter illustrates the System.Messaging namespace and demonstrates how you can
leverage it to add Message Queuing to your applications. We start by introducing Messaging,
including key concepts and design considerations. We then quickly move into the classes and
related example code—all designed to get you writing code.
The chapter is rounded out with a Web-based Messaging application using ASP.NET. The
application allows users to explore queues and their messages; it also supports transactional
Messaging.
We think that you’ll find Messaging to be an indispensable paradigm for application architec-
ture. Moreover, .NET offers a complete set of components to make you effective in leveraging
this tool in your applications.
After reading this chapter, you will be able to
• Know when, where, and why to use Messaging
• Connect to a queue
• Enumerate queues on a server
• Send a message to a queue
• Receive a message from a queue both synchronously and asynchronously
• Execute priority-based Messaging
• Enumerate messages within a queue
• Send and receive messages in various formats (serialization)
• Execute a Messaging transaction
• Control access to queues and queue objects
• Encrypt messages
• Authenticate message senders
make up the bulk of functionality exposed by the Messaging framework. The size of these
classes more than makes up for the relatively small namespace.
MESSAGING
Message. This class allows you to limit this list to
only the ones that are of interest, and thus reduce
network traffic and increase performance.
MessageQueue The MessageQueue class is used to provide access
to a message queue on a server (think MSMQ).
Message queues are typically servers that allow dis-
parate applications to exchange data in an asynchro-
nous and transactional manner.
MessageQueueEnumerator The MessageQueueEnumerator class can be used to
enumerate (loop) through message queues on a
server or network.
MessageQueueTransaction The MessageQueueTransaction class provides a
transaction context in which to execute a message-
queuing transaction. This class provides methods
for committing (Commit) and rolling back (Abort)
transactions.
Security
Trustee The Trustee class represents a person or service
with a set of access rights that is trying to access
an item.
Working with the .NET Namespaces
600
PART II
Messaging
To help better understand Message Queuing, consider the following analogy of Instant
Messaging (IM) and e-mail. Although each technology is very similar (bits from one user to
another across a wire), each tool’s actual use is quite different. IM is more conversational—a
Messaging
601
CHAPTER 13
short burst followed by a reply, and so on. The communication is synchronous, real-time, and
usually light. We use e-mail for our more important communications. E-mail guarantees deliv-
ery or sends an undeliverable message, notifies us of delivery (read receipt), stores a record of
the message, can secure a message, and continues to communicate when the other user is
“down.” As users, we understand the ramifications and instinctively choose the appropriate tool
for the job.
As programmers and system architects, we have to make the same kinds of choices. We use a
simple solution similar to IM when we have a reliable network, are less concerned when pro-
cessing goes down, and are communicating only with our own, known systems. Processing
messages in real-time with a database connection to a server gives us a simple solution but
provides a single point of failure: the database server. We do what we can to keep this thing up
by creating backups and emergency measures, but when it goes down (and it will), all process-
ing stops. Applications at the enterprise level need to do everything to guarantee that process-
ing continues without the database. Better yet, they must message with disparate systems on
varied networks, even if the systems are not running at the same time. These cases require an
e-mail-like paradigm: Message Queuing.
MESSAGING
distribution. It can be thought of as a kind of middleware, or software that is used to mediate
communications and transactions between applications. The design pattern is simple in con-
cept. Messages (orders, invoices, credit card transactions, and so on) are sent by one or more
parties to a central queue. The message is stored in its native format (XML, text, binary, and so
on) in the Active Directory. The sending application hands off the message and returns to other
business. A receiving application is either triggered by the message drop or set to check the
queue for messages at various intervals. When it finds messages that belong to it, they are
picked up and processed accordingly. If the sending application requests a response or receipt
that the message was retrieved, the receiving application places a response message in a spe-
cial response queue. That’s Messaging.
Figure 13.1 illustrates a simple message-queuing design. Orders are sent by various systems to
an order queue. Orders can be in EDI format, XML, text—whatever. The important thing to
note is that orders are sent to the queue and are not blocked, waiting for a response. The order-
processing application is not taxed, either. It can wait until downtime to pick up its orders. All
this processing is asynchronous, or not concurrent.
Of course, Messaging in .NET provides more than simple queuing. The Messaging service
built into Windows provides guaranteed delivery of messages, efficient routing, security and
encryption, priority-based Messaging—just to name a few features. We also can implement
both asynchronous and synchronous transactional Messaging. This functionality is exposed to
our application through the Framework Class Library.
Working with the .NET Namespaces
602
PART II
3. R
equ
est
Me
ssa
ges
4. D
eliv
er O
Message rde
r
Queuing
FIGURE 13.1
Typical message-queuing scenario.
Practical Applications
Message queuing is used primarily in enterprise-level, mission-critical applications as a fail-
safe way to execute business processes. Message queuing is a compelling design strategy for
multiple scenarios, for many reasons. We will walk through various application scenarios and
demonstrate how Message Queuing can be leveraged to solve particular challenges.
Applications whose users work remotely, such as applications used by a sales force, are solid
candidates for Message Queuing. Users out in the field might not have a reliable network con-
nection, if they have one at all. These users still need to be capable of processing customer
requests—and not on paper. Enter Microsoft Message Queuing. It can be configured to work
offline. When these users are not connected, they still can execute orders, create invoices, do
their timesheet, and so on. When their network connection is restored, the messages are deliv-
ered from their internal private queue to the central application-processing queue. Message
queuing works to guarantee delivery and the transaction.
Applications with a high degree of concurrent requests need a message-like solution. When
bursts of user requests flood a server, they begin to pile up synchronously. If everyone wants
the same resource (such as a database connection or an object), the resource becomes blocked.
Requests will time out or be rejected by the server. The server’s capability to process requests
will rapidly degrade. We’ve seen this repeatedly when a “dotcom” gets an unexpected article in
a major newspaper or magazine. One solution is to put a queue in front of the requests. That
way, each is stored and processed in turn. The queue might get large, but the server maintains
its capability to process, and everybody waits in line; nobody is rejected.
Messaging
603
CHAPTER 13
Applications that need to allocate resources (bandwidth and processing power) based on prior-
ity can benefit from a message-queuing strategy. The concept of priority-based Messaging
allows those messages with a higher level of priority to be executed ahead of those with lower
priority. Suppose that you want to process new orders ahead of cancellations, or, for certain
customers, you want to guarantee a response time. By implementing priority Messaging, you
can make sure that you handle the urgent requests first.
e-Commerce applications that process credit card transactions can benefit from Message
Queuing. If the credit card processor goes down, users should still be capable of entering
orders. If the processor is blocked, users will just move on to another site whose processor is
up and running. When the credit processor comes back up, the transactions are committed. If
there is an error, your code suspends the order and notifies the user of the problem. Overall, a
Messaging strategy is less affected by typical failures because the transactions are persisted. If
an error occurs when the message is received, the message still sits until the problem is fixed.
There are a number of other creative ways to leverage Messaging in your applications. One
involves using message queues to implement workflow. Suppose that the entities (timesheets,
invoices, purchase orders, and so on) in a corporation need to be tracked by various depart-
ments, external suppliers, and disparate systems. Users throughout need to know the item’s sta- 13
tus as it’s handed back and forth for revisions, approvals, payment, and so on. The systems
themselves are in constant flux, always being modified or upgraded. It almost seems impossible
MESSAGING
to manage. With Message Queuing, you can create various agents that interact with each system
to learn the status of an item. Messages that update status can be posted to a queue, where
receiving applications can use an agent to read an entity’s status. This loose coupling between
systems provides the flexibility for each system to be managed independently of the collective.
As you can see, a message-queue design strategy affords applications stable, reliable, and flex-
ible communication between the various layers and different systems that make up enterprise
infrastructure. The remainder of this chapter is dedicated to writing Messaging code using the
.NET Framework.
NOTE
The current version of Microsoft Message Queuing is 2.0. At the time of this writing,
Microsoft has announced 3.0 as part of Windows 2002. .NET will have full support for
Microsoft Message Queuing 3.0.
Microsoft Message Queuing is installed as part of Windows 2000 (Professional
or Server). To view queue information, go to Administrative Tools/Computer
Management and expand the Services and Applications node. You also can explore
queues on your network to which you have access using the Server Explorer in the
Visual Studio development environment.
Working with the .NET Namespaces
604
PART II
Message Queues
Without message queues, you can’t have Messaging. Message queues are required to send and
receive messages. (For this section, all you need to know about messages is that they are units
of information in text or binary form. We will delve into more detail on messages later in the
chapter.)
A message queue is a file structure (similar to a folder) that stores messages in transit. It can
be thought of as a mediator between a sender and a receiver, similar to your mailbox. A letter
is sent by the power company, and it is stored at the post office. The message is picked up by
the mail carrier, who takes it to your mailbox. The message is again stored; finally, you pick it
up. The message queues in this analogy are the post office and your mailbox. They store mes-
sages until the receiver is ready to pick them up.
Messaging applications send messages to queues, read from queues, and create and delete
queues. System administrators browse queues to gather information and troubleshoot issues.
The Messaging service itself uses queues for logging and writing out acknowledgements and
responses.
Two primary types of message queues exist: user created queues and system queues. Users
(administrators, applications, developers, and end users) can create public, private, administra-
tion, and response queues. System queues fall into the categories of journal, dead-letter, report,
and private system. Most applications are built around public queues. Table 13.2 provides
more information on the types of queues.
Managing Queues
When working with message queues, we are primarily concerned with the methods and prop-
erties of the MessageQueue class. This class allows us to explore existing queues, create and
delete queues, as well as send and receive messages. This class provides us a wrapper for
Microsoft Message Queuing. With the class, we can obtain a reference to a specific message
queue on a server or manipulate queues using the object’s static members. 13
To reference a queue on a server, we have two basic options: path or format name reference.
MESSAGING
To use the path paradigm, we must follow some basic syntax conventions. Public queues are
referenced by the queue name—for example, MyComputer\MyQueue. Private queues are referred
to using a dollar sign ($), as in MyComputer\Private$\MyQueue. Two kinds of journal queues
exist. One is specific to a single queue and is referred to with the syntax MyComputer\
MyQueue\Journal$. The other is a journal queue for a server. You access it in the following
manner: MyComputer\Journal$. The last queue type is the dead-letter queue. You refer to it
with MyComputer\Deadletter$ or MyComputer\XactDeadletter$. The latter refers to the
transactional dead-letter queue. Transactional queues are explained later in the chapter.
The format name method allows you to reference a queue based on a queue or machine GUID
on the network and a string value to indicate whether the queue is public or private. The syntax
should look familiar to those who are accustomed to creating SQLOLEDB connection strings.
For example, FORMATNAME:PUBLIC=QueueGUID, FORMATNAME:PUBLIC=QueueGUID;JOURNAL, and
FORMATNAME:PRIVATE=MachineGUID\QueueNumber;Journal are all ways to access a queue
based on a formatted name. Format names are sometimes preferred because they do not require
the domain controller on the network to resolve the path. The FormatName property of the
MessageQueue class returns a queue’s format name.
The MessageQueue class supports both static and instance instantiation. Static versions of the
object allow us to call methods that manipulate queues without actually equating our object
instance to an actual queue. This allows us to do things such as create, delete, and verify the
existence of queues without loading the queue’s properties into our object instance.
Working with the .NET Namespaces
606
PART II
Listing 13.1 gets us started with some sample code that demonstrates some of the static meth-
ods of the MessageQueue class. It is designed as a console application with one class and a
module that calls the class’s functions. Both functions use the Exists method to verify a
queue’s existence based on the Path parameter. Provided that the queue exists, one method
creates a new queue, and the other deletes a queue. Note that, when using the namespace, you
must set a reference to System.Messaging.
Try
Return True
Catch
Return False
End Try
Else
‘raise a message that queue already exists ...
End If
End Function
Messaging
607
CHAPTER 13
Try
Return True
Catch
13
MESSAGING
Return False
End Try
Else
‘could raise a message that there is no queue to delete
End If
End Function
End Class
Module Module1
Sub main()
‘purpose: call the functions & write the result to the console
Console.WriteLine( _
staticExamples.CreateQueue(“.\Private$\TestQueue”))
Console.WriteLine( _
staticExamples.DeleteQueue(“.\Private$\TestQueue”))
Working with the .NET Namespaces
608
PART II
End Sub
End Module
This example assumes that you have access rights to the machine and queues. We specifically
use the private queues to help ensure this. Security and permission are discussed later in the
chapter.
The Delete method deletes the queue and all its messages. Messages are not sent to a dead-
letter queue. They are gone and cannot be retrieved.
These examples are straightforward, with just a few method calls. However, we can use a static
version of the object to do other things, such as return an array of public queues on the server.
The next section describes different strategies for accessing and managing queues as a group.
Enumerating Queues
The MessageQueue class provides us with a number of methods to return groups of queues.
Two prime strategies exist for accessing queues. One is to return a static snapshot of the
queues in a given moment in time. Methods such as GetPublicQueues and
GetPrivateQueuesByMachine return an array of MessageQueue objects at the time of the
method call. This is useful if you need the queue information over a short period of time or
if the queues on your network do not frequently change.
The other strategy for accessing queues is to maintain a more dynamic view of the queues on a
server or network. The MessageQueueEnumerator class gives us this functionality. As you
cycle through the queue objects, the object reflects queues that have been added or removed
since the method call. This can be very useful if your queues are constantly changing or if you
need a real-time depiction of the queues on a network or machine.
It is important to remember that MessageQueueEnumerator represents a forward-only cursor.
This means that any queue appended or deleted beyond the cursor’s current position will be
reflected by the object. This is the concept of a forward-only cursor. A queue that is deleted or
added before the current position, however, will not be reflected by the object. Again, “for-
ward-only” implies that you cannot look back on queues that you’ve already accessed. The
Messaging
609
CHAPTER 13
class does offer us a method that refreshes its view of the queues. By calling the Reset
method, we return the object’s cursor to the beginning and get a fresh look at the queues. One
caveat of using this class is that its access to queues is completely random. We can never be
sure when iterating queues that they will be accessed in a logical order, such as by name or ID.
To loop through and access the message queues of the enumerator, we use the MoveNext
method and the Current property. MoveNext moves the cursor forward to the next queue mem-
ber. Calls to the method return true if a new queue was found and return false if the cursor
could not advance beyond the current queue. When a new MessageQueueEnumerator is
instantiated, the cursor is positioned before the first member of the enumeration. Therefore, the
first call to MoveNext accesses the first message queue. After the cursor is positioned on valid
queue, accessing its Current property returns a MessageQueue object representing the current
queue. Calls to the property when no current queues exist throw an exception.
Table 13.3 lists the methods of the MessageQueue class that provide us the capability to return
groups of queues. Additionally, queue properties such as category, label, and machine name are
described.
MESSAGING
machineName as String) As as an array of MessageQueue objects. Filters
MessageQueue() the result set by the server’s name.
GetPublicQueues() As MessageQueue() Returns a static snapshot of all public queues
on the network as an array of MessageQueue
objects.
GetPublicQueues(ByVal criteria As Returns a static snapshot of public queues on
MessageQueueCriteria) As the network based on a specified set of criteria.
MessageQueue() The criteria are defined by an instance of the
MessageQueueCriteria class explained later in
the chapter.
GetPublicQueuesByCategory(ByVal Returns a static snapshot of all public queues
category as GUID) As MessageQueue() on the network that belong to a specific cate-
gory. Categories in message queue terms allow
us to categorize a set of queues. For example,
we might have a group of public queues for
orders and another for invoices.
GetPublicQueuesByLabel(ByVal label Returns a static snapshot of all public queues
As String) As MessageQueue() on the network that have a specific label. A
message queue label represents the queue’s
description. By default, message queues have
no label (empty string). Labels are not neces-
sarily unique across queues; hence, we get an
array of objects back from the method.
Working with the .NET Namespaces
610
PART II
Notice that when returning groups of queues, we often want only those on a specific machine
or of a particular type, such as public or private. The capability to access only those queues to
which we have interest is paramount. In fact, System.Messaging provides us another class
intended to help further refine our filtering. MessageQueueCriteria exposes properties
designed for this specific purpose. Some filter properties include: Category, CreatedAfter,
CreatedBefore, Label, MachineName, ModifiedAfter, and ModifiedBefore. To use the class,
we simply set as many of its properties as apply to our needs and pass it as a parameter to
method’s such as GetPublicQueues and GetMessageQueueEnumerator. The following code
illustrates this concept:
‘local scope
Dim queues As MessageQueueEnumerator
Dim queueCriteria As New MessageQueueCriteria()
In Listing 13.2, we write code that demonstrates access to all the private queues on a machine.
We return the queues as an array of MessageQueue objects using the static method
GetPrivateQueuesByMachine. We then loop through the array and output QueueName
to the console window.
Module Module1
Sub main()
‘local scope
Dim privateQueues() As MessageQueue
Dim queue As MessageQueue
MESSAGING
privateQueues = MessageQueue.GetPrivateQueuesByMachine( _
machineName:=”myMachineName”)
Next
End Sub
End Module
Working with the .NET Namespaces
612
PART II
NOTE
You might have already figured this one out, but a Windows 2000 Professional (or
workgroup) installation supports only private queues. Attempts to access a public
queue will raise an exception. This is one reason why most examples were written for
private queues. To work with public queues, install Message Queuing under Windows
2000 Server.
So far, we’ve seen how to access our message queues and garner some control over them. Now
we will delve into some actual Messaging functionality. For now, all messages should be
assumed to be text-based. Additional message types are discussed in the “Messages” section
later in this chapter.
Sending Messages
Before a message can be sent, received, or even peeked at, we must have a reference to a phys-
ical queue on a server. We’ve already looked at a few ways to accomplish this in the preceding
“Enumerating Queues” section. Remember, the methods presented returned a group of
MessageQueue instances. Each instance represented an actual queue rather than simply a static
object. In fact, properties such as Category, Label, and MachineName are available only to
physical queue instances.
The easiest way to reference a single queue is to create an instance of the MessageQueue
class and pass the queue’s path on the constructor—for example, Dim queue As New
MessageQueue(path:=myPath). Sometimes, however, you need to convert a static instance of
the object into a physical reference. This is useful if you need to do some preprocessing, such
as checking whether the queue actually exists. In this case, you can create the queue object, do
your preprocessing, and then set its Path property to actually reference the physical queue. The
following is a quick example:
Dim queue As New MessageQueue()
If (queue.Exists(path:=myPath)) Then
queue.Path = myPath
End If
Now that we have a physical queue reference, let’s send a message. To do so, we use (you
guessed it!) the Send method of the MessageQueue class. This method has a number of over-
loads, most of which will be discussed in the chapter. Each version of the method contains the
obj parameter. We use this to define the message (object) that should be sent to the queue. Of
course, messages can be simple text, a data object, a structure, or any other managed object.
Message types other than text will be discussed in the “Messages” section.
Messaging
613
CHAPTER 13
It is often important to set the Label property of the message when sending. This can be done
on the function call. The message label is yours to use for your application. It can serve several
purposes. Primarily, it helps identify messages in human-readable terms. Additionally, it can be
used to group, sort, or selectively process messages. Suppose that you create a queue that
received messages from several suppliers. Perhaps each supplier’s messages are processed dif-
ferently or on different days. You might not want to read each message to determine the sup-
plier. You might instead use the Label property to filter messages for a given supplier. This
saves your application valuable processing resources and allows you to quickly access mes-
sages from a given supplier when browsing your queue.
Code for sending a simple message along with its label is presented in Listing 13.3. This code
can be pasted into a console application and stepped through in the IDE.
MESSAGING
‘purpose: send a message to a queue
‘local scope
Dim queue As New MessageQueue()
Else
‘raise some exception
End If
End Sub
End Class
Module Module1
Working with the .NET Namespaces
614
PART II
‘purpose: call the function(s) & write the result to the console
‘send a message
‘note: the dot (.) in the path is a shortcut to indicate you are
‘ referencing the same server on which the code is executing
sendExample.sendMessage( _
queuePath:=”.\Private$\myqueue”, msg:=”This is a test”, _
msgLabel:=”TEST MESSAGE”)
End Sub
End Module
Peek allows our code to read the message without removing it from the server. The method is
again synchronous and returns only the first message in the queue. It is often necessary to look
beyond the Peek, beyond the first message in the queue. For more information on this, see the
section “Enumerating Messages,” later in the chapter.
A couple other methods allow us direct access to a message. ReceiveById and PeekById return
a Message object from a known, unique identifier. Identifiers are generated by the Messaging
application. These methods are most useful when your application allows users to dynamically
read and process messages, or in similar cases when the message’s Id property is known. If no
message is found with the given identifier, the method immediately throws an exception. We’ll
use these methods in our “Learning by Example” sample application later in this chapter.
Now let’s look at some code. Listing 13.4 presents a simple paradigm for message reading.
First, in Sub Main, we use the previous send example to create a few messages in the queue.
Next, we call our custom function, peekAndWait. As messages arrive in the queue (in this case
they are already there), they are examined (Peek) for content. If they do not match the correct
content, they are deleted (received without processing) and we continue to wait. If we find a
message that we do want, it is returned by the function.
13
LISTING 13.4 Peeking and Receiving a Message
MESSAGING
Public Class ReceiveExample
‘local scope
Dim queue As New MessageQueue(path:=queuePath)
Dim msg As Message
Else
End If
‘function return
Return msg
End Function
End Class
Module Module1
Sub Main()
‘purpose: call the function(s) & write the result to the console
sendExample.sendMessage( _
queuePath:=”.\Private$\myqueue”, msg:=”Test”, _
msgLabel:=”NOT ORDER 2”)
sendExample.sendMessage( _
queuePath:=”.\Private$\myqueue”, msg:=”Test”, _
msgLabel:=”ORDER”)
sendExample.sendMessage( _
queuePath:=”.\Private$\myqueue”, msg:=”Test”, _
msgLabel:=”NOT ORDER 3”)
MESSAGING
‘wait for the user to stop the console application
‘this allows you to view the results
Console.WriteLine(“Enter ‘s’ to stop the application.”)
End Sub
End Module
The preceding code presented a couple of new concepts. First, in Sub Main, we called the
Purge method of the MessageQueue class to empty the queue of all messages. This helped to
ensure that we were looking at only our test messages. Next, when reading the messages in the
peekAndWait function, we used the MessageReadPropertyFilter to peek at only message
labels. This can be useful when processing large messages or working with low bandwidth. It
allows us to read only the items of which we have interest. Note that when we found a valid
message, we called the SetAll method to make sure that we returned all properties of the
message.
Working with the .NET Namespaces
618
PART II
Figure 13.2 represents the output of our console application. Remember, we created three
nonorders and only one order. Where is the other nonorder? Well, our method returns process-
ing to the module after an order has been found. In a real-world scenario, we would process
the message and call the peekAndWait function again.
FIGURE 13.2
Output of peekAndWait.
Asynchronous Messaging
Messaging is inherently asynchronous; messages are sent to a queue and received in a separate
process. Asynchronous Messaging is the capability for our applications to be notified of an
incoming message without blocking or waiting on the message queue. Asynchronous
Messaging is useful for retrieving messages without tying up application processing. The best
analogy is Microsoft’s Outlook e-mail client. When active, it receives messages as they arrive,
but it does not block access to other features when waiting to receive a message.
Standard implementation of asynchronous Messaging is straightforward. The following details
the basic steps involved in coding asynchronous Messaging:
1. We tell the queue that we want to be notified of all incoming messages by making a call
to the BeginReceive method of the MessageQueue class. This method immediately
returns processing to our application; there is no blocking as in the
MessageQueue.Receive method.
3. We call EndReceive to end the operation. EndReceive actually returns the message to
our application. At this point, we will no longer be notified of additional incoming mes-
sages. If we want to continue to listen to the queue, we must again call BeginReceive.
That is the basic overview. Two more methods, BeginPeek and EndPeek, allow us to asynchro-
nously peek at messages rather than receive. These methods work identically to the receive
methods. They have a corresponding PeekCompleted event and PeekCompletedEventArgs
class.
In Listing 13.5, we set up an asynchronous message receiver. This console application does
other processing (writes text to the console) while waiting for a message to arrive in the queue.
(To put messages in the queue, simply execute some additional send code from another exam-
ple.) When a message arrives, the application retrieves the message and goes back to work.
Module Module1
13
‘module-level scope
Private msgReceived As Boolean = False
MESSAGING
Sub Main()
‘local scope
Dim queue As New MessageQueue(path:=”.\private$\myqueue”)
End Sub
Sub ReceiveCompletedEvent( _
ByVal source As Object, _
ByVal result As ReceiveCompletedEventArgs)
‘local scope
Dim msg As Message
Dim queue As MessageQueue
Console.WriteLine(“ReceiveCompletedEvent Fired”)
Console.WriteLine(queue.QueueName)
Console.WriteLine(msg.Label)
End Sub
End Module
In the example, we created the Boolean value msgReceived to indicate when the processing
should stop. This is solely for reading the example text in the console. If we don’t pause the
application, the message is read and processing continues so fast that you can hardly see it in
the console.
Messaging
621
CHAPTER 13
The peek methods work the same way. In fact, inside both event handlers we have the option
to peek or receive the message. The events just tell us that there is a message in the queue.
Because we get a reference the queue from within the event, all queue methods and properties
are still valid.
By now, you should be familiar with the MessageQueue basics. This class is the backbone of
the components that make up Messaging in .NET. As such, it will be referred to often in the
remainder of the chapter. The next sections explore the details of messages and how to work
with them in your applications.
MESSAGING
network
➲ WriteHandle—Provides a Windows handle for the message queue
Messages
Applications send messages to one another to indicate status, execute transactions (orders,
credit cards, and so on), submit invoices, and generally update data. A message is a unit of
information (text or binary) sent from one computer to another. A message can be a simple
string, formatted XML, or even an embedded object of executable code. .NET gives us the
Message class for working with messages.
The Message class, through its properties, gives us granular control over sending and receiving
messages to and from queues. In fact, the Message class has no methods. All manipulation
(sending, receiving, formatting, deleting, and so on) of messages is done through associated
classes. Remember, when we peeked at and received messages in the previous section, the
MessageQueue instance used the Message class for these operations.
The Message class contains two types of properties. Those that are read-only are designed to
work with receiving messages. The read/write properties apply to both sending and receiving
messages. These properties will be discussed in context throughout the remainder of this
chapter.
Working with the .NET Namespaces
622
PART II
If either interval expires, the message will be discarded. Message Queuing provides a few
options for handling discarded messages. A copy of the message can be sent to a specified
dead-letter queue. To do so, we must first set the message’s UseDeadLetterQueue property to
true. Additionally, we can have Message Queuing send us a negative acknowledgement mes-
sage to indicate that the message was discarded. Acknowledgements are discussed in detail
later in this section. In either case, our code should be trained on the dead-letter queue or a
similar acknowledgement queue to intercept expired messages and process them accordingly.
Listing 13.6 illustrates expired messages. We first create a message and set its
TimeToBeReceived property to 15 seconds. We then watch the dead-letter queue and wait for
MSMQ to expire the message. Finally, we process the message upon its arrival in the dead-
letter queue.
Module Module1
Sub Main()
MESSAGING
‘set the message body
msg.Body = “Expiring message test”
‘wait to continue
Console.WriteLine(“Enter ‘s’ to stop”)
Do While Not Console.ReadLine = “s” : Loop
End Sub
End Module
Working with the .NET Namespaces
624
PART II
Notice the error handling around each attempt to access the dead-letter queue. Attempts to
access this information on a professional installation of Windows throw an exception. You
must have Window Server installed to access this queue through code. However, you can still
run the code and navigate to the queue to see the results.
what types of acknowledgements we want to receive back. This property is of the type
AcknowledgmentTypes enumeration. Its values can be combined using a bitwise operator.
Table 13.4 describes the values of the enumeration.
Value Description
FullReachQueue FullReachQueue indicates that we want both positive and
negative (full) acknowledgement on messages arriving to
their destination (reaching the queue).
FullReceive FullReceive indicates that we want both positive and neg-
ative (full) acknowledgement on messages being received
by the destination application.
NegativeReceive NegativeReceive indicates that we want acknowledgement
when messages fail (negative) to be received by the desti-
nation application.
None None indicates that we want no acknowledgement whatso-
ever. This is the default value of Message objects.
13
NotAcknowledgeReachQueue NotAcknowledgeReachQueue indicates that we want
acknowledgement when messages fail (not) to reach their
MESSAGING
intended destination queue.
NotAcknowledgeReceive NotAcknowledgeReceive indicates that we want acknowl-
edgement when messages fail (not) to be received by the
destination application.
PositiveArrival PositiveArrival indicates that we want acknowledgement
when messages succeed (positive) in reaching their
intended destination queue.
PositiveReceive PositiveReceive indicates that we want acknowledgement
when messages succeed (positive) in being received by the
destination application.
Module Module1
Sub Main()
‘local scope
Dim msg As New Message()
Dim queue As New MessageQueue(path:=”.\private$\myQueue”)
Dim ackQueue As New MessageQueue(path:=”.\private$\myQueueAckn”)
Dim ackMsg As Message
‘wait to continue
Console.WriteLine(“Enter ‘s’ to stop”)
Do While Not Console.ReadLine = “s” : Loop
End Sub
End Module
Note that we used the Acknowledgement property of Message to retrieve the type of acknowl-
edgement that the acknowledgement message represented. This is our only indication of
exactly what the message represents. Figure 13.3 illustrates the routine’s output. You can see
that the first acknowledgement is telling us that the message was received, and the second indi-
cates that the message timed out.
13
MESSAGING
FIGURE 13.3
Output of acknowledgements example.
Setting Priority
Message Queuing supports the concept of priority-based Messaging. This design dictates that
messages will have an associated priority level relative to one another. Those with the higher
priority are processed (received or routed) first. Messages that have identical priority levels are
processed based on their arrival time in the queue. This allows our applications to respond
faster to the special customer or to put new orders ahead of information requests.
We use Message.Priority to set a message’s priority using the namespace. The Priority
property is of the type MessagePriority enumeration. This enumeration has the following val-
ues (from high to low priority): Highest, VeryHigh, High, AboveNormal, Normal, Low, VeryLow,
Lowest.
Listing 13.8 sends four messages with varying degrees of priority. Each message then is read
back from the queue in turn, to illustrate priority-based Messaging.
Working with the .NET Namespaces
628
PART II
Module Module1
Sub Main()
‘local scope
Dim queue As MessageQueue = _
New MessageQueue(path:=”.\Private$\myqueue”)
Dim msg(3) As Message
Dim cnt As Short
‘wait to continue
Console.WriteLine(“Enter ‘s’ to stop”)
Do While Not Console.ReadLine = “s” : Loop
End Sub
End Module
Messaging
629
CHAPTER 13
The console’s output is provided in Figure 13.4. Notice the difference in the output compared
to the order in which the messages were sent to the system.
FIGURE 13.4
Priority example output.
Default Properties
.NET provides us with a shortcut for sending similar messages in bulk. We might want to set 13
all messages to generate an acknowledgement message or to time out in the same specified
MESSAGING
time. To do so, we use the DefaultPropertiesToSend property of MessageQueue; this takes an
instance of the DefaultPropertiesToSend class. To use the class, we simply set its various
properties.
Listing 13.9 illustrates the concepts with some basic code. We start by setting a number of
default properties. We then send 10 messages to the queue. When you run the application, use
a message explorer to watch the messages spool into the queue and then time out after a
minute and send an acknowledgement to that effect.
Module Module1
Sub Main()
‘lcoal scope
Dim queue As New MessageQueue(path:=”.\private$\myQueue”)
Dim cnt As Short
Working with the .NET Namespaces
630
PART II
End Sub
End Module
Enumerating Messages
Similar to enumerating queues, the MessageQueue class provides us with a number of methods
to return groups of messages. There are again two primary strategies: return a static snapshot
of the messages, or maintain a dynamic connection to the messages in queue.
To obtain a snapshot of all messages in a queue at a given moment in time, we use the
MessageQueue.GetAllMessages method. This method returns an array of Message objects.
GetAllMessages does not remove messages from the queue. It is equivalent to peeking at all
the messages in the queue.
We use the MessageQueue.GetEnumerator method to maintain a more dynamic relationship
with the messages in a queue. This method returns a valid IEnumerator interface. We can set
this interface to an instance of the class MessageEnumerator. This class allows us to access all
messages in the queue.
Listing 13.10 shows how to use the GetAllMessages and GetEnumerator methods.
Messaging
631
CHAPTER 13
Module Module1
Sub Main()
‘local scope
Dim queue As New MessageQueue(path:=”.\private$\myQueue”)
Dim cnt As Short
Dim msg() As Message
Dim cursor As MessageEnumerator
MESSAGING
msg = queue.GetAllMessages()
‘wait to continue
Console.WriteLine(“Enter ‘s’ to stop”)
Do While Not Console.ReadLine = “s” : Loop
End Sub
End Module
Working with the .NET Namespaces
632
PART II
Note that the MoveNext method does not actually remove the message from the queue. Instead,
it allows you to examine its contents and decide what you would like to do. If you decide that
you want to remove the message, you can use the RemoveCurrent method. This method returns
the current Message object and removes the item from the queue. MoveNext also provides
another overload that allows you to pass it a TimeSpan value. This value indicates how long the
method should wait if it finds no additional messages.
Serialization
Earlier, we used an instance of MessageQueue to send a message composed of a string. We will
now look at other ways to format messages using the Message class and associated formatting
classes (XMLMessageFormatter, BinaryMessageFormatter, ActiveXMessageFormatter).
These classes are responsible for serializing and deserializing our messages when sending and
receiving from queues.
Message Body
When we discuss message formatting, we are primarily concerned with the contents, or body,
of the message. When we specify a format for a message or encrypt a message, only its body
is affected. The message body can consist of nearly any type of information. It can contain a
simple string value, a date, a currency, an array of bytes, or even a managed object. Listing
13.11 sends a message, receives it, and writes its body out to the console.
Module Module1
Sub Main()
‘local scope
Dim queue As MessageQueue
Dim msg As Message
Messaging
633
CHAPTER 13
MESSAGING
‘loop until users presses s key
Do While Console.ReadLine <> “s” : Loop
End Sub
End Module
FIGURE 13.5
Return message body output.
Working with the .NET Namespaces
634
PART II
Note that the message’s format was output as a simple string. However, if we view the body of
the message using the queue explorer, we see that the message is stored as XML with some
surrounding binary data (see Figure 13.6).
FIGURE 13.6
Message queue explorer output.
How did the message get stored as XML, and how was it returned as a string? Well, when we
send messages, we must specify a special formatting class to dictate the message’s format.
These classes serialize data into the correct format when sending messages and deserialize
messages when receiving. Of course, we didn’t explicitly set a formatter class. However, the
.NET Messaging components use XML, by default, to send and receive messages. This should-
n’t come as a surprise because Microsoft has publicized .NET’s XML capabilities. In fact, the
messages that we sent in the previous section using the MessageQueue class also were sent with
XML. This is the default formatter used by MessageQueue.
In our example, MessageQueue.Send created an instance of a Message object and used the
XmlMessageFormatter class to serialize our string into XML before sending the message to
the queue. When we read back the message, MessageQueue.Receive used the same class
to deserialize the body of the message into a Message instance. In fact, you can use
MessageQueue.Send to send any object whose type is not Message. The object is simply serial-
ized by the XmlMessageFormatter and is stored in the message body. For simple Messaging,
this works great. We don’t have to set many properties or create additional objects; it just
works. When we want to control our message format, however, .NET gives us that capability
as well. We will cover this in more detail in the coming sections.
Messaging
635
CHAPTER 13
NOTE
There is a limit to message size in Microsoft Message Queuing. Messages must be less
than 4MB. This includes the properties and all contents.
When we set the Message.Body property, the Message instance uses a formatter class to format
and serialize our contents out to the BodyStream property. Sometimes it is not convenient to
write the body contents to the Body property and let the formatter do the work for us. Instead,
we might want to stream the contents of an object or file directly to the BodyStream property.
Listing 13.12 demonstrates this use of the BodyStream property.
Imports System.Messaging
Module Module1
Sub Main() 13
‘purpose: stream a file into a message body and send the message
MESSAGING
‘local scope
Dim fileStream As IO.FileStream
Dim queue As MessageQueue
Dim msg As Message
Dim fileByte As Integer
‘read the contents of the file and write to the message as bytes
fileByte = fileStream.ReadByte()
Do While fileByte <> -1
msg.BodyStream.WriteByte(value:=fileByte)
fileByte = fileStream.ReadByte()
Loop
Working with the .NET Namespaces
636
PART II
End Sub
End Module
In the code example, we created the contents of a message 1 byte at a time. We read bytes
from a file using the FileStream object (for more information on System.IO, see Chapter 7,
“Stream and File Operations”) and output those bytes directly to our Message object by calling
Message.BodyStream.WriteByte. We then sent the message to the queue. If you look at the
message, you will notice that there is no XML formatting. Because we are streaming the mes-
sage, the message formatter is skipped and the message is written to the queue byte by byte.
No additional serialization is done by a formatter class. This can be useful in situations that
require streaming or when you need more granular control over message contents and format.
we create. We then send the message across the wire to our central office. When an application
reads the message from the queue, the XmlMessageFormatter class automatically triggers an
instance of CTimesheet whose properties are set based on the message body. The CTimesheet
instance could contain code to write its contents directly to the database. Remember, the
queue-reading application simply made a call to receive the message. Listing 13.13 further
defines our timesheet example.
Imports System.Messaging
‘property declarations
Public employeeId As String
Public hours As Short
Public periodStart As Date
Public periodEnd As Date 13
Public Sub recordTimesheet()
MESSAGING
‘purpose: record the timesheet to the database
Console.WriteLine(value:=”Timesheet recorded.”)
End Sub
End Class
Module Module1
Sub Main()
‘local scope
Dim queue As MessageQueue
Dim timeSheetWrite As CTimesheet
Dim timeSheetRead As CTimesheet
End Sub
End Module
In the code example, we demonstrated how to use the XmlMessageFormatter class to serialize
a custom class into a message, receive that message, and deserialize it back into a usable
object. You can see from Figure 13.7 that the formatter actually wrote the message contents to
human-readable XML. Suppose that something happened to your application and you could no
Messaging
639
CHAPTER 13
longer deserialize messages. If they were formatted as XML, you could at least view their con-
tents and get a better understanding of what went wrong. It gives the application a lot of flexi-
bility when sending messages, reading their contents, and triggering business rules directly
from a message based solely on its type. We do not have to create any wrapper code to write
and read our objects as messages; it is all done for us by the formatter.
13
MESSAGING
FIGURE 13.7
CTimesheet message.
Listing 13.14 writes a bitmap to a message, reads the bitmap message, and deserializes the
contents out to a new bitmap file. In the example, we first set the Formatter property of the
Message instance to a new instance of BinaryMessageFormatter. We then use some of our
BodyStream code to read the file into the message, receive it, and write it back out as a new
message.
Module Module1
Sub Main()
‘local scope
Dim fileStream As IO.FileStream
Dim queue As MessageQueue = _
New MessageQueue(path:=”.\Private$\myqueue”)
Dim msg As New Message()
Dim fileByte As Integer
‘read the contents of the file and write to the message as bytes
fileByte = fileStream.ReadByte()
Do While fileByte <> -1
msg.BodyStream.WriteByte(value:=fileByte)
fileByte = fileStream.ReadByte()
Loop
fileStream.Close()
Messaging
641
CHAPTER 13
MESSAGING
origin:=IO.SeekOrigin.Begin)
‘read the contents of the message and write to the new file
fileByte = msg.BodyStream.ReadByte()
Do While fileByte <> -1
fileStream.WriteByte(value:=fileByte)
fileByte = msg.BodyStream.ReadByte()
Loop
fileStream.Close()
End Sub
End Module
Transactional Messaging
Transactional Messaging is the bundling of one or more messages into a cohesive set that can
be processed as a single transaction. A transaction, in the Messaging sense, is simply the send-
ing or receiving of one or more messages. Transactional messages are grouped inside a trans-
action context. The transaction context stores the state of the transaction independent of the
state of a given message.
Working with the .NET Namespaces
642
PART II
Transactional Messaging provides our applications the capability to cancel or abort the send-
ing or receiving of multiple messages. Suppose that we have an application that sends three
messages to a queue. The three messages are part of an order transaction. One is the request
from the buyer, the second is the approval by the bank, and the third is the confirmation from
our inventory application. Let’s say that the first two messages send just fine, but the third
encounters a problem. We are out of stock and cannot fulfill the order. Our business rules state
that the transaction is not complete without all three messages. If we wrap each Send call
inside a transaction context, we can just tell the context that we want to abort the operation. At
this time, all three messages are cancelled; the transaction is aborted. This includes the first
two messages that already were sent. Suppose that we did have stock. We simply tell the trans-
action context to finish or commit the transaction. At this time, the transaction is finished and
the messages are sent to the queue. This process of committing or aborting is said to be
atomic—that is, the transaction succeeds or fails as a whole.
Of course, the same holds true for receiving messages. If the valid transaction represents multi-
ple messages, we wrap the Receive calls inside a transaction context. If all messages are
received without incidence, we commit the transaction. If there is an error, we abort.
To implement transactional Messaging with the .NET components, we use the MessageQueue
class and the MessageQueueTransaction class. The remainder of this section explores these
classes and gets you started sending and receiving transactional messages.
Transactional Queues
A transactional queue is any queue inside Microsoft’s Messaging Queuing that is marked as
having the capability to receive transactional messages. All queues in MSMQ can be marked
transactional. Transactional queues also can receive nontransactional messages. Of course,
these messages cannot make use of the fact that the queue is transactional.
To use the transactional features of Messaging, you first must make sure that you are using a
transactional queue. Probably the easiest way to create a transactional queue is to check the
Transactional check box on the management interface dialog box used to create a queue. If
you create a queue programmatically, you can call this overloaded method:
MessageQueue.Create(ByVal path as string, ByVal transactional as Boolean)
In this version of the Create method, you simply set the transactional property to True.
You can read the MessageQueue.Transactional property to make sure that your application is
communicating with a transactional queue. This property returns a Boolean value of True if
the queue supports transactions.
Messaging
643
CHAPTER 13
Transactional Sending
The MessageQueueTransaction class is used to wrap a transaction context for our use. Each
instance of the class represents one transaction. We use one property and three methods from
this class. Its property, Status, is used to indicate the transaction’s current status. Status is of
the type MessageQueueTransactionStatus enumeration. The possible values that this read-
only property can return are as follows:
• Aborted—Indicates that the transaction was cancelled
• Committed—Indicates that the transaction is complete and has been committed
• Initialized—Indicates that the transaction has begun
• Pending—Indicates that the object is in mid-transaction
The methods of this class are Begin, Abort, and Commit. Begin tells the context to initialize the
transaction. Abort cancels all pending activity within the transaction context. Commit tells the
context that all is okay and that we want to finish the transaction.
We use an overload of the MessageQueue.Send method to actually send transactional mes-
sages. This overload, as you might expect, takes an instance of MessageQueueTransaction as
one of its parameters. The full function signature is as follows:
13
Send(ByVal obj As Object, ByVal transaction As MessageQueueTransaction)
MESSAGING
All messages sent using this transaction context are wrapped as part of the same transaction.
The rest of the sending operation is the same; you can still pick a formatter for your messages,
for instance.
Listing 13.15 provides an example of how to send messages as a grouped transaction. It also
demonstrates the Transactional property of MessagQueue and the Status property of
MessageQueueTransaction.
Module Module1
Sub Main()
‘local scope
Dim queue As New MessageQueue(path:=”.\private$\myqueue”)
Working with the .NET Namespaces
644
PART II
End Sub
End Module
Messaging
645
CHAPTER 13
Within the listing, we write out a number of interesting details of the executing code. The out-
put is provided in Figure 13.8 for you to follow along. Notice that the messages are not even in
the queue until the Commit method is called. Try changing Commit to Abort. You will notice
that no messages are written to the queue. Thus, the entire set of messages was cancelled and
the transaction rolled back. Also notice the Status of the transaction within the various sec-
tions of our code. It moves from Initialized to Pending and finally to Committed.
FIGURE 13.8 13
Transactional send output.
MESSAGING
Transactional Receiving
Receiving messages within a transaction context is very similar to sending. We use the
MessageQueueTransaction class, except that this time we pass a parameter of the Receive
method. The Begin, Abort, and Commit methods are all the same. However, now when you call
Abort, the messages that you had previously received are returned to the queue, untouched.
Messages are not permanently removed from the queue until we call Commit.
Message Queuing does guarantee our applications that messages can be identified as being part
of a set transaction. We use the TransactionId property of the Message instance to identify
those messages that are part of the same transaction. Each message in the transaction will have
the identical TransactionId. The TransactionId property is not guaranteed unique by
MSMQ. However, it is guaranteed to be sequentially different. This ensures that a transaction
that follows another transaction will not have the same ID as the previous transaction.
Additionally, we can be assured by Message Queuing that messages can be received
in the same order that they were sent. To check the bounds of a transaction, we use the
IsFirstInTransaction and IsLastInTransaction properties of the Message instance. These
read-only properties allow us to know when a transaction starts and when it ends. The mes-
sages in between will be received in the order of the transaction.
Working with the .NET Namespaces
646
PART II
Listing 13.16 sends a set of four messages to the queue as a transaction. To prove some of the
features that we’ve been discussing, we stuck a nontransactional message in the middle of
the send operation. We then start a Receive transaction, call Commit, and write the results to
the screen.
Module Module1
Sub Main()
‘local scope
Dim queue As New MessageQueue(path:=”.\private$\myqueue”)
Dim trxSend As New MessageQueueTransaction()
Dim trxRec As New MessageQueueTransaction()
Dim cnt As Short
Dim msg(4) As Message
If cnt = 2 Then
Next
‘----Receive transaction------
MESSAGING
For cnt = 0 To 3
msg(cnt) = queue.Receive(transaction:=trxRec)
Next
End Sub
End Module
Figure 13.9 represents our output. Notice that we did not return all four messages in our trans-
action. At first glance, this might look like a bug. After all, we sent a group of four messages
into the queue as a transaction. We want to receive the same four messages back as a transac-
tion. Remember, Messaging guarantees only that we will receive transactional messages in the
order that they were sent to the queue. If we look closely, the messages were received in the
correct order. It just sent us the nontransactional message first (which was actually sent as the
third message). The remaining messages were delivered in their correct order.
FIGURE 13.9
Transactional receive output.
Managing Permissions
Our applications need the capability to control permissions that users have to our Messaging
objects. For instance, if you have a half-dozen suppliers sending shipping confirmations, you
do not want them reading each other’s queues. Perhaps they are allowed only to send messages
and not receive, or they can only retrieve their acknowledgements. Additionally, when your
application runs, it should have rights to view and manipulate all Messaging objects on the
13
server. Rules such as these must be enforced through permission management.
MESSAGING
Permission management (also called access control) allows us to lock down various Messaging
objects to individual users or groups. Messaging objects that we can target for controlling
access include servers, queues, routing links, and message-queuing settings objects. Permission
management has two discrete levels. The first allows us to control a user’s access to a given
object. The second manages the user’s actual permissions after they’ve been granted access.
User permissions can include peek, receive, send, and many others.
Table 13.5 lists items with System.Messaging that pertain to permission management.
MESSAGING
Although this list is a little long, the classes and enumerations actually work quite well
together to provide your application flexibility when managing and accessing security. For
instance, to grant, deny, or revoke permissions on a queue, you use the SetPermissions
method of a MessageQueue instance. This method takes an instance of
MessageQueueAccessControlEntry. This class defines the access that you want to set for the
given message queue. Before you do anything, however, you must create a valid Trustee
object. A trustee represents a computer, a group, or an individual user with which you want to
set rights. Now, when creating the MessageQueueAccessControlEntry instance, we both pass
the Trustee instance and set the trustee’s rights with the MessageQueueAccessRights enumer-
ation. Rights can be any combination of things, such as DeleteMessage, PeekMessage, and
WriteMessage. You then use the AccessControlEntryType enumeration to indicate the type of
access that is being applied for the trustee and right. Types can include Allow, which allows
user access with the specified rights, Deny, which denies the user the specified rights, and
Revoke, which revokes specified rights for the user. Listing 13.17 illustrates this further.
Working with the .NET Namespaces
652
PART II
Module Module1
Sub Main()
‘local scope
Dim myAccessEntry As MessageQueueAccessControlEntry
Dim myTrustee As Trustee
Dim queue As New MessageQueue(path:=”.\private$\myqueue”)
End Sub
End Module
The code simply applies full control access to myQueue for the user mike. Figure 13.10 shows
the Security tab inside the Message Queue management interface.
Authentication
Depending on your application’s design, it is often imperative that you know who is sending a
message. Applications should not blindly accept messages—even if they are properly format-
ted. Incoming messages should be verified, or authenticated, against a list of acceptable
senders. Authentication is yet another layer of protection that your applications can use to
thwart undesirable behavior and ensure security.
Messaging
653
CHAPTER 13
FIGURE 13.10
myQueue permissions.
To authenticate a message, you must use some type of a security certificate. A certificate con-
tains the sender’s information and the public signature key of the sender. Messaging provides 13
internal certificates automatically. These can be used from within a Windows 2000 environ-
ment. To authenticate outside Windows 2000, you must create (or purchase) an external certifi-
MESSAGING
cate from a certified certificate authority.
NOTE
Workgroup installations of Windows do not allow for authentication. You must have
MSMQ Server installed to use authentication.
Table 13.6 lists the items in the namespace that correspond directly to message and message-
queue authentication.
Item Description
CryptographicProviderType The CryptographicProviderType enumeration
indicates the type of cryptographic provider used by
the digital signature. This enum typically is used when
working with foreign (non-MSMQ) queues. Some
types include Ssl, RsaFull, MicrosoftExchange, and
Fortezza.
Working with the .NET Namespaces
654
PART II
MESSAGING
MessageQueue Class Properties
Authenticate The Authenticate property is used to read and to
indicate that a message queue on the server (not sim-
ply an instance of the object) accepts only authenti-
cated messages. If this value is set to True or returns
True, then only authenticated messages will be
accepted by the queue.
You set the Authenticate property of the MessageQueue instance to True to indicate that a
message queue requires authentication. If a queue requires authentication, messages that
appear in the queue are considered, by default, to be authenticated. If the message fails authen-
tication, it is discarded by MSMQ or ends up in the dead-letter queue (provided that the
UseDeadLetter queue property has been set to True). External messages require you to set the
SenderCertificate property of your message object. External certificates must be registered
with the domain of the receiving queue. External certificates can be used by your receiving
application to identify the sender and the certificate authority, as well as provide other
information.
To indicate that you want Messaging to create an internal certificate, you mark a Message
object’s properties UseAuthentication and AttachSenderId to True. This forces Messaging
to create a digital signature and use it to sign your message. This signature then is used by the
Working with the .NET Namespaces
656
PART II
receiving queue to authenticate the message. Internal certificates have no value to your receiv-
ing application. They simply are used as a means for the queue to authenticate the message.
Listing 13.18 demonstrates the use of internal authentication. We first indicate that the queue
requires authentication by setting queue.Authenticate = True. Note that this setting
applies to the queue, not just the object instance. We then create a message and mark
UseAuthentication and AttachSenderId as True. Next, we write out some of
the default values that the message uses to sign the object using the properties
AuthenticationProviderName, AuthenticationProviderType, and HashAlgorithm.
Finally, we send the message to the queue.
Module Module1
Sub Main()
‘local scope
Dim queue As New MessageQueue(path:=”myServer\myPublicQueue”)
Dim msg As New Message()
End Sub
End Module
Taking a look at the message (see Figure 13.11) we can see that the sender identification (SID)
has an algorithm (MD5) and that the message was indeed authenticated.
13
MESSAGING
FIGURE 13.11
Authenticated message.
Auditing
Auditing allows us to record events inside MSMQ. You may implement an auditing strategy
whenever you or your application needs to know when queues have been accessed and when
messages were sent or received. For instance, suppose that you have a process that generates
an invoice after an order has been received by the application. If orders are messages inside
your queue, your invoicing application can use the journal queue to know when the order mes-
sages were received.
By default, every queue created inside MSMQ has an associated journal queue. This queue is
used to track messages as they are received. Both the Message and the MessageQueue classes
provide the property UseJournalQueue. This property indicates that Messaging should write a
copy of the message to the journal queue after the message has been received. The following
code snippet can be used to test this principle. Copy it into a console application and check the
journal queue for myQueue after its execution.
Working with the .NET Namespaces
658
PART II
queue.UseJournalQueue = True
queue.Send(“test message”)
msg = queue.Receive()
You can do additional auditing to see that your message is routed to its destination. The
UseTracing property of the Message class can be set to True if you want Messaging to gener-
ate a report message each time that your message is moved through a MSMQ server. To use
tracing, you must have Active Directory installed and must specify a report queue for the
enterprise.
Encrypting Messages
The contents of your messages are private. They can contain credit card information, ordering
information, Social Security numbers, and so on. As they move across a network, it’s impor-
tant to make sure that only the intended audience can view their contents. You would not want
the message intercepted, read, modified, or resent. Encryption ensures that only those with the
private key can unlock the message contents.
Table 13.7 lists the items in the namespace that pertain to enforcing encryption.
Item Description
Message Class Properties
DestinationSymmetricKey You set the DestinationSymmetricKey property when
you want to encrypt the message yourself.
EncryptionAlgorithm The EncryptionAlgorithm property returns or sets the
algorithm used to encrypt the body of the message.
The property is of the type EncryptionAlgorithm enu-
meration. The possible values are None, Rc2 (default),
and Rc4.
UseEncryption The UseEncryption property is used to return or set a
Boolean value indicating whether the message should
use encryption.
Messaging
659
CHAPTER 13
You can encrypt the message yourself using the DestinationSymmetricKey property, or you
can let Messaging encrypt the message for you. To use automatic Messaging encryption, you
must be running a version of MSMQ Server with access to a domain’s directory services.
Messaging uses the directory services to encrypt the message before it is sent and again to
automatically decrypt the message when it reaches its destination. Encryption differs from
authentication in that it does not verify the sender of the message; instead, it protects the con- 13
tents of the message itself from being read by anyone without the capability to decrypt.
MESSAGING
Listing 13.19 sends an encrypted message to a public queue on a domain. We first indicate that
the queue accepts only encrypted messages by setting the EncryptionRequired property of the
MessageQueue instance to True. Then we indicate that the message must use encryption by set-
ting our Message object’s UseEncryption property to True. Finally, we set the algorithm used
to encrypt the message by assigning the EncryptionAlgorithm property a value from the
EncryptionRequired enumeration.
Module Module1
Sub Main()
‘local scope
Dim queue As New MessageQueue(path:=”myServer\myPublicQueue”)
Dim msg As New Message()
End Sub
End Module
NOTE
Of course, this is a Web-based example, and, as a result, the code is written inside
ASP.NET. We used the Web Forms engine to build our forms. Longtime VB developers
will appreciate this new model—we did. There is now a complete separation of code
and UI, just like in Windows Forms. This made coding a lot easier (once we got used
to it). If you’ve been working with ASP for a while, you’ll need some time to poke
around and figure things out. But don’t worry—the Request and Response objects are
still there and work much the same as before.
For the sake of clarity and brevity, we eliminated the HTML and style sheet information from
the code listings. To ASP.NET’s credit, the HTML is no longer very interesting. You can get
the gist of the form just by looking at the corresponding figures.
Messaging
661
CHAPTER 13
Selecting a Machine
The Machine Select page allows users to query a machine on the network for public and pri-
vate queues. Users are required to indicate a machine name, select Private or Public, and sub-
mit the form to return a list of queues on a given machine. Figure 13.12 captures an example
of this ASP.NET page.
13
MESSAGING
FIGURE 13.12
Machine Select page.
Code Walkthrough
The Web page’s code is used to initialize the page and to process the Select button’s click
event. Listing 13.20 is an output of the form’s code.
Working with the .NET Namespaces
662
PART II
End Sub
InitializeComponent()
End Sub
#End Region
End Sub
The click event for the Select button creates a new instance of our CExplore class. This class
returns a list of queues based on machine name and public versus private. If no queues are
Messaging
663
CHAPTER 13
‘local scope
Dim explore As CExplore
MESSAGING
& machineText.Value & _
“&isPublic=” & CStr(publicQueues.Checked))
Catch
End Try
End Sub
End Class
Exploring Queues
We created the class CExplore to handle all things related to exploring queues and their mes-
sage contents.
Code Walkthrough
Listing 13.21 details the contents of the CExplore class.
Working with the .NET Namespaces
664
PART II
The class starts by importing the System.Messaging library to make our calls to the Messaging
objects easier. Next, we set a global-level variable to maintain the name of the machine
(c_machineName) containing the message queue that the user is browsing. We created the
required constructor Public Sub New to allow users to set the machine name upon object
instantiation.
Imports System.Messaging
‘class-level scope
Private c_machineName As String
End Sub
The returnQueues method is used to return an array of MessageQueue objects for the given
machine. We pass the variable isPublic as Boolean to indicate whether the user has requested
to browse public or private queues. If the function cannot return queues, it simply raises an
error to the calling client.
Public Function returnQueues(ByVal isPublic As Boolean) As MessageQueue()
Try
End Try
Else
Try
Catch
MESSAGING
End Try
End If
End Function
The returnQueue function returns a MessageQueue object from a queue path (qPath). If no
queue is found at the path, an error is raised to the calling application.
Public Function returnQueue(ByVal qPath As String) As MessageQueue
Try
Catch
End Function
The returnMessages method returns an array of Message objects for a given queue. Note that
the function takes a snapshot of the messages in the queue. It does not receive the messages.
To do so, we call the GetAllMessages method.
Public Function returnMessages(ByVal qPath As String) As Message()
‘local scope
Dim queue As MessageQueue
Try
Catch
End Try
End Function
The returnMessage function returns a Message instance from a queue path (qPath) and mes-
sage ID (msgId). We set the MessageReadPropertyFilter to SetAll to indicate that we want
to return all properties of the message. To get the message without actually receiving it, we call
the PeekById method of the MessageQueue class.
Messaging
667
CHAPTER 13
‘local scope
Dim queue As MessageQueue
Try
MESSAGING
Return queue.PeekById(id:=msgId)
Catch
End Try
End Function
End Class
Queue Properties
The Queue Properties screen allows users to view the various properties of their selected queue.
Message queues on the selected server are listed to the left. When a user clicks a given queue’s
properties link, the Queue Properties table within the form is updated. This screen also provides
users access to a queue’s messages and journal contents. Figure 13.13 shows this Web page.
Working with the .NET Namespaces
668
PART II
FIGURE 13.13
Queue Properties page.
The same page is used to display the queue’s contents. When users click on the “Messages”
link, the main table to the right of the left message queue list is updated to display the mes-
sages contained inside the queue. Similarly, when users select the “Journal” link, this table is
updated to display the message contents of the queue’s journal.
If a queue is transactional and the messages in the queue are transactional, the page displays
this using color banding. A green message indicates a message that is first in the transaction
sequence. Single message transactions are simply green. A gray band is used to display mes-
sages that are in the middle of the transaction (not first or last). A red-coded message indicates
the last message in the transaction set.
The page allows users to accept or reject messages in the queue. Next to each green-banded
message (gray for nontransactional queues) is a check box for either Accept or Reject. Users
click the appropriate check box next to the message and press their Select button to either
reject or accept messages as a transactional unit.
Figure 13.14 displays transactional messages in a private queue.
Code Walkthrough
Listing 13.22 details the code used to build the Web page.
The code starts by declaring a number of page-level variables for storing user settings and
such. These variables are set in the Page_Load event via the query string and hidden form field
variables.
Messaging
669
CHAPTER 13
FIGURE 13.14
Messages and Transactions page. 13
MESSAGING
LISTING 13.22 qExplore.aspx.vb
Imports System.Messaging
‘form-level scope
Private machineName As String
Protected WithEvents queueList As _
System.Web.UI.HtmlControls.HtmlGenericControl
Private isPublic As Boolean
Private qPath As String
Private showMsg As String
Private showJournal As String
Protected WithEvents qDetails As _
System.Web.UI.HtmlControls.HtmlGenericControl
Private showProps As String
End Sub
InitializeComponent()
End Sub
#End Region
‘check if the user is submitting the form (post) or requesting the page
‘(get)
If Request.ServerVariables(“REQUEST_METHOD”) = “GET” Then
End If
Else
qDetails.InnerHtml = “”
End If
Else
MESSAGING
‘determine the button the user clicked
Select Case Request.Form(“subMsg”)
Case “Accept”
Case “Reject”
End Select
Call displayQueueMessages()
End If
End Sub
‘local scope
Dim messageIds() As String
Dim i As Int16
Dim trx As New CTrx(machineName:=machineName, qPath:=qPath)
Try
‘get messages to process
Select Case processType
Case msgProcessing.accept
‘call receiveTrxMsg
trx.receiveTrxMsg(trxId:=Request.Form(messageIds(i)))
Else
‘call receiveMsg
trx.receiveMsg(msgId:=messageIds(i))
End If
Next
Case msgProcessing.reject
Else
‘call rejectMsg
trx.rejectMsg(msgId:=messageIds(i))
End If
Next
End Select
Catch
End Try
13
End Sub
MESSAGING
The displayQueueInfo procedure writes each queue to the left queue selection bar on the
page. We create an instance of CExplore to use to return the various queues on the selected
server. We then loop through the array of MessageQueue objects and write the queue name to
the screen. Along with the name, we set a number of query string variables to be used when
refreshing the page after a user has selected a queue link.
Private Sub displayQueueInfo()
‘local scope
Dim explore As CExplore
Dim queues() As MessageQueue
Dim i As Int16
Dim display As String
Try
End Try
End Sub
The displayQueueProperties procedure writes properties of the selected queue to the main
table on the page. We again create an instance of the CExplore class and, with it, return a valid
MessageQueue object. We then read the various properties of the queue and write them to the
Web page.
Private Sub displayQueueProperties()
‘local scope
Dim display As String
Dim mq As MessageQueue 13
‘create a new instance of the explore class
MESSAGING
Dim explore As New CExplore(machineName:=machineName)
Try
Catch
End Try
‘show q properties
display = “<table border=0 cellpadding=1 cellspacing=0 width=100%>”
qDetails.InnerHtml = display
End Sub
The displayQueueMessages subroutine returns all the messages using the returnMessages
method of the CExplore class. We then loop through the message array and use each Message
instance’s properties to build the Message Details table. This includes transactional messages.
Private Sub displayQueueMessages()
‘local scope
Dim display As String
Dim mq As MessageQueue
Dim msg() As Message
Dim i As Int16
Dim trxPos As String 13
‘create a new instance of the explore class
MESSAGING
Dim explore As New CExplore(machineName:=machineName)
Try
Catch
End Try
End If
‘close row
display = display & “</tr>”
MESSAGING
Else
End If
Else
‘non-trx queue
display = display & “<tr class=queueCell>”
End If
Catch
End Try
Else
Messaging
681
CHAPTER 13
End If
Else
End If
13
MESSAGING
‘close the row
display = display & “</tr>”
Next
Else
End If
End Sub
The displayQueueJournal procedure outputs the queue’s journal messages to the main table
in the Web page. It uses the returnMessages method of the MessageQueue object to get an
array of journal messages. It then loops through the array and outputs the messages to the
screen.
Working with the .NET Namespaces
682
PART II
‘local scope
Dim display As String
Dim mq As MessageQueue
Dim msg() As Message
Dim i As Int16
Dim trxPos As String
Try
Catch
End Try
‘display messages
display = “<table border=0 cellpadding=1 cellspacing=0 width=100%>”
display = display & “<tr><td class=tableTitle colspan=4>”
display = display & “Queues Messages: “ & qPath & “</td></tr>”
display = display & “<tr><td class=colTitle>Sent</td>”
display = display & “<td class=colTitle>Label</td></tr>”
Next
Else
End If
13
‘close the table
MESSAGING
display = display & “</table>”
End Sub
End Class
Transaction Management
The CTrx class is used to receive and reject messages. Its methods are called by the click event
for the given Accept or Reject button. You might extend this class to enforce specific business
rules or call other classes that enforce these rules. Additionally, you might log information to
the event log, send special messages to other queues, and so on. Currently, the code simply
receives messages and writes them to the event log.
Code Walkthrough
Listing 13.23 shows the output of the code for CTrx.
Working with the .NET Namespaces
684
PART II
We declare a global-level queue variable to maintain a reference to the queue for the class. The
class has a constructor that is used to set this global queue value.
Imports System.Messaging
Imports System.Diagnostics
‘class-level scope
Private queue As MessageQueue
End Sub
The receiveTrxMsg routine receives a transactional set of messages based on the parameter
trxId (transaction identifier). The method starts a transaction with the line trx.Begin.
Messages then are received and written to the event log. If an error occurs, trx.Abort is called
and all messages received as part of the transaction are rolled back to their original state (not
received). Upon success, we call trx.Commit.
Public Sub receiveTrxMsg(ByVal trxId As String)
‘local scope
Dim msg() As Message
Dim trx As New MessageQueueTransaction()
Dim i As Integer
Dim msgRec As Message
Messaging
685
CHAPTER 13
‘start a transaction
trx.Begin()
‘loop through our snapshot of messages and pull out the transaction
For i = 0 To UBound(msg)
Try
Catch
13
MESSAGING
‘error, abort the transaction
trx.Abort()
Err.Raise(number:=Err.Number, description:=Err.Description)
End Try
‘do something with the message, business rules, write to db, etc.
‘write the item to the event log
EventLog.WriteEntry(source:=”messageManager”, _
Message:=”Message Received: “ & msgRec.Body, _
type:=EventLogEntryType.Information)
End If
Next
End Sub
Working with the .NET Namespaces
686
PART II
‘local scope
Dim msg() As Message
Dim trx As New MessageQueueTransaction()
Dim i As Integer
Dim msgRec As Message
‘start a transaction
trx.Begin()
‘loop through our snapshot of messages and pull out the transaction
For i = 0 To UBound(msg)
Try
Catch
End Try
‘do something with the message, business rules, write to db, etc.
‘write the item to the event log
EventLog.WriteEntry(source:=”messageManager”, _
Message:=”Message Rejected: “ & msgRec.Body, _
type:=EventLogEntryType.Information)
Messaging
687
CHAPTER 13
End If
Next
End Sub
The receiveMsg procedure is used for receiving nontransactional messages. It takes a message
ID as its one parameter. It uses the ReceiveById method of the MessageQueue object to pull
the message from the queue and write its contents to the event log. The following routine,
rejectMsg, is identical except that it writes a rejection notice to the event log.
MESSAGING
Dim msg As Message
Dim eventLog As New EventLog()
‘do something with the message, business rules, write to db, etc.
‘write the item to the event log
eventLog.WriteEntry(source:=”messageManager”, _
Message:=”Message Received: “ & msg.Body, _
type:=EventLogEntryType.Information)
End Sub
‘local scope
Dim msg As Message
Dim eventLog As New EventLog()
‘do something with the message, business rules, write to db, etc.
End Sub
End Class
Summary
The items we’ve covered in this chapter represent the foundation of Messaging. Because the
topic is such a large one, with so many features available, there are undoubtedly details that
you will uncover as you write your applications and continue to explore the library. These
details should be easily learned because they complement and extend your foundation.
The following are key points regarding Messaging with the .NET Class Library:
• The Messaging design pattern provides our applications with guaranteed delivery, secu-
rity, encryption, priority-based Messaging, and transactional Messaging.
• The MessageQueue class is used to connect to a message queue, set and return its many
properties, and send and receive messages.
• You can return a static snapshot of all queues on a machine using the GetPublicQueues
or GetPrivateQueuesByMachine methods of the MessageQueue class, or you can main-
tain dynamic connection to the queues on a machine using the MessageQueueEnumerator
class.
• You use MessageQueue.Send to send messages to a given queue.
• MessageQueue.Peek allows you to view the contents of a message without actually
removing it from the queue.
• MessageQueue.Receive is used to receive a message and pull it off the queue.
• Asynchronous Messaging is accomplished via the MessageQueue class’s BeginReceive
and EndReceive methods and corresponding ReceiveCompleted event.
• You can receive message acknowledgements by setting Message.ResponseQueue, pro-
vided that the receiving message queue supports this feature.
• Setting the Message.Priority property forces a queue to process a message with a
higher priority first, regardless of the order in which it was received.
Messaging
689
CHAPTER 13
MESSAGING
http://www.microsoft.com/msmq/downloads/MSMQ_CE.zip.
➲ Microsoft Message Queuing interoperates with the IBM MQSeries products through the
Microsoft MSMQ-MQSeries Bridge. Check out http://www.microsoft.com/msmq/
interop.htm for more information on MSMQ interoperability.
14
Communications
IN THIS CHAPTER
• Key Classes Used for Browser/Server
Communication 692
ASP.NET evolved out of Microsoft’s original Active Server Pages, but that is where the simi-
larities end. Sure, some of the objects are still named the same, but the capabilities and pro-
gramming paradigms that ASP.NET offers have really grown up. VB developers will be
pleased to know that it is often difficult to distinguish in which environment you are
programming—VB or ASP!
This chapter starts by detailing System.Web, the namespace related to Web development and
ASP.NET. We then present a number of key classes in the namespace, each related to Web
development basics. Our discussion then moves to handling client requests and issuing
responses. We also talk about why proper state management within your application is so criti-
cal and illustrate how to use ASP.NET new features related to managing Web application state.
Finally, we finish the chapter with a sample ASP.NET application that lets users indicate where
they are, when they’ll be back, and how they can be reached.
After reading this chapter, you should
• Understand how to handle form data submitted to your ASP.NET page
• Be able to verify whether a Web client can accept cookies, write a cookie value out to the
client, and retrieve it at a later date
• Manage client-side and server-side user data (aka state) between page requests
COMMUNICATIONS
BROWSER/SERVER
• System.Web.SessionState—Provides classes that enable you to manage user state (on
the Web server) between requests. This namespace is covered within this chapter.
• System.Web.UI—Provides classes that are used to create ASP.NET HTML, Web, User,
and Server controls.
• System.Web.UI.Design—Provides classes that are used to support design-time function-
ality for Web Forms.
• System.Web.UI.Design.WebControls—Provides designer classes that are used to sup-
port design-time functionality for Web controls.
• System.Web.UI.HtmlControls—Provides classes that are used to create and control
HTML controls on your ASP.NET form.
• System.Web.UI.WebControls—Provides classes that wrap Web controls. These are con-
trols that run on the server and provide easier management than HTML controls.
Working with the .NET Namespaces
694
PART II
As you can see, Web development is well represented in the .NET Framework Class Library.
Indeed, entire books are dedicated to the subject. For our purposes, we are going to focus on
those classes you will most commonly access. Table 14.1 lists the key classes we will be
discussing.
COMMUNICATIONS
BROWSER/SERVER
localhost/Chapter14/WebForm1.aspx/temp.
The ApplicationPath property returns the virtual path
of the request (/Chapter14).
CurrentExecutionFilePath Both the FilePath and the CurrentExecutionFilePath
FilePath property return the virtual path and the filename of the
current request (/Chapter14/WebForm1.aspx).
Path The Path property returns the virtual path, filename, and
trailing information (/Chapter14/WebForm1.aspx/temp).
PathInfo The PathInfo property returns only the trailing
information (/temp).
PhysicalApplicationPath The PhysicalApplicationPath returns the physical
path on your server to your application’s directory
(c:\inetpub\wwwroot\Chapter14\).
Working with the .NET Namespaces
696
PART II
COMMUNICATIONS
BROWSER/SERVER
the requesting client.
Methods
BinaryRead The BinaryRead method is used to read portions of the
input stream.
MapPath The MapPath method is used to map the requested URL
to a physical path on your server.
SaveAs SaveAs property allows you to save the request to disk.
The HTML <form> tag has two key attributes: Method and Action. The Method attribute indi-
cates the request method used to package and send the data to your site. There are two HTTP
request methods, GET and POST. The Action attribute is used to indicate to which page the
request should be sent. Typically, a request for a page is sent to the server as a GET method.
This is when a user types in a URL or links to your page using a hyperlink. The POST method
is reserved for when users submit form data to one of your pages.
After you’ve defined your form’s properties, you create an <input> control whose Type
attribute is Submit. This control defines your button. When users click this button, your form
will be submitted to the target of your Action attribute using the request method defined by
the form’s Method attribute.
If the form is submitted using the GET method, all your data is appended to the URL of the tar-
get page on what is called the query string. The query string is indicated by a question mark
(?) and contains name/value pairs separated by the “&” sign. The names of each item in the
query string are equivalent to the names of all your form elements. Each item’s value is either
the data entered (or selected) by the user or the value attribute of the given control. The query
string names and values are packaged into a collection accessible from the QueryString prop-
erty of the HttpRequest object. The query string, however, is typically reserved for passing
data from page to page using a hyperlink rather than submitting form data.
Many times you’ll pass hidden fields and other sensitive data as part of the request. For this
reason, you should use the POST method when submitting data as part of a request. In this sce-
nario, data is hidden from the user; it does not appear on the URL. This has the added benefit
of preventing users from hacking at your URL and producing unwanted results. When a Web
Form submits or “posts” its information, the submitted data is accessible via the Form property
of the HttpRequest object.
Both the QueryString and the Form property are of the type NameValueCollection. This col-
lection class enables you to access items using a key value (or name). You simply pass the
name of the query string element or name of the form control to the class, and it returns the
value.
To demonstrate reading the query string and form values using these objects, we’ll create a
simple ASP.NET form with two text boxes and a submit button. When a user clicks the button,
the request is sent to the server in the type defined by the form’s method. The HttpRequest
object (ASP’s Page.Request property) is accessible inside the button’s server-side
click event.
Listing 14.1 represents this event. In this code we, check the request type and then write out
the contents of the form using either the QueryString or the Form property’s collection
(depending on GET or POST). Note that we’ve eliminated the form definition code for clarity;
full source code is available from the Web site.
Browser/Server Communications
699
CHAPTER 14
End If
End Sub
Now, simply toggling the Method of the form definition between the GET and POST values pro-
duces different results.
Figure 14.1 shows the results for a GET method; notice the highlighted query string.
14
COMMUNICATIONS
BROWSER/SERVER
FIGURE 14.1
Results of the QueryString code example.
Working with the .NET Namespaces
700
PART II
Responding to Requests
Data is sent to the server (and to your page) as a client request. You respond to these requests
by processing information and sending data back to the client. This information (also called
the response) is managed by an instance of the HttpResponse class. This class is accessible by
calling the ASP.NET intrinsic Response object (Page.Response property) from within your
ASP.NET page. Developers can simply reference the Response property as you did in the ASP
of old. This allows for backward compatibility. However, the property is of the type
HttpResponse class.
COMMUNICATIONS
BROWSER/SERVER
standard for content labeling and rating.
Redirect The Redirect method is used to send the client to a differ-
ent URL.
Write The Write method is used to write information to the
HTTP content that gets sent to the client as part of the
response.
WriteFile The WriteFile method is used to write the contents of a
file directly to the HTTP content stream.
a form, you might write some of that information off to a data store and then direct the user
on to another menu page or the like. To do so, you simply call the Redirect method, as in the
following:
Response.Redirect(“WebForm2.aspx”)
Often, however, you’ll make decisions in your code that require you to change content sent to
the client. The simplest example of this is when returning data from a data store that is
intended for users. In this case, you’ll want to use the Write method to embed this information
into the response. This method is accessed as follows:
Response.Write(someVariable)
To write binary data into the response content, you use the BinaryWrite method. This can be
useful if you store binary objects, such as graphics, in your data store. As an example, we’ll
create a binary stream based on a JPEG file. We’ll send this stream down to the client using
the BinaryWrite method.
Listing 14.2 represents the click event of a button defined on a Web Form. When users click
the button, they are presented with graphics streamed to them using BinaryWrite.
‘local scope
Dim myFileSize As Long
End Sub
You can also write the contents of a file directly to the output response. To do so, you use the
WriteFile method. This is a new feature specific to ASP.NET. The method works with both
text and binary file data. For instance, in the preceding example, we could use WriteFile to
output the same image to the response.
Response.WriteFile(fileName:=”c:\temp\myGraphic.jpg”)
COMMUNICATIONS
BROWSER/SERVER
Browsers are classified into the two groups: uplevel and downlevel. Downlevel browsers pro-
vide support for HTML 3.2. Uplevel browsers are those that support HTML 4.0 and later,
JavaScript 1.2, and Cascading Style Sheets (CSS).
To determine the capabilities of a requesting client’s browser, you use the Request object’s
Browser property. For example, to determine whether a browser supports cookies, you can use
the following code:
If Request.Browser.Cookies then
...
End If
Working with the .NET Namespaces
704
PART II
The Browser property returns an instance of the HttpBrowserCapabilities class. This class is
made up of a number of Boolean properties that indicate whether the requesting browser sup-
ports a given capability. A majority of these properties are listed in Table 14.4.
State Management
How you manage the storage and retrieval of global, local, and user data is one of the most
important design considerations you can make when writing Web applications. Three areas
where your application will typically store state are the client, your Web server, or your data-
base server. Getting the right mix of state management for your application will lead to scal-
able and fast ASP.NET applications.
14
COMMUNICATIONS
Storing Global State
BROWSER/SERVER
Values that are used globally by your Web application are referred to as application state. A
Web application is defined by all the ASP files and classes in your domain. When your appli-
cation is started (or restarted), IIS creates an application-scoped area inside your server’s mem-
ory space to store data. Effective use of globally scoped data can speed page requests and trim
development time. However, mismanagement can severely limit the scalability of your applica-
tion. Let’s look at how to use, and how not to use, application state.
Application state is stored in memory on your server. This can be great because a page is no
longer required to make a trip to a data store to retrieve data. At the same time, the data is eat-
ing up a precious memory on your server. This memory is not recovered until the application
data is removed or the site is stopped and started. When you decide whether to store data
within application state, it is important to be sure of the following:
Working with the .NET Namespaces
706
PART II
HttpApplicationState
To store application state on the server, you use the HttpApplicationState class. This is a
collection class containing name/value pairs. To access this class, you use the Application
property of the Page instance. The following code demonstrates adding a value to the
HttpApplicationState object inside the global.asax when the application starts:
Browser/Server Communications
707
CHAPTER 14
End Sub
This application scoped value is available to any page (or class) from within your ASP.NET
application. To access the value, you simply pass the key to the collection and it returns the
value. For example:
Private Sub Page_Load(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles MyBase.Load
End Sub
COMMUNICATIONS
BROWSER/SERVER
HTTP and HTML try to solve some of the issues with user state by providing the query string
(discussed previously) and the concept of hidden fields. Query strings are ideal if you are
working only with page requests (and not posts) and you are not sending sensitive data to the
client or sending information a user might tamper with. How many sites can be messed with
just by altering some of the values in the query string? How many developer hours have been
spent trapping for this case? Query strings do serve a purpose in a limited, well-planned role—
they are, however, only part of the solution.
Hidden fields are a little better in that they are “hidden” from the user. A hidden field is sent as
part of the request in the form of <input> tags whose type attribute is set to the value of
hidden. Of course, they are hidden only from the browser window. They are still sent to the
client, and as such, can be viewed by a user. Simply right-clicking the browser window and
Working with the .NET Namespaces
708
PART II
choosing View Source exposes all the hidden fields. Of course, this is still a viable option for
data that is not too sensitive.
ASP.NET provides a new object for managing hidden form fields. This object is accessed from
the ViewState property, which derives from IStateManager. This object is easy to use and
provides for object-oriented development instead of HTML coding when managing hidden
form fields. Additionally, items added to the ViewState collection are hashed, compressed, and
encoded before being sent to the client. This severely limits the likelihood of nefarious users
manipulating your form input. (ViewState is also used by Web controls for the same effects.)
NOTE
All client data can be tampered with by mischievous developers sending posts to your
pages and embedding their own input values or cookies in the request. You need to
make sure you are aware of this and be sure to trap for it if you consider it a likely
occurrence.
To add a hidden form field to your response, you call the Add method of ViewState and pass
both the item’s name and its value as follows:
ViewState.Add(key:=”HiddenValue”, value:=”Some hidden value”)
When the input field is sent to the client, the field’s hidden value looks like the following:
<input type=”hidden” name=”__VIEWSTATE”
value=”dDwtNTMwNzcxMzI0O3Q8cDxsPEhpZGRlblZhb
HVlOz47bDxTb21lIGhpZGRlbiB2YWx1ZTs+Pjs7Pjs+” />
To read the hidden form field from the posted request, you simply call the Item method of the
ViewState collection. The following writes this value to the screen:
Response.Write(ViewState.Item(key:=”HiddenValue”))
HttpCookie
Another option available for storing client-based state information is inside the user’s cookie. A
cookie is a small, text-based file or an area in memory that you can use to place information
onto the client’s machine. Cookies are specific to your application and are sent along with
every page request. Typically, these are used for applications that remember your settings.
When you log in, a cookie containing your user ID might get written to your machine for later
retrieval. Cookies work well in this scenario, provided the user hasn’t turned them off on the
browser.
Browser/Server Communications
709
CHAPTER 14
Cookies in ASP.NET are wrapped by the HttpCookie class. Cookies are accessed using the
Cookies property of the Request object. Cookies are written using the same property of the
Response object. Both properties use the HttpCookieCollection class to add and retrieve
items from the cookie collection.
To write a cookie as part of the HTTP response, you call the Add method of
HttpCookieCollection and pass the cookie’s name and its value. For example:
To read a cookie from the client request, you simply access an item from the collection using
the cookie’s key as follows:
‘retrieve a cookie value from the request
‘ and write out its value to the response
Response.Write(Request.Cookies.Item(name:=”myCookies”).Value)
It is generally good form to make sure your cookies expire. The Expires property of the
HttpCookie class is useful for setting (and returning) an expiration date and time for the
cookie. Depending on your site’s traffic patterns, if a user hasn’t visited your site in more than
a month, he should expect that his cookie has expired.
HttpSessionState
The last weapon in your client-state arsenal is the client’s session state. When users request a
page from your server, each user is assigned a session—provided the server is configured in
this manner—and a session identifier. This information is used by the server to store client-
specific information for the lifetime of the session, and even beyond. Again, depending on how
you use this tool, you can either end up optimizing your application for performance and eas-
ing your development effort—or crippling your application’s scalability. Because session state 14
COMMUNICATIONS
BROWSER/SERVER
is managed by the server, it consumes valuable memory for every client with an active session
(an active session is one that has yet to time out because of user inactivity).
The session identifier can either be written to the user’s cookie (provided the user’s browser
supports cookies) or sent with each request on the URL as a query string value. The ID is a
120-bit ASCII string generated by an algorithm that guarantees uniqueness and randomness.
To access a user’s session ID, you can call Session.SessionID. To determine whether the ses-
sion ID came from a URL or a cookie, call the Session.IsCookieless property. This property
returns True if the session ID is in the URL.
ASP developers will be happy to know that ASP.NET’s Session object has little in common
with the Session object of old. ASP.NET’s Page.Session property is of the type
Working with the .NET Namespaces
710
PART II
To read items from the session state, you simply access the session collection’s Item property
and pass the name of the element you want to access. For example:
Response.Write(Session.Item(name:=”SessionItem”))
Most of the new session state features revolve around where the session state gets managed.
Session state can be configured to work in one of four modes. You can return the mode your
application is running under by calling the Mode property of the SessionState class. This class
returns a value of the type SessionStateMode enumeration. The possible modes are as follows:
• InProc—The InProc mode indicates that the server manages user sessions in the same
process in which your application runs. This is the default mode configuration. This pro-
vides fast access to session state because items are in the same memory space as your
application. However, the InProc configuration is problematic if your application stops,
is restarted, if the user gets redirected to another server based on a load-balancing situa-
tion, or if the machine is restarted. In any of these cases, this session state is lost. InProc
is equivalent to how IIS managed session state in past versions.
• SqlServer—In the SqlServer mode, session state is persisted to and managed from a
SQL Server database. This provides the capability for your application to live beyond
users closing their browsers or even in the event of a server shutdown or reboot.
• StateServer—With the StateServer mode, session state is stored out-of-process from
your IIS server. This ensures that it survives service restarts. The StateServer mode can
be used in a Web farm to share states between servers.
• Off—Many developers choose to manage their state themselves using cookies or a SQL
server. In this case, it’s best to turn session management off to free resources.
You can alter the mode in which your application manages state by setting the special tag
<SessionState> in your application’s configuration file. This file is stored in the root of your
ASP.NET application and is called Web.config. The following is an example of the default
settings for this tag:
<sessionState
mode=”InProc”
stateConnectionString=”tcpip=xxx.x.x.x:42424”
sqlConnectionString=”data source=xxx.x.x.x;user id=sa;password=”
Browser/Server Communications
711
CHAPTER 14
cookieless=”false”
timeout=”20”
/>
When a user’s session is created, an event is fired on the server. You can easily trap this event
inside the global.asax file. To do so, you create a Session_Start event as follows:
Sub Session_Start(ByVal sender As Object, _
ByVal e As EventArgs)
End Sub
Notice that in the example, we set the session’s Timeout property to the value 2. This value
indicates the number of minutes that the session can stay dormant on the server before it is
destroyed.
You can trap a similar event when the session is torn down. You create a Session_End event.
This can be useful to clear any session-level resources you might have been consuming, or
even to persist information from the session out to a data store. An example follows:
Sub Session_End(ByVal sender As Object, _
ByVal e As EventArgs)
COMMUNICATIONS
BROWSER/SERVER
‘ or release some session-level resources ...
End Sub
➲ For more information on creating scalable ASP.NET applications, see the document
“Developing High-Performance ASP.NET Applications” in MSDN.
Working with the .NET Namespaces
712
PART II
Home Page
The MyStatus application’s home page could not be much simpler. We’ve added two hyper-
links: one for users to change their current status and the other to view the status of everyone
else in the data store. We’ll leave the View Others’ Status page for you to implement because
this is XML coding and unrelated to our System.Web demonstration. A screenshot of the home
page is provided in Figure 14.2.
Code Walkthrough
There is not much code behind this page; the only code is that which is generated by
ASP.NET. In a production application, we would suggest building this page in simple HTML.
The code is presented in Listing 14.3.
Note: There is no code in the global.asax file for this application.
Browser/Server Communications
713
CHAPTER 14
FIGURE 14.2
MyStatus home page.
Inherits System.Web.UI.Page
COMMUNICATIONS
BROWSER/SERVER
Private Sub InitializeComponent()
End Sub
#End Region
End Class
Working with the .NET Namespaces
714
PART II
MyStatus Page
The MyStatus page represents all the functionality in our sample application. Here, the users
enter their current status. Notice that we store and indicate the last time current status was
updated. This ensures that users will know the accuracy of the information. Figure 14.3 is a
capture of the MyStatus page.
FIGURE 14.3
MyStatus screenshot.
Code Walkthrough
The code behind the MyStatus page is straightforward. We start by importing the System.Xml
namespace to make our coding easier and clearer. The basic form code is then presented; this is
automatically generated by ASP.NET. This is shown in Listing 14.4.
Inherits System.Web.UI.Page
End Sub
#End Region
Inside the page load event, we try to load the user’s XML data. This data is stored to a folder
within our Web domain. The filename is made up of the user’s original session ID when the
user came to the site. This session ID was written out to the user’s browser and returned each
time as part of the request. If the user’s session ID doesn’t exist, we simply present a blank
form. 14
COMMUNICATIONS
BROWSER/SERVER
In the event that the user has saved data on our server and the cookie has not expired, we cre-
ate an instance of XmlTextReader class to read the user’s last status from a file. Notice that we
use the Request.PhysicalApplicationPath property to point the text reader at the appropriate
file. We then loop through the elements in this data file and trap for the ones related to our
Web page controls. When found, we set the Text property of the appropriate control to the
string value of the XML element. For the drop-down box, we have to call the FindByValue
method of the control and mark the found value as selected.
Private Sub Page_Load(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles MyBase.Load
‘purpose: check if the user has a valid cookie from a prior visit
‘ if so, grab their XML file and fill form fields
‘ if not, simply display the form
Working with the .NET Namespaces
716
PART II
‘block scope
Dim xReader As XmlTextReader
‘read items out of the file and update set values on the form
xReader.WhitespaceHandling = WhitespaceHandling.None
While xReader.Read()
Case “TextBoxBack”
TextBoxBack.Text = xReader.ReadString()
Case “TextBoxReach”
TextBoxReach.Text = xReader.ReadString()
Case “DropDownListCurrently”
DropDownListCurrently.Items.FindByValue( _
xReader.ReadString()).Selected = True
Case “TextBoxEmergency”
TextBoxEmergency.Text = xReader.ReadString()
Browser/Server Communications
717
CHAPTER 14
End Select
End Select
End While
Else
End If
End Sub
When users submit their status data, we must write it out to the file store, which is simply a
directory in our application. The first thing we do is check the user’s cookie for an original ses-
sion ID. If the user has no cookie—whether it is because it is the user’s first time accessing the
page or because the cookie has expired—we write one out using the Response object. We set
the cookie’s expiration date to be one month from the current date.
After we have a valid cookie, we are ready to write our XML stream. We use the session ID
taken from the cookie value as the filename. We first create an instance of the XmlTextWriter 14
class. This class allows us to write XML elements directly to the file. We pass the element
COMMUNICATIONS
BROWSER/SERVER
name and the string value on the method call WriteElementString.
Finally, we close the XmlTextWriter instance and redirect the user back to the home page.
Private Sub Button1_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles Button1.Click
‘local scope
Dim origSessionId As String
‘block scope
Dim myCookie As HttpCookie
‘create a cookie
myCookie = New HttpCookie( _
name:=”UserSession”, _
value:=origSessionId)
End If
xWriter.WriteElementString( _
localName:=”TextBoxBack”, _
value:=Request.Form(“TextBoxBack”).ToString)
xWriter.WriteElementString( _
localName:=”TextBoxReach”, _
value:=Request.Form(“TextBoxReach”).ToString)
xWriter.WriteElementString( _
localName:=”DropDownListCurrently”, _
value:=Request.Form(“DropDownListCurrently”).ToString)
xWriter.WriteElementString( _
localName:=”TextBoxEmergency”, _
value:=Request.Form(“TextBoxEmergency”).ToString)
xWriter.WriteElementString( _
localName:=”LastUpdated”, _
value:=Now.ToString)
COMMUNICATIONS
BROWSER/SERVER
‘redirect the user back to the home page
Response.Redirect(“home.aspx”)
End Sub
End Class
Working with the .NET Namespaces
720
PART II
The XML
An example of the XML that gets written to the file is presented in Figure 14.4.
FIGURE 14.4
XML data file.
Summary
ASP.NET, although still familiar, represents a radical upgrade from previous versions. This
chapter simply scratched the surface of what is available. The use of the .NET Framework
Class Library ensures consistent object-oriented development rather than the spaghetti code of
old. We think that as you explore more of its features, you’ll soon find ASP.NET and the
System.Web namespace as indispensable as VB .NET itself.
15
IN THIS CHAPTER
• Key Classes Related to Data 722
Some of the fundamental questions that developers ask during system design are: Should the
application persist its data somewhere? If so, where and how? Persisting, or storing, data for
later use is such a core concept that many of us simply take it as a “given” when we write our
applications. Whether we store state in a database, in an INI file, or in the registry, we are
forced to employ design patterns ranging from simple data queries and record inserts, to com-
plicated transactional programming and schema manipulation.
Visual Basic traditionally has had very rich support for implementing data storage, particularly
with relational databases. Although in the history of Visual Basic it is a relatively recent
“invention,” ADO (ActiveX Data Objects) is a cornerstone technology for allowing developers
to abstract data storage concepts in their code. ADO has been extended into the .NET world
with the System.Data, System.Data.Common, System.Data.OleDb, System.Data.SqlClient,
and System.Data.SqlTypes namespaces. Collectively, the System.Data.* namespaces are
often referred to as ADO.NET—a clear indication of its technical legacy.
In this chapter, we examine common programming tasks related to the storage and access of
data, primarily with relational databases.
We’ll begin by providing a brief introduction to ADO.NET and examining the several para-
digm changes it brings. Then, we discuss how to connect to a database, issue SQL commands
to a database, and work with stored procedures. Finally, we examine how to handle errors,
work with transactions, and examine relational schemas.
After reading this chapter, you should have a firm grasp of the following:
• Connecting to a database
• Querying a database for data, and then processing the data that has been returned
• Programming SQL commands in a transactional environment
Table 15.1 itemizes the classes and related structures that are covered in this chapter across the
referenced namespaces.
Data Storage and Access
723
CHAPTER 15
An Overview of ADO.NET
In ADO.NET, components are categorized into one of two categories: data manipulation and
access through direct communication to a database, and data manipulation and access against
in-memory copies of data (generated from a variety of different sources). Data providers sup-
ply the direct communication pipeline; they function as a data manipulation layer that sits
between your code and a specific underlying data source. The DataSet component is responsi-
ble for allowing in-memory representations of both data and data schema.
agnosticism, this is the provider you should use. Although it currently supports only OLE DB
AND ACCESS
Provider for SQL Server, Oracle, and Microsoft Jet, other OLE DB providers will be added as
they become available. This data provider fully supports transactions.
Working with the .NET Namespaces
726
PART II
Whereas the OLE DB Data Provider defines its connection class like this:
NotInheritable Public Class OleDbConnection
Inherits Component
Implements ICloneable, IDbConnection
The naming convention that is used by data providers for their specific connection class fol-
lows this format: <provider_tag><Connection>, in which <provider_tag> is the unique class
prefix shared by that particular provider’s classes. This naming standard is actually applied to
all the classes in a given data provider’s namespace—thus, we have SqlConnection versus
OleDbConnection, and SqlCommand versus OleDbCommand, and so on.
The IDbConnection interface supports the properties and methods shown in Tables 15.2 and
15.3. We look at the specific implementations of each of these later in the chapter when we
deal with code actually written for a selected data provider.
Data Storage and Access
727
CHAPTER 15
Command objects are responsible for encapsulating SQL statements and conveying them to a
data source for processing. Table 15.3 shows the properties and methods supported by the
IDbCommand interface.
15
DATA STORAGE
AND ACCESS
Working with the .NET Namespaces
728
PART II
Table 15.4 shows the properties and methods of the IDataReader interface.
Data adapters primarily exist to fill DataSet objects with data from a data source and act as the
link between in-memory data representations and a selected data source. The properties and
methods of the IDataAdapter interface are documented in Table 15.5.
Schema Representation
DataTable objects manifest their schema through their DataColumn objects (referenced from
the DataTable’s Columns property, which returns a DataColumnCollection instance). Column
objects will store their name and data type and can also contain “expression” formulas for
computing column values.
Data Representation
Inside of each data set, data is represented as DataRow objects. Each DataRow object will con-
form to a specific schema for a specific table; for this reason, you can’t directly create a
DataRow instance. It must be created using the NewRow method on a DataTable object. Each
DataRow object will specify a value for each DataColumn instance represented in a DataTable’s
schema.
Figure 15.1 shows how all the different ADO.NET building blocks interact with one another.
Data Storage and Access
731
CHAPTER 15
System.Data.OleDb
System.Data.SqlClient
.NET Data Provider Database
Connection
DataAdapter System.Data.Common
Command
DataReader
DataSet
System.Data
FIGURE 15.1
ADO.NET architecture.
Now that we have examined some of the core building blocks for ADO, let’s look at how to
exercise them through code. We will use the OLE DB Data Provider in most of the examples;
where significant or interesting items of note exist with the SQL Server Data Provider, we
cover those as well.
You can query and affect data stored in a database by two principle means: The first involves
SQL commands issued through a command object. The second involves populating and manip-
ulating data sets in memory. In the next two sections, we examine each of these methods.
Establishing a Connection
Using ADO.NET, how would you approach this relatively simple task? Normally, the first step
is to establish a connection to the desired database. For this example, we make the following
assumptions: the HR database we are interested in resides inside a Microsoft SQL Server 2000 15
database, on a server called HR-DB01. The database itself is called “dbHumanRes,” and the
DATA STORAGE
AND ACCESS
We also assume that a design goal requires us to use the OLE DB Data Provider instead of the
SQL Server Data Provider. That means that we will want to reference the System.Data.OleDb
namespace:
Imports System.Data
Imports System.Data.OleDb
Next, we need to create our connection string. We can assign this to a string variable that we
will then use to create the connection object:
Dim connStr As String
Note that the actual syntax and elements required for a connection string can and will change,
depending on the data source you are going after and on the particulars of the particular data-
base installation you are programming against. See the MSDN Platform SDK documentation
for the complete definition of OLE DB and native SQL Server connection strings.
After we have the connection string set up the way we need it, we can create our connection
object (in this case, an OleDbConnection object) by passing the string into the object’s con-
structor, like this:
Dim dbConn As OleDbConnection = New OleDbConnection(connStr)
Now, the only thing left to do is to actually open the connection. This is done with the Open
method:
bConn.Open()
Let’s leave the topic of error handling alone for the time being and assume that the connection
works. With a valid and working connection to the database, we can now create the SQL com-
mand and issue it through the open connection.
After setting the Command object’s CommandText property (which we have done
implicitly by supplying the command text in the constructor), and after setting the
OleDbCommand.CommandType property, we are ready to issue the command to the database.
There are three ways to actually issue the command to the database: with the ExecuteNonQuery
method, the ExecuteReader method, and the ExecuteScalar method. If we think back to the
definitions that we have provided for each of these (back in our discussion of Command objects
and the IDbCommand interface), we can see that only one option exists for us: the
ExecuteReader method. The ExecuteScalar method won’t work, because it is designed to
return only the first column of the first row in the resultset. The ExecuteNonQuery method
won’t work, because it won’t actually return a resultset; it simply returns a count of the num-
ber of rows affected by the issued command.
The ExecuteReader method will send our SELECT query through the associated database con-
nection object and create an OleDbDataReader instance to deal with the returning resultset:
Dim dataReader As OleDbDataReader = selectQry.ExecuteReader()
Normally, calling the ExecuteReader with no parameters is sufficient; it certainly does the job
that it was needed to do for this example. But an overloaded version of ExecuteReader exists, 15
which accepts a CommandBehavior enumeration value as a parameter. To see some of the
DATA STORAGE
AND ACCESS
options that are available when using this version of the ExecuteReader method, see Table
15.7, which documents the different values of the CommandBehavior enumeration. Note that
these CommandBehavior values can be combined bitwise.
Working with the .NET Namespaces
734
PART II
We can use two methods to extract information from the data reader: the Read method
advances the read cursor through the resultset record by record. When it reaches the end of the
resultset, it will return False (thus ending our While loop). Inside the While loop, we are call-
ing the GetString method off our dataReader object. The OleDbDataReader class adds quite a
few Get methods to the default roster of methods defined by the IDbDataReader interface.
Each of these Get methods is designed to return a resultset value for a specific column, in a
specific format. Because we know that we are going after a “last name” value with our query,
we are explicitly asking for values back from the resultset as strings. The zero that we pass
into the GetString method simply identifies the column that we want the data from (columns
are numbered with a zero-based system).
Table 15.8 identifies some of the Get methods supported by the OleDbDataReader class.
Data Storage and Access
735
CHAPTER 15
Now that you have seen a simple example of connecting to a database, issuing a SELECT
statement, and processing the returned data, we can look at a slightly more complicated sce-
nario through which we can explore the concepts of data updates.
15
Suggestions for Further Exploration
DATA STORAGE
➲ Cases can occur in which multiple resultsets are returned to a DataReader (consider a
AND ACCESS
command or stored procedure that issues two SELECT commands). For information on
how to move the DataReader from one resultset to another, investigate the
DataReader.NextResult method.
Working with the .NET Namespaces
736
PART II
FIGURE 15.2
Entity relationship diagram: employee and division tables.
We should also go ahead and define the full schema for both the employee table (shown in
Table 15.9) and the division table (shown in Table 15.10).
Suppose that we want to build a simple SQL statement that inserts the data for a new employee
into the employee table. Let’s assume, for instance, that we want to process the following
INSERT statement:
The first parameter provides the last name, the second parameter provides the first name, and
so on (per the schema discussed in Table 15.9), closing out with the ID of the division record
related to this employee (3).
Working with the .NET Namespaces
738
PART II
Issuing this INSERT statement involves some of the same steps as our previous example that
issued a SELECT statement against the database. We must first open a connection to the data-
base and then issue a command across that connection. Finally, we want to be able to respond
to any data returned from the database as a result of the SQL command.
Assuming that we have previously captured all the pertinent employee information in vari-
ables, we have a choice in this case. We can either build up a string containing the whole of
the SQL command, or we can build up a basic SQL command with placeholders for the actual
record values and then pass in these values in a second step using parameters.
Module Module1
Sub Main()
Dim dbConn As OleDbConnection
Dim connStr As String
Try
End Sub
End Module
To run this code sample, you will need to create your own database schema in a local SQL
Server database and change the connection string accordingly.
We have done a few things of note here. For one, because we are building up a SQL command
string by concatenating variables, we are using the StringBuilder class instead of the String
class to avoid performance penalties dealing with the immutable characteristic of String val-
15
ues. Second, we are using the ExecuteNonQuery method instead of the ExecuteReader method
DATA STORAGE
because we don’t expect our SQL to return data. The ExecuteNonQuery method returns a count
AND ACCESS
of the rows affected, providing us with a convenient way to determine if the insert worked as
expected. Notice that we have also implemented a rudimentary error handler.
Working with the .NET Namespaces
740
PART II
Module Module1
Sub Main()
Dim dbConn As OleDbConnection
Dim connStr As String
Try
insertQry.Parameters.Add(“lastname”, lastName)
insertQry.Parameters.Add(“firstname”, firstName)
insertQry.Parameters.Add(“mi”, middleInitial)
insertQry.Parameters.Add(“ext”, extension)
insertQry.Parameters.Add(“jobcode”, jobCode)
insertQry.Parameters.Add(“div”, divID)
End Sub
OLE DB .NET Data Provider will resolve these placeholders to actual values taken, in order,
from the Parameters collection. The SQL Server .NET Data Provider enables you to specify
parameters by name, thereby eliminating the need to add parameters to the Parameters collec-
tion in the exact order that they will be used in the SQL statement.
Working with the .NET Namespaces
742
PART II
First, the transaction is created from the connection object and assigned to an
OleDbTransaction object. Then, the transaction object is assigned to a command component
(represented here by the cmd object) so that each command issued is enrolled in the transac-
tion. Note that the BeginTransaction method accepts an IsolationLevel enumeration value
and a name for the transaction. Table 15.11 shows the different IsolationLevel enumeration
values.
The transaction object enables you to complete the transaction through its Commit method or
roll back any current changes in the transaction with its Rollback command.
In the code shown in Listing 15.3, the intent is to insert all three divisions into the division
table or fail the insert operation altogether. Notice that we leverage the Try Catch block to
determine whether we should issue a Commit command or a Rollback command.
Data Storage and Access
743
CHAPTER 15
Module Module1
Sub Main()
Dim dbConn As OleDbConnection
Dim connStr As String
Dim trans As OleDbTransaction
Dim rowsAffected As Integer
Dim query As String
Try
Console.WriteLine(“Starting transaction...”)
‘First insert
query = “INSERT INTO division VALUES (‘Operations’)”
insertQry.CommandType = CommandType.Text
insertQry.CommandText = query
insertQry.ExecuteNonQuery()
‘Second insert
15
query = “INSERT INTO division VALUES (‘Administration’)”
DATA STORAGE
AND ACCESS
insertQry.CommandType = CommandType.Text
insertQry.CommandText = query
insertQry.ExecuteNonQuery()
Working with the .NET Namespaces
744
PART II
Console.WriteLine(“Transaction committed.”)
Finally
Console.WriteLine(“Hit the ENTER key to exit...”)
Console.ReadLine()
dbConn.Close()
End Try
End Sub
End Module
VALUES
( @id,
@first_name,
@middle_initial,
@last_name,
@extension,
@job_code,
@division_id)
This SQL code can be directly compiled as a stored procedure in SQL Server. Notice that the
insert_employee stored procedure accepts seven input parameters. Because parameters are
parameters in ADO.NET, regardless of whether we are talking about plain SQL commands or
stored procedures, our design pattern established with Listing 15.2 doesn’t need to change
much. Listing 15.5 shows how we can rework our previous parameterized query code to work
with a stored procedure.
To help illustrate the differences and similarities between the OLE DB Data Provider and the
SQL Server Data Provider, we have written the code this time around using the components 15
from the System.Data.SqlClient namespace and not the System.Data.OleDb namespace.
DATA STORAGE
AND ACCESS
Working with the .NET Namespaces
746
PART II
Module Module1
Sub Main()
Dim dbConn As SqlConnection
Dim connStr As String
Try
Finally
dbConn.Close()
End Try
End Sub
End Module
The parameter statements look a bit different from our previous example, as well. Notice that
we are free to use named parameters when working with the SqlClient namespace. That is,
we can actually identify a parameter by its name (as defined and declared inside the target
stored procedure). We don’t have to pass parameters in along a fixed order. We are also speci-
fying the data type and size. These should match the parameter declarations in the stored pro-
cedures as well. Table 15.12 shows the possible SqlDbType enumeration values.
Stored procedures also have the capability to place values in output parameters.
by SQL Server. Because the primary key ID column for the employee table is defined as an
identity column, the value we see returned here should be the assigned ID value for the
employee record that we have just inserted.
VALUES
( @id,
@first_name,
@middle_initial,
@last_name,
@extension,
@job_code,
@division_id)
With this stored procedure in mind, let’s consider the changes that we would have to make to
our applet. First, we would need another addition to our parameters collection to account for 15
the new @new_id parameter:
DATA STORAGE
AND ACCESS
insertQry.Parameters.Add(“@new_id”, SqlDbType.Int, 4)
There is a problem here: The Add method doesn’t allow you to specify a “direction” to the para-
meter. In other words, there is no way for you to indicate that the @new_id parameter is not an
Working with the .NET Namespaces
750
PART II
input parameter but is instead an output parameter. However, a property is available on the
SqlParameter and OleDbParameter classes that enables you to specify a direction. The
Direction property returns or accepts a ParameterDirection enumeration value. All the
possible ParameterDirection values are listed in Table 15.13.
The Parameters.Add method actually returns a parameter instance (in this case,
SqlParameter). With all our previous parameter additions, we have not worried about “catch-
ing” this returned parameter object. Here, because we need to set the Parameter.Direction
property, we do need to worry about it. The code change is simple:
Dim parm As SqlParameter
parm = insertQry.Parameters.Add(“@new_id”, SqlDbType.Int, 4)
parm.Direction = ParameterDirection.Output
To examine the contents of the output parameter after the stored procedure has been executed,
you would just reference the Value property of the appropriate SqlParameter object like this:
Console.WriteLine(“Assigned ID: {0}”, _
insertQry.Parameters(“@new_id”).Value)
Stored procedures also have the capability to return values outside the parameters interface
with a Return statement.
VALUES
( @id,
@first_name,
@middle_initial,
@last_name,
@extension,
@job_code,
@division_id)
Even though return values aren’t technically treated as part of the parameters collection by
SQL Server, they are lumped into that category by the SqlClient namespace. In fact, they are
treated the same way as all other parameters with the exception of their Direction property,
15
which should be set to ParameterDirection.ReturnValue:
DATA STORAGE
AND ACCESS
NOTE
The OLE DB .NET Data Provider requires that any return parameters be added to
the parameters collection first. The SQL Server provider does not suffer from this
limitation.
You can examine the return value in an identical fashion to the way that we examined the con-
tents of the output parameter:
If insertQry.Parameters(“ext_flag”).Value <> 0 Then
Console.WriteLine(“Phone needs configuration”)
End If
Module Module1
Sub Main()
Dim dbConn As SqlConnection
Dim connStr As String
Try
insertQry.CommandType = CommandType.StoredProcedure
Finally
dbConn.Close()
End Try 15
DATA STORAGE
End Sub
AND ACCESS
End Module
Working with the .NET Namespaces
754
PART II
DataTableCollection
DataSet
DataTable DataTable DataTable
DataRelation
DataRelation
DataRowCollection DataRowCollection
FIGURE 15.3
Composition of a DataSet object.
Data Storage and Access
755
CHAPTER 15
Creating a DataSet
To create a DataSet, you first physically instance a new DataSet object:
Dim hrDataSet As DataSet = New DataSet(“dbHumanResources”)
The constructor accepts a name for the DataSet object (this is optional; if you don’t provide a
name, the runtime will assign NewDataSet as its name).
Notice that we are calling the Tables.Add method to create a new DataTable instance. The
Tables property on the DataSet class is used to manage the collection of tables (this is actu-
ally a DataTableCollection class instance). The Add method returns a new instance of a
DataTable object that we have assigned into our employee object.
If that code looks confusing to you, remember that Visual Basic .NET allows you to initialize
AND ACCESS
elements of an array using the braces {} after the array name. We are just creating a new array
of type DataColumn and assigning the “id” DataColumn object as its first element.
Working with the .NET Namespaces
756
PART II
empRow(“first_name”) = “Sandy”
empRow(“middle_initial”) = “K”
empRow(“last_name”) = “Cartmann”
.
.
.
employee.Rows.Add(empRow)
Listing 15.9 consolidates all the DataSet code we have talked about up to this point. In this
console application, we build up a DataSet that matches the human resource database we have
been using for our coding examples. After the DataSet is built, a few rows of data are added
into the employee and division table and are redisplayed to the screen for confirmation.
Module Module1
Dim id As System.Int32 = 1 ‘identity seed (used for primary keys)
Data Storage and Access
757
CHAPTER 15
Console.WriteLine()
Console.WriteLine()
divID)
AND ACCESS
Console.WriteLine()
Finally
Console.WriteLine(“Hit the ENTER key to exit...”)
Console.ReadLine()
End Try
End Sub
End Sub
End Sub
id = id + 1
AND ACCESS
empDiv(“id”) = id
empDiv(“name”) = name
Working with the .NET Namespaces
760
PART II
‘Pass the id that was used back out through the function
‘signature
AddDivisionRecord = id
End Function
Console.WriteLine(rowOut)
Next
Console.WriteLine()
Console.WriteLine()
Next
End Sub
Data Storage and Access
761
CHAPTER 15
For a complete description of each of these constructors, consult the MSDN documentation;
for now, just know that the constructor is typically used to tie the DataAdapter to a specific
SQL command, whether that exists as a string SQL statement or as a SqlCommand object. The
following code creates a SqlDataAdapter object that references our employee table:
Dim hrAdapter As SqlDataAdapter = _
New SqlDataAdapter(“SELECT * FROM employee”, dbConn)
By supplying a SQL command and a connection, the adapter is made aware of the underlying
data source and subset of data with which we want to populate the DataSet object. To actually
transfer the data from the data store into the DataSet, we use the DataAdapter.Fill method.
The Fill method also has a variety of overloaded variants; in this example, we are supplying it
with a DataSet instance and a table name:
workDA.Fill(workDS, “employee”)
Calling the Fill method will immediately cause the DataSet to conform to the data selected
from the data source. If columns or rows have been added or changed, those changes will be
reflected inside the DataSet object.
NOTE
The process of mapping actual data store elements to DataSet elements can be a
tedious chore for mid- to large-size databases. Thankfully, Visual Studio .NET supports
a way to do this in a drag-and-drop fashion. Consult the MSDN Visual Studio docu-
mentation (specifically, the topic “Mapping Data Source Tables to Dataset Tables”) for
more information on this and other aids present in the IDE for DataSet design and
development.
The Update method is overloaded to enable you to update data for a specific DataRow object, a
DataTable object, or even across an entire DataSet.
FIGURE 15.4
The DatabaseExplorer application.
Listing 15.10 details the DatabaseExplorer. This application starts with the typical form setup
code. We have created an OleDbConnection object of global scope to maintain a constant con-
nection to the targeted database.
End Sub
Me.TextConnStr.TabIndex = 0
Me.TextConnStr.Text = _
“Provider=SQLOLEDB;Data Source=Atreides;Integrated _
Security=SSPI;Initial Catalog=N” & ”Northwind”
Working with the .NET Namespaces
766
PART II
Me.ColumnHeader4})
AND ACCESS
End Sub
#End Region
Data Storage and Access
769
CHAPTER 15
End Sub
Try
Me.Cursor.Current = System.Windows.Forms.Cursors.Default
End Try
End Sub
ListView1.Items.Clear()
AND ACCESS
itm.SubItems.Add(col.DataType.ToString)
itm.SubItems.Add(col.MaxLength)
End Try
End Sub
End Function
Dim i As Integer
Dim itm As ListViewItem
Dim colsSet As Boolean
Dim dataReader As OleDbDataReader
Dim cmd As OleDbCommand
Try
dataReader = cmd.ExecuteReader()
colsSet = False
For i = 0 To dataReader.FieldCount - 1
If Not colsSet Then
ListView2.Columns.Add(“ “, 100, _
HorizontalAlignment.Left)
End If 15
DATA STORAGE
If i = 0 Then
AND ACCESS
itm = ListView2.Items.Add(dataReader.GetValue(i))
Else
itm.SubItems.Add(dataReader.GetValue(i))
End If
Working with the .NET Namespaces
772
PART II
colsSet = True
End While
Me.Cursor.Current = System.Windows.Forms.Cursors.Default
Finally
dataReader.Close()
End Try
End Sub
Button2.Enabled = True
ListBox1.Enabled = True
ListColumns(itm.ToString)
End Sub
Summary
The ADO.NET components offer a variety of mechanisms for data access from Visual Basic
.NET. In this chapter we have examined both .NET Data Providers and DataSet components.
Specifically, we covered the following:
• Connecting to a database
• Issuing queries against a database connection and processing the resultsets
• The differences between direct database access through data providers and indirect
access through data cached in a DataSet
• Creating a DataSet from scratch; adding tables, columns, relationships, and constraints
to build our own in-memory database
• Executing stored procedures and dealing with input, output, and return parameters 15
• Placing queries inside transactions
DATA STORAGE
AND ACCESS
Directory Services CHAPTER
16
IN THIS CHAPTER
• Key Classes Related to Directory Services 776
Microsoft Windows 2000 first introduced the concept of Active Directory (AD) to the
Microsoft product family. With Active Directory on the scene, Windows 2000 was capable of
providing a consistent and standard way of storing and referencing network resources. This
chapter focuses on the System.DirectoryServices namespace, which consists of classes that
encapsulate and abstract Active Directory objects and operations. This namespace will allow
you to query and modify directory entries through a small set of classes and enumerations. For
instance, you can examine a directory object’s properties, search for specific objects in the
directory, and walk up or down a directory tree.
We’ll start by examining the purpose of a directory and how directories are organized. Then we
will move into simple programming tasks with the System.DirectoryServices classes, and
we’ll wrap up the chapter by explaining some more advanced directory query functions avail-
able to .NET programmers.
By the end of this chapter, you should be able to:
• Describe what a directory is
• Understand the key components of Active Directory
• Understand the roles of the Active Directory Services Interface (ADSI) and the
Lightweight Directory Access Protocol (LDAP)
• Bind to a specific object in a directory, query its attributes, and change them
• Create collections of directory objects that represent a portion of a directory tree
• Examine a directory’s schema information
• Query a directory tree
DIRECTORY
Searching
SERVICES
SearchResultCollection Holds any directory entries returned by the
DirectorySearcher class.
SortOption Is a utility class used to specify how search results
are sorted.
ResultPropertyCollection Holds the list of directory entry properties for
each object returned by a search using the
DirectorySearcher class.
ResultPropertyValueCollection Holds all the property values for each property in a
ResultPropertyCollection instance.
Accessing Directory Entries
DirectoryEntry Represents an object in an Active Directory.
PropertyCollection Stores a DirectoryEntry’s properties.
PropertyValueCollection Holds the actual values for entries in the
PropertyValueCollection class.
What Is a Directory?
A directory is really a database specifically designed to store information about different
resources that reside on a network. Directories have actually been around for a while—for
instance, the Banyan VINES NOS (network operating system) incorporated a directory service
called StreetTalk back in the late 1980s. A directory is supposed to make life easier on those
who need to administer networks or access network resource listings in a structured way.
Network directories allow you to do all sorts of things that would otherwise be impossible (or
at least extremely difficult). Take the example of locating a printer on a network. On a large,
enterprise-scale network, thousands of printers might be installed—some type of facility is
needed to organize these printers into a coherent list so that a user new to the network can
quickly locate the one to which he wants to print. A network directory helps in this case by
Working with the .NET Namespaces
778
PART II
providing a one-stop shop for the information. By simply querying one data store (the direc-
tory), you can get to all the printer information on the network rather than querying separate
print servers or domain servers, all potentially located in different offices, buildings, states, or
even countries! By cataloging pertinent printer information (Is it color or black-and-white?
Inkjet or laser? Is it in my current building, floor, and area?), a user can simply browse the
directory for the right printer. Because directories are organized hierarchically, just like files
and folders in a file system, an otherwise complex process becomes quite simple.
This is just one scenario that shows the potential benefit of network directories. There are
many others, such as providing a central place to store user information, implementing a secu-
rity layer to protect network resources, and so on.
Active Directory is simply Microsoft’s implementation of a network directory service. Many
of the Windows 2000 networking functions are centralized inside Active Directory. AD pro-
vides a secure storage for such networking resources as logins, printing services, remote con-
nections, and so on.
NOTE
As we will see later in this chapter, both LDAP and ADSI are valid and supported
interfaces in the Microsoft Active Directory Service, and they are supported with the
.NET Framework Class Library.
DIRECTORY
SERVICES
level resources (such as Administrator). These are considered containers and leaf nodes,
respectively. Drawing on the file system analogy, you would say that containers are similar to
file folders, and leaf nodes are similar to files.
root
FIGURE 16.1
A directory tree.
NOTE
In Microsoft’s Active Directory, the class responsible for schema definitions is called
schemaClass.
Classes
Your knowledge of classes from earlier discussions in this book can be put to good use with
directory service programming as well. A class in a directory service is a template for an
object—it defines what attributes an object can have. In this way, directory objects are
Working with the .NET Namespaces
780
PART II
instances of a specific class. The classes are defined in the schema section of the directory tree,
and each class is capable of specifying which attributes an object can have and which attrib-
utes it must have.
Attributes
Attributes are the properties of a directory object. Each object has a set of name-value pairs
that constitute its attribute list. For example, each object in a particular directory tree typically
has a common name attribute called Common-Name. This attribute has a value that identifies the
object’s name. A user object might have the user’s name assigned to its Common-Name; a printer
object might have its manufacturer and type as its Common-Name value. The exact set of possi-
ble attributes for each object is defined by the class that provides the template for that object.
Active Directory ships with a default schema, but there is nothing stopping developers or oth-
ers from adding to this schema, thus defining their own classes and attributes.
NOTE
Similar to the way that you can relate database entities to one another through keys,
attributes can be used to link directory objects together as well. For more informa-
tion, see the Active Directory Platform SDK documentation.
Although the schema for Active Directory is extensible, the actual types of attributes supported
are not; they are hard-coded into the system. Table 16.2 shows a partial list of the different
attribute data types currently supported by Active Directory.
DIRECTORY
A value consisting of an OctetString that represents a
SERVICES
NTSecurityDescriptor
Windows 2000 security descriptor.
NumericString A string value that consists entirely of numerical digits.
OctetString A value consisting of an array of bytes (most commonly used
to store binary data).
PrintableString A value consisting of a string of characters. Each character is
a valid, printable character.
Sid A security identifier value.
GeneralizedTime A time value (string format).
UTCTime A time value (string format).
DN A DN (distinguished name) value in string format.
DNWithBinary A DN value with a binary value, in OctetString format.
DNWithString A DN value with a string value.
Class Types
Each class in a directory’s schema is either a structural class, an abstract class, or an auxiliary
class.
• Structural classes are meant to encapsulate the actual content of the directory. In other
words, they are the only type of class that can have actual instances in the directory.
• Abstract classes are abstract in the sense that you cannot directly create an object from
them. They exist for other structural classes to inherit from in a very similar vein to
abstract classes in the Framework Class Library.
• Auxiliary classes cannot be used for direct object creation either. You can think of them
as being similar to interfaces in the class library: They can be included in another class’s
definition as a form of “code reuse” (although, in this case, we are reusing schema
definitions).
NOTE
The actual term for the entire tree of containers is the Directory Information Tree,
often abbreviated as DIT.
Working with the .NET Namespaces
782
PART II
The GUID
Each object is also assigned a globally unique identifier. GUID creation and assignment is
managed by the Active Directory service and is never changed after it is assigned.
Directory Services
783
CHAPTER 16
Typically, simply moving an object from one container to another, you would, by definition, be 16
changing its DN. And you certainly can change an object’s RDN. GUIDs are the only object
identifiers that are not subject to change—GUIDs alone are static and never duplicated.
DIRECTORY
SERVICES
An object’s GUID is referenced by the objectGUID attribute.
ConsultingCo
Users Printers
FIGURE 16.2
A directory tree.
Working with the .NET Namespaces
784
PART II
A provider is the specific code implementation that is providing access to the directory.
Common examples are LDAP, WinNT, and NDS. Each provider implements the path syntax in
its own way.
As an example, an LDAP-specified path to the user DavidG might look like this:
LDAP://CN=DavidGraham,OU=OfficeMI,OU=Users,OU=OfficeMI,DC=ConsultingCo,DC=com
At this point, you would have a valid reference to the directory entry pointed to by the path,
with all its attendant properties populated.
DIRECTORY
SERVICES
you use the DirectoryEntries.Find method by passing it the name of the object that you want:
Dim dirNodes As DirectoryEntries
.
.
.
Dim myNode As DirectoryEntry
myNode = dirNodes.Find(“DavidG”)
The DirectoryEntries object also gives you the capability to identify exactly which types of
objects are contained inside its collection. It does this through its SchemaFilter property. This
property works in conjunction with another helper class, SchemaNameCollection, to show only
those directory entries that are based on the schemas contained in the SchemaNameCollection.
In other words, if an entry was not created from one of the identified entries in
SchemaNameCollection, it won’t show up in the DirectoryEntries collection. For instance,
you could display the names of all the represented object types by writing code like this:
Dim dirNodes As DirectoryEntries
Dim schemaNames As SchemaNameCollection
Dim schemaName As Object
.
.
.
schemaNames = dirNodes.SchemaFilter
NOTE
If the SchemaNamesCollection is empty, the DirectoryEntries collection is considered
to contain all types of entries.
NOTE
Keep in mind that the actual, valid list of attributes for an object can be dynamic.
That is, they are based off its schema class, which can define any number of attributes
and valid values. It is because of this capability to extend a directory entry’s possible
attributes that these are implemented in a name/value pair collection instead of stati-
cally as methods on the DirectoryEntry class.
This outwardly convoluted process turns out to be simple when considered at the code level.
Listing 16.1 retrieves a directory object’s attributes and displays them to the screen.
Module Module1
Sub Main()
Const path As String = “LDAP://RootDSE”
Try
Dim dirEntry As DirectoryEntry = New DirectoryEntry(path)
Dim indx As Long
Dim propCollection As PropertyCollection
Dim propValueCollection As PropertyValueCollection
Dim prop As Object
Dim propVal As Object
DIRECTORY
SERVICES
‘These loops will iterate through the name/value pairs
For Each prop In propCollection.PropertyNames
Next
End Module
Figure 16.3 shows the output run against a directory path of LDAP://RootDSE.
FIGURE 16.3
Directory query output.
Working with the .NET Namespaces
788
PART II
To change the path, simply change the path constant in Sub Main.
NOTE
You also can assign values to an object’s properties. The assignment is cached until
you commit it using the DirectoryEntry.CommitChanges method.
Object Security
By default, all the actions that you perform against a directory are done under the guise of the
currently logged-in user. However, you can choose to explicitly identify a username and pass-
word when accessing directory entries. The UserName and Password properties (which are both
strings) allow you to specify another set of credentials, other than those of the logged-in user,
for requesting a bind to a directory object.
You also can query or set the type of authentication that the Active Directory will perform. The
DirectoryEntry.AuthenticationType returns an AuthenticationTypes value (documented
in Table 16.3). These authentication methods most likely will look unfamiliar to all but the
most experienced ADSI developer. You likely will need to seek out in-depth information on the
machinations of Active Directory before you feel comfortable with their actual uses.
DIRECTORY
write operations to a directory object. For more information on how the .NET classes deal with
SERVICES
this, consult the .NET Platform SDK documentation on the CommitChanges method, the
RefreshCache method, and the UsePropertyCache property, all available off the
DirectoryEntry class.
Although not written specifically for Visual Basic developers, Gil Kirkpatrick’s book Active
Directory Programming offers a thorough treatment of Active Directory and ADSI program-
ming concepts and techniques.
Searching a Directory
So far, we have touched on how to bind to objects inside a directory without worrying about
how to find a specific object in the first place. In this section, we review the namespace sup-
port for querying directories for objects and dealing with the results.
It should be noted that the classes in this section—and, indeed, the entire concept of querying
against an Active Directory—is really only directly supported by the LDAP provider.
This property is a string that you can set using LDAP’s own search string syntax. We won’t
get into the specifics of this syntax, but the .NET Framework SDK does offer up some basic
information:
• The filter string must begin and end with a parenthesis.
• The search criteria can be grouped and evaluated with Boolean logic by using the & (for
“and”) and the | (for “or”) operators.
• The search criteria follows the general pattern of name=value.
If you want to search for any user in the AD with the last name of Powers, you could set the
Filter property like this:
search.Filter = “(&(objectClass=user)(lastName=Powers))”
Working with the .NET Namespaces
790
PART II
The SearchRoot property tells the class which DirectoryEntry to use as its starting point.
Supplying it with the actual tree root, for instance, will cause the directory to be searched from
the very top. You can specify any valid DirectoryEntry as the start of the search, regardless of
how deep in the tree it sits.
The SearchScope property uses the SearchScope enumeration to identify how far the search
will extend. The possible values for the SearchScope enumeration are listed in Table 16.4.
DIRECTORY
SERVICES
search.Sort = sort
If you don’t select any properties at all, the search will default to return only the name and path.
NOTE
You also have the capability to not return any properties that don’t have values
assigned to them. Use the DirectorySearcher.PropertyNamesOnly property. Set it to
True to indicate that the search results should contain only property names for those
properties that have actual values assigned to them. Set it to False to indicate that
the search results should contain all the property names, regardless of whether they
have assigned values.
The SearchResult class is nearly identical to the DirectoryEntry class. The difference is that
the SearchResult class is populated with only those properties returned.
Figure 16.4 shows the application in action; here we have enumerated the user nodes for a PC
sitting on an AD Windows 2000 network.
FIGURE 16.4
The DirectoryBrowser application.
Keep in mind that you must be connected to a network that actually can see an Active
Directory for this application to work. Also, the System.DirectoryServices DLL is not nor-
mally referenced inside Visual Studio. You will need to add this to your project references for
the code references to work.
Code Walkthrough
This application makes extensive use of the Children property, in a recursive manner, to dis-
play the entire directory tree. After building up the tree, you can select any one of the nodes by
clicking on it. This causes the program to attempt to bind to that node and shows you all its
properties in addition to all its attribute name/value pairs.
The application starts off with all the form setup. Beyond just the general control instantiation
and configurations, nothing special is going on here other than the Imports statement, which
references the System.DirectoryServices namespace.
Directory Services
793
CHAPTER 16
DIRECTORY
SERVICES
Public Class Form1
Inherits System.Windows.Forms.Form
End Sub
DIRECTORY
SERVICES
Me.TextBoxSchemaClass.Location = New System.Drawing.Point(100, 108)
Me.TextBoxSchemaClass.Name = “TextBoxSchemaClass”
Me.TextBoxSchemaClass.ReadOnly = True
Me.TextBoxSchemaClass.Size = New System.Drawing.Size(176, 20)
Me.TextBoxSchemaClass.TabIndex = 7
Me.TextBoxSchemaClass.Text = “”
‘
‘StatusBar1
‘
Me.StatusBar1.Location = New System.Drawing.Point(0, 347)
Me.StatusBar1.Name = “StatusBar1”
Me.StatusBar1.Size = New System.Drawing.Size(554, 20)
Me.StatusBar1.TabIndex = 4
‘
‘TreeView1
‘
Me.TreeView1.ImageIndex = -1
Me.TreeView1.Location = New System.Drawing.Point(8, 60)
Me.TreeView1.Name = “TreeView1”
Me.TreeView1.SelectedImageIndex = -1
Me.TreeView1.Size = New System.Drawing.Size(220, 276)
Me.TreeView1.TabIndex = 3
‘
‘Button1
‘
Me.Button1.Location = New System.Drawing.Point(148, 32)
Me.Button1.Name = “Button1”
Me.Button1.Size = New System.Drawing.Size(80, 20)
Me.Button1.TabIndex = 2
Me.Button1.Text = “&Show Nodes”
‘
‘TextBoxPath
‘
Me.TextBoxPath.Location = New System.Drawing.Point(68, 72)
Me.TextBoxPath.Name = “TextBoxPath”
Me.TextBoxPath.ReadOnly = True
Me.TextBoxPath.Size = New System.Drawing.Size(208, 20)
Me.TextBoxPath.TabIndex = 5
Me.TextBoxPath.Text = “”
‘
‘TextBoxName
‘
Me.TextBoxName.Location = New System.Drawing.Point(68, 48)
Working with the .NET Namespaces
796
PART II
DIRECTORY
SERVICES
‘
Me.Label4.Location = New System.Drawing.Point(16, 28)
Me.Label4.Name = “Label4”
Me.Label4.Size = New System.Drawing.Size(48, 16)
Me.Label4.TabIndex = 0
Me.Label4.Text = “GUID:”
‘
‘Label2
‘
Me.Label2.Location = New System.Drawing.Point(16, 52)
Me.Label2.Name = “Label2”
Me.Label2.Size = New System.Drawing.Size(48, 16)
Me.Label2.TabIndex = 2
Me.Label2.Text = “Name:”
‘
‘TextBoxRoot
‘
Me.TextBoxRoot.Location = New System.Drawing.Point(40, 8)
Me.TextBoxRoot.Name = “TextBoxRoot”
Me.TextBoxRoot.Size = New System.Drawing.Size(188, 20)
Me.TextBoxRoot.TabIndex = 1
Me.TextBoxRoot.Text = “WinNT://acclaran-dgvwad”
‘
‘Label1
‘
Me.Label1.Location = New System.Drawing.Point(8, 12)
Me.Label1.Name = “Label1”
Me.Label1.Size = New System.Drawing.Size(32, 16)
Me.Label1.TabIndex = 0
Me.Label1.Text = “Path:”
‘
‘Form1
‘
Me.AutoScaleBaseSize = New System.Drawing.Size(5, 13)
Me.ClientSize = New System.Drawing.Size(554, 367)
Me.Controls.AddRange(New System.Windows.Forms.Control() _
{Me.GroupBox1, Me.StatusBar1, Me.Button1, Me.TreeView1, _
Me.TextBoxRoot, Me.Label1})
Me.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog
Me.Icon = CType(resources.GetObject(“$this.Icon”), System.Drawing.Icon)
Me.MaximizeBox = False
Me.MinimizeBox = False
Me.Name = “Form1”
Me.Text = “Active Directory Browser”
Me.GroupBox1.ResumeLayout(False)
Working with the .NET Namespaces
798
PART II
End Sub
#End Region
This is the button click event that reacts to a user clicking the Show Nodes button. This event
merely calls the OpenRoot subroutine.
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As _
System.EventArgs) Handles Button1.Click
Cursor.Current = System.Windows.Forms.Cursors.WaitCursor
OpenRoot(TextBoxRoot.Text)
Cursor.Current = System.Windows.Forms.Cursors.Default
End Sub
This application really consists of two macro-level features. The first one, represented by the
following routine called OpenRoot, is responsible for taking the entered directory path and dis-
playing all its containers and objects in a hierarchical fashion inside the TreeView control.
Each item in the ListView control is named after the directory entry’s name, and the item Tag
is set to the Path property of the directory entry.
Public Sub OpenRoot(ByVal root As String)
Dim myEntry As DirectoryEntry
Dim rootNode As TreeNode
Dim entry As DirectoryEntry
Try
TreeView1.Nodes.Clear()
‘We hold the path in the Tag property of the TreeView item;
‘this will be used to actually bind to the node and view its
‘properties
newNode.Tag = myEntry.Path
DIRECTORY
SERVICES
‘Refresh the TreeView just in case
TreeView1.Refresh()
End Sub
The AddNodes routine is meant to be called recursively by the OpenRoot routine. It accepts a
TreeNode instance and a DirectoryEntry instance, and it takes care of both querying the
directory entry and writing its information as TreeView nodes.
Public Sub AddNodes(ByVal tNode As TreeNode, ByVal entry As DirectoryEntry)
Dim childEnumerator As IEnumerator
Dim newNode As TreeNode
Dim newEntry As DirectoryEntry
Me.Refresh()
‘Call into this routine again with the new tree view node
‘and the new directory entry object (need to find its
‘children as well)
AddNodes(newNode, newEntry)
End While
End Sub
End Sub
End Sub
End Sub
The AfterSelect event fires when you click on one of the nodes in the TreeView control. It
fires off the BindToNode routine.
Private Sub TreeView1_AfterSelect(ByVal sender As System.Object, ByVal e As _
System.Windows.Forms.TreeViewEventArgs) Handles TreeView1.AfterSelect
Cursor.Current = System.Windows.Forms.Cursors.WaitCursor
BindToNode(TreeView1.SelectedNode.Tag)
Directory Services
801
CHAPTER 16
DIRECTORY
SERVICES
The BindToNode routine represents the second largest chunk of functionality in this applica-
tion. It is responsible for binding to an indicated DirectoryEntry instance by using the path to
that entry (previously stashed in the ListView item’s Tag property). After binding to the entry,
this routine then displays its class properties and its attribute list to the form.
Public Sub BindToNode(ByVal nodePath As String)
Try
Dim dirEntry As DirectoryEntry = New DirectoryEntry(nodePath)
Dim indx As Long
Dim propCollection As PropertyCollection
Dim propValueCollection As PropertyValueCollection
Dim prop As Object
Dim propVal As Object
Dim itm As ListViewItem
ListView1.Items.Clear()
itm = ListView1.Items.Add(prop.ToString)
Next
Working with the .NET Namespaces
802
PART II
End Sub
End Class
Summary
Through a short series of simple classes, the System.DirectoryServices namespace is capa-
ble of exposing a rich set of features for programming Active Directory applications. In this
chapter, we investigated the classes of the DirectoryServices namespace and learned how to:
• Bind to a directory object using the DirectoryEntry class
• Examine a directory object’s ancestors and children in a directory tree by using the
Parent and Children properties on a DirectoryEntry object
• View and change a directory object’s attribute list by using the PropertyCollection and
PropertyValueCollection classes
17
IN THIS CHAPTER
• COM+ Services 806
• Events 838
COM+ programming is still relevant in a .NET world. The services that COM+ offers enter-
prise-level application developers still apply to components written with .NET code. VB devel-
opers, especially, should be thrilled to know that .NET now provides full access to the services
of COM+ from their favorite language. If you write .NET enterprise-level applications, you
will want to take advantage of COM+ services.
A lot of misinformation and misunderstanding surround this one technology. For some reason,
COM+ and its predecessor, MTS, carry a weight of complexity and are considered advanced
concepts by many VB programmers. This is unfortunate because the technology has tended to
be misused—or not used—for these reasons. In reality, COM+ exists to make developers’ lives
easier. With COM+, things such as transaction processing, loosely coupled events, and object
pooling become extremely simple. Without COM+, many applications would suffer perfor-
mance and scalability issues, or many developer hours would be wasted creating services simi-
lar to those that COM+ provides.
This chapter does not explain the inner workings of COM+. It is intended for developers who
are familiar with COM+ programming in VB and need to understand how to create serviced
components with .NET and access the new features .NET programming exposes to VB develop-
ers. Those new to COM+ development should pay close attention to the chapter content but will
also want to make sure they follow up on a number of the “For Further Exploration” sugges-
tions. The chapter’s principal focus is on the .NET namespaces System.EnterpriseServices.
After reading this chapter, you should be able to do the following:
• Understand when, where, and how to use COM+ services in your applications
• Access the services of COM+ from .NET managed code
• Register your objects with the COM+ catalog
• Use COM+ role-based security
• Configure your components to take advantage of object pooling and Just-in-Time (JIT)
activation
• Work with the shared property manager
• Execute Automatic Transactions with COM+ and the Distributed Transaction
Coordinator (DTC)
• Incorporate loosely coupled events into your application
• Work with objects asynchronously
COM+ Services
The primary advantage COM+ provides to your application is the capability to share resources.
This has a direct, positive impact on the scalability of your system. Whether you are creating
Accessing COM+ Services
807
CHAPTER 17
applications for managing inventory or facilitating e-commerce, you can take advantage of the
COM+ services to ease your burden when creating your middle tier.
NOTE
This chapter focuses squarely on COM+ as it exists in Windows 2000. Windows XP will
introduce new services inside of COM+ 1.5.
17
Introduction
COM+ SERVICES
COM+ handles resource-management tasks such as thread allocation, object pooling, and acti-
ACCESSING
vation. It provides services for managing security, protecting data, and executing loosely cou-
pled events. These services directly relate to your application’s capability to scale and to the
amount of code you will need to write. First, scalability is built into COM+. Provided you
design your application correctly, COM+ will manage your resources efficiently and increase
your capability to support requests.
COM+ also offers services such as queued components that allow your application to continue
to process should blocking occur or in the case when a database goes down. Transaction pro-
cessing becomes rudimentary using the COM+ transaction manager.
Components that take advantage of COM+ are said to be serviced components. That is, they
are managed by and take advantage of services of COM+.
Practical Application
Of course, if your application is currently using COM+ effectively, you will want to continue
to do so. The key point for these applications (provided they were written in VB) is that they
now can take full advantage of the COM+ services. You will want to port your most-used com-
ponents over to objects written in .NET that take advantage of COM+ services (also called
serviced components) immediately.
For applications that currently exist and that do not take advantage of COM+, you must con-
sider your application’s state-management strategy before jumping into COM+. The best-
performing application architecture for COM+ is a stateless design. This design dictates that
your COM+ components do not retain state information between method calls. This flies in the
face of many OO design principles; however, it is one of those real-world trade-offs for
increased efficiency. COM+ cannot manage your components effectively if you maintain state
between method calls. To be effective, COM+ must be able to use your objects that sit idle
between method calls. If your object is holding state waiting for user interaction, it is simply
eating an expensive resource on the server without providing real value.
Real-World .NET Programming
808
PART III
When creating new applications, you should take a long look at COM+. We suggest examining
the application’s requirements in relation to the services provided by COM+. Can you see the
need for any or all of these services in the application? If the answer is yes, the application is a
strong candidate for COM+. Another question to ask is, “How long will it take to create the
service in the absence of COM+?” Don’t forget to take into account support for the additional
code.
COM+ SERVICES
you to initialize information such as a data-
ACCESSING
base connection string at the time of object
creation. COM+ provides this service to
save you from having to store this informa-
tion within the code and thus recompile
whenever configuration information
changes.
Object Pooling Object Pooling is a service that places (or
pools) objects into memory when requested.
An object that is accessed often will have a
larger pool of cached objects. When an
object is released, it is put back into the pool
to be used again without full instantiation.
This saves the overhead of re-creating
objects at every request.
Queued Components Queued Components is a service that
enables you to create and execute objects
asynchronously, as if they were simply mes-
sages in a queue that needed to be received.
Role-Based Security Role-Based Security is a service that con-
trols access to COM+ code based on
Windows .NET users and roles.
Synchronization Synchronization is a service that allows only
one caller at a time to enter a component.
COM+ controls access to the component
from the requestors.
XA Interoperability XA Interoperability is a service that allows
access to databases that use the X/Open
transaction-processing model.
Real-World .NET Programming
810
PART III
The Basics
.NET makes accessing COM+ services mostly a new task. Developers should be familiar with
COM+ services and terminology, but the code is all new. Before we discuss various services
and their design implications, we will first walk you through the process of creating a simple
serviced component. We present the concepts at a high level, write some code, and then delve
deeper into the programming constructs. Our code will register the component with COM+ and
actually call it directly from a console application.
To access the services provided to us by COM+, we must first make sure our components
inherit directly from the class ServicedComponent. In doing so, we make sure that our object
has the necessary interface for operating with COM+. This is done simply by calling Inherits
ServicedComponent from within class-level scope.
The next important step is to mark various attributes of our class and methods as taking advan-
tage of COM+ services. .NET uses attributes, a kind of metadata, to indicate this information
to the compiler and pass it on to COM+. For example, to indicate that a class requires transac-
tion, we set an attribute on the class as follows:
<System.EnterpriseServices.Transaction(TransactionOption. Required)> Public
Class myServicedComponent. The code between the <> is the metadata. The compiler uses
this metadata to register our component with the appropriate COM+ application.
Finally, to use COM+ services from .NET, we must register our object. To do so, we strongly
name, or sign, the object.
Accessing COM+ Services
811
CHAPTER 17
In summary, steps required to take advantage of COM+ services include the following:
1. You must set a reference to System.EnterpriseServices in your application. This
enables you to access the various classes in the namespace.
2. In the assembly, you should indicate the name of the COM+ application with which you
want to register. This name should match an application-name setup inside Component
Services. For example, <Assembly: ApplicationName(“myComApp”)>.
3. You must indicate the key pair to the compiler for signing your strong name. You should
generate the key pair file using the utility sn.exe and indicate the key pair to the compiler
using an attribute. For example, <Assembly: 17
System.Reflection.AssemblyKeyFile(“keyPair.snk”)>.
COM+ SERVICES
ACCESSING
4. Next, mark your classes with the appropriate transaction option. For example, our exam-
ple requires transactions, so we set the attribute
<Transaction(TransactionOption.Required)> Public Class myServicedComponent.
5. Classes that are hosted by COM+ must inherit directly from the ServicedComponent
class. This class allows context sharing between COM+ and the .NET Framework. To do
so, simply write, Inherits ServicedComponent.
6. Next, as in MTS/COM+, you must indicate each transactional method’s transaction vote.
In COM+, we used the enumeration values TxCommit and TxAbort. In MTS, we called
SetComplete or SetAbort. Our example uses another attribute at the method level,
<AutoComplete()> Public Function test() As Boolean. AutoComplete indicates
that we want the method to call commit unless an exception is thrown, in which case we
want the method to automatically call abort.
7. Finally, all that is left to do is compile the class and call it directly as you would any
other .NET component. We do this in our module code.
Listing 17.1 represents a simple serviced class that gets executed from a module. It illustrates
the basic steps required to write a serviced component.
Imports System.EnterpriseServices
‘indicate the key pair used to generate the strong name for the assembly
<Assembly: System.Reflection.AssemblyKeyFile(“keyPair.snk”)>
End Function
End Class
Module Module1
Sub Main()
‘local scope
Dim sc = New servicedCompTest()
End Sub
End Module
We’ve looked at the basic steps required to write a simple serviced component. We will now go
backward and dig more deeply into each of the basic steps presented. We will then run through
the namespace and indicate how and why various COM+ services can be accessed
appropriately.
Accessing COM+ Services
813
CHAPTER 17
COM+ SERVICES
ACCESSING
attribute on the class as follows:
<System.EnterpriseServices.Transaction(TransactionOption.Required)> _
Public Class myClass
The code between the <> is the metadata or attribute. This annotation is stored with the meta-
data of the MSIL file. It is then used by the runtime to register our component with the appro-
priate COM+ application. The COM+ catalog holds this configuration information and allows
us to take advantage of COM+ services.
Note that you can apply multiple attributes to a single class. This is often necessary when set-
ting up a COM+ class. To do so, you simply separate the attributes with commas (,) as in the
following:
<Transaction(TransactionOption.Required), _
ObjectPooling(True)> Public Class myClass
Additionally, that attribute syntax enables you to set properties of the class within the tag. To
do so, you simply use the named parameter syntax to indicate the property and its value, as in
the following:
<ObjectPooling(MinPoolSize:=5)> Public Class myClass
The attribute programming construct might seem odd at first because VB developers have been
used to setting properties on their objects to access transaction services. However, the trade-off
for this new model is worth the extra effort. Advantages to this model include the following:
• Our code is stored as code and not hidden from us by the IDE. This enables us to move
between IDEs more easily. It also makes our programming more explicit and ensures that
code is stored with code. This should reduce the chance for misunderstandings and
thus, bugs.
• With metadata, the VB language can support future versions of COM+ services without a
rewrite of the language.
Real-World .NET Programming
814
PART III
• The attributes concept can be leveraged to access a number of similar services. For
instance, to mark your component as an XML Web service, you use attributes.
• You can create your own custom attributes to be used by other developers or within your
application. Custom attributes are created as classes within .NET that derive from
System.Attribute. For example, you could create a code-versioning attribute class to be
used by developers and referenced by a source safe-like application.
• Attributes unlock the VB language from Windows services and allow the programming
elements written with the language to support an unlimited number of “properties.”
Table 17.2 references the attributes in the System.Enterprise namespace. For each attribute,
the level to which it applies, or scope, is indicated.
COM+ SERVICES
exception, SetAbort() will automatically be
ACCESSING
called to abort the transaction.
ComponentAccessControl Class The ComponentAccessControl attribute is
used to indicate that a given class requires a
security check on calls. The type of this class
is Boolean. True indicates that a security
check is required and False indicates it is not.
COMTIIntrinsics Class The COMTIntrinsics attribute indicates that a
class is designed to participate in a COMTI
service.
ConstructionEnabled Class The ContructionEnabled attribute is a
Boolean value that indicates True if the class
supports object construction and False if it
does not. Object construction enables you to
pass configuration data to a class at the time
of COM+ instantiation. To use this service,
your code must support the
IObjectConstruct interface.
Description Assembly The Description attribute is a String value
Class that is used to describe the contents of the
Method given assembly, class, method, or interface.
Interface
EventClass Class The EventClass attribute is used to mark a
class as an event class. This allows method
calls to a class marked as EventClass to be
passed to other listening components as
events. Note that these methods do not actual-
ly process; rather, they pass processing off to
event subscribers.
Real-World .NET Programming
816
PART III
COM+ SERVICES
ponent does not support synchronization.
ACCESSING
Required, which indicates that the component
requires synchronization.
Supported, which indicates that the compo-
nent does not require synchronization but sup-
ports it should the caller request it.
Transaction Class The Transaction attribute is used to indicate
the type of transaction your object supports.
The attribute is of the type
TransactionOption enumeration. Values
include the following:
Disabled, which indicates that your class
should ignore any transaction.
NotSupported, which indicates that your
class should be created without regard to
transactions.
Required, which indicates that your class
should share a transaction should it exist. If
not, it should create a transaction context for
processing.
RequiresNew, which indicates that the compo-
nent should be created with a new transaction
context regardless of the state/request of the
caller.
Supported, which indicates that the class
should use a transaction context should it exit;
otherwise, just ignore it.
Real-World .NET Programming
818
PART III
A number of attributes in the namespace have a default value. In these cases, it is not necessary
to add the attribute to your code; COM+ registration will automatically apply the value.
Additionally, you do not always need to set the value of the attribute. Simply by adding the
attribute to your code, you set what is called a configured attribute value. For example, if you
omit the ObjectPooling attribute, the compiler assumes this is set to False (unconfigured
default value). However, if you simply add <ObjectPooling> My Class and never set the
value, the compiler assumes a True value. This is a nice shortcut, but we suggest you write
your code as explicitly as possible and actually set the value.
Application Identity
The ApplicationName attribute class enables you to set the name of the COM+ application to
which your component should be installed. The attribute applies at the assembly scope and can
be a string value or a GUID. For example, <assembly: ApplicationName(“myCOMApp”)> indi-
cates that your component should be installed in the COM+ application called myCOMApp.
Note that if the component has not previously been set up, it will be created for you by .NET.
Additionally, should you not provide the ApplicationName attribute, .NET will create the
application based on the name of your library. For instance, myLibrary.dll would get put into
an application called myLibrary; in the case of the IDE, it will create an application based on
your project name.
[ assembly: ApplicationName(“BankComponent”)]
Strong Names
To access COM+ services, you must register your object. To do so, you strongly name, or sign,
the object. COM+ applications written with .NET must be deployed in the Global Assembly
Cache (GAC) and, therefore, must have a strong name. A strong name is made up from the
assembly’s name, version, any culture information, a public key, and a digital signature.
Accessing COM+ Services
819
CHAPTER 17
VS .NET supports creating strong names for our assemblies. But first, you must have a pub-
lic/private key pair. To generate a key pair, we use the utility sn.exe. For example, the com-
mand sn –k MyComp.snk creates a new strong name key in the active directory. To sign our
assembly, we indicate the key pair to the compiler using an attribute. For example, <Assembly:
System.Reflection. AssemblyKeyFile(“keyPair.snk”)>.
Dynamic Registration
.NET automatically registers and hosts the component within the appropriate COM+ applica-
tion when it is first accessed. This is considered dynamic registration. This process has the
advantage of storing all your COM+ configuration information directly inside your component, 17
so you do not need to concern yourself with the actual setup of the component with the COM+
COM+ SERVICES
catalog. You simply copy the file into the application.
ACCESSING
Although this process is easy and often preferred, it does offer a couple drawbacks. COM
applications will be unable to reference your component prior to registration. Therefore,
your component will need to be manually registered to be accessible from COM clients.
Additionally, dynamically registered components are not stored in the GAC. Again, only
those manually registered make it into the GAC.
Manual Registration
Although not always preferred, manual registration of your components can offer a few advan-
tages. For instance, the utility used to manually register components, Regsvcs.exe, provides
direct feedback and error messages of the registration process. This can be quite helpful when
trying to debug errors. The following is an example of this utility in action: regsvcs
myComp.dll /appname:myApp /tlb:myComp.tlb.
Role-Based Security
COM+ offers a role-based security model that groups Windows users into COM+ roles. This
model relies on the NT user accounts (tokens) associated with executing code as the basis for
identity. To gain access, these users must belong to a COM+ role that has been assigned access
to the class.
This simple model is used to enforce basic access to your components.
NOTE
Note that the .NET Framework’s code access security (see Appendix C) is independent
from COM+ role-based security. In fact, .NET allows only one or the other. You cannot
use both models simultaneously.
In addition to the assembly, you can also associate security roles at the class, method, and
interface levels. Marking a method with the SecurityRole attribute, for instance, indicates that
only those roles marked have access to the actual method. Figure 17.1 illustrates the COM+
catalog interface for setting this information. The example shown is for setting a class-level
property.
Note that you can also add roles and users to those roles through the MMC interface.
Figure 17.2 illustrates this interface.
17
COM+ SERVICES
ACCESSING
FIGURE 17.1
Enforce role access at the class level.
FIGURE 17.2
Roles and users setup.
To create an instance of the SecurityCallContext, you use the CurrentCall property within
your serviced component. CurrentCall is a static property that returns a valid instance of
SecurityCallContext. For example, the following code creates a valid SecurityCallContext
object:
Real-World .NET Programming
822
PART III
‘local scope
Dim mySecurityContext As System.EnterpriseServices.SecurityCallContext
To know the principal caller (that is, the identity of the person or application that made the
direct request for your method or component), you access the DirectCaller property. To
return the chain of current callers, you access the Callers property, which returns an instance
of the SecurityCallers class. This class is a collection of all callers in the chain leading
up to the current call. You also have direct access to the first caller in the chain using the
OriginalCaller property. Both DirectCaller and SecurityCallers.Item return an instance
of the SecurityIdentity class. Table 17.3 lists the properties available to you through the
SecurityIdentity class.
Property Description
AccountName The AccountName property is a string value representing
the name of the user (or account) that the
SecurityIdentity instance represents.
AuthenticationLevel The AuthenticationLevel property returns a member of
the AuthenticationOption enumeration that indicates how
the user was authenticated. Authentication options include
Call, Connect, Default, Integrity, None, Packet,
Privacy.
AuthenticationService The AuthenticationService property returns the authenti-
cation server described by the security identity.
ImpersonationLevel The ImpersonationLevel property returns a member of
the ImpersonationLevelOption enumeration indicating
the level of impersonation used in the call. Impersonation
levels include Anonymous, Identify, Delegate,
Impersonate, and Default.
Note that you must turn on authorization at the application level for the security context to be
made available to your application. You do this from the Security tab on the Application
Properties window. Figure 17.3 provides a screenshot.
Accessing COM+ Services
823
CHAPTER 17
17
COM+ SERVICES
ACCESSING
FIGURE 17.3
Security enabled.
Listing 17.2 demonstrates controlling security from within the actual method call. Suppose that
all roles have access to the actual method. However, you want to block certain portions of the
method to users who are not of a certain level. This example simply gets the security call con-
text and uses it to check the IsCallerInRole method.
<Assembly: ApplicationName(“rbApp”)>
<Assembly: System.Reflection.AssemblyKeyFile(“rbSec.snk”)>
Inherits ServicedComponent
<AutoComplete()> _
Public Function execute() As Boolean
‘local scope
Dim mySecCtx As SecurityCallContext
End Function
End Class
Resource Management
One of the more expensive things in any application is creating and loading objects into memo-
ry. A key way in which COM+ can help make applications work more efficiently is through
resource management. COM+ provides resource management through the services of Object
Pooling, Just-in-Time activation and deactivation, and State Management. These services,
together with the stateless programming model COM+ enforces, can give your application bet-
ter overall scalability.
• MinPoolSize enables you to control the minimum number of objects that are maintained
in the pool. As objects are requested or on startup of the COM+ application, the pool
reaches its minimum size and COM+ maintains at least this number of objects for use by
additional callers.
• MaxPoolSize sets the maximum number of objects that can exist in the pool. If all pool
objects are in use and the pool has not reached its maximum size, COM+ creates an
additional component for the caller. When the maximum pool size is reached, however,
COM+ queues the client requests and each must wait in turn for an object to become
available from the queue.
17
• CreationTimeout sets the number of milliseconds COM+ should wait for an object from
COM+ SERVICES
the pool when requesting an object. If the timeout value is reached, COM+ throws an
ACCESSING
exception.
COM+ manages an object’s life cycle; as such, it calls methods of the IObjectControl inter-
face exposed by the ServicedComponent class. You can override and intercept these calls for
use by your application. The following are methods of IObjectControl that can be overridden:
• Activate is called by COM+ when your object is removed from the pool (handed out to
the calling client).
• Deactivate is called when an object is placed back into the pool (released from the
client).
• When COM+ returns an object to the pool after deactivation, it first calls CanBePooled.
This enables you to check your object to make sure it is okay to be returned to the pool.
If so, you return True from the function call; if not, returning False causes COM+ to
discard the object.
In Listing 17.3, we create a COM+ object called myPooledObject. We tag it as a pooled object
and set the pooling parameters of minimum pool size to 5 and maximum pool size to 10.
Imports System.EnterpriseServices
‘indicate the key pair used to generate the strong name for the assembly
<Assembly: System.Reflection.AssemblyKeyFile(“keyPair.snk”)>
<Transaction(TransactionOption.Required), _
ObjectPoolingAttribute(MinPoolSize:=5, MaxPoolSize:=10)> _
Public Class myPooledObject
Real-World .NET Programming
826
PART III
‘class-level scope
Inherits ServicedComponent
<AutoComplete()> _
Public Function doTask() As Boolean
Return True
End Function
Return True
End Function
End Sub
End Sub
End Class
How do you verify that COM+ is pooling your objects? One way is to view the components’
real-time inside the COM+ Component Services management console application. This enables
you to see the number of objects in a given pool at a specific time. Figure 17.4 is a screenshot
of this interface as it is reporting resource-management statistics.
Accessing COM+ Services
827
CHAPTER 17
FIGURE 17.4
Component Services monitoring.
To make sure your components are tracked by the COM+ Component Service monitor, you
must set the attribute EventTrackingEnabledAttribute to True. Additionally, your component
must be running in a server and not in a library application.
17
Of course, you can use the Component Services manager application to mark Object Pooling
COM+ SERVICES
attributes directly or to override the attributes your component has set at the time of install.
ACCESSING
Your object should still override the CanBePooled method and mark this value to True to indi-
cate that COM+ can pool your object. Figure 17.5 illustrates the interface.
Just-in-Time Activation
COM+ provides the service of Just-in-Time (JIT) activation and deactivation of components.
Components that take advantage of this service are instantiated as context-only objects on
request. That is, the objects are not activated (and locked) by calling the client at the time of
creation. COM+ activates the JIT object on a method call from a client. When the method
returns, COM+ deactivates the component. This allows COM+ to pass out these expensive
resources only when they are really needed (on the method call) rather than when they are
created.
FIGURE 17.5
Object Pooling Component Services interface.
Real-World .NET Programming
828
PART III
As an example of the benefits this service can provide, suppose you have an application that
creates a number of COM+ objects in the declaration section at the top of your procedure.
Now suppose that as your procedure executes, it uses some of the objects some of the time and
deactivates all the objects after the procedure has completed. Without JIT activation, COM+
would be forced to hand your application an instance of each object on its creation. These
objects would be blocked from access by other requesting applications until your application is
finished with them, regardless of when or if your application actually uses them. Thus, COM+
would have to create another instance for a second requesting client. With JIT activation,
COM+ knows that you may want the resource, but until you actually use it by calling one of its
methods, it is available to be used by other clients. After they are used, on method call return,
COM+ deactivates the object and makes it again available to others, regardless of whether your
application is finished using it.
Of course, this model—and COM+ in general—requires that your object architecture is a state-
less design. Stateless objects are those that do not maintain object state or data between
method calls. A stateless object fires up at method call, gets its work done, passes results to the
client call, and then dies. Therefore, even though your application holds a reference to a JIT
object, the object does not maintain state between method calls. It is deactivated, and thus all
data from the method call is wiped out. This design prevents objects from blocking or holding
expensive resources open and thereby increases the scalability of your application.
To indicate that your object is ready for deactivation, you set the doneness bit (a flag value
indicating that your method has completed). This is automatically set for you when using
AutoComplete or calling SetComplete or SetAbort of the ContextUtil class. Additionally,
setting DeactivateOnReturn = True indicates that your object is ready to be deactivated.
To mark a class as taking advantage of JIT activation and deactivation, you simply use the
JustInTimeActivationAttribute class, as in
<JustInTimeActivation(True)> Public Class myJITClass
Note that components marked as AutoComplete automatically set the JIT attribute to True.
Sometimes it’s preferable to store your user-specific state somewhere within the user-interface
tier. You can push state out to the client in the form of cookies or session variables in the case
of a Web application. In the case of a client/server application, you could store state out to
locally executing code inside the client.
But what if you need to manage a global value between objects, and you want neither to return
the data to the calling client nor store it to (or access it from) the database?
COM+ provides the Shared Property Manager (SPM) for precisely this occasion. The SPM is a
resource dispenser that allows you to share state between objects executing in the same server
process. The SPM state is transient—that is, shared state does not survive system failures, 17
reboots, or application shutdowns. The SPM is meant to be accessed across a transaction and
COM+ SERVICES
ACCESSING
not across a process. Because of this transient nature, it is important for you to ensure the exe-
cution sequence of your methods that require shared data. Alternatively, you should implement
your method in such a way that on shared property access exceptions, your method knows how
to create and populate the shared property values.
Shared properties are available only to objects running in the same process. Therefore, objects
that require access to global data must be installed inside the same COM+ application.
To create a shared property, you must first create a SharedPropertyGroupManager object. This
object enables you to create and access property groups. One of the problems with global data
that the shared property manager solves is naming collisions among global variables. For
example, if Object A creates a value called mySharedValue, Object B should be restricted from
creating the same shared variable or a collision will occur—or worse, Object B could simply
overwrite Object A’s value. Object B and Object A know nothing about each other and
therefore could easily create this collision. The SharedPropertyGroup class and the
SharedPropertyGroupManager solve this problem by enforcing unique namespaces for
properties to use.
The CreatePropertyGroup method of SharedPropertyGroupManager is used to define a new
property group and returns a SharedPropertyGroup instance. This method call requires that
you indicate the following parameters:
• name—The property group name. This is a string value that you use to reference the
property group when returning property values.
• dwIsoMode—The property group isolation (or lock) mode. The value is of the type
PropertyLockMode enumeration, whose values include Method and SetGet. Setting the
lock mode to SetGet indicates that after you set or return the value of the property
group, you are finished using it and it can be released for others to share. If you make a
call that sets a value, this call is atomic. Setting the value to Method indicates that the
property access locking should correspond with the execution of your method. When the
method is complete, you can unlock the property value.
Real-World .NET Programming
830
PART III
<Assembly: ApplicationName(“myApp”)>
‘indicate the key pair used to generate the strong name for the assembly
<Assembly: System.Reflection.AssemblyKeyFile(“sharedProp.snk”)>
Inherits ServicedComponent
<AutoComplete()> _
Public Sub setProp(ByVal propVal As String)
‘local scope
Dim spGroupMgr As SharedPropertyGroupManager
Accessing COM+ Services
831
CHAPTER 17
COM+ SERVICES
ACCESSING
‘create a property
sharedProp = spGroup.CreateProperty(name:=”spProp”, fExists:=True)
End Sub
End Class
Inherits ServicedComponent
<AutoComplete()> _
Public Function getProp() As String
‘local scope
Dim spGroupMgr As SharedPropertyGroupManager
Dim spGroup As SharedPropertyGroup
Dim sharedProp As SharedProperty
End Function
End Class
Module Module1
Sub Main()
‘local scope
‘wait
Do While Console.ReadLine <> “s” : Loop
End Sub
End Module
Transaction Processing
A transaction is simply a group of tasks that either succeed or fail as a unit. For instance, sup-
pose that your application runs a nightly import process to update its product-specification
data. The specification data is stored in a set of six tables that are interrelated. That is, if you
update one table, you must ensure that all new data makes it into the associated tables. These
six tasks represent a transaction. They either all succeed (commit), or in the event of a failure,
they all must fail (rollback). Imagine the chaos if you updated the model line but it still had the
old colors and dimensions—or worse, prices. With proper transaction processing, no operation
is permanent until all have successfully completed. 17
COM+ SERVICES
Objects created in COM+ and marked transactional automatically take advantage of the COM+
ACCESSING
transaction service. On creation, these objects have a transactional context and can enlist or
nest other objects into this context. In fact, transactions can be supported across heterogeneous
data sources, computers, and networks.
NOTE
COM+ uses the Windows Distributed Transaction Coordinator (DTC) to provide trans-
action processing. From managed code you can use a manual or automatic model of
transaction management. Manual transactions require that you write code directly to
the given transaction manager to start, end, and roll back a transaction.
The topics of transactions and transaction processing can span many technologies. For the pur-
pose of this chapter and book, we will focus our discussion specifically on COM+ transactions
in the .NET Framework Class Library.
Transaction Management
For you to mark that your COM+ object is transactional, .NET provides the
TransactionAttribute class. This class simply indicates to COM+ how your object should be
managed when dealing with automatic transactions. The Timeout property of the class indi-
cates the number of milliseconds the transaction can run before COM+ raises an exception.
The class has a Value property that is of the type TransactionOption enumeration. Setting
this value is the key to getting the desired behavior out of your objects and transactions. Note
that when you mark a class as transactional, COM+ enforces both JIT activation and concur-
rency protection (also called synchronization. Table 17.4 lists the enumeration’s values and a
brief description of each.
Real-World .NET Programming
834
PART III
Member Description
Disabled The Disabled value disables automatic transactional support on the
given object.
Note: Objects marked Disabled can still manually participate in DTC
transactions.
NotSupported The NotSupported value indicates that the object does not support
transactions. Instances of objects marked NotSupported are created
without transaction support, regardless of the transactional nature of
the creator.
Supported The Supported value indicates that the given object can run inside
(supports) a transaction context, provided one is present. If not, the
object is created without regard for transactions.
Required The Required value indicates that the given object requires a transac-
tion context. If one is present, the object will enlist itself in the scope
of the active transaction context. If no context exists, a new transaction
context will be created.
RequiresNew The RequiresNew value indicates that the given object requires a new
transaction context for each request.
Each task within a transaction must indicate whether it succeeded or failed. You can indicate
this with your code in a number of ways. One is to use the AutoCompleteAttribute class.
AutoComplete indicates that a tagged method should automatically be considered successful
(commit), provided that it does not raise an exception. On exception, however, the method will
vote to abort the transaction.
COM+ also provides the ContextUtil class for this purpose. ContexUtil is available to any
object running as a serviced component. Calling it returns access to the object’s transaction
context. With this context, you can explicitly call SetComplete or SetAbort. SetComplete
indicates that the object succeeded, whereas SetAbort indicates that the transaction failed.
Note, as we stated previously, COM+ transactional objects automatically are JIT enabled.
Calling SetComplete also indicates that the object should be deactivated (sets the doneness bit
to True) after the DeactivateOnReturn method call.
The last way you can indicate to COM+ that your object succeeded is by setting the
ContextUtil.MyTransactionVote, also called the consistency bit. This property takes a
TransactionVote enumeration value. Values are Commit and Abort. What is the difference
between setting the transaction vote versus calling SetComplete? The MyTransactionVote
property gives you finer control because it does not automatically set the doneness bit (indicat-
ed by DeactivateOnReturn = True), and it allows you to manage the two properties
independently.
Accessing COM+ Services
835
CHAPTER 17
Transaction Example
In Listing 17.5, we create a two-part transaction. The class trxWrite writes data to the data-
base. We set its TransactionOption to Required and indicate that the method writeTrx
should allow COM+ to automatically manage its transaction state by setting the attribute
AutoComplete. The trxManage class, on the other hand, is set to RequiresNew for its transac-
tion option. It enlists writeTrx into its transaction context by creating the object and calling its
method. Its method, manageTrx, explicitly aborts the transaction by setting MyTransactionVote
to Transaction.Abort. Finally, we create a simple console-based client that calls trxManage.
17
LISTING 17.5 Transaction Example
COM+ SERVICES
ACCESSING
Imports System.EnterpriseServices
Imports System.Data.OleDb
‘indicate the key pair used to generate the strong name for the assembly
<Assembly: System.Reflection.AssemblyKeyFile(“trx.snk”)>
‘local scope
Dim myConn As OleDbConnection
Dim myCommand As New OleDbCommand()
Dim sql As String
End Sub
End Class
‘local scope
Dim myTrxEvent As New trxWrite()
COM+ SERVICES
ACCESSING
‘abort the transaction
ContextUtil.MyTransactionVote = TransactionVote.Abort
End Sub
End Class
Module Module1
Sub Main()
‘local scope
Dim trxManage As New trx.trxManage()
End Sub
End Module
Events
COM+ provides a service for connecting objects that transmit information with those that want
to receive this information. This service can be essential in today’s distributed environments.
When designing components for enterprise applications, it’s just not possible to know all the
external or internal objects with which your new class will need to interact. Therefore, it is
important to anticipate the types of access to your object’s events in which other objects will
have interest. The loosely coupled event model creates a programming paradigm of event pub-
lishers and event subscribers that know nothing of one another at their time of inception.
FIGURE 17.6
COM+ event model.
Accessing COM+ Services
839
CHAPTER 17
The Interface
By using an interface, you ensure that your subscribers receive events the same way your pub-
lisher fires them. Inside Listing 17.6, we create an example of a simple interface called
myEventInterface. This interface has one method (someEvent) that takes someData as
17
String as a parameter. We give the interface a strong name, compile it, and register it with the
COM+ SERVICES
COM+ catalog, mylcEventApp. We then set a reference to it from our subscriber and publisher.
ACCESSING
LISTING 17.6 Event Interface
Imports System.EnterpriseServices
‘indicate the key pair used to generate the strong name for the assembly
<Assembly: System.Reflection.AssemblyKeyFile(“myEventI.snk”)>
End Interface
Imports myEventInterfaceLib
Imports System.EnterpriseServices
‘indicate the key pair used to generate the strong name for the assembly
<Assembly: System.Reflection.AssemblyKeyFile(“myEventSNK.snk”)>
<EventClass()> _
Public Class myEventClass
Implements myEventInterface
End Sub
End Class
Accessing COM+ Services
841
CHAPTER 17
The Subscriber
The event subscriber receives notification when the event class is fired by the publisher. We
again set reference to System.EnterpriseServices and myEventInterfaceLib. The class
implements myEventInterface and its method, someEvent. On execution, the event data is
passed to this subscriber, which logs the data to the event log (see Listing 17.8).
COM+ SERVICES
‘indicate the COM+ application to use
ACCESSING
<Assembly: ApplicationName(“mylcEventApp”)>
‘indicate the key pair used to generate the strong name for the assembly
<Assembly: System.Reflection.AssemblyKeyFile(“myEventSub.snk”)>
‘implement event
<AutoComplete()> _
Public Sub someEvent(ByVal someData As String) _
Implements myEventInterface.someEvent
End Class
No attribute tags allow us to indicate the events to which the object subscribes. These must be
set through the Component Services Management application. To do so, right-click the
Subscriptions folder and select the event to which you want to subscribe. Figure 17.7 illustrates
this interface.
The Publisher
The publisher is any application that makes a direct call to the event class. In Listing 17.9, we
create a simple module application. We first set references to System.EnterpriseServices,
myEventClassLib, and myEventInterfaceLib. We then create the object, fireEvent, based on
the myEventInterface interface and convert it to the type of myEventClass. This is necessary
because you cannot instantiate the event class directly. Finally, we call the someEvent method
and pass it a string value. The COM+ event system intercepts this call and doles it out to all
subscribers. In our case, mySubscriberClass writes the data to the event log.
FIGURE 17.7
Subscribing to COM+ events.
Imports myEventClassLib
Imports myEventInterfaceLib
Module Module1
Accessing COM+ Services
843
CHAPTER 17
Sub Main()
‘local scope
Dim fireEvent As myEventInterface
COM+ SERVICES
fireEvent = CType(New myEventClass(), myEventInterface)
ACCESSING
‘fire the event
fireEvent.someEvent(“Event data ...”)
End Sub
End Module
Asynchronous Components
COM+ provides our applications the capability to create and execute components asynchro-
nously. COM+ uses Messaging and a service called Queued Components to provide this func-
tionality. Queued Components work like this: Suppose you have an order-processing system
and you want to take your database offline for a few hours every night to do backup. In the
meantime, you do not want to stop your site from taking orders. Instead, you can queue your
order objects and their requests inside a message queue. When the database comes back online,
COM+ will play back the messages and the orders will be processed accordingly. Of course,
backing up a database is one thing, but what if your database failed? Or what if your order-
processing server started blocking because of high volume? In either of these events, you could
code your application to queue your components in these desperate scenarios; no data would
be lost and processing could continue seamlessly to the user. Queued Components are certainly
not for everyone. When creating your application, you must consider the users’ needs, the
availability of your system, and the disconnectedness of your enterprise.
NOTE
Chapter 13 is entirely dedicated to Messaging. In Chapter 13, we discussed storing a
class as a message using the System.Messaging namespace. In this section, we discuss
the COM+ service (Queued Components) that provides similar functionality.
Real-World .NET Programming
844
PART III
Imports System.EnterpriseServices
‘indicate the key pair used to generate the strong name for the assembly
<Assembly: System.Reflection.AssemblyKeyFile(“qc.snk”)>
Namespace qCompNamespace
COM+ SERVICES
ACCESSING
‘purpose: interface of queued component
End Interface
<InterfaceQueuing(Interface:=”myQueuedInterface”)> _
Public Class qClass
Inherits ServicedComponent
Implements myQueuedInterface
End Sub
End Class
End Namespace
You can, of course, set the same queuing properties from within the Component Services
Explorer application. Figure 17.8 illustrates enabling queuing and marking the application as
a listener.
Real-World .NET Programming
846
PART III
FIGURE 17.8
Queued Component MMC.
Again, you can use the Component Services MMC to mark your interfaces as queued, as illus-
trated in Figure 17.9. To get this dialog box, you navigate the interfaces on the component,
right-click, and choose Properties.
FIGURE 17.9
Queued Component MMC.
Accessing COM+ Services
847
CHAPTER 17
The Sender
To actually queue a component, we create a simple client application that uses our qComp
namespace. We first set a reference to this namespace and then dimension a variable as the
interface type myQueuedInterface. We then create an instance of the Queued Component
using CType and bind the component to a message queue using Marshal.BindToMonker.
Finally, we call the method on the component, which gets recorded into the message queue. In
fact, if your COM+ application is stopped, you can view the message from the Computer
Management application. After you start your COM+ application, it picks up the message and
plays it back—in our case, writing the data out to the event log. 17
COM+ SERVICES
Listing 17.11 presents an example that creates a queued component.
ACCESSING
LISTING 17.11 Message Sender
Imports qComp.qCompNamespace
Imports System.EnterpriseServices
Imports System.Runtime.InteropServices
Module Module1
Sub Main()
‘local scope
Dim queuedComp As myQueuedInterface
End Sub
End Module
Real-World .NET Programming
848
PART III
Summary
COM+ offers developers a number of key services. These services are still very much relevant
with the advent of .NET and continue to be extended by Microsoft. As you’ve seen, COM+ is
very accessible and makes your job easier. If your application requires you to write scalable
and highly available objects, you should consider incorporating COM+ services.
The following are key points of the chapter:
• COM+ services can increase the scalability of your application and ease the burden of
writing resource-management code.
• To gain the full benefit of COM+, you need to design your objects as stateless.
• To create a serviced component, you must inherit directly from
System.Enterprise.ServicedComponent.
• To be accessible from other objects, serviced components should have a strong name and
be registered in the global assembly cache. .NET provides the utility sn.exe for creating
strong names and regsvcs.exe for manually registering managed code with COM+.
• You can assign roles-based access to assemblies, classes, methods, and interfaces using
the SecurityRole attribute class.
• You can access the security context of your executing code with the
SecurityCallContext class and its CurrentCall property.
• The ObjectPooling attribute class allows you to mark your class as pooled and set the
minimum and maximum pool size.
• Use the JustInTimeActivation attribute class to take advantage of the JIT activation
and deactivation service provided by COM+.
• The classes SharedPropertyGroupManager, SharedPropertyGroup, and SharedProperty
allow you to maintain global state between otherwise stateless object calls.
• You can set your components to automatically manage transaction state by setting the
AutoComplete attribute.
• To access the transaction context and call things such as SetComplete and SetAbort, use
the ContexUtil class.
• The EventClass attribute class is used to mark your components as event classes that can
be loosely coupled and subscribed to by other objects.
• Queued Components enable you to store a transaction for asynchronous execution at a
later date. To enable queuing from your component, use the ApplicationQueueing
attribute class.
.NET Interop with COM CHAPTER
18
Applications
IN THIS CHAPTER
• .NET Interop with COM 850
Microsoft has devoted a number of resources to ensure that your current code base is not
trashed with the advent of .NET. As a result, .NET and the CLR understand how to talk to
COM, and Microsoft has provided features that make COM forward compatible with .NET!
Therefore, before you fire up that porting engine or rewrite your entire middle tier, make sure
that porting—and not simply interoperating—is the answer.
In this chapter, we discuss how to determine which pieces of existing applications you might
migrate, as well as the task of creating new functionality in .NET while keeping your legacy
code base intact. We then walk through the process of creating both a COM-to-.NET and a
.NET-to-COM interop example. Finally, we detail the specifics of interoperation and illustrate
some of the key classes inside the System.Runtime.InteropServices namespace.
After reading this chapter, you should be able to do the following:
• Determine when it is more feasible to interoperate, port, or write a custom wrapper
• Call a COM server component from a .NET client
• Call a .NET server object from a COM client
• Design your .NET interop components to work well with COM
• Understand how to leverage the attribute classes inside of the InteropServices name-
space
.NET INTEROP
APPLICATIONS
WITH COM
sweeping path through the system.
The Basics
Calling a COM server from a .NET client can be a very simple task in the majority of cases,
but depending on your requirements, the process can quickly become complex. We’ll start with
the simple. In this section, we walk you through the steps involved with calling a COM server
from a .NET client.
First, we create a simple COM object to act as a server for our .NET client calls. The follow-
ing code creates a VB6 ActiveX dll project with one method, Add, that simply adds two num-
bers and returns the resulting sum.
18
Public Function add( -
.NET INTEROP
APPLICATIONS
ByVal numberOne As Integer, _
WITH COM
ByVal numberTwo As Integer) as Integer
End Function
This project gets compiled the same way as any VB6 project does. Remember, we could just as
easily use an existing COM server.
Next, inside the VS .NET IDE, we create a simple console application to act as our .NET
client. We initially attempt to set a reference to our COM server from the References dialog
box for our project. However, on doing so, we are presented with the warning message, as
indicated in Figure 18.1.
This warning message indicates that no Primary Interop Assembly (PIA) exists for our
COM server. An interop assembly is simply a managed code dll that wraps our COM server’s
interface. This allows us to work with the COM server as if it were a native .NET server and
assists the CLR in translating calls between .NET and COM. To be clear, interop assemblies
contain no implementation code. They are simply type definitions for the COM component’s
equivalent .NET types.
Real-World .NET Programming
854
PART III
FIGURE 18.1
Setting a reference to our COM server.
The primary interop assembly differs from a standard (sometimes called alternate) interop
assembly in that it is signed by the publisher as the unique interop source for the given COM
server. This ensures that two .NET clients access a given COM server in the same manner and
provides the official definition of the assembly’s types. Standard interop assemblies are gener-
ated by a developer wanting to access a COM server. As a result, two developers generating
two distinct interop assemblies against the same COM object result in two incompatible
interop assemblies. This is fine as long as the two applications never try to pass an instance of
the assembly to one another. The COM server may be identical, but its interop assembly is
very different, resulting in an exception. For this reason, COM servers that are typically shared
should have one primary interop assembly generated for them by the publisher.
.NET provides the tools to generate both standard and primary interop assemblies. In fact,
because the PIA is managed code, you can even write them yourself. To do so, you would tag a
given assembly with the PrimaryInteropAssemblyAttribute class located inside System.
Runtime.InteropServices. However, it is often much simpler to use the .NET utilities.
The Type Library Importer (TlbImp.exe) utility is used to convert the types inside a COM
server to an interop assembly. Because we are the publisher, we will use the utility to generate
the PIA for our COM server. Because PIAs are strongly named, we must first create a new key
pair to sign our server. We do this with the Strong Name tool (sn.exe) and create the file,
MyPIAKey.snk. The following represents the command prompt call
Sn.exe –k MyPIAKey.snk
.NET Interop with COM Applications
855
CHAPTER 18
Next, we simply run the TlbImp.exe against our COM server and attach our key pair as in the
following:
Tlbimp.exe /primary /keyfile:MyPIAKey.snk
/out:MyComObjectPIA.dll MyComObject.dll
Note that we specified the option /primary to indicate that this was a PIA and not simply an
IA. We then indicate the key file (/keyfile:) and finally tell the utility where to output the
resulting PIA (/out:).
Now, from our .NET client, we can simply reference the newly created PIA file instead of the
COM server library. This interop object appears to our client as any other .NET object would.
You can see this by examining the PIA inside the object browser, as Figure 18.2 indicates.
18
.NET INTEROP
APPLICATIONS
WITH COM
FIGURE 18.2
Our COM server PIA.
All that is left is to make our object calls to the COM server. The following code creates a new
instance of the PIA and calls its method, Add.
Module Module1
Sub Main()
‘local scope
Dim comObject As MyComObjectPIA.MyComClass
End Sub
End Module
Although PIAs are a best practice, it is often necessary to allow VS .NET to generate an alter-
nate interop assembly for you. If you choose Yes in response to the dialog box question, Would
you like to have a wrapper generated for you?, VS .NET will generate an interop
assembly whose filename in this case is Interop.MyComObject_1_0.dll. Again, this is a stan-
dard IA and can be used to the same result, but warnings apply. The code to access this IA and
the COM server from our .NET client is as follows:
‘local scope
Dim comObject As MyComObject.MyComClass
VB .NET developers have one more tool available to them to support interop with ActiveX
COM servers—the CreateObject function. As in VB of old, CreateObject provides late bind-
ing to ActiveX COM libraries. In this case, no PIA or IA is required; the CLR handles all tasks
for you. Of course, calls are late bound, which disables compiler type checking and can create
undesirable effects. However, it is worth mentioning because it is a very simple method you
can use to call COM servers from .NET. The following represents making a late-bound call to
our MyComObject:
during an interop session. The RCW acts as a proxy between the .NET client and the COM
server. The RCW uses your interop assembly to understand exactly how to marshal data
between the two layers. Figure 18.3 illustrates a .NET client communicating to a COM object
via an RCW proxy.
Unknown
Dispatch
COM
SomeInterface
RCW Object
.NET Client
Managed Unmanaged
FIGURE 18.3
Runtime Callable Wrapper (RCW).
The CLR uses the interop assembly’s metadata to generate the RCW and to create the COM 18
server instance. The RCW wraps the COM server and mediates method calls and data type
.NET INTEROP
APPLICATIONS
WITH COM
translations between the two environments. Its job is to make the COM server appear as a
native .NET object to the CLR and make the .NET client appear as a COM client to the COM
server.
The COM object is reference counted, and as such, references are maintained by the RCW.
The RCW, on the other hand, is managed and therefore subject to garbage collection. It is the
RCW that manages the releasing of references to the COM server.
Some limitations exist to COM servers that bleed through to your interop assembly and the
RCW. These include the lack of support for parameterized constructors, inheritance, and static
methods. These are features that .NET clients will come to expect from server objects, whether
they are COM or .NET. To bridge these limitations, you can create a custom interop wrapper.
End Function
End Function
Now suppose that our requirement is to make the add function operate with decimal values.
For our .NET client, we want to continue to support the COM interface and continue to use the
other logic embedded inside the COM server (the Subtract method). However, as we extend
our object, we want to build our new code inside .NET.
To accomplish this, you create a wrapper class for your COM server. This wrapper class should
inherit directly from the interop assembly. Your new code can be implemented as an override to
the Add method while Subtract continues to interoperate with the COM server. An example of
this wrapper class follows:
Public Class MyComObject
Inherits MyComObjectPIA3.MyComClass
End Function
End Class
.NET Interop with COM Applications
859
CHAPTER 18
Your .NET client now simply references your wrapper classes and makes the same calls to Add
and Subtract.
Module Module1
Sub Main()
‘local scope
Dim comObject As MyComObject
End Sub
End Module
The biggest advantage to creating a custom wrapper is that you can slowly replace your COM 18
server with managed code, and clients will continue to function regardless. You can apply this
.NET INTEROP
APPLICATIONS
design pattern to solve a number of interop issues.
WITH COM
Calling .NET from COM Clients
You undoubtedly will want to quickly take advantage of the benefits .NET development pro-
vides your application. Chances are, you will be extending an existing application rather than
executing a complete rewrite for .NET. For this reason, Microsoft has made sure that .NET
components can be written to support forward compatibility from COM.
Suppose you have the requirement to add a new component to your system that relies on a
COM client application and other COM servers. You do not intend to replace the client appli-
cation or rewrite your middle tier. However, you are anxious to create your new components to
take advantage of .NET’s features and flexibility. The CLR allows you to create a managed
component that looks native to COM.
The Basics
Creating .NET servers for COM clients involves properly defining a component’s architecture,
understanding the constraints COM imposes, creating a type library, and registering the com-
ponent. In this section, we walk through the necessary steps involved with exposing managed
code to COM.
Real-World .NET Programming
860
PART III
First, we create a simple .NET server object. In our case, we define two functions: Multiply
and Divide.
Imports System.Reflection
Imports System.Runtime.InteropServices
<Assembly: AssemblyKeyFile(“MyKey.snk”)>
End Interface
<ClassInterface(ClassInterfaceType.None)> _
Public Class MyDNetClass
Implements IDNetServer
‘multiply 2 numbers
Return numberOne * numberTwo
End Function
‘divide 2 numbers
Try
Return numberOne / numberTwo
Catch
‘do nothing
End Try
End Function
End Class
Notice that we explicitly define an interface (IDNetServer) for use by our class
(MyDNetClass). COM requires our classes to publish an interface. It is a best practice to define
interfaces explicitly. When doing so, it is best to use the ClassInterfaceAttribute class to
tag the class as not requiring an autogenerated class interface.
NOTE
We are free to use the default class interface that is generated by utilities such as the
table exporter (TlbExp.exe). By default, when we export a type library, the utility cre-
ates a class interface for each class in the .NET server. On the surface, this sounds like
18
a great idea; indeed, it can be in the right situation. However, because of the inter-
.NET INTEROP
APPLICATIONS
WITH COM
face rules COM imposes and the possible restructuring of those interfaces by a type
library generator, it is best to define these interfaces explicitly.
Also notice that we define a default constructor for our object, even though we do not attach
any code to the routine. This is a requirement of COM. COM services cannot create the object
without a defined default constructor.
Another requirement for COM interoperation is that all members visible to COM must be
declared as public. Additionally, COM, by its nature, imposes a few restrictions on what our
components are allowed to do. For instance, parameterized constructors are not permitted, sta-
tic methods are out, and COM has no idea of constant fields. Although you need to be acutely
aware of these restrictions, you undoubtedly have good reason for creating .NET servers for
COM clients. These restrictions should not dissuade you from your course; they represent fea-
tures of .NET that, as VB COM developers, we’re used to living without.
Finally, to register our managed server with COM, we must digitally sign it. In our case, we
create the file MyKey.snk using the strong name utility, SN.exe, and apply the attribute
AssemblyKeyFile to the assembly.
Real-World .NET Programming
862
PART III
After compilation, we must register the .NET server with COM inside the Windows Registry to
make it visible. Prior to registration, we need to create a type library. .NET provides a number
of means for creating type libraries for interop purposes. Two of the most popular are the Table
Exporter utility (TlbExp.exe) and the Assembly Registration tool (Regasm.exe).
In our example, we’ll use the Regasm.exe utility. This enables us to create our type library and
register it at the same time. The following creates a type library called MyDNetServer.tlb and
registers our library (MyDNetServer.dll) with COM:
regasm MyDNetServer.dll /tlb:MyDNetServer.tlb
We have a couple options open to us for storing our library. When COM requests a .NET
server, the CLR searches either the requesting application’s directory tree for the library or the
Global Assembly Cache (GAC). Public servers—those that are used by more than one applica-
tion—should, as a rule, be stored in the GAC. See Chapter 22, “Deploying, Configuring, and
Licensing .NET Components,” for more information on the GAC.
In our example, we intend our math functions to apply to many applications, so we will install
the object to the GAC. To do so, we use the GacUtil.exe executable as follows and the para-
meter “/i” to indicate that we are installing.
gacutil /i MyDNetServer.dll
Last, we create a COM client that uses our .NET server. The following list outlines the steps
involved:
1. Create a simple VB6 forms application and add a button to the form.
2. Set a reference to the managed server, the same as you would any other COM server.
Note that you are actually setting a reference to the type library.
3. Inside the button’s click event, create a variable to reference the server. Notice that you
dimension the variable as the interface to .NET server. This is useful—although not nec-
essary—so that your code can get Intellisense on your server’s methods.
4. Finally, create an instance of your .NET server (MyDNetClass) and call its methods.
Private Sub Command1_Click()
‘call methods
MsgBox myServer.Divide(10, 5)
MsgBox myServer.Multiply(10, 5)
End Sub
.NET Interop with COM Applications
863
CHAPTER 18
Unknown
Dispatch
SomeClass
SomeInterface CCW SomeInterface
.NET
COM Client
Object
Unmanaged Managed
18
FIGURE 18.4
.NET INTEROP
APPLICATIONS
COM Callable Wrapper (CCW).
WITH COM
The runtime uses the metadata associated with the managed class to generate the CCW. This
metadata describes the types your object exposes and allows the CCW to proxy those types to
and from COM. Also notice that the COM client interacts with the CCW via the traditional
interfaces, IUnknown and IDispatch, which are created by the CLR. It should be noted that the
runtime always provides implementation for these specific interfaces. The CCW also imple-
ments a number of additional traditional COM interfaces, all of which are available for over-
ride by the .NET server object. Some of these include IErrorInfo, ITypeInfo,
IProvideClassInfo, IProvideErrorInfo.
Another key point about the CCW is that although it is created by the CLR, it is actually an
unmanaged COM object. As a result, it is reference counted like any other COM object and not
garbage collected. When the CCW ceases to have a reference, it is destroyed and the managed
object that it wraps is marked for garbage collection.
Interop Considerations
This brief section describes some additional considerations you need to be aware of when
designing and coding your application components for interoperation.
Real-World .NET Programming
864
PART III
Handling Events
The .NET event model based on delegates is very different from that of COM’s connection
point events. Fortunately, when you create a .NET client based on the interop assembly for a
COM server, the conversion process generates a .NET delegate for each COM event. Using
this method, you simply wire your event handlers to the given delegates as if they were native
to .NET. Similarly, VB6 COM clients interacting with .NET events will see those events as
native to COM, thanks to the exported type library.
Attribute Classes
The namespace System.Runtime.InteropServices provides a number of attribute classes that
can be used to further refine your interop servers. These classes enable you to control marshal-
ing, define methods that get exposed, and the like. Table 18.1 lists a number of these attribute
classes.
.NET INTEROP
APPLICATIONS
WITH COM
ated for the given class or set of classes
when a .NET export utility is used
to create a type library for COM.
ClassInterface has a Value
property that is of the type
ClassInterfaceType enumeration.
This enum’s values include
AutoDispatch, AutoDual, and None.
COMRegisterFunction Methods The COMRegisterFunction attribute
class tags a given function to be run
when the assembly is registered. This
enables you to ensure the execution of
your code during the registration
process. The CLR takes care of calling
the method when registering with the
RegAsm.exe utility. A given assembly
can have only one method marked as
COMRegisterFunction.
The counterpart to this attribute class is
COMUnregisterFunction, which gets
called when a .NET server is unregis-
tered with the RegAsm.exe utility.
Real-World .NET Programming
866
PART III
Summary
Interoperability is paramount to the success of modern application development and .NET
itself. For this reason, .NET exposes a number of interop possibilities and provides the level of
control that a given developer may need or can handle.
The following are key points of the chapter:
• .NET can reach into COM servers through the CLR-generated and managed Runtime
Callable Wrapper.
• COM developers integrating with .NET clients should create a Primary Interop Assembly
to wrap their object’s interfaces.
• You can create a custom wrapper for a given COM object through inheritance and over-
riding methods of a COM server’s PIA.
• .NET components can be called from COM clients through the CLR-generated unman-
aged COM Callable Wrapper.
• .NET components shared with a number of clients (COM or .NET) should be installed in
the Global Assembly Cache.
18
.NET INTEROP
APPLICATIONS
WITH COM
Managing Collections CHAPTER
19
of Objects
IN THIS CHAPTER
• Managing Collections of Objects 870
As Visual Basic programmers, we have a number of new options available for managing col-
lections of objects. The simple, but effective, collection object that we had become accustomed
to as VB developers is a relic. The System.Collections namespace adds a number of special-
ized collection classes to our programmer’s tool belt. This chapter focuses on those collections
and on indicating when you might implement a given solution.
After reading this chapter, you should
• Understand the differences between all the collection classes in the namespace
• Decide when to implement a given type of collection class
• Be able to write code using the various collection classes
• Create custom, strongly typed collection classes
System.Collections Namespace
Collections differ based on how elements are stored, sorted, searched, and compared. These are
the key considerations to examine when searching for the right class for the task at hand. For
example, items in collections are typically accessed by a key value or by indicating the index
of the item. Additionally, classes such as Queue and Stack are used to store items you expect to
remove from a collection. Table 19.1 presents the various collections in the library across these
parameters.
Managing Collections of Objects
871
CHAPTER 19
Stack
The Stack collection class stores objects in the reverse order they were placed in the collec-
tion. That is, adding a tenth item to a collection with nine items makes the new item the first
item in the collection and pushes each subsequent item down the “stack.” One becomes two,
two, three, and so on. As items are accessed or removed from the stack, they are done so
sequentially. So even though item one represents the last item to be added (10), it is the first
item to be accessed. This is also known as “last in, first out.”
One interesting thing about a stack is that you cannot change an item’s value after it has been
placed on the stack. You can only remove it from the stack. 19
Stacks can be very useful in the right context. They offer temporary storage for data that is
COLLECTIONS
OF OBJECTS
MANAGING
accessed in a particular manner. One of the most common uses of the stack collection is to
store the state of an object or variable between calls.
Constructors
Three constructors are available for creating a Stack instance. The first, which takes no para-
meters, creates a Stack object with a default initial capacity of 10. The Stack class doubles its
current size when its capacity is reached. If you have a good idea of what your Stack size will
be, you can save the overhead of doubling every time the capacity is reached, or from allocat-
ing more memory than is necessary, by using the constructor, New Stack(initialCapacity
as Integer). This constructor lets you initialize the Stack object to your intended size. Last,
you can create a new Stack instance from an existing collection object. This constructor takes
Real-World .NET Programming
872
PART III
an instance of a collection object derived from the ICollection interface. Items are added to
the Stack object, based on the order they are enumerated, using the IEnumerable interface.
We’ll discuss these interfaces in greater detail in just a minute.
myEnum = myStack.GetEnumerator
COLLECTIONS
function pushes a value onto the stack when it is called. The resulting Stack collection is
OF OBJECTS
MANAGING
exposed as a property of the class CallStack.
Additionally, we create a console application that creates an instance of StackClass, calls its
function SomeFunction, and writes the Stack contents to the command window.
Module Module1
Sub Main()
‘local scope
Real-World .NET Programming
874
PART III
‘pause
Do While Console.ReadLine <> “s” : Loop
End Sub
End Module
End Sub
End Function
End Function
COLLECTIONS
OF OBJECTS
MANAGING
Call SomeFunction2()
End If
End Function
Get
‘return the stack object for examination
Return myStack
End Get
End Property
End Class
Real-World .NET Programming
876
PART III
Queue
The Queue class is the opposite of the Stack collection and mimics the functionality most
closely associated with a message queue. Items are added and removed from a queue in a
“first-in, first-out” (FIFO) order. That is, the oldest item in the queue has the top priority and
is removed first. Queues are best used for managing message priority and transaction order.
Unlike message queues, the Queue collection class is persisted only in memory and not to disk.
Constructors
Four options are available for creating a Queue instance. The first allows us to create a Queue
instance that takes no parameters. It simply creates a Queue object with an initial capacity of 32
and a growth factor of 2. The initial capacity defines the starting size of the queue. When this
size is reached, the queue is expanded by multiplying its current size by the growth factor (in
this case it, doubles the queue’s size).
The second constructor enables you to indicate the initial size using the parameter capacity
as Integer for the Queue collection. This can save the overhead of doubling every time the
capacity is reached or from allocating more memory than is necessary.
A third constructor, capacity as Integer, growFactor as Single, enables you to indicate
both the starting capacity and the value by which the capacity is multiplied when the Queue
instance reaches its maximum.
Last, you can create a new Queue instance from an existing collection object. This constructor
takes an instance of a collection object derived from the ICollection interface. Items are
added to the Queue object based on the order in which they are enumerated using the
IEnumerable interface.
COLLECTIONS
OF OBJECTS
MANAGING
Learning by Example: Queue Collection
Listing 19.2 is a simple console application that writes three items to a Queue collection
instance using the Enqueue method. It then pulls those items out of the collection using the
Dequeue method. The results are output to the console window.
Module Module1
Sub Main()
Real-World .NET Programming
878
PART III
‘local scope
Dim myQueue As Collections.Queue
Dim i As Int16
Next
‘pause
Do While Not Console.ReadLine = “s” : Loop
End Sub
End Module
ArrayList
The ArrayList collection class is best used for working with a group of objects after they have
been added to the collection. Unlike Stack and Queue, the ArrayList collection enables you to
modify the value of an item after it has been added. Additionally, you can use the Sort method
to order the items in the array or the Reverse method to reverse all the items in the array. The
class has strong support for working with items as a range. Methods such as AddRange,
GetRange, InsertRange, and SetRange all support the manipulation of the ArrayList in
chunks. A number of methods also are used to find items within the collection, including
BinarySearch, Contains, IndexOf, and LastIndexOf. If you need a powerful and flexible col-
lection object that enables you to do a lot of manipulation on the collection, rather than simple
storage, you should consider an ArrayList.
Managing Collections of Objects
879
CHAPTER 19
Constructors
Three options are available for creating an ArrayList instance. The first, which takes no para-
meters, creates an ArrayList object with a default initial capacity of 16. The initial capacity
defines the starting size of the queue. When the ArrayList reaches its capacity, it automati-
cally doubles its capacity.
The second constructor enables you to indicate the initial size of the ArrayList collection,
capacity as Integer. Provided that you have a good idea of the size your collection needs
to support, this can save the overhead of doubling or from allocating more memory than is
necessary.
Last, you can create a new ArrayList instance from an existing collection object. This con-
structor takes an instance of a collection object derived from the ICollection interface. Items
are added to the ArrayList object based on the order in which they are enumerated using the
IEnumerable interface.
COLLECTIONS
ArrayList is of a fixed size. If True, items cannot be added or removed
OF OBJECTS
MANAGING
from the collection. See the FixedSized method for more details.
IsReadOnly The IsReadOnly property is a Boolean value indicating True if the col-
lection is read-only. See the ReadOnly method to create a read-only
ArrayList.
Item The Item property is used to return or to set the value of an item in the
collection. The property takes the parameter index as integer and is
of the type Object.
Methods
Add The Add method adds an item to the end of the collection. When the col-
lection’s capacity is reached, its capacity is doubled.
Real-World .NET Programming
880
PART III
COLLECTIONS
ArrayList object.
OF OBJECTS
MANAGING
Reverse The Reverse method is used to reverse the order of the elements con-
tained within the collection. The method can apply to the entire array, or
you can use an overloaded member to specify a range to reverse.
SetRange The SetRange method is used to copy a given collection into the
ArrayList, starting a specified index.
Sort The Sort method is used to sort the array using the QuickSort algorithm.
TrimToSize The TrimToSize method is used to set the collection’s capacity to the
current count of items contained in the collection. This enables you to
reclaim unused memory when you are sure that the collection will no
longer require additional items.
Real-World .NET Programming
882
PART III
Module Module1
Sub Main()
‘local scope
Dim myArray As New Collections.ArrayList()
Dim myRange As Collections.ArrayList
Dim myEnumerator As IEnumerator
Dim i As Short
‘pause
Do While Console.ReadLine() <> “s” : Loop
End Sub
End Module
Hashtable
The Hashtable class provides fast and easy access to items based on key values. Users of VB’s
dictionary object will find the Hashtable class very familiar because it derives from the inter-
face IDictionary.
Items are added to a Hashtable collection using a unique key value. In turn, keys are internally
converted to hash codes. A hash code is a numeric value that represents the unique key. Hash
codes are used to identify buckets inside the collection. Buckets are a subgroup of elements
within the collection; splitting the collection into buckets makes access and retrieval of ele-
ments faster. When the collection is partitioned into buckets and each bucket is indexed by a
hash code, there is no need to search the entire collection for an item. Instead, the bucket is
19
accessed based on the key and the item returned.
COLLECTIONS
The Hashtable class is best used when working with items that naturally contain a unique key.
OF OBJECTS
MANAGING
A row in a recordset is a good example of a candidate. Each column represents a unique key
for the value it contains. Users access the values by indicating the column name or key.
Constructors
The Hashtable class can be created using a large variety of constructors. The quantity is sim-
ply the result of the various combinations of all the possible parameters that can be passed on
the constructor. Rather than list all these constructors, we’ll describe all the possible parame-
ters and define how you might use them.
• capacity as Integer—Indicates the number of Hashtable buckets to be allocated to
the collection. The default capacity for a Hashtable instance is zero.
Real-World .NET Programming
884
PART III
COLLECTIONS
OF OBJECTS
MANAGING
GetObjectData The GetObjectData enables you to serialize the Hashtable
collection.
KeyEquals The KeyEquals method returns a Boolean value indicating whether
an item and key, both passed as parameters, are of equal value.
OnDeserialization The OnDeserialization method is used to raise an event when a
Hashtable deserialization is complete.
Remove The Remove method enables you to remove an item from the
Hashtable collection. To do so, you must specify the key of the
item to be removed.
Real-World .NET Programming
886
PART III
Module Module1
Sub Main()
‘local scope
Dim myHashtable As New Collections.Hashtable()
‘pause
Do While Console.ReadLine() <> “s” : Loop
End Sub
End Module
reason has to do with performance. The process of making objects thread-safe degrades
performance.
This process of ensuring thread safety is called synchronization. Synchronization ensures that
properties and methods of a given object are accessed by only one thread at a time. Sounds
good and safe, right? It is, but it requires the CLR to block threads from accessing synchronized
objects while another thread is executing a method or property. However, blocking equates to a
performance penalty. Therefore, thread safety via synchronization requires a performance hit.
This undesired performance hit coupled with the fact that the majority of .NET applications
will execute on a single thread makes objects in the library not thread-safe by default. If, how-
ever, you need a thread-safe version of a given object, typically an appropriate synchronization
method exists.
The collection classes that we’ve discussed thus far have an IsSynchronized property. This
property is of the type Boolean. The property returns True if the collection class instance is
synchronized. To create a synchronized version of a given collection, you use the
Synchronized method. This method returns a synchronized wrapper for your collection class.
The method takes the instance of the collection you want synchronized as its parameter.
The following code snippet creates an ArrayList object, adds a few items to the collection,
and then outputs a thread-safe version.
Dim myArray As New Collections.ArrayList()
Dim mySafeArray As Collections.ArrayList
myArray.Add(value:=”Test 1”)
myArray.Add(value:=”Test 2”)
myArray.Add(value:=”Test 3”)
mySafeArray = myArray.Synchronized(list:=myArray)
19
Console.WriteLine(myArray.ToString & “: “ & myArray.IsSynchronized)
COLLECTIONS
OF OBJECTS
MANAGING
Console.WriteLine(mySafeArray.ToString & “: “ _
& mySafeArray.IsSynchronized)
CollectionBase
The CollectionBase class is the abstract base class specifically designed for defining custom,
strongly typed collections. Internally, the CollectionBase class uses an IList object for stor-
ing elements. This object is accessed through the CollectionBase.List property. The IList
class should look familiar because a number of frequently used objects implement it, including
ArrayList, ImageList.ImageCollection, ComboBox.ObjectCollection, and
ListView.ListViewItemCollection, to name a few. IList has an Item property used to store
and retrieve collection elements based on an index value. Additionally, it has the standard
methods Add, Insert, Remove, Clear, Contains, IndexOf, and RemoveAt that you are familiar
with from the previous section.
We’ll now walk through the process of defining a custom collection class. Before we begin, we
must define a class to use as our type. In our example, we define a simple order object. The
object has a constructor that requires an order number to create the object. Additionally, we
define two other properties: Quantity and Price.
Public Class MyOrder
End Sub
End Class
Next, we begin defining a custom collection class called MyOrders. We indicate that the class 19
inherits from CollectionBase. We then create a couple of methods used for adding objects to
COLLECTIONS
OF OBJECTS
MANAGING
our collection. AddFromValues enables a user of our class to add an element to the collection
by passing all the appropriate values of a MyOrder object. In this method, we create a new
instance of MyOrder, set its properties, and add the object to the CollectionBase class’s inter-
nal IList object MyBase.List.Add. We also create a method called Add that simply takes a
MyOrder instance and adds it to the underlying collection.
Inherits CollectionBase
End Sub
End Sub
Next, we create an Item property to allow other developers to access and set items within our
custom collection. We define the property as the default for our collection class. This is
allowed because the property has a parameter. To access items in the base class’s collection
object, we call MyBase.List.Item and pass an index parameter.
Get
Return CType(MyBase.List.Item(index:=index), MyOrder)
End Get
End Property
We expose a couple of additional methods for working with our collection: IndexOf and
Insert. The IndexOf method enables users to return the index value of the given MyOrder
instance within the collection. The Insert method allows users to insert a specified MyOrder
object at a specific index position.
Managing Collections of Objects
891
CHAPTER 19
‘purpose: return the index of the specified object within the array
Return MyBase.List.IndexOf(value:=value)
End Function
End Sub
Finally we override three type-checking events on the base class: OnInsert, OnSet, and
OnValidate. These events are raised when items are inserted, changed, or validated. This
ensures that items added to the collection are not changed to an invalid type afterward. If the
value of the item being checked is not of the type MyOrder, we throw an exception.
COLLECTIONS
OF OBJECTS
MANAGING
Protected Overrides Sub OnSet(ByVal index As Integer, _
ByVal oldValue As Object, ByVal newValue As Object)
End Sub
End Sub
End Class
That’s it. We’ve just created a custom, strongly typed collection using CollectionBase. All
that is left is to demonstrate its use by defining a simple client. The following is a console-
based application that creates an instance of MyOrders and adds MyOrder objects to it.
Module Module1
Sub Main()
‘local scope
Dim orders As New MyOrders()
Dim order As MyOrder
‘insert an order
orders.Insert(index:=2, value:=New MyOrder(“104”))
‘reset a value
order = New MyOrder(number:=105)
orders.Item(index:=2) = order
‘display index
Console.WriteLine(orders.IndexOf(value:=order))
‘pause
Do While Console.ReadLine <> “s” : Loop
End Sub
End Module
Managing Collections of Objects
893
CHAPTER 19
DictionaryBase
The System.Collections namespace also defines the class DictionaryBase for creating cus-
tom collections based on a dictionary format (key/value pairs). Fortunately, the concepts
here are the same as in CollectionBase. Instead of an underlying IList object, the
DictionaryBase class has a Dictionary property that is of type IDictionary. This is the
same object from which collection classes such as Hashtable and SortedList derive.
Using our preceding order example, we can create a new collection class that derives from
DictionaryBase. This class accesses items in the base class’s dictionary collection by a key
value. In our example, we’ll create an Add method that takes a key as a parameter. We will then
change the Item property to access items in the collection by a key value.
Public Class MyOrders
Inherits Collections.DictionaryBase
End Sub
Get 19
Return CType(MyBase.Dictionary.Item(key:=key), MyOrder)
COLLECTIONS
End Get
OF OBJECTS
MANAGING
Set(ByVal Value As MyOrder)
MyBase.Dictionary.Item(key:=key) = Value
End Set
End Property
End Class
Now we’ll modify our client to add and access items from the collection based on a key. In our
case, we’ll use the order number as our key value.
Real-World .NET Programming
894
PART III
Module Module1
Sub Main()
‘local scope
Dim orders As New MyOrders()
Dim order As MyOrder
‘pause
Do While Console.ReadLine <> “s” : Loop
End Sub
End Module
Summary
The Framework Class Library provides a host of powerful, easy-to-use collection classes. The
next time you need to manage a group of data, you should be well equipped to choose the right
solution.
The following key points were presented in this chapter:
• The Stack collection class is used for last-in, first-out access.
• The Queue collection class provides first-in, first-out access.
• The ArrayList collection class is powerful when manipulating a collection in ranges.
• The Hashtable collection class provides fast access to items based on keys.
• The Synchronized method on a collection class returns a thread-safe wrapper for the
given class.
• You can use classes such as CollectionBase and DictionaryBase to implement your
own strongly typed collection classes.
Profiling, Debugging, CHAPTER
20
and Exception Handling
IN THIS CHAPTER
• Handling Errors with Structured Exception
Handlers 896
In this chapter, we discuss the different methods of debugging and optimizing managed .NET
applications. The task of troubleshooting and debugging software is a difficult one that begins
in the design phase of the software development process. We will look at the different options
open to Visual Basic .NET developers for ferreting out runtime and logic problems that may be
present in your code. Starting with exception handling, we examine the different varieties of
error-handling constructs available in Visual Basic .NET. Then we examine the concepts of
instrumentation and tracing as an additional complement to error handling. Finally, we look at
how to use various .NET components in an attempt to identify and fix performance issues with
managed code.
Although this chapter does not focus on any one namespace, we will be seeing a lot of the
System.Diagnostics namespace because of its tight integration in the debugging mission.
After reading this chapter, you should have a firm grasp of the following:
• Implementing structured exception handlers
• Instrumenting your code using the Debug and Trace classes from the
System.Diagnostics namespace
NOTE
Unstructured exception handling is also supported through the “old style” method of
using On Error commands and inline procedure blocks for handling errors.
reader = File.OpenText(fileName)
content = reader.ReadToEnd()
reader.Close()
End Function
Catch statements can actually be set up using one of two kinds of filters. The first, an excep-
tion class filter, attempts to determine the nature of the error that was encountered. When man-
aged code in the .NET runtime encounters a runtime error, it classifies the error according to
the Exception class it represents. In other words, each class in the Framework Class Library
will typically have a documented list of exceptions that may be thrown on an error. For
instance, the StreamReader.ReadToEnd method will throw an IOException when an error
occurs. This thrown exception can be caught using a Catch statement, like this:
Catch ioErr As IOException
The second type of evaluation that can be forced with a Catch statement is a simple Boolean
expression. You might use this to test for a specific error number or to test whether a switch
has been set inside your code. The following Catch block will be executed only if the error
number is 1001:
Catch When ErrNum = 1001
‘code to execute
Neither the exception class identifier nor the Boolean When evaluator is required. A naked
Catch statement will simply be executed anytime an exception that derives from
System.Exception is raised:
Catch
‘code to execute
You can specify multiple Catch blocks. The runtime will execute the first one that matches on
the current conditions.
Profiling, Debugging, and Exception Handling
899
CHAPTER 20
NOTE
If an exception occurs and the runtime is unable to find a Catch statement that can
handle the error, the error will get passed up through the stack of calling procedures
until a valid Catch statement is found. If no Catch statements are found by the time
the error reaches the outermost procedure on the stack, an actual runtime error mes-
sage will be displayed to the screen. This is exactly what you don’t want to happen in
your applications.
Module Module1
Sub Main()
SearchInFile(“test”, “c:\abc.txt”)
End Sub
Try
PROFILING,
EXCEPTION
HANDLING
reader = File.OpenText(fileName)
Try
Real-World .NET Programming
900
PART III
Finally
reader.Close()
End Try
End Try
End Function
End Module
The console application in Listing 20.1 immediately fires off the SearchInFile function with
the values “test” and “abc.txt”. The Try...Catch...Finally blocks in the SearchInFile func-
tion are obvious.
Notice that we have actually nested two exception handlers in the code. One deals with prob-
lems calling the OpenText method, and the other deals with problems encountered issuing the
ReadToEnd method call. We test explicitly for two classes of exceptions with the OpenText
method: a SecurityException, which would be thrown if the application didn’t have permis-
sion to access the file, and a FileNotFoundException, which would be thrown if the file could
not be located at the specified location or filename.
The exception handler that deals with the reader.ReadToEnd statement explicitly catches
IOException instances that are thrown. In this case, because it is almost impossible to guess
the cause of the error, we write out the error message to the screen so that the user can perhaps
troubleshoot the problem. A few different properties and methods implemented by the
Exception class objects are useful inside of error handlers.
Profiling, Debugging, and Exception Handling
901
CHAPTER 20
Variable Scope
It is important to know that each block in a Try...Catch...Finally statement carries its own
scope. For instance, in the following code, we see a routine that is trying to open a database
connection. The code is wrapped in an exception handler, complete with a Finally block that
will close the connection:
Sub ConnectToDb()
Try
Dim dbConn As OleDbConnection = New OleDbConnection(connStr)
.
.
.
Catch
‘handle any error here
Finally
dbConn.Close()
End Try
End Sub
This code, as written, will not even compile. Why? The dbConn object is declared inside the
Try block and therefore does not exist (is out of scope) inside the Finally block. The
dbConn.Close method call won’t be allowed; a syntax error of ‘The name dbConn is not
declared’ will result. To really make this work the way it was intended, the code would have
to be rewritten to place the database connection object declaration in a code block that has
scope superiority over the Finally block:
Sub ConnectToDb()
Try
dbConn = New OleDbConnection(connStr)
.
.
.
Catch
‘handle any error here
20
DEBUGGING, AND
Finally
dbConn.Close()
PROFILING,
EXCEPTION
HANDLING
End Try
End Sub
Real-World .NET Programming
902
PART III
NOTE
You can define your own custom exception classes to deal with errors specific to your
application. As a general rule, if you are going to create your own exception class, it
should inherit from System.Exception (or one of its children classes).
Figure 20.2 shows a stack trace message triggered from the SearchInFile function in our sam-
ple console application. The StackTrace property will return a string that identifies the path of
execution inside the application, from the latest method call through to the application module.
It will also identify the line number of the offending code. This is not the sort of thing that you
would want to show a user, but it is useful to a developer because of the sheer volume of tech-
nical information it presents.
Profiling, Debugging, and Exception Handling
903
CHAPTER 20
Exception
ApplicationException SystemException
InvalidOleVariantTypeException TypeLoadException
InvalidOperationException TypeUnloadedException
bold = has derived classes
InvalidProgramException UnauthorizedAccessException
LicenseException VerificationException
ManagementException WamingException
MarshallDirectiveException XmlException
MemberAccessException XmlSchemaException
NullReferenceException XmlSyntaxException
MultiCastNotSupportedException XaltException 20
DEBUGGING, AND
NotImplementedException
PROFILING,
EXCEPTION
HANDLING
NotSupportedException
FIGURE 20.1
System.Exception and derived classes.
Real-World .NET Programming
904
PART III
FIGURE 20.2
A StackTrace message.
Table 20.1 shows all the defined properties and methods of the System.Exception class. Keep
in mind that specific classes that derive from the base System.Exception class will implement
their own versions of these properties. For example, we saw in Chapter 8, “Networking
Functions,” that a SocketException class returns an ErrorCode property that corresponds to
the error codes defined in the Winsock library.
Throwing Exceptions
So far, we have concentrated on what to do if your code encounters an exception. But what do
you do if you want to throw an exception on purpose? There will certainly be times when you
must develop a component that will flag to its consumers that something has gone wrong:
Either an actual runtime error has occurred, or perhaps a business rule has been broken. Either
way, you can infuse into your components the capability for them to throw their own excep-
tions, much like the Framework Class Library classes throw theirs.
The key is the Throw statement. The Throw statement enables you to instantiate a new excep-
tion object, provide it with an appropriate error message, and raise it to any listening error han-
dlers. Consider the following code, which we could insert into the SearchInFile function:
If searchValue.Length = 0 Then
Throw New ApplicationException(“You must provide a search value.”)
End If
In this case, we are checking to make sure that one of the function’s input parameters,
searchValue, actually has content. If it doesn’t, we enforce the business rule by throwing an
ApplicationException instance with the message You must provide a search value.
Putting this code into Listing 20.1 doesn’t make a whole lot of sense, but if we choose to com-
pile the SearchInFile function into a class library to be consumed by other objects, it would
make a whole lot of sense. It protects the function from returning a value that might be mis-
leading; for instance, was the searchValue really not found in the file, or was a problem else-
where causing this value to be empty by the time it makes it into the function?
In a similar fashion, we can assist an exception to bubble up with the correct information. For
instance, consider this code:
Sub Main()
Dim appSettings As String
Try
20
DEBUGGING, AND
.
.
.
Real-World .NET Programming
906
PART III
End Try
End Function
It shows a hypothetical (albeit poorly designed) application that stores some of its startup
information in a text file that it expects to find in a specific, hard-coded directory. If something
happens to this file, an exception will be raised on the line that tries to read it:
reader = File.OpenText(path & FILENAME)
Because we have an exception handler to deal with this problem, this is not a complete disas-
ter. However, the user may not be able to determine enough information to accurately trou-
bleshoot and solve the problem, because the user has no knowledge of the internal
machinations of the program.
In other words, the user possibly would be presented with an error message saying that the
application could not locate the specified file, but it would be far more useful if the message
stated exactly what was happening—such as, This application can’t find its configu-
ration file in its default directory. Please try to locate the file on the
original CD and copy it into the directory, or reinstall the application.
To be faithful to the idea of structured exception handling, you want the calling component to
be responsible for dealing in an appropriate way with any errors encountered. By extending the
structured exception handling design pattern throughout all your code, you can treat each error
in a consistent fashion.
Consider this small rewrite that handles the problem by throwing a new exception with more
detailed information for the user or calling component:
Sub Main()
Dim appSettings As String
Try
Profiling, Debugging, and Exception Handling
907
CHAPTER 20
.
.
.
End Try
End Function
Now that you have a good handle on the mechanics of error handlers, we can look at the
System.Diagnostics namespace support for debugging applications.
NOTE
We don’t touch on the use of unstructured exception handlers in this chapter because
their use is not recommended (performance and general design penalties can be
involved with implementing them). However, the “old” Visual Basic style of error
handling with On Error GoTo and On Error Resume Next is still supported in Visual
Basic .NET. If you must use unstructured exception handlers in your code, be aware
that you cannot mix structured and unstructured handlers inside the same procedure.
The .NET Framework Class Library has some great support for instrumentation within the
PROFILING,
EXCEPTION
HANDLING
ProcedureA()
ProcedureB()
ProcedureC()
Debug.Unindent()
Debug.WriteLine(“Main: Exit”)
End Sub
Sub ProcedureA()
Debug.WriteLine(“ProcedureA: Enter”)
‘
‘Do some work here
‘
Debug.WriteLine(“ProcedureA: Exit”)
End Sub
Sub ProcedureB()
Debug.WriteLine(“ProcedureB: Enter”)
‘
‘Do some work here
‘
Debug.WriteLine(“ProcedureB: Exit”)
End Sub
Sub ProcedureC()
Debug.WriteLine(“ProcedureC: Enter”)
‘
‘Do some work here
‘
Debug.WriteLine(“ProcedureC: Exit”)
End Sub
Profiling, Debugging, and Exception Handling
909
CHAPTER 20
In the preceding code, we are using the Debug.WriteLine method to log some informational
messages that will be useful in terms of tracing the flow of the application. These messages are
actually being broadcast to a collection of objects called listeners. (We’ll get more into the con-
cept of listeners later in the chapter.) Even without the explicit knowledge of exactly where
these debug messages are going, it is easy to discern the main function of the Debug (and the
Trace) class. They expose a series of methods and properties that are useful for logging infor-
mation about the execution of a program.
If you are like many developers, you have probably used message boxes or event log entries to
do the same thing. In .NET, the Framework provides the Debug and Trace classes to enable
you to do this in a more formal and structured manner. Of course, you aren’t limited to writing
static messages; you could be writing out the contents of variables, whether specific conditions
are present, who is logged in, and so on. This is the essence of instrumentation. Fully instru-
menting your code, however, could have serious repercussions for code size and performance.
Chances are that you will not want the production release of your code spewing out thousands
of lines of debug information every time it is run. In the past, this has usually involved brute
force maintenance of separate source-code branches in a version control system; one version
has the debug code in it, the other doesn’t. This is not a good way to go, because it has a lot of
potential for human error. A slightly better way in Visual Basic 6.0 was the use of compiler
constants. You could bracket certain blocks of code and then set a compiler flag to indicate
whether that code should be executed. But this still did not address the actual presence of the
debug code in the released executable.
In .NET, this process has become much more formalized. The Visual Studio .NET IDE and the
command-line compilers all have the capability to specifically compile a debug or a release
build of the software. Before getting more in-depth with the Debug class, let’s quickly see
how we can use the Visual Studio IDE to build a debug or a release version of a project.
The beauty of the Debug and Trace classes is that if you have your project set to a Debug
PROFILING,
EXCEPTION
HANDLING
build, all the Debug class code you have embedded in it will be compiled into the resulting
assembly and will execute. If you have your project set to a release build, the Debug code will
not be included and will not execute. Because the .NET compilers support the capability to
Real-World .NET Programming
910
PART III
turn Debug and Trace code on and off individually, you have full control over the exact level of
tracing that will happen in any build configuration of your application.
FIGURE 20.3
The Visual Studio Project Property dialog box.
To actually set the options for enabling Debug code and Trace code, you need to click the
Build entry under the Configuration Properties (see Figure 20.4). Notice the two check boxes
toward the center and bottom of the screen. This is where you can explicitly tell the compiler
to include or not include Debug or Trace code.
FIGURE 20.4
Compiler options for Debug and Trace code.
In practice, Debug code is for Debugging alpha or beta software; Trace code is for instrument-
ing production code.
Profiling, Debugging, and Exception Handling
911
CHAPTER 20
NOTE
All the listener classes are derived from the base TraceListener class. This base class
can also be used to create your own listener classes.
As each Debug or Trace object issues a write command, the content of the write command is
made available to each listener object identified in the Listeners property (which returns an
instance of TraceListenerCollection). No difference exists between the listeners in terms of
which trace messages they receive; if they are in the listeners collection, they receive the same
message.
To see how all this comes together, look at the code presented in Listing 20.2. Here we are tak-
ing a simple console application that, when launched, calls two functions that manipulate an
integer value. At the end of the program, the final value is written out to the console.
Module Module1
HANDLING
Sub Main()
‘This is the debug log file
Dim logFile As Stream = File.Create(“debug.log”)
Real-World .NET Programming
912
PART III
Debug.WriteLine(“Main: Enter”)
Debug.Indent()
someVal = ProcedureB(ProcedureA(someVal))
Debug.Unindent()
Debug.WriteLine(“Main: Exit”)
End Sub
Dim i As Integer
For i = 1 To 10
Debug.Indent()
Debug.WriteLine(“Loop counter = “ & i & “; number = “ & number)
Debug.Unindent()
ProcedureA = number
Debug.WriteLine(“ProcedureA: Exit”)
Debug.Unindent()
End Function
Profiling, Debugging, and Exception Handling
913
CHAPTER 20
Dim i As Integer
For i = 10 To 1 Step -1
Debug.Indent()
Debug.WriteLine(“Loop counter = “ & i & “; number = “ & number)
Debug.Unindent()
number = number + i
Next
ProcedureB = number
Debug.WriteLine(“ProcedureB: Exit”)
Debug.Unindent()
End Function
End Module
To investigate the actual output of the debug code, we have littered the application with Debug
statements. We have also added a TextWriterTraceListener object to our Debug.Listeners
collection; this will have the effect of logging each debug message to the specified debug.log
file.
Note that you can also affect the listeners in force for your application by modifying the appli-
cation’s configuration file. For instance, you can edit the application configuration file to add a
TextWriterTraceListener, like this:
<configuration>
<system.diagnostics>
<listeners>
<add name=”listener” type=”TextWriterTraceListener”
parameter=”c:\\debug.log” />
</listeners>
</system.diagnostics>
</configuration>
20
DEBUGGING, AND
Figure 20.5 shows the content of the debug.log file after running the application once. Notice
PROFILING,
EXCEPTION
HANDLING
that the display is kept readable by using the Debug.Indent and Debug.Unindent commands to
format the messages.
Real-World .NET Programming
914
PART III
FIGURE 20.5
Debug output directed to a file.
To see the effect that the project build configuration has on the debug code, try creating a new
console application inside Visual Studio .NET, copying the code from Listing 20.2 into it and
then running the code with a debug configuration. You should notice two things: The debug
output window in the IDE should show all the debug messages written out during execution,
and in the application’s bin directory, you should see a file that matches the file shown in
Figure 20.5. Now try setting your build configuration to release, and rerun the application.
Nothing will be written to the debug output window, and nothing will be written to the
debug.log file.
NOTE
A command-line executable called dbmon.exe ships with the MSDN Platform SDK.
This monitor tool enables you to see any and all debug messages issued by any appli-
cation running on the local machine. This can be a quick and easy way to intercept
and view trace messages sent out through the DefaultTraceListener.
certain areas of an application. If the switch is enabled, tracing information is broadcast to the
listeners. Toward this end, the BooleanSwitch class exposes exactly one property, Enabled,
which is a Boolean value representing the current state of the switch.
The TraceSwitch class is used to provide a more granular level of control. Instead of just
working against an “on” or “off” position, a TraceSwitch can be set to a number of levels.
TraceSwitches are useful when you want to control the level of debugging from off to verbose
and steps in between. The TraceSwitch.Level property is used to indicate the current value of
the TraceSwitch object. This returns a TraceLevel enumeration value (see Table 20.3), which
indicates the current state of the switch.
The TraceSwitch class also lets you check individual Boolean properties to indicate a level of
tracing by exposing a separate Boolean property for each TraceLevel enumeration value. Thus
we have TraceSwitch.TraceError, TraceSwitch.TraceInfo, TraceSwitch.TraceOff,
TraceSwitch.TraceVerbose, and TraceSwitch.TraceWarning.
NOTE
Note that the Level property operates on a continuum; if you look at the TraceLevel
enumeration values, you can see that they represent a cumulative level of tracing
that follows this order:
Off -> Error -> Warning -> Info -> Verbose
A level of Info will enable trace messages for warnings and errors, as well.
Creating Switches 20
DEBUGGING, AND
To create a switch, you simply dimension an instance of a particular switch class, assigning it
PROFILING,
EXCEPTION
HANDLING
some initial values. In the following, we are creating both a trace and a Boolean switch:
Dim dataSwitch As New BooleanSwitch(“appDebug”, “Application level”)
Dim generalSwitch as New TraceSwitch(“dbAccess”, “All database access”)
Real-World .NET Programming
916
PART III
Notice that the constructors for both a BooleanSwitch and a TraceSwitch take two parameters:
The first one is the display name of the switch, and the second is the description of the switch.
In a similar fashion, we can use a TraceSwitch instance to determine the level of trace mes-
sages that are sent out.
Dim debugSwitch As New TraceSwitch(“debugSwitch”, “Global debug switch”)
Try
‘Do work here
End Try
<configuration>
<system.diagnostics>
<switches>
<add name=”debugSwitch” value=”4” />
</switches>
</system.diagnostics>
</configuration>
The numeric values for the TraceSwitch.Level property follow in sequence from the
TraceLevel enumeration values, which are
0 = Off
1 = Error
2 = Warning
3 = Info
4 = Verbose
Using Assertions
Peripherally related to tracing is the concept of assertions. The Debug and Trace classes have a
method, Assert, which enables you to test for a specific condition in your code. Assert state-
ments are typically used to test assumptions in your code and are particularly useful during the
active debugging process. The most common version of the Assert method takes a Boolean
expression and a string parameter; if the expression evaluates to True, the program will
immediately enter break mode and the string will be raised to the screen.
A common example used to illustrate the case for assertions is a block of code that performs
division, such as
Private Function DivideSomeNumbers(ByVal num1 As Integer, _
ByVal num2 As Integer) As Double
Try
DivideSomeNumbers = num1 \ num2
Catch
‘error code goes here
End Try
End Function 20
DEBUGGING, AND
Even though an error handler is present, an assertion in this function will help to heavily
PROFILING,
EXCEPTION
HANDLING
Try
Debug.Assert(num2 <> 0, _
“!!!num2 parameter is unexpectedly equal to 0”)
DivideSomeNumbers = num1 \ num2
Catch
‘error code goes here
End Try
End Function
Assertions are a good form of defensive programming. They can highlight brittle areas of code
and point out areas of an application that need to have more safeguards implemented so that
assumptions always hold true.
NOTE
Assertion messages are actually raised using the Fail method (on either the Debug or
Trace class). This has the subsequent effect of calling the Fail method on each lis-
tener in the listeners collection. If you want to change how and where assertions are
displayed to the screen, you can simply alter the listeners collection or write your own
listener that implements the Fail method in the fashion you desire.
Like the other debugging mechanisms we have discussed, assertions can also be set up in an
applications configuration file like the following (for more information on the exact syntax
requirements, see the MSDN documentation):
<configuration>
<system.diagnostics>
<assert assertuienabled=”true” logfilename=”.\TraceLog.txt”/>
</system.diagnostics>
</configuration>
Profiling Applications
Profiling is nothing more than the analysis of performance data collected from an application
while it is running. Profiling is a useful technique for pinpointing the slowest performing areas
of code so that they can be targeted for code improvements. Microsoft has a tool designed to
view and track performance data: the Performance Monitor MMC snap-in. This can be
launched from a command prompt by just typing “perfmon.” The Performance Monitor
Profiling, Debugging, and Exception Handling
919
CHAPTER 20
application feeds off objects called performance counters. These are individual, instrumented
pieces of the core operating system that expose a numerical value representing some dimension
of performance.
Because the .NET Framework ships with a comprehensive set of performance counters, the
first step in profiling a .NET application can start with the default set of performance counters.
A set of performance counter classes in the Systems.Diagnostics namespace also allows you
to define your own performance counter.
FIGURE 20.6
.NET runtime performance counters.
Each of these can be used to trap historical, benchmark data across a variety of different .NET
operations. Using perfmon in conjunction with specific, carefully identified counters is proba-
bly the quickest and easiest way to profile an application. But this may not yield the exact
information that you are looking for at an application level. In some cases, you may need to
create your own performance counters.
can create your own performance counter by instancing an object of this class and then assign-
PROFILING,
EXCEPTION
HANDLING
FIGURE 20.7
.NET runtime performance counters.
Notice that there are three discrete identifiers for a performance counter: the object (set to
Processor in this example), the actual counter (set to % Processor Time), and the instance (set
to _Total). The object can be thought of as the category for a performance counter. The counter
is the actual dimension being measured, and the instance represents what application or refer-
enced unit the dimension applies to (_Total is often used to represent the sum or average of all
available instances).
Following is a short example of the creation of a custom performance counter. We are using
the PerformanceCounterCategory.Create method to do two things at once: It will create a
new category for our counter and create the counter itself.
PerformanceCounterCategory.Create(“myApp”, “myApp Description”, _
“lingerCounter”, “lingerCounter Description”)
After creating the performance category and the counter, we can bind to it by creating a
PerformanceCounter object and passing in the counter name, and so on, to the constructor:
After the counter is created, you can write values to the counter using its RawValue property.
The PerformanceCounter class also has methods for incrementing and decrementing the raw
value by a set amount (IncrementBy and DecrementBy) and for incrementing or decrementing
the RawValue by 1 (Increment and Decrement). Before attempting to write to the counter, you
first need to set its ReadOnly property to False.
Performance counters may contain calculated values instead of a raw value. This can be deter-
mined or set through the PerformanceCounter.CounterType property. This property returns or
accepts a PerformanceCounterType enumeration (see Table 20.4) that indicates the exact
behavior of the counters value.
Profiling, Debugging, and Exception Handling
921
CHAPTER 20
such as the number of logins for your application over time or the number of files, orders,
parts, and so on processed over time. All these clues can help to determine whether any perfor-
mance problems exist in a project and exactly what body of code is responsible.
Summary
The .NET Framework supplies developers with an adequate stock of components and technolo-
gies aimed squarely at improving the quality of applications. In this chapter, we have looked at
a few of these items that are focused on the task of debugging applications. We covered
• Implementing structured exception handlers
• Instrumenting applications using the Debug and Trace classes
• Controlling the actions of the Debug and Trace classes through switches
• Leveraging stock and custom performance counters for general application performance
profiling
20
DEBUGGING, AND
PROFILING,
EXCEPTION
HANDLING
Globalization and CHAPTER
21
Localization Techniques
IN THIS CHAPTER
• Globalization and Localization 926
Distributed applications built today often transcend one language or one culture. More and
more developers and architects are being presented with the task of creating interfaces that are
multilingual and with translating data across cultural and regional boundaries. In fact, it is typi-
cal that architects of a system are unsure regarding all the specific cultures a language may
need to support. These applications have to be developed with a global perspective—that is,
they must assume the need to support as many languages and cultures as possible. In the past,
this has meant either multiple versions of the same application (and thus multiple support and
extension teams) or bulky and slow code that is difficult to extend. .NET intends to make the
task of building a global application easy and manageable.
This chapter is focused on teaching you how to create applications with VB .NET and the FCL
that supports multiple cultures. The chapter discusses classes in the System.Globalization
and System.Resource namespaces. We start by defining the process of globalization and local-
ization. We then move through the specifics of working with data and creating and accessing
resource files. We end the chapter by creating a multicultural form application.
After reading this chapter, you should
• Understand the process of designing an international application
• Be able to work with dates, currencies, and the like across cultures
• Be able to create and access a resource file
• Be able to create and deploy a global application
Culture
When we refer to culture, we are talking about a set of predefined preferences based on a lan-
guage, country, and possibly a region. Language is not enough to define a user’s preferences.
For example, both the U.S. and Great Britain have a default language of English. However,
when formatting a currency value, the U.S. uses dollars ($), whereas the U.K. uses pounds (£)
or even Euros (€). A user’s culture signifies how currencies, dates, time, and the like should be
displayed, compared, and sorted.
Globalization and Localization Techniques
927
CHAPTER 21
A specific culture is one that defines both a language and a country or region. A standard exists
21
(RFC 1766) that defines a culture by a two-letter language code, a dash, and a two-letter coun-
GLOBALIZATION
try/region code that is associated with a specific country or region. Table 21.1 demonstrates a
LOCALIZATION
TECHNIQUES
number of specific culture codes (for a complete list, see MSDN, CultureInfo Class).
AND
TABLE 21.1 Common Culture Codes
Culture Code Language–Country/Region
de-DE German–Germany
en-AU English–Australia
en-CA English–Canada
en-GB English–Great Britain
en-IE English–Ireland
en-NZ English–New Zealand
en-US English–United States
es-Mx Spanish-Mexico
fr-CA French–Canada
fr-FR French–France
he-IL Hebrew–Israel
hi-IN Hindi–India
ja-JP Japanese–Japan
Culture codes are used by your application to determine how to represent dates, currency, num-
bers, and the like. They are also used to look up resources for your application. Resources are
files that contain cultural and language-specific translation of items inside your application. For
example, your user interface might have a button that reads Cancel. A resource file would con-
tain information on how to translate the word cancel for a given language and culture
(resources are covered more fully later in the chapter).
A neutral culture is defined by only a language code. For example, “en” and “fr” refer to
English and French, respectively. Your application can use neutral rather than specific culture
codes when no need exists to communicate data down to the region or country. Additionally,
you can create both a neutral and specific culture for your application to implement. This way,
your application can fall back to a neutral culture when a specific one does not exist. For
example, if your application defines resources for en-CA (English-Canada), en-GB (English-
Great Britain), and a user with a setting of en-US (English-United States), your application
will first try to find the U.S. version of your resource. After failing, it will search for a neutral
culture definition (en in our case).
Real-World .NET Programming
928
PART III
There also exists the concept of an invariant culture. An invariant culture is said to be culture
insensitive, neither neutral nor specific. It is a culture based on English. The invariant culture
ensures a known format when data is read or written by users of varying cultures. For instance,
a date value stored in the format of the invariant culture can be read by the system, translated
to the appropriate format, modified by the user, and then retranslated back to the invariant cul-
ture before the underlying value is updated. Another good use for invariant cultures is system
services because they typically can run independently of language or culture.
Design Considerations
Now that you understand the concept of culture, you can start to appreciate how different the
architecture of an international application will look. In .NET, the typical global application
has a core assembly that contains resources based on a default or neutral culture. The code
in the application understands how to manage culture and language. The default cultural
resources are compiled with the assembly and used as the fallback culture. Additionally, the
application links to a number of satellite assemblies (made up of resource files) for specific
culture translation. This model is illustrated by Figure 21.1.
de-DE
ja-JP
Core
Default/ Assembly
Neutral
Resources
en-US
FIGURE 21.1
Satellite assemblies.
If you are in the midst of creating a new application and have an indication that you might
need to support more than one culture, you should consider the benefits of switching to this
model sooner rather than later. It is relatively straightforward to design new applications using
this model; however, retrofitting existing applications (even those originally created with .NET)
Globalization and Localization Techniques
929
CHAPTER 21
can be a much bigger challenge. In addition, the costs to convert an application are far greater
21
than the extra dollars spent at the time of the application’s inception.
GLOBALIZATION
LOCALIZATION
TECHNIQUES
When designing your application, it’s important that you determine cultural support. It is not
imperative that you define all the cultures you want to support, but you should at least be able to
AND
define a default or neutral culture. A default culture represents the primary culture of the major-
ity of your users. It is also the fallback option when a user’s specific culture cannot be found.
After an application has been built to support more than one culture, the process of adding sup-
port for additional cultures is rather simple and requires little, if any, actual coding changes
(and no architectural changes). You simply create a new resource file for the target culture and
deploy it accordingly. Users who now prefer the newly added culture will see your application
localized for their needs.
CultureInfo
A user’s preferred cultural setting is represented by the CultureInfo class in .NET. This class
represents everything about a culture: its writing system, calendar, currency formats, and so on.
To create an instance of the class, you can pass a culture code to it on the constructor. For
example:
Dim myCulture As New System.Globalization.CultureInfo(name:=”en-GB”)
To use the CultureInfo class to influence the behavior of your code, you change the
CurrentCulture property on the executing thread, as in the following:
Threading.Thread.CurrentThread.CurrentCulture = myCulture
Using this example and writing out the value of a call to Now prior to setting the thread’s cul-
ture produces “9/23/2001 9:18:52 PM.” After changing the culture to the U.K., you get the date
in the format of month/day/year and the time in 24-hour format: 23/09/2001 21:18:52.
This example demonstrates the result of explicitly setting your application’s culture. This may
be useful only when testing your application or when users are allowed to change their culture
on-the-fly. More than likely, your application will implicitly change cultures based on the
user’s machine settings as set by the Regional Options control panel. This dialog box allows
users to override specific regional settings for their given culture. These overrides might cause
unexpected results for your application. Therefore, you might consider creating a CultureInfo
object based on the user’s settings, but choose not to use the cultural overrides. The following
code demonstrates this concept:
myCulture = New CultureInfo(name:= _
Threading.Thread.CurrentThread.CurrentCulture.Name(), _
useUserOverride:=False)
Threading.Thread.CurrentThread.CurrentCulture = myCulture
Real-World .NET Programming
930
PART III
Explicitly defining culture is also useful in ASP.NET applications in which it is unlikely that a
user’s culture will match that of the server that is executing your code. In this case, it is best to
use the user’s Web browser language setting to initially set the culture by which your code will
execute. This setting is available from the Request object. The CultureInfo class exposes a
CreateSpecificCulture method that returns a CultureInfo object based on a culture name.
The following is an example:
Threading.Thread.CurrentThread.CurrentCulture = _
CultureInfo.CreateSpecificCulture(Request.UserLanguages[0])
CultureInfo is a class that you will undoubtedly need time and again. For that reason, we’ve
listed a number of additional key properties and methods of the class in Table 21.2.
Property Description
Calendar The Calendar property is used to return the default calendar
used by the culture. The property is of the type Calendar
class.
CompareInfo The CompareInfo property returns the CompareInfo object
that defines how strings are compared within the current
culture.
CurrentCulture The CurrentCulture property returns a CultureInfo object
that represents the culture the current thread is using.
CurrentUICulture The CurrentUICulture property returns a CultureInfo object
that represents the culture used by the Resource Manager.
DateTimeFormat The DateTimeFormat property returns or sets the culture-
specific format for displaying dates and times. The property
is of the type DateTimeFormatInfo.
DisplayName The DisplayName property returns the culture name in the for-
mat [language]<country/region>. For example, en-US returns
“English <United States>.”
EnglishName The EnglishName property is the same as DisplayName but
always returns in English.
InstalledUICulture The InstalledUICulture property returns a CultureInfo
object that represents the culture of the operating system at the
time of installation.
InvariantCulture The InvariantCulture property returns a CultureInfo object
that is culture insensitive.
IsNeutralCulture The IsNeutralCulture property returns True if the instance
represents a neutral culture. For example, “en” for English.
Globalization and Localization Techniques
931
CHAPTER 21
GLOBALIZATION
LOCALIZATION
TECHNIQUES
LCID The LCID property returns the instance’s culture identifier
AND
value.
Name The Name property is used to return the name of the culture in
the format language code, dash, country/region code (en-US).
NativeName The NativeName property returns the name of the culture in
the language that the instance is set to display.
NumberFormat The NumberFormat is used to return or set a value indicating
how numbers, currency, and percentages are formatted for
display.
OptionalCalendars The OptionalCalendars property returns a list of optional
calendars a given culture can use.
Parent The Parent property returns a CultureInfo object that repre-
sents the current, specific culture’s parent. For example, “en-
US” has the parent (also called neutral) culture of “en.”
TextInfo The TextInfo property is used to return the TextInfo object
that defines the writing system for a given culture. TextInfo
defines things such as case and other behaviors.
UseUserOverride The UseUserOverride property returns True if the
CultureInfo instance uses the user-overridden culture
settings.
Method
ClearCachedData The ClearCachedData method is used to refresh the culture
object. Data is loaded when the object is created. The infor-
mation can change during the object’s lifetime based on user
settings.
CreateSpecificCulture CreateSpecificCulture is a shared method that returns a
CultureInfo instance from a culture code.
GetCultures GetCultures is a shared method that returns a collection
of CultureInfo objects based on a CultureTypes
filter. CultureTypes can be AllCultures,
InstalledWin32Cultures, NeutralCultures,
and SpecificCultures.
RegionInfo
Additional information about a culture’s region, such as measurement system or currency code,
can be gathered using the RegionInfo class. To create a RegionInfo instance, you can pass
Real-World .NET Programming
932
PART III
either a two-letter region code (as defined by ISO 3166 standard) or a culture identifier gained
by calling the CultureInfo’s LCID property. The following are two examples:
‘create a region object based on a region code (Zimbabwe)
myRegion = New RegionInfo(name:=”ZW”)
After you’ve created a valid RegionInfo instance, you have access to a number of important
properties. For example, CurrencySymbol returns the symbol used to denote currency within
the local region, and an ISOCurrencySymbol property returns the three-letter ISO standard
(4217) for a region’s currency. The United States’ ISO currency symbol, for example, is USD.
The property IsMetric is a Boolean value that returns True if the region uses the metric sys-
tem for measurement.
You can also set the same property from the CultureInfo object when working with custom
culture objects.
To function with a given culture, the NumberFormatInfo class is not required by your applica-
tion. Instead, it provides finite control over and access to how numbers get formatted. Let’s use
the following example: Suppose we have to display the price of our product in both yen and
dollars. We can simply switch our application’s culture and write out the price. The formatting
is done for us automatically. Or suppose that the user’s current culture is English-United States,
but your requirement is to always display both dollars and yen. We can create a custom
CultureInfo object and pass it as a provider to the ToString method when outputting our
price. The following illustrates this example:
Globalization and Localization Techniques
933
CHAPTER 21
Imports System.Globalization
21
GLOBALIZATION
Module Module1
LOCALIZATION
TECHNIQUES
AND
Sub Main()
End Sub
End Module
The result of this routine is the values $38.99 and ¥39 written to the console window. The
value c passed as the first parameter to the ToString method is a format character that denotes
currency. Some other common format characters include: d for decimal, e for exponential,
f for fixed-point, and x for hexadecimal.
Of course, you can override the default behavior of the NumberFormatInfo class for a given
culture. What if, for instance, you wanted to show the yen price from the preceding example
using two digits after the decimal (¥38.99)? You would add the following line:
myJapan.NumberFormat.CurrencyDecimalDigits = 2
The following is an example of the result of simply switching cultures and displaying the cur-
rent date and time.
‘set the current culture to English-US
Threading.Thread.CurrentThread.CurrentCulture = _
New CultureInfo(name:=”en-US”)
The code outputs the English date of 9/24/2001 9:27:44 a.m. followed by the Japanese version
of the same date and time of 2001/09/24 9:27:44. However, suppose you want to override the
default date separate for the English-United States culture (“/”). You would do so using the
DateSeparator property, as in the following:
Threading.Thread.CurrentThread. _
CurrentCulture.DateTimeFormat.DateSeparator = “.”
The DateFormatInfo class also enables you to return dates in various formats or patterns.
These properties include LongDatePattern, LongTimePattern, MonthDayPattern,
ShortDatePattern, ShortTimePattern, YearMonthPattern. Other notable properties
of the DateFormatInfo class include the following:
• AMDesignator—Used to return or set a string value that indicates time that is “ante meri-
diem” (before noon).
• CalendarWeekRule—Specifies the rule to use (of type CalendarWeekRule enumeration)
to determine the first calendar week of the year.
• DayNames—Used to return or set an array that indicates the culture’s names for the days
of the week.
• FirstDayOfWeek—Used to return or set the culture’s accepted day that a week starts. The
property is of the type DateTime.DayOfWeek enumeration.
• MonthNames—Used to return or set an array that indicates the culture’s names for the
months in a year.
• PMDesignator—Used to return or set a string value that indicates time that is “post meri-
diem” (after noon).
Globalization and Localization Techniques
935
CHAPTER 21
Calendars 21
A culture’s calendar is another important item to consider when creating applications of a
GLOBALIZATION
LOCALIZATION
TECHNIQUES
global scale. Calendar classes in .NET are used to divide time into a culture’s recognized units.
For instance, a year can be divided into a number of months, a month into days, and so on.
AND
Each calendar can represent these items differently. .NET provides the base class Calendar for
creating classes that define a culture’s calendar. The Framework Class Library defines the cal-
endars listed in Table 21.3.
To access a given culture’s calendar, you can use the Calendar property of the CultureInfo
class. This will return its default calendar class. Some cultures, however, support multiple
calendars. Access to these calendars can be gained through the CultureInfo class’s
OptionalCalendars property. As an example, the following code outputs the calendars used in
the culture Hebrew-Israel. The output is HebrewCalendar followed by GregorianCalendar.
Dim myCulture As CultureInfo
Dim i As Short
The calendar classes contain a number of methods for getting information about how a specific
time is represented by a given calendar. These methods primarily use the DateTime class as a
parameter. The methods of the DateTime class always use the Gregorian calendar to perform
calculations. However, the results can be mapped directly to an instance of another calendar.
For example, the GetDayOfWeek method returns a value of the type DayOfWeek enumeration for
a given DateTime value. Other key methods include AddMonths, AddWeeks, AddYears,
GetDayOfMonth, GetDayOfYear, GetDaysInMonth, GetDaysInYear, GetMonth, IsLeapDay,
IsLeapMonth, and IsLeapYear.
GLOBALIZATION
➲ The .NET Framework uses the Unicode Transformation Format with 16-bit encoding
LOCALIZATION
TECHNIQUES
(Unicode UTF-16) to represent characters. Unicode is the universal standard for encod-
AND
ing characters and text. For more information, check out www.unicode.org.
➲ If you need to work with Unicode surrogate pairs and combining characters to represent
characters from other languages, try the StringInfo class.
Resource Files
Resource files enable you to separate your core application from its resources. A resource file
is data (not code) that gets deployed with your application. A global application will typically
define one resource file for every culture it needs to support. The data in a resource file
includes things such as application help text, form text, and image files all translated for the
target culture. This section details creating and using resource files in your application using
the System.Resources namespace and associated VS .NET utilities.
Creation
The process of creating resource files involves the following steps:
1. Define your application’s cultural support—This is a requirements step in which you
decide which cultures your application will support and what the default or fallback cul-
ture will be. Of course, you can easily add support for additional cultures at a later time,
but understanding the primary cultures will help you make decisions about deployment
and design.
2. Identify which items in your user interface require translation—This step is part of build-
ing your core application to understand and use resources. It is not sufficient to hard-code
the word “cancel” on a button or write the instruction text in English on your Web page.
You must pull this information directly from resource files for a user’s selected culture.
3. Translate your resource—In this step, you will typically pass a file containing all your
application’s resources off to a translator. Remember, resources can include text and
images.
4. Convert translated values into resource files—Finally, you must take the resources for a
given culture and define a resource file for your application. The remainder of this sec-
tion is focused on this step.
Resource files, at their most basic level, are composed of name/value pairs grouped for a spe-
cific culture. The name is used as a key to retrieve a value. Names are case sensitive and are
followed by an equals (=) sign to denote the name’s value, as in Name = Value. At least three
file types are used for resource files.
Real-World .NET Programming
938
PART III
Text files can be used to create a resource file that contains only string values. In this case, you
can define comments in the text file by preceding the line with a semicolon (;). The following
is an example of a simple text-only resource file:
;define names and values
CommandCancel = Cancel
WindowTitle = MyApplication
You can also create an XML-based resource file. These files have the extension .resx and can
be created with a resource editing tool embedded within VS .NET. The preceding resource file
is represented as the following using XML inside a .resx file:
<?xml version=”1.0” encoding=”utf-8” ?>
<root>
<xsd:schema ... > ... </xsd:schema>
<resheader name=”ResMimeType”>
<value>text/microsoft-resx</value>
</resheader>
<resheader name=”Version”>
<value>1.0.0.0</value>
</resheader>
<resheader name=”Reader”>
<value>System.Resources.ResXResourceReader</value>
</resheader>
<resheader name=”Writer”>
<value>System.Resources.ResXResourceWriter</value>
</resheader>
<data name=”CommandCancel”>
<value>Cancel</value>
</data>
<data name=”WindowTitle”>
<value>MyApplication</value>
</data>
</root>
Note that we omitted the xsd information in the preceding example for clarity and brevity.
Before you use a resource file, it must be converted into a .resource file. These are binary files
used by .NET to embed within assemblies and to create satellite assemblies (see packaging).
Files of type .resources can be created in at least two ways. One is to use the utility, Resource
File Generator (ResGen.exe). This utility takes a text (.txt extension) or an XML-based
resource file (.resx extension) and outputs a .resource file. The utility can also work in the
other direction. That is, create a .txt or .resx file from a .resource file.
Note that if you work with XML resource files, you will want to use the .resx file type. Files of
type .txt will not be translated as XML by the utility regardless of content.
Globalization and Localization Techniques
939
CHAPTER 21
The ResGen utility is used from a command prompt. For example, to output the text file from
21
the preceding example, we would enter the following command:
GLOBALIZATION
LOCALIZATION
TECHNIQUES
Resgen myRes.txt myRes.resources
AND
This command used the text file myRes.txt to output the binary myRes.resources.
You can also create resource files using the .NET Framework Class Library. The class
ResourceWriter inside the System.Resources namespace is used to write resource values to a
file or stream. For example, the following code creates a resource file called myRes.resources.
It then adds two items to the resource file using the AddResource method and passing a
name/value pair as parameters.
Imports System.Resources
Module Module1
Sub Main()
‘local scope
Dim myResWriter As ResourceWriter
‘add resources
myResWriter.AddResource(name:=”ApplicationName”, value:=”Res Example”)
myResWriter.AddResource(name:=”HelpButton”, value:=”Help”)
End Sub
End Module
The counterpart to the ResourceWriter class is ResourceReader. This class can be used to
iterate through the contents of a .resource file. In the following example, we create a
ResourceReader instance based on our myRes.resources file. We then call the method
GetEnumerator, which returns a dictionary-style collection. We iterate this collection
and output the name/value pairs to the console.
Imports System.Resources
Module Module1
Sub Main()
Real-World .NET Programming
940
PART III
End While
‘pause
Do While Not Console.ReadLine = “s” : Loop
End Sub
End Module
The ResourceReader class can be handy at times, especially when you’re debugging or coding
a tool whose purpose is to read .resource files. For your global-ready application, however, you
should use the ResourceManager class to access your resources. This class is covered in the
following section.
Accessing
The classes ResourceManager and ResourceSet are most often used when accessing resource
files for use inside your application. The code to use both classes is very similar. For example,
to access string values, you call the GetString method. For images and other binary informa-
tion, you call GetObject. The key differences lie in how the objects are created.
The ResourceSet object is created against a specific resource file. For this reason, it has no
support for “falling back” to a neutral culture; it works only with the file you specify.
Additionally, the ResourceSet object caches the entire resource file’s contents when it is
created. This provides faster access to resources but consumes memory regardless of whether
you access the resource items.
Globalization and Localization Techniques
941
CHAPTER 21
The ResourceManager class, on the other hand, determines which culture’s resource file or
21
associated satellite assembly to access based on a user’s current culture setting. If it does not
GLOBALIZATION
find the requested culture, it can process a set of fallback rules that determine what culture to
LOCALIZATION
TECHNIQUES
access (the ResourceSet class throws an exception when it cannot find a specified culture-
AND
resource file). Falling back typically involves switching your application to a neutral culture
you’ve defined during packaging and deployment. In addition, the ResourceManager class
loads only a resource file’s keys at the time of instantiation. This consumes less memory, espe-
cially if users don’t typically access all your resources in one session. It has the effect of seem-
ing faster initially; however, the time to access resources is spread over the lifetime of your
application. Every time a new resource is requested, the ResourceManager must retrieve it
from your file. After the initial retrieval, however, the item is cached for the next use. For these
reasons, the ResourceManager class is the preferred solution when creating global applications.
Deploying
Deploying resource files that are of the type .txt, .resx, or .resources is called loose
deployment. This type of deployment is contrasted with compiled satellite assemblies that con-
tain your resources (more on these in a minute). Loose resource files are somewhat easier to
manage; however, they do not provide the versioning and signature support of a satellite
assembly. Nor can they be deployed into the Global Assembly Cache.
Typically, you deploy these loose resource files into a subdirectory within your application.
Each file has what is referred to as a base name, followed by the culture the resource file repre-
sents and the .resource extension. Figure 21.2 represents the deployment of three resource files
into a folder called Resources.
FIGURE 21.2
Loose deployment.
Notice that the previous figure has two culture resource files (en-US and es-MX) and another
file that did not indicate culture (myResources.resources). In this deployment, this file will
act as the fallback set of resources. When a user’s cultural preferences dictate something other
than these two cultures, the ResourceManager object will fall back to the file with the base
name. This is better illustrated with some code.
Real-World .NET Programming
942
PART III
In Listing 21.1, we use these three resource files to output text based on a given culture. The
text items we are going to output in this example are named GoodMorning and Hello in each
resource file. To simulate different user culture settings, we are going to cheat and simply set
the CurrentUICulture of the executing thread to a new culture. This enables us to switch cul-
tures on-the-fly and watch as the ResourceManager object reacts accordingly.
To create a ResourceManager instance based on a .resource file, we use the shared method
CreateFileBasedResourceManager. In this function call, we indicate the base name of our
resource files (myResources) and the directory in which they are located (./resources). We
then simply call the GetString method of the class, passing it the key (as the name parameter)
of our name/value pair.
Imports System.Resources
Imports System.Globalization
Module Module1
Sub Main()
‘local scope
Dim myRm As ResourceManager
GLOBALIZATION
LOCALIZATION
TECHNIQUES
Console.WriteLine(myRm.GetString(name:=”GoodMorning”))
Console.WriteLine(myRm.GetString(name:=”Hello”))
AND
‘set the current thread to Japanese-Japan
Threading.Thread.CurrentThread.CurrentUICulture = _
New CultureInfo(name:=”ja-JP”)
‘pause
Do While Console.ReadLine <> “s” : Loop
End Sub
End Module
Notice that as we switched cultures, the ResourceManager understood how to find the appro-
priate resource file without our interaction. This would be the same for users. Your application
need not respond to events when users change their regional settings. Instead, the CLR and the
class manage this for you.
Also notice that our last culture switch was to ja-JP. We did not define a resource file for this
culture. However, we did define a fallback option. The ResourceManager class picked up our
base name file and used it to respond to ja-JP. This is illustrated in the console’s output as
rendered in Figure 21.3.
FIGURE 21.3
ResourceManager loose access output.
Real-World .NET Programming
944
PART III
Note that the only thing required to support an additional culture in this case is to create an
additional resource file. No code changes nor recompiling are necessary!
Notice that we output each file with the same name (myResources.dll). This is possible for
two reasons: first, we indicated culture when we created the assembly (c:en-US), and second,
the files are actually deployed in different directories. Private satellite assemblies are
deployed into a directory structure within your application. This allows the CLR and the
ResourceManager class to locate these files. Each directory gets its name from the culture that
it represents. For example, the English-United States directory would be called en-US. Public
satellite assemblies get deployed into the Global Assembly Cache. When doing so, you must
strongly name your assembly.
The next step is to embed our default resource file into our executable. Before we do this, how-
ever, we are going to create a separate class file that wraps the ResourceManager access to our
name/value pairs. Listing 21.2 presents this code.
GLOBALIZATION
LOCALIZATION
TECHNIQUES
myRm = New ResourceManager(baseName:=”myResources”, _
assembly:=Me.GetType().Assembly)
AND
End Sub
End Class
This class file is compiled from the command line using the following switches. The switch,
“/resource” indicates that the compiler is to embed our default resource file as part of the
executable.
vbc /target:library /out:satClass.dll /r:system.dll
/resource:myResources.resources Class1.vb
Next, we create a client to access our newly created library. This client switches cultures and
accesses resources accordingly. To make this work, we must deploy our class and its associated
resource files into the client application’s directory (bin). This code is presented in Listing 21.3.
Module Module1
Sub Main()
‘local scope
Dim mySatClass As SatelliteTestClass
Real-World .NET Programming
946
PART III
‘pause
Do While Console.ReadLine <> “s” : Loop
End Sub
End Module
Summary
In this chapter, we’ve covered the basic steps for creating global applications with the .NET
Framework Class Library. This seemingly daunting task now should seem well within your
reach.
Globalization and Localization Techniques
947
CHAPTER 21
GLOBALIZATION
LOCALIZATION
TECHNIQUES
inception rather than retrofitting your application at a later date.
AND
• When designing for the global scale, it is important to partition your application’s data
into what is core and what is translated.
• The System.Globalization namespace defines key classes for writing global-ready
applications.
• To define a culture, you use the language-country/region syntax (en-US for example).
• The CultureInfo class represents a culture in .NET.
• The RegionInfo class is used to get more information about a culture’s region.
• The NumberFormatInfo class defines how to format numbers for a given culture.
• The DateTimeFormatInfo class is used to work with dates and times in a given culture’s
format.
• A number of Calendar derived classes enable you to manage dates for the current
culture.
• You store your application’s localized resources in resource files (.txt, .resx, and
.resources).
• You can deploy your resource files loosely or as satellite assemblies.
Deploying, Configuring, and CHAPTER
22
Licensing .NET Components
IN THIS CHAPTER
• The Deployment Dilemma 950
A developer’s job is not over after an application is written. After spending countless hours try-
ing to craft a software solution that addresses a business problem, developers now face what is
sometimes the most complex task in the development cycle: making sure that the application
can be installed and used by its intended audience. A smooth application deployment requires
extensive research and insight into both the computing environment on which the software was
written and the environment in which it will run.
With .NET, developers certainly are not relieved of this responsibility, but, considerable in-
roads have been made with respect to removing technical complexity and issues arising from
the convoluted interactions of software on any particular machine.
In this chapter, we will show you how to successfully deploy applications written for the .NET
Framework. We’ll discuss the problems inherent with deploying traditional COM components,
and then look at how .NET components have been structured to eliminate or drastically reduce
these problems. We will also investigate how to license applications to ensure that only valid,
authenticated users can run a given application.
By the end of this chapter, you should be able to:
• Explain the physical structure of assemblies and their role in application deployment
• Understand how the .NET runtime handles component versioning
• Deploy an application inside the Global Assembly Cache
• Enforce licensing restrictions on components
DLL Hell
“DLL Hell” is a term that has developed over the years to describe the near misery
experienced by developers while trying to distribute and register their applications into a
Microsoft Windows environment. Registering a component with the operating system and
COM involves touching the registry; this complicates the whole install process by forcing
installers to physically touch the registry, introducing the potential for system-wide errors and
security problems that can prevent the install from taking place. To understand what DLL Hell
is all about, you really need to understand how Windows and COM deal with applications.
Deploying, Configuring, and Licensing .NET Components
951
CHAPTER 22
CONFIGURING, AND
LICENSING .NET
lems are traced back to the spell-checker component that, as we have stated, is used not just
COMPONENTS
DEPLOYING,
by one application, but by many on any given machine. It seems that the latest revision of the
spell-checker component is not backward-compatible with the previous versions; any applica-
tion developed against one of the previous versions fails on any call into the new component.
This scenario outlines a typical issue that developers who reuse components face. On the one
hand, leveraging a common, shared component is a great way to speed development and avoid
reinventing the wheel every time a piece of software is written. Doing so, however, exposes
you to some risk. If another application comes along that does something to that shared com-
ponent, it has the potential to break your application. What’s more, you have no way of pre-
venting it!
So, why does this happen? The key is in the way that Windows and COM identify components.
Component Identification
Windows and COM use the registry to describe components. That means that the Windows
registry is used to tell COM certain things about a component, such as where it is located and
what interfaces it supports. Deploying a COM component onto a machine was usually a two-
step process: The DLL was copied to the Windows System32 directory (or some other global
directory), and the registry was updated with its information. Although this seems like an easy
way to approach things, it is fraught with danger (as we have shown). For one thing, versioning
the component is not enforced by anyone or anything. If application A was specifically written
to use Version 2.0 of the spell-checker component and the component was then revised to
Version 3.0, there is no way for that application to realize that a potentially disastrous
“upgrade” has been done to the component. Simply put, component identification with COM
meant a few registry entries that matched up a physical file with its interfaces. This can be a
brittle mechanism: COM does not actively police its registry entries, registries can become cor-
rupt, and access to certain areas of a registry force developers to deal with obscure security
issues as well.
Real-World .NET Programming
952
PART III
Component Locks
The scenario just presented didn’t even deal with another common issue facing deployment
efforts: that of replacing components that are currently in use by a process running on the tar-
get machine. This has been the bane of Web developers for quite some time. As long as a sin-
gle process is using the component that you want to replace, the component will be locked:
The file system physically won’t allow it to be replaced. The solution is usually to hunt down
and kill or stop any processes that might attach to the component. This whole procedure needs
to be done at a time when no one can access the application, or another lock could be gener-
ated on the component at any time!
Even stopping the offending process is sometimes not enough, necessitating a reboot of the
machine. This tends to be a pain on client machines—in a server environment where opera-
tions are expected to continue 24 × 7, it is anathema.
In summary:
• Registering a component with the operating system and COM involves touching the reg-
istry; this complicates the whole install process by forcing installers to physically touch
the registry, introducing the potential for system-wide errors and security problems that
can prevent the install from taking place.
• Shared component replacement has the potential to break applications. Applications have
no way of knowing that a shared component has been replaced, and they might have spe-
cific calling code that is affected by a change in a globally shared component.
• Versioning is not enforced. Newer or older versions of components can be installed over
the top of current components. Applications have no good way to specify which version
Deploying, Configuring, and Licensing .NET Components
953
CHAPTER 22
of the DLL they were developed against. What’s more, no central authority monitors
component versioning for the system as a whole.
• Backward compatibility is not a valid approach in the long term. Development teams
must keep extending their applications to deal with new business problems and remain
competitive in the market place. This can continue only for so long under the require-
ment to retain backward compatibility.
• Process locks on components must be handled before replacing the targeted component.
This could mean anything from shutting down the currently running processes to reboot-
ing the server or PC. 22
CONFIGURING, AND
LICENSING .NET
The Deployment Solution?
COMPONENTS
DEPLOYING,
Before the arrival of .NET, there have been some preliminary steps take to combat DLL Hell
(primarily arriving on the scene with Windows 2000). .NET builds on the efforts of Windows
2000 to ease the complexities of component deployment.
Application A
1.0.1.216
Application B Component A
Application C 1.2.0.2090
FIGURE 22.1
Side-by-side components.
Real-World .NET Programming
954
PART III
Self-Describing Components
Components deployed in the .NET runtime do not rely on the registry any longer. Instead, they
are said to be self-describing: Each component carries its own information around as baggage
that uniquely describes itself, including its version number. Components do not rely on any
other mechanism to indicate their identity, nor can any other system entity affect their function-
ing. In this way, components are said to be isolated within the .NET runtime. Changing com-
ponent A should have absolutely no effect on component B.
Version Control
The .NET runtime also provides a gatekeeper that monitors and records version information for
each component. This also provides the side benefit of allowing newer versions of components
to be introduced without breaking other applications: Each application is inherently aware of the
specific versions of components that it needs to run. When a component is encountered that
does not match this reference, it is up to the developer how to handle this situation. These set-
tings can be controlled universally per machine, or on a per-application or per-component basis.
The runtime also keeps track of the last known set of components (and their versions) that
actually worked for a given application. This allows developers or administrators to roll back
components to a point in time that everything was still working, and then work out the issues
from there.
So, how does .NET do all of this? A discussion of component deployment with .NET starts
with assemblies.
Deploying, Configuring, and Licensing .NET Components
955
CHAPTER 22
CONFIGURING, AND
LICENSING .NET
COMPONENTS
DEPLOYING,
Manifest
Type Metadata
SomeAssembly.dII
MSIL Code
Sources
FIGURE 22.2
Anatomy of an assembly.
An assembly can span multiple files or can be entirely contained in a single file. If the assem-
bly spans more than one file, each file could contain one or more of the four different parts of
the assembly. It could be, for instance, that the resources for an assembly are placed into a sep-
arate file to ease globalization of the application. Figure 22.3 shows an example of the same
assembly with its graphics resources contained in a separate file.
The Manifest
The manifest section is what makes the assembly self-describing. It contains information about
the assembly’s identity, the list of all the physical files that are within it, a list of any other
assemblies that need to be referenced by the compiled code, any types or resources “exported”
by the assembly, and, finally, any code access security attributes that have been applied to the
assembly.
NOTE
The identity of an assembly consists of three different parts: a name (required), a ver-
sion (also required), and a culture (optional). Each is used together to provide a way
to reference the specific assembly.
Real-World .NET Programming
956
PART III
SomeAssembly.dII Resource.resx
Manifest Resources
Type Metadata
MSIL Code
FIGURE 22.3
Multiple files in an assembly.
Type Metadata
Type metadata is information that identifies the various types used in the assembly (such as
classes).
MSIL Code
MSIL code provides the instructions that will get parsed by the MSIL engine when the applica-
tion is truly compiled during runtime.
Resources
These are the actual resources compiled into the assembly. String or image resources could be
included for easy extension or localization, if the need arises.
Examining an Assembly
Ildasm.exe is a tool provided with the .NET Framework that enables you to decompile an
assembly and view its contents. This tool is useful in terms of studying the composition of
assemblies, in particular their manifest and type metadata information.
After launching the Ildasm utility, you can open an assembly using the File Open command.
Figure 22.4 shows the Ildasm utility with an assembly loaded into it. It presents you with a tree
view of the different constituents of the loaded assembly.
Deploying, Configuring, and Licensing .NET Components
957
CHAPTER 22
22
CONFIGURING, AND
LICENSING .NET
COMPONENTS
DEPLOYING,
FIGURE 22.4
Ildasm.exe in action.
To check out the manifest information, double-click on the Manifest node. You should get a
new window that looks something like Figure 22.5.
FIGURE 22.5
Examining the manifest.
Visible in this example are all the external assemblies that the loaded assembly depends on.
They are prefaced with the .assembly extern keywords, like this:
Real-World .NET Programming
958
PART III
The actual reference contains three distinct pieces of information about the assembly depen-
dency. It shows the name of the assembly—in this case, mscorlib. It also shows its version
(1.0.2411.0). The third nugget of information that is evident is a public key token. (We’ll get
more into the reason for identifying a public key later in the chapter when we talk about the
concept of strong names. For now, just know that this is a way for the runtime to identify that
the referenced assembly really is what it says it is.)
If you scroll down farther, you should see the actual assembly definition.
.assembly AssemblyTest
{
.custom instance void _
[mscorlib]System.Runtime.InteropServices.GuidAttribute::.ctor(string) = _
( 01 00 24 46 44 37 42 39 42 30 35 2D 45 39 36 43
2D 34 34 38 36 2D 42 42 30 34 2D 41 33 46 39 30 _
46 37 45 42 34 36 37 00 00 )
.custom instance void [mscorlib]System.CLSCompliantAttribute::.ctor(bool) = _
( 01 00 01 00 00 )
.custom instance void _
[mscorlib]System.Reflection.AssemblyTrademarkAttribute::.ctor(string) = _
( 01 00 00 00 00 )
.custom instance void _
[mscorlib]System.Reflection.AssemblyCopyrightAttribute::.ctor(string) = _
( 01 00 00 00 00 )
.custom instance void _
[mscorlib]System.Reflection.AssemblyProductAttribute::.ctor(string) = _
( 01 00 00 00 00 )
.custom instance void _
[mscorlib]System.Reflection.AssemblyCompanyAttribute::.ctor(string) = _
( 01 00 00 00 00 )
.custom instance void _
[mscorlib]System.Reflection.AssemblyDescriptionAttribute::.ctor(string) = _
( 01 00 00 00 00 )
.custom instance void _
[mscorlib]System.Reflection.AssemblyTitleAttribute::.ctor(string) = _
( 01 00 00 00 00 )
.hash algorithm 0x00008004
.ver 1:0:602:29468
}
Deploying, Configuring, and Licensing .NET Components
959
CHAPTER 22
Here, there is evidence of some attributes that have been applied to the assembly (such as
InteropServices.GuidAttribute and Reflection.AssemblyProductAttribute). The last
line of the assembly definition shows its version number: 1.0.602.29468.
The last thing that you see in the assembly manifest is a list of the files and resources that
belong to the assembly.
.mresource public AssemblyTest.Resource1.resources
{
}
.mresource public AssemblyTest.Form1.resources
{
22
CONFIGURING, AND
}
LICENSING .NET
COMPONENTS
DEPLOYING,
.mresource public AssemblyTest.exe.licenses
{
}
.module AssemblyTest.exe
// MVID: {0D0F2EE1-A719-4729-BFB5-5424F124E5AD}
.imagebase 0x11000000
.subsystem 0x00000002
.file alignment 512
.corflags 0x00000001
// Image base: 0x03680000
Versioning
As you might have noticed in the previous manifest examples, each assembly has a version
number. The version number actually consists of four parts, arranged like this:
major.minor.build.revision
The runtime treats major and minor version differences as entirely different assemblies. This is
necessary to implement the concepts that we have talked about related to side-by-side shared
components. In other words, changes to an assembly’s major or minor version number cause
that assembly to not be compatible with applications that were using the older version. This is
an important behavior to understand. Let’s walk through a possible deployment scenario:
1. A developer deploys two different applications (WordEditor.exe and FinanceEditor.exe).
2. As part of the deployment, the developer also installs a shared component into the GAC
that both the WordEditor and the FinanceEditor executables use (CommonFuncs.dll). The
shared component is installed as Version 1.0.92.106.
3. Over time, the developer comes up with some optimizations and feature enhancements to
the CommonFuncs.dll component. After revising the component, it is now at 1.1.0.1028.
Real-World .NET Programming
960
PART III
4. The developer deploys the new component and tests the WordEditor and FinanceEditor
applications to verify the added functionality.
5. Neither application appears to have changed at all, even though the use of the new com-
ponent should be immediately obvious!
What happened here? The answer is simple if you understand how the versioning policy
works. Because the new component was considered to be an entirely new component by .NET
(due to the fact that its minor version number had changed), it is being served up side by side
with the older component. The default version policy resolves assembly references at runtime
and does so based on the version of the assembly registered at the time the application was
installed. This means that both WordEditor.exe and FinanceEditor.exe actually have hard-coded
into their metadata a reference to the CommonFuncs.dll version 1.0.92.106. Installing the new
version has done nothing to their functionality because they are still loading the old version.
Had the new component possessed a version number of 1.0.93.0, the link would happen auto-
matically because .NET does not recognize a build number change as an incompatible change.
NOTE
Why are build and revision number changes not treated as new assemblies by .NET?
The default version policy was implemented in this manner to allow for hot fixes and
patches to be applied to a system without worrying about applications still referenc-
ing the older components. A build or revision number change equals a hot fix or
patch in .NET’s eyes.
You can use application configuration files to override or change the default version policies
maintained by the runtime. We discuss configuration files in general later in the chapter (see
the section “Using Application Configuration Files”).
Distribution
Distribution is the process of placing executable code onto the end user’s machine, and it is
really the first step in the installation process. With .NET, all the traditional methods of deliver-
ing an application still exist. For instance, you can use CAB files to compress the install
Deploying, Configuring, and Licensing .NET Components
961
CHAPTER 22
components to significantly shorten download or transmission time. You also can build
Windows Installer packages; Version 2.0 of this technology is .NET-aware and is capable of
deploying both private and shared components (more on these in a minute).
But the real attraction with .NET deployment is its treatment of self-contained components.
.NET components truly are self-contained and don’t rely on the registry or any other external
apparatus to work. As a result, you can simply copy an executable along with its attendant
DLLs from a disc into a directory, and it will run with no other required effort.
If you want to deploy a local copy of a component or an entire application, all you have to do
is copy it to the application’s intended directory. These are referred to as XCopy deployments 22
CONFIGURING, AND
because you can simply XCopy an entire application’s files and directory structure over to the
LICENSING .NET
COMPONENTS
DEPLOYING,
target machine, with no additional effort required.
This begs the question, “What is the difference between a local and a global install?” The
answer lies in the intended use of the component—specifically, whether your component is
intended for use by multiple applications (a global install) or by only one application (a local
install). The decision of whether to deploy locally or globally is one that is left to the developer
based on the design and installation requirements of a particular application.
NOTE
We are using the terms “global components” and “shared components” interchange-
ably; both refer to pieces of an application that also are used by other applications
(and thus are subject to special treatment by the .NET runtime). Likewise, when we
say “local,” “private,” or “isolated,” we are referring to applications and components
being deployed inside a “local” application directory.
Local Components
Local components are isolated from other applications because they physically sit inside the
calling applications directory. When the application is started, the runtime automatically
“probes” for its components in the application directory (if it doesn’t find one in the global
cache).
There is no need for the runtime to perform any other type of policing for locally installed
components because they are meant to be consumed by only one application. In general,
deploying components as private components is the easiest and the least technically challeng-
ing means of deploying applications in the .NET environment.
Real-World .NET Programming
962
PART III
Shared Components
Because of the dangers previously discussed with using shared components, .NET treats these
differently and imposes some additional requirements (and thus overhead) on the deployment
process of shared components.
Shared components are written into a special area of the file system called the Global Assembly
Cache (GAC). The GAC functions much like the System32 directory did under native Windows.
It provides a central repository for components that will be used across many applications and
users. Because of the process overhead involved, the decision to deploy a component into the
GAC is not one that should come lightly. Only if you are absolutely certain that your particular
component needs the benefits of the GAC should you continue with a GAC install.
NOTE
It is important to note that although the deployment process is different for local and
shared components in .NET, there is no actual physical difference between a local
assembly and a shared assembly.
The GAC builds on the same side-by-side component sharing supported by Windows 2000 and
Windows 98 SE: It allows multiple versions of an assembly to coexist peacefully. Because one
assembly that calls another explicitly records the version number and name of the referenced
assembly, the runtime is capable of reaching the correct version in the GAC and completing
the reference call.
The GAC also provides you with a central store for code so that system administrators and
developers can apply patches and quick fixes in one location and have them affect the system
as a whole. This works to substantially lower administrative efforts with respect to application
updates.
NOTE
The GAC, by default, is restricted to administrative user level access only. This means
that you must be an administrator, or must have your administrator grant you permis-
sion, to publish code there.
The safety and features of the GAC come with some overhead, in that you cannot simply copy
an assembly into the GAC as you could copy DLLs into the System32 directory. Placing an
assembly into the GAC requires the use of one of the .NET tools designed just for this:
Windows Installer 2.0 or a .NET Framework SDK utility called gacutil.exe. Before that can be
done, however, the assembly must be given a strong name.
Deploying, Configuring, and Licensing .NET Components
963
CHAPTER 22
Strong names are .NET’s mechanism for ensuring a unique name for each assembly in the
GAC. Remember that the name must be unique to prevent another component developer from
interfering with your components, in case they are named alike. Unlike previous Microsoft
platforms, the .NET platform actively seeks to protect assembly names, preventing another
author’s components from overwriting your components.
CONFIGURING, AND
LICENSING .NET
COMPONENTS
DEPLOYING,
Strong Names and Signing Assemblies
A strong name is constructed of an assembly’s text name (called its simple name, to contrast
with its strong name), a public key, and a digital signature.
NOTE
Strong named assemblies can reference only other strong named assemblies. If a
strong named assembly referenced an assembly with a simple name, it would open
itself up to DLL Hell all over again. This is the “chain is only as strong as its weakest
link” analogy in action.
Strong names actually rely on public key cryptography to secure and protect the operations of
assemblies both inside and outside the GAC (you can have an assembly with a strong name
outside the GAC). Although we don’t have the time or the space to get into a discussion of
public key cryptography here, we can summarize its usefulness with a few statements:
• Public key cryptography relies on the concept of key pairs. There is a public key, which
is distributed freely, and a private key, which is held close and is not released outside its
owning organization.
• As part of its strong name, an assembly exposes both the public key and a signature that
was created using the private key.
• By comparing the keys stored in an assembly’s manifest, the runtime can figure out
whether the assembly is valid and ensure that it has not been tampered with.
• Because assemblies leverage the public-private key pair, you are guaranteed that no one
can spoof the assembly’s strong name because the public-private key pair is unique and
can’t be duplicated.
Real-World .NET Programming
964
PART III
Retrieve
Assembly
Version Info
Check for
Cached
Assembly
no
no
no no
no
Request from
Windows
Installer
FIGURE 22.6
Locating assemblies.
Deploying, Configuring, and Licensing .NET Components
965
CHAPTER 22
By using cryptographic keys in this fashion, the .NET runtime can assure itself of an assem-
bly’s purported identity every time it is called during execution.
There are two ways that .NET currently supports the signing of assemblies with strong names.
One involves the use of another .NET Framework command-line utility (sn.exe). The other
uses the Visual Studio .NET IDE (or any other .NET compiler).
CONFIGURING, AND
LICENSING .NET
COMPONENTS
Let’s look at the syntax for using this command-line utility:
DEPLOYING,
Usage: SN [-q(uiet)] <option> [<parameters>]
Options:
-c [<csp>] Sets or resets the name of the cryptographic service
provider (CSP) to use for signing assemblies with
strong names.
-d <container> Deletes the key container with the name
<container>.
-D <assembly1> <assembly2> Verifies that the assemblies identified by <assem-
bly1> and <assembly2> differ only by signature.
-e <assembly> <outfile> Extracts the public key from the assembly identified
by <assembly> and writes it to the file identified by
<outfile>.
-I <infile> <container> Installs a key pair (contained in the <infile>) into
the container <container>.
-k <outfile> Generates a key pair and writes it into the file iden-
tified by <outfile>.
-m [y | n] Enables, disables, or returns an indication of
whether key containers are machine-specific instead
of user-specific.
-o <infile> [<outfile>] Converts the public key in the file <infile> to a
comma-separated list of decimal byte values, writ-
ten to the output file <outfile>. If no output file is
specified, the data is written to the Clipboard.
-p <infile> <outfile> Extracts the public key from the key pair in the
input file <infile> and writes it to the output file
<outfile>.
Real-World .NET Programming
966
PART III
-pc <container> <outfile> Extracts the public key from the key pair in the con-
tainer <container>, and writes it to an output file
<outfile>.
-q When used as the first parameter on the command
line, forces the utility into quiet mode, which sup-
presses all messages other than error messages.
-R <assembly> <infile> Re-signs a previously signed or partially signed
assembly <assembly> with the key pair in the input
file <infile>.
-Rc <assembly> <container> Re-signs a previously signed or partially signed
assembly <assembly> with the key pair in the con-
tainer <container>.
-t[p] <infile> Displays the token for the public key in the input
file <infile>. If the p parameter is specified, also
displays the entire public key itself.
-T[p] <assembly> Displays the token for the public key in the assem-
bly <assembly>. If the p parameter is specified, also
displays the entire public key itself.
-v[f] <assembly> Verifies the assembly <assembly> for strong name
consistency. If the f parameter is used, this verifica-
tion is forced even if turned off in the registry.
-Vl Displays a list of the current strong name verifica-
tion settings.
-Vr <assembly> [<userlist>] Registers the assembly <assembly> for verification
skipping; can apply to specific usernames if pro-
vided in the <userlist>.
-Vu <assembly> Unregisters the assembly <assembly> for verifica-
tion skipping.
-Vx Removes all previously registered verification-
skipping entries.
-? Displays the command-line syntax help.
-h Displays the command-line syntax help.
The AssemblyKeyFileAttribute class is used to specify the name of a file that contains the
key pair to be used when signing the assembly. To specify this in the IDE, you simply include
the attribute tag in your code:
<Assembly:AssemblyKeyFileAttribute(“KeyPair.snk”)>
When the application or component is compiled, the compiler uses this attribute to build the
assembly manifest data and associate a strong name with the assembly. You alternatively can
use the AssemblyKeyNameAttribute class in the same way:
<Assembly:AssemblyKeyNameAttribute(“KeyPairContainer”)>
22
The AssemblyKeyNameAttribute class performs the same function as the
CONFIGURING, AND
LICENSING .NET
COMPONENTS
AssemblyKeyFileAttribute class, but instead of specifying a file with the key pairs, it speci-
DEPLOYING,
fies a cryptographic service provider container that holds the key pairs.
NOTE
The decision of whether to give an assembly a strong name should come early in the
game: Strong names cannot be associated with assemblies that already have been
created with only simple names.
Delayed Signing
When dealing with public-private key pairs, developers might encounter a situation in which
they do not have ready access to the private key half of the key pair. Because the private key is
the key to the kingdom, so to speak, access to private keys often is limited to very few people.
In other words, development teams just might not be trusted with an organization’s private
keys. The dilemma here is how a developer can create a strong named assembly if he does not
know the private key. The answer is with delayed signing.
Delayed signing allows assemblies to be created that you intend to be strongly named. A pub-
lic key must be provided, but the private key can wait until later in the deployment process.
To make the delayed signing work, the developer must do the following:
1. Obtain the public key for the public-private key pair.
2. Place that public key inside of a key file. This file can be created using the sn.exe utility.
3. Mark the assembly with the appropriate attribute tags. You use the
AssemblyKeyFileAttribute class, as outlined previously, to provide the public key file.
You also use the AssemblyDelaySignAttribute class, passing it a value of True, as
shown in this code:
<Assembly:AssemblyDelaySignAttribute(true)>
Real-World .NET Programming
968
PART III
Now, when the assembly is created by the compiler, it places the public key into the manifest
just as it normally would. Because it can’t generate the digital signature without the private
key, it just reserves space in the assembly’s manifest for where the signature eventually will go
(after the private key has been provided). Because the .NET runtime expects to see the signa-
ture in the assembly, you must turn off strong name verification for that assembly by using the
-Vr switch with the sn.exe utility.
The development team can continue to develop the assemblies in this manner until just before
the assembly must be shipped to the end user(s). At this point, the actual persons who have
access to the private key step in and complete the signing by again running the sn.exe program
with the -R switch, feeding it the private key container or file.
NOTE
You actually can create an installation package inside Visual Studio .NET. In the New
Project dialog box, select a project type of Setup and Deployment Projects’ and then
select Setup Project. These Visual Studio .NET projects will generate .msi files to be
installed with the Microsoft Windows Installer. For more information, consult the
Visual Studio documentation.
Using Gacutil.exe
Gacutil.exe is a standalone command-line executable that has the capability to both add and
remove assemblies from the Global Assembly Cache.
Deploying, Configuring, and Licensing .NET Components
969
CHAPTER 22
Options:
/i <manifestfile> Installs an assembly into the GAC. You must provide it
with the assembly file that contains the assembly manifest
information.
/u <assembly> Uninstalls an assembly from the GAC. The <assembly> can
identify strong names by using commas to separate their parts:
componentA ,Version=2.0.3.2406,
22
CONFIGURING, AND
PublicKeyToken=394b78ae90878ab.
LICENSING .NET
COMPONENTS
DEPLOYING,
/ungen <assembly> Removes the indicated assembly <assembly> from the .NET
native image cache.
/l Displays all the assemblies in the GAC.
/cdl Deletes the contents of the download cache.
/ldl Displays the assemblies in the download cache.
/nologo Suppresses display of the logo banner.
/silent Suppresses display of all output.
/? Displays the command-line syntax help.
After you have compiled an assembly, making sure that it has been signed with a strong name,
you can install it into the GAC quite easily:
gacutil /i testAssembly.dll
Most of the options are self-explanatory, but a few require further details.
For example, the /ungen switch might be confusing with its reference to the “.NET native
image cache.” Specifically, the documentation says that it “removes the indicated assembly
<assembly> from the .NET native image cache”—but what is the .NET native image cache?
Put simply, it’s a cache that holds a precompiled “image” of assemblies; it is meant to speed up
loading times of assemblies because their types and structure don’t have to be constituted from
the manifest or MSIL information in the assembly. They are stored and ready to go in the
cache. You can even explicitly “compile” an assembly into the native image cache by using the
Native Image Generator tool (Ngen.exe). For more information, consult the MSDN .NET
Framework documentation.
There are also two switches, /cdl and /ldl, that make reference to a download cache. The
download cache is a special storage area designed to contain components that have been down-
loaded from the Internet.
Real-World .NET Programming
970
PART III
FIGURE 22.7
The Assembly Cache Viewer.
You can display information about an assembly by right-clicking on it and selecting Properties.
Notice in Figure 22.8 that the assembly Properties dialog box shows some very basic informa-
tion about the selected component, including its name, its version number, the date it was last
modified, and its public key token.
FIGURE 22.8
Assembly properties.
Deploying, Configuring, and Licensing .NET Components
971
CHAPTER 22
You also can deploy assemblies into the GAC by dragging and dropping them into the
Assembly Cache window. Deleting assemblies is just as easy: Simply select the assembly and
press Delete.
CONFIGURING, AND
LICENSING .NET
COMPONENTS
NOTE
DEPLOYING,
An application configuration file is called app.config and sits in the root of the appli-
cation directory. There is actually a Visual Studio .NET template that you can add to
your project, to allow you to easily create one of these configuration files. Just select
the Project menu inside the IDE and then select Add New Item. When the dialog box
of possible items appears, scroll down until you see the application configuration
item.
Runtime Settings
A total of nine different XML tags are allowed in the runtime settings section of the configura-
tion file. Each of them is described in Table 22.1.
Real-World .NET Programming
972
PART III
As an example, Listing 22.1 shows a configuration file making use of the assemblyBinding
element and child elements.
</assemblyBinding>
</runtime>
</configuration>
Deploying, Configuring, and Licensing .NET Components
973
CHAPTER 22
CONFIGURING, AND
LICENSING .NET
• It defines a specific path that the runtime should search when trying to resolve any
COMPONENTS
DEPLOYING,
local/private assemblies.
Using the application configuration file allows you to control a vast variety of settings on a
per-application basis. For machine-wide settings, there is also a machine configuration file
(machine.config) that you will find in the .NET Framework deployment directory. It is an
XML document as well, but it has a different schema. (Consult the Framework reference docu-
mentation for more information on machine configuration files. Better yet, open one in
Notepad or your favorite XML editor and examine the contents.)
Enabling Licensing
Licenses are meant to provide some proof that the entity running the component is authorized
by the component author to do so. The most obvious example comes in the form of companies
protecting their copyrights and sales by ensuring that only valid, paid-for copies of their soft-
ware can be executed.
If you are going to develop components that need to be licensed, the steps to do so are straight-
forward: First, you apply an attribute to the class that identifies a license provider that will dis-
pense the license for the component. Then, inside the component’s code, you validate the
dispensed license, releasing it when the component expires. All Framework code that makes
this happen is in the System.ComponentModel namespace.
inheriting the LicenseProvider base class, that must be “attached” to the licensed component
using an attribute class: LicenserProviderAttribute. This attribute class takes the type of
license provider into its constructor, like this:
<LicenseProviderAttribute(GetType(LicFileLicenseProvider))>
This declaratively marks the component for licensing enforcement. From there, you must
implement code using an instance of the LicenseManager class to determine whether the
license dispensed was valid.
End Sub
CONFIGURING, AND
End Class
LICENSING .NET
COMPONENTS
DEPLOYING,
Summary
.NET provides the developer with a host of tools that make application packaging, configura-
tion, deployment, and licensing almost easy. From its support for XCopy deployment to its
robust mechanisms for coping with shared component problems, the deployment system
offered for managed code sits fully exposed for Visual Basic developers to use as needed for a
wide variety of deployment scenarios.
In this chapter, we touched on
• The deployment problems that .NET has focused on eliminating
• Distributing private versus shared assemblies
• Signing assemblies with strong names
• The various methods for publishing components into the Global Assembly Cache
• Implementing custom application configuration through application configuration files
• Checking for valid licenses during component instantiation
Calling the Win32 API APPENDIX
A
from Managed Code
IN THIS APPENDIX
• Platform Invoke 978
Windows’ new API, the Framework Class Library, replaces many of the Win32 API functions
and wraps others. However, you will find that some API functions are not to be found inside
the class library, and some class library methods will not work exactly the same as their equiv-
alent API functions. Not to worry, you can still access the Win32 API from .NET code. In fact,
for VB developers, .NET makes it easier to access this massive library.
This appendix focuses on writing VB .NET code to access the Win32 API. Accessing the
Win32 API will be the most common reason for using this type of interoperability, but of
course the content of the appendix can be applied to access other legacy APIs.
(For .NET equivalents to specific Win32 API functions, please read Appendix B, “Win32 API-
to-Namespace Cross-Reference.”)
Platform Invoke
Platform Invoke is a service provided by .NET that allows you to call functions inside of
unmanaged code directly from managed code. Unmanaged code is any code that is not running
under the service of the Common Language Runtime (CLR). The entire Win32 API is unman-
aged code because it predates .NET. Managed code is code that executes under, and conforms
to, the confines of the CLR. As you might imagine, there is a boundary between code that is
managed and code that exists outside of the CLR. To cross this boundary you must rely on the
services of Platform Invoke.
Platform Invoke provides a number of services to your managed code. First, it locates an
unmanaged function on the machine and inside a Windows’ DLL. Next, it invokes that function
for your application. Additionally, it marshals data (parameters and return values) between the
boundaries of the CLR and the given API. Finally, it returns exceptions raised by the unman-
aged function call directly to your .NET code. Platform Invoke relies on metadata to operate. It
uses this metadata to locate functions and perform marshalling at runtime. Figure A.1 illus-
trates how Platform Invoke is used to interoperate with unmanaged code.
Managed Unmanaged
Platform Invoke
Class/
Module Function
CLR
Interop Boundary
FIGURE A.1
Platform Invoke—managed to unmanaged.
Calling the Win32 API from Managed Code
979
APPENDIX A
NOTE
If you are unfamiliar with what the API has to offer, we suggest perusing the MSDN
SDK documentation or taking a look at the title from which this book derives its
name, Visual Basic Programmer’s Guide to the Win32 API.
CALLING THE
inside a class or module. We suggest you wrap your API functions with a class. This provides
easier access from other managed code and is often easier to maintain. Finally, you call the
function directly, passing it the necessary parameters.
The code in Listing A.1 illustrates three simple API calls from managed code. We first
create a class called EventLog. Inside this class we declare the functions OpenEventLog,
ClearEventLog, and CloseEventLog. We then simply call the API functions from inside
the sub Main of a module.
End Class
Module Module1
Sub Main()
‘local scope
Dim handleLog As Integer
End Sub
End Module
Note that these API functions have equivalents within the Framework Class Library inside of
the System.Diagnostics namespace. Also notice that each function declared inside the class
actually becomes a method of that class.
Of course, there are a number of additional settings available to you when using Platform
Invoke. The rest of this section details those settings.
Calling the Win32 API from Managed Code
981
APPENDIX A
Sub Main()
‘call function A
Beep(300, 1000)
CALLING THE
End Sub
End Module
Platform Invoke has a set of default values that indicate how the service operates. These
values are available for you to override and tweak to your needs. For declarations that require
additional settings, use the DllImportAttribute class available to you through the
System.Runtime.InteropServices namespace. This class allows you to mark a
method as being handled by Platform Invoke and passed to an unmanaged API.
The following code declares the same function as the preceding but using the
DllImportAttribute class.
Imports System.Runtime.InteropServices
Module Module1
End Function
Sub Main()
‘call function
Beep(300, 1000)
End Sub
End Module
The functionality of the two code blocks is identical. For the majority of function declarations
it comes down to personal preference. Some will like the better readability and .NET-like
code written with the attribute class while others will want to stick with the old VB-looking
declarations.
Sometimes, however, you will need to access specific settings inside of Platform Invoke.
Tweaking these settings can only be done via the attribute class. For this reason, we suggest
always using the attribute class syntax.
EntryPoint
The EntryPoint object field inside of the DllImportAttribute class allows you to set the
functions alias. This is equivalent to using the Alias keyword inside a standard Declare state-
ment. An alias is simply an alternate name for your function outside of the Win32 API’s name.
Aliases are useful when your code needs to comply with a specific naming standard or you
wish to declare multiple versions of the same DLL function with different names.
The following are examples of setting aliases. The first creates an alias using the Declare syn-
tax and the Alias keyword for a Unicode version of the Beep function. Note that the alias is a
string literal.
Declare Unicode Function BeepUnicode Lib “Kernel32.dll” Alias “Beep” _
(ByVal dwFreq As Integer, ByVal dwDuration As Integer)
The second example uses the DllImportAttributeClass to create an ANSI version of our
Beep function. Notice that we set the field EntryPoint this time, rather than using the Alias
keyword.
<DllImport(“Kernel32.dll”, CharSet:=CharSet.Ansi, EntryPoint:=”Beep”)> _
Public Function BeepAnsi(ByVal dwFreq As Integer, _
ByVal dwDuration As Integer)
‘passes to Kernel32 through Platform Invoke
End Function
Calling the Win32 API from Managed Code
983
APPENDIX A
CharSet
The CharSet enumeration inside of DllImportAttribute controls how the Platform Invoke
service marshals strings and finds function names inside an unmanaged DLL.
As you saw with the beep function, many libraries export both a Unicode and an ANSI version
of the same function. The ANSI version typically carries the letter A at its end (MessageBoxA).
The Unicode usually carries the letter W as a suffix (MessageBoxW). CharSet allows you to
specify which function you are trying to access. The enumeration has the following three val-
ues: ANSI, Unicode, and Auto. The first two are self-explanatory while the last simply passes
the job of determining to the Platform Invoke service. If you do not specify CharSet inside of
VB, the default is ANSI.
NOTE
ANSI is a 1-byte character set that is typically used for operating systems running
Windows 95/98. Unicode applies to Windows NT/2000/XP and uses a 2-byte
character set.
MANAGED CODE
CALLING THE
enumeration. The following are examples of setting CharSet. The first uses the keyword Auto
and the Declare syntax.
Declare Auto Function Beep Lib “Kernel32.dll” _
(ByVal dwFreq As Integer, ByVal dwDuration As Integer)
The second example uses the DllImportAttributeClass and the CharSet enumeration to cre-
ate a Unicode version of our Beep function.
<DllImport(“Kernel32.dll”, CharSet:=CharSet.Unicode)> _
Public Function Beep(ByVal dwFreq As Integer, _
ByVal dwDuration As Integer)
‘passes to Kernel32 through Platform Invoke
End Function
ExactSpelling
The ExactSpelling field of DllImportAttribute works directly with the CharSet enumera-
tion to manage how the compiler and Platform Invoke interprets your request for a given func-
tion name. The following are possible combinations and their descriptions:
• CharSet.Ansi and ExactSpelling = True—Platform Invoke only searches for the exact
name you specify.
• CharSet.Unicode and ExactSpelling = True—Platform Invoke only searches for the
exact name you specify.
984
APPENDIXES
End Function
CallingConventions
The CallingConventions enumeration inside of the DllImportAttribute class allows you to
specify the conventions used when calling the function. The default value of this enumeration
is WinAPI. As calling the Win32 API from managed code is the focus of this chapter, we will
leave it to the readers to peruse the other values of this enumeration should they require their
use. Setting this enumeration is only possible using the attribute-like syntax for function
declarations.
PreserveSig
The PreserveSig object field is used to indicate whether the HRESULT or Retval should be
suppressed. The default value for PreserveSig is True; that is, the signature transformations
are preserved. Setting this value to False indicates that you wish the HRESULT to be trans-
formed into the Retval and thus discarded. This field is only accessible via the
DllImportAttribute class and the attribute-like syntax for function declarations.
SetLastError
The SetLastError object field is used to indicate that the API should allow you to call the
GetLastError API call after a given API call. This was common practice for VB developers.
The GetLastError API allowed you to determine whether an error occurred while executing a
given API call. For this reason, inside of VB, the default value of the field is True. This field is
only available to those using the attribute-like syntax for function declarations.
Calling the Win32 API from Managed Code
985
APPENDIX A
CALLING THE
LISTING A.2 Passing a Class
Imports System.Runtime.InteropServices
End Class
‘local scope
Dim sTimeSet As New SystemTime()
Dim sTimeGet As New SystemTime()
End Sub
End Class
Callback Functions
Creating callback functions is a typical task of the Win32 API developer. Callback functions
work directly with the unmanaged code to execute a given task. For example, suppose that you
needed to execute the same task on a group of objects. The API call would find the first object
and return it to your callback function. Your callback function would execute another task and
the API would then find the next object. This is illustrated by Figure A.2.
Calling the Win32 API from Managed Code
987
APPENDIX A
Managed Unmanaged
Callback
Function Returns To Callback Function
CLR
Interop Boundary
FIGURE A.2
Callback functions.
As you can see, callbacks are implemented by passing the DLL function a pointer to the call- A
CALLING THE
using them. To do so, you can often check the argument of a given parameter. An argument
whose prefix is lp and whose suffix is Func usually indicates a callback. The prefix stands for
Long Pointer and the suffix, Function.
After you’ve identified a need for a callback function, you implement it via delegates. Platform
Invoke converts a given delegate to a familiar callback format for you.
Listing A.3 represents a DLL function that makes a callback to a managed function. We ini-
tially create a delegate (CallBack) that has a Windows handle as its first parameter. Note that
EnumWindowsProc defines the proper format for our delegate used with the DLL function
EnumWindows. Next, we declare the EnumWindows function and set its first parameter to a type
of our delegate, CallBack. We then define the class that will actually receive the delegate calls,
ReceiveCallBack. This function takes the Windows handle and writes it to the console.
Finally, create a simple Main in our module to call the EnumWindows API. We then pass the
AddressOf ReceiveCallBack to it. This is standard for delegates in VB. Note that the
ReceiveCallBack function automatically returns True. This indicates to the API function that
we wish to continue. Returning False would halt the callbacks.
The result of this code is a list of all the top-level Windows handles displayed inside the
console.
988
APPENDIXES
Module Module1
End Function
Sub Main()
‘pause
Do While Not Console.ReadLine = “s” : Loop
End Sub
End Module
Win32 API-to-Namespace APPENDIX
B
Cross-Reference
990
APPENDIXES
CROSS-REFERENCE
WIN32 API-TO-
Graphics Class
NAMESPACE
DrawArc Method
AnimatePalette —
AnimateWindow —
AnyPopup —
APCProc —
AppendMenu System.Windows.Forms Namespace
MenuItem Class
MenuItems Property
Menu.MenuItemCollection Class
Add Method
992
APPENDIXES
CROSS-REFERENCE
WIN32 API-TO-
CascadeWindows System.Windows.Forms Namespace
NAMESPACE
Form Class
LayoutMdi Method
CBTProc —
CCHookProc —
CFHookProc —
ChangeClipboardChain System.Windows.Forms Namespace
Clipboard Class
ChangeDisplaySettings —
ChangeDisplaySettingsEx —
994
APPENDIXES
CROSS-REFERENCE
WIN32 API-TO-
ShowDialog Method
NAMESPACE
ChooseFont System.Windows.Forms Namespace
FontDialog Class
ShowDialog Method
Chord System.Drawing.Imaging
Graphics Class
DrawEllipse Method
ClearCommBreak —
ClearCommError —
ClearEventLog System.Diagnostics Namespace
EventLog Class
Clear Method
996
APPENDIXES
CROSS-REFERENCE
WIN32 API-TO-
Pause Method
NAMESPACE
Stop Method
ConvertDefaultLocale —
ConvertThreadToFiber —
CopyAcceleratorTable —
CopyCursor System.Windows.Forms Namespace
Cursor Class
CopyHandle Method
CopyEnhMetaFile —
CopyFile System.IO Namespace
File Class
Copy Method
998
APPENDIXES
CROSS-REFERENCE
WIN32 API-TO-
Constructors
NAMESPACE
CreateDIBPatternBrush System.Drawing Namespace
TextureBrush Class
Constructors
CreateDIBPatternBrushPt System.Drawing Namespace
TextureBrush Class
Constructors
CreateDIBSection —
CreateDirectory System.IO Namespace
Directory Class
CreateDirectory Method
1000
APPENDIXES
CROSS-REFERENCE
WIN32 API-TO-
Constructor
NAMESPACE
IsMdiContainer Property
CreateMenu System.Windows.Forms
MenuItem Class
Constructor
CreateMetaFile —
CreateMutex System.Threading Namespace
Mutex Class
Constructor
CreateNamedPipe —
CreatePalette —
1002
APPENDIXES
CROSS-REFERENCE
WIN32 API-TO-
Constructor
NAMESPACE
CreateTapePartition —
CreateThread System.Threading Namespace
Thread Class
Constructor
Start Method
ThreadStart Class
CreateTimerQueue System.Threading Namespace
Timer Class
CreateTimerQueueTimer System.Threading Namespace
Timer Class
1004
APPENDIXES
CROSS-REFERENCE
WIN32 API-TO-
DdeGetData —
NAMESPACE
DdeGetLastError —
DdeImpersonateClient —
DdeInitialize —
DdeKeepStringHandle —
DdeNameService —
DdePostAdvise —
DdeQueryConvInfo —
DdeQueryNextServer —
DdeQueryString —
DdeReconnect —
1006
APPENDIXES
CROSS-REFERENCE
WIN32 API-TO-
ServiceInstaller Class
NAMESPACE
RollBack Method
DeleteTimerQueue —
DeleteTimerQueueEx —
DeleteTimerQueueTimer —
DeleteVolumeMountPoint —
DeregisterEventSource System.Diagnostics Namespace
EventLog Class
DeleteEventSource Method
DestroyAcceleratorTable —
DestroyCaret —
1008
APPENDIXES
CROSS-REFERENCE
WIN32 API-TO-
DrawAnimatedRects System.Drawing Namespace
NAMESPACE
ImageAnimator Class
DrawCaption —
DrawEdge System.Drawing Namespace
Graphics Class
DrawRectangle Method
DrawEscape —
DrawFocusRect —
DrawFrameControl —
DrawIcon System.Drawing Namespace
Graphics Class
DrawIcon Method
1010
APPENDIXES
CROSS-REFERENCE
WIN32 API-TO-
EndPaint —
NAMESPACE
EndPath System.Drawing.Drawing2D Namespace
GraphicsPath Class
EndUpdateResource —
EnhMetaFileProc —
EnterCriticalSection —
EnumCalendarInfo —
EnumCalendarInfoEx —
EnumCalendarInfoProc —
EnumCalendarInfoProcEx —
EnumChildProc —
1012
APPENDIXES
CROSS-REFERENCE
WIN32 API-TO-
EnumMonitors —
NAMESPACE
EnumObjects —
EnumObjectsProc —
EnumPorts —
EnumPrinterData —
EnumPrinterDataEx —
EnumPrinterDrivers —
EnumPrinterKey —
EnumPrinters —
EnumPrintProcessorDatatypes —
EnumPrintProcessors —
1014
APPENDIXES
CROSS-REFERENCE
WIN32 API-TO-
ExpandEnvironmentStringsForUser —
NAMESPACE
ExtCreatePen System.Drawing Namespace
Pen Class
ExtCreateRegion System.Drawing Namespace
Region Class
ExtEscape —
ExtFloodFill System.Drawing Namespace
Graphics Class
Fill Region Method
ExtractAssociatedIcon —
ExtractIcon —
1016
APPENDIXES
CROSS-REFERENCE
WIN32 API-TO-
FindWindowEx —
NAMESPACE
FlashWindow —
FlashWindowEx —
FlattenPath System.Drawing Namespace
GraphicsPath Class
Flatten Method
FloodFill System.Drawing Namespace
Graphics Class
FlushConsoleInputBuffer —
FlushFileBuffers System.IO Namespace
FileStream Class
Flush Method
1018
APPENDIXES
CROSS-REFERENCE
WIN32 API-TO-
GetCharABCWidthsFloat
NAMESPACE
GetCharABCWidthsI —
GetCharacterPlacement —
GetCharWidth —
GetCharWidth32 —
GetCharWidthFloat —
GetCharWidthI —
GetClassInfo —
GetClassInfoEx —
GetClassLong —
GetClassLongPtr —
1020
APPENDIXES
CROSS-REFERENCE
WIN32 API-TO-
String Class
NAMESPACE
Format Method
GetCurrentDirectory System.IO Namespace
Directory Class
GetCurrentDirectory Class
GetCurrentHwProfile —
GetCurrentObject —
GetCurrentPositionEx System.Drawing
GetCurrentProcess System.Diagnostics
Process Class
GetCurrentProcess Property
1022
APPENDIXES
CROSS-REFERENCE
WIN32 API-TO-
Exception Class
NAMESPACE
GetExceptionInformation System Namespace
Exception Class
Message Property
Source Property
StackTrace Property
GetExitCodeProcess System.Diagnostics Namespace
Process Class
HasTerminated Property
1024
APPENDIXES
CROSS-REFERENCE
WIN32 API-TO-
ProductName Property
NAMESPACE
ProductPrivatePart Property
ProductVersion Property
GetFileVersionInfoSize —
GetFocus System.Windows.Forms
Control Class
Focused Property
GetFontData System.Drawing
Font Class
GetFontLanguageInfo System.Drawing
Font Class
1026
APPENDIXES
CROSS-REFERENCE
WIN32 API-TO-
GetMappedFileName —
NAMESPACE
GetMenu System.Windows.Forms Namespace
Form Class
Menu Property
GetMenuBarInfo —
GetMenuCheckMarkDimensions —
GetMenuDefaultItem System.Windows.Forms Namespace
MenuItem Class
DefaultItem Property
GetMenuInfo System.Windows.Forms Namespace
Menu Class
MenuItem Class
1028
APPENDIXES
CROSS-REFERENCE
WIN32 API-TO-
GetOEMCP —
NAMESPACE
GetOldestEventLogRecord System.Diagnostics Namespace
EventLog Class
Entries Property
EventLogEntryCollection Class
GetOpenClipboardWindow —
GetOpenFileName System.Windows.Forms Namespace
FileDialog Class
GetOutlineTextMetrics —
GetOverlappedResult —
GetPaletteEntries —
1030
APPENDIXES
CROSS-REFERENCE
WIN32 API-TO-
GetRegionData Method
NAMESPACE
GetRegisteredRawInputDevices —
GetRgnBox System.Drawing Namespace
Region Class
GetBounds Method
GetROP2 —
GetSaveFileName —
GetScrollBarInfo System.Windows.Forms Namespace
ScrollBar Class
GetScrollInfo System.Windows.Forms Namespace
ScrollBar Class
1032
APPENDIXES
CROSS-REFERENCE
WIN32 API-TO-
GetTextExtentExPoint —
NAMESPACE
GetTextExtentExPointI —
GetTextExtentPoint —
GetTextExtentPoint32 —
GetTextExtentPointI —
GetTextFace —
GetTextMetrics —
GetThreadContext —
GetThreadDesktop —
GetThreadLocale —
1034
APPENDIXES
CROSS-REFERENCE
WIN32 API-TO-
GetWindowPlacement —
NAMESPACE
GetWindowRect —
GetWindowRgn —
GetWindowRgnBox —
GetWindowsDirectory System.Management Namespace
GetWindowText —
GetWindowTextLength —
GetWindowThreadProcessId —
GetWinMetaFileBits —
GetWorldTransform —
GetWriteWatch —
1036
APPENDIXES
CROSS-REFERENCE
WIN32 API-TO-
ImmGetCompositionFont —
NAMESPACE
ImmGetCompositionString —
ImmGetCompositionWindow —
ImmGetContext —
ImmGetConversionList —
ImmGetConversionStatus —
ImmGetDefaultIMEWnd —
ImmGetDescription —
ImmGetGuideLine —
ImmGetImeFileName —
ImmGetImeMenuItems —
1038
APPENDIXES
CROSS-REFERENCE
WIN32 API-TO-
InterlockedExchangeAdd —
NAMESPACE
InterlockedExchangePointer —
InterlockedFlushSList —
InterlockedIncrement System.Threading Namespace
Interlocked Class
Increment Method
InterlockedPopEntrySList —
InterlockedPushEntrySList —
IntersectClipRect System.Drawing Namespace
IntersectRect System.Drawing Namespace
Region Class
Intersect Method
1040
APPENDIXES
CROSS-REFERENCE
WIN32 API-TO-
LineDDAProc —
NAMESPACE
LineTo System.Drawing Namespace
Graphics Class
DrawLine Method
LoadAccelerators —
LoadBitmap System.Drawing Namespace
Bitmap Class
Constructor
LoadCursor —
LoadCursorFromFile —
1042
APPENDIXES
CROSS-REFERENCE
WIN32 API-TO-
lstrlen System Namespace
NAMESPACE
String Class
Length Property
LZClose System.IO Namespace
FileInfo Class
LZCopy System.IO Namespace
FileInfo Class
LZInit System.IO Namespace
FileInfo Class
LZOpenFile System.IO Namespace
FileInfo Class
1044
APPENDIXES
CROSS-REFERENCE
WIN32 API-TO-
Height Property
NAMESPACE
Left Property
Size Property
Top Property
Width Property
MsgWaitForMultipleObjects System.Threading Namespace
ThreadPool Class
RegisterWaitForSingleObject Method
MsgWaitForMultipleObjectsEx System.Threading Namespace
ThreadPool Class
RegisterWaitForSingleObject Method
1046
APPENDIXES
CROSS-REFERENCE
WIN32 API-TO-
NetGroupGetInfo —
NAMESPACE
NetGroupGetUsers —
NetGroupSetInfo —
NetGroupSetUsers —
NetJoinDomain —
NetLocalGroupAdd —
NetLocalGroupAddMember —
NetLocalGroupAddMembers —
NetLocalGroupDel —
NetLocalGroupDelMember —
NetLocalGroupDelMembers —
1048
APPENDIXES
CROSS-REFERENCE
WIN32 API-TO-
NetShareDel —
NAMESPACE
NetShareEnum —
NetShareGetInfo —
NetShareSetInfo —
NetStatisticsGet —
NetUnjoinDomain —
NetUseAdd —
NetUseDel —
NetUseEnum —
NetUseGetInfo —
NetUserAdd —
1050
APPENDIXES
CROSS-REFERENCE
WIN32 API-TO-
Mutex Class
NAMESPACE
OpenPrinter System.Drawing.Printing Namespace
PrintDocument Class
OpenProcess System.Diagnostics Namespace
Process Class
OpenSCManager —
OpenSemaphore —
OpenService System.ServiceProcess
ServiceController Class
OpenThread System.Threading Namespace
Thread Class
1052
APPENDIXES
CROSS-REFERENCE
WIN32 API-TO-
PdhRemoveCounter —
NAMESPACE
PdhSelectDataSource —
PdhSetCounterScaleFactor —
PdhSetDefaultRealTimeDataSource —
PdhSetQueryTimeRange —
PdhUpdateLog —
PdhUpdateLogFileCatalog —
PdhValidatePath —
PeekConsoleInput —
PeekMessage —
PeekNamedPipe —
1054
APPENDIXES
CROSS-REFERENCE
WIN32 API-TO-
PrintDialog
NAMESPACE
PrinterSettings Property
System.Drawing.Printing
PrinterSettings Class
PrintHookProc —
PrintWindow —
Process32First —
Process32Next —
PropEnumProc —
PropEnumProcEx —
1056
APPENDIXES
CROSS-REFERENCE
WIN32 API-TO-
FileSystemWatcher Class
NAMESPACE
ReadEventLog System.Diagnostics Namespace
EventLog Class
Entries Property
ReadFile System.IO Namespace
FileStream Class
Read Method
ReadFileEx System.IO Namespace
FileStream Class
Read Method
ReadFileScatter —
1058
APPENDIXES
CROSS-REFERENCE
WIN32 API-TO-
RegisterEventSource —
NAMESPACE
RegisterHotKey —
RegisterRawInputDevices —
RegisterServiceCtrlHandler —
RegisterServiceCtrlHandlerEx —
RegisterWaitForSingleObject —
RegisterWindowMessage —
RegLoadKey Microsoft.Win32 Namespace
RegistryKey Class
RegNotifyChangeKeyValue —
RegOpenCurrentUser —
1060
APPENDIXES
CROSS-REFERENCE
WIN32 API-TO-
RestoreDC —
NAMESPACE
ResumeThread System.Threading Namespace
Thread Class
Resume Method
ReuseDDElParam —
RoundRect System.Drawing Namespace
Graphics Class
DrawRectangle Method
Rectangle Structure
Pen Class
1062
APPENDIXES
CROSS-REFERENCE
WIN32 API-TO-
CombineMode Enumeration
NAMESPACE
GraphicsPath Class
System.Drawing.Namespace
RegionClass
Graphics Class
SetClip Method
SelectObject —
SelectPalette —
SendAsyncProc —
SendDlgItemMessage —
SendInput —
1064
APPENDIXES
CROSS-REFERENCE
WIN32 API-TO-
Cursor Class
NAMESPACE
Current Property
Cursors Class
SetCursorPos System.Windows.Forms Namespace
Cursor Class
Position Property
SetDCBrushColor —
SetDCPenColor —
SetDefaultCommConfig —
SetDefaultPrinter —
SetDIBColorTable —
1066
APPENDIXES
CROSS-REFERENCE
WIN32 API-TO-
SetMenuDefaultItem System.Windows.Forms Namespace
NAMESPACE
MenuItem Class
DefaultItem Property
SetMenuInfo System.Windows.Forms Namespace
MenuItem Class
SetMenuItemBitmaps —
SetMenuItemInfo System.Windows.Forms Namespace
MenuItem Class
SetMessageExtraInfo —
SetMetaFileBitsEx —
SetMetaRgn —
1068
APPENDIXES
CROSS-REFERENCE
WIN32 API-TO-
SetTapeParameters —
NAMESPACE
SetTapePosition —
SetTextAlign —
SetTextCharacterExtra —
SetTextColor —
SetTextJustification —
SetThreadAffinityMask —
SetThreadContext —
SetThreadDesktop —
SetThreadExecutionState —
1070
APPENDIXES
CROSS-REFERENCE
WIN32 API-TO-
SetWorldTransform System.Drawing Namespace
NAMESPACE
Graphics Class
RotateTransform Method
ScaleTransform Method
TranslateTranform Method
ShellProc —
ShowCaret —
ShowCursor System.Windows.Forms Namespace
Cursor Class
Show Method
Hide Method
1072
APPENDIXES
CROSS-REFERENCE
WIN32 API-TO-
TerminateProcess System.Diagnostics Namespace
NAMESPACE
Process Class
Close Method
TerminateThread System.Threading Namespace
Thread Class
Abort Method
TextOut —
Thread32First —
Thread32Next —
ThreadProc —
1074
APPENDIXES
CROSS-REFERENCE
WIN32 API-TO-
UnloadUserProfile —
NAMESPACE
UnlockFile System.IO Namespace
FileStream Class
Lock Method
Unlock Method
UnlockFileEx System.IO Namespace
FileStream Class
Lock Method
Unlock Method
UnlockServiceDatabase —
UnmapViewOfFile —
1076
APPENDIXES
CROSS-REFERENCE
WIN32 API-TO-
Write Method
NAMESPACE
WriteLine Method
WriteConsoleInput —
WriteConsoleOutput System Namespace
Console Class
Write Method
WriteLine Method
WriteConsoleOutputAttribute —
WriteConsoleOutputCharacter —
1078
APPENDIXES
C
IN THIS APPENDIX
• Security Policies 1080
The .NET Framework provides developers with a multitude of constructs for enforcing applica-
tion-level security. This appendix describes how security policies work in the Framework and
details the two major application security models embraced by the .NET Framework: role-
based security and code access security.
Security Policies
When code is loaded into the runtime, the first things the runtime must figure out are “what
actions should I allow this code to perform, and what resources should I allow it access to?”
This is handled in a straightforward way: Each assembly in .NET exposes a series of character-
istics (called evidence) that the runtime will examine in order to answer the aforementioned
questions. Examples of this evidence include who signed the assembly, where the assembly
came from, and so on.
NOTE
Remember that assemblies are units of deployable code.
The supplied evidence is used in conjunction with the established security policy to determine
which permissions the assembly will be assigned. A security policy is a set of rules that are
followed when determining which set of permissions to apply to a specific assembly. In addi-
tion to these default permissions applied to the running code, an assembly can also specifically
request a set of permissions, usually to indicate that they are needed in order for the assembly
to run properly. This request won’t cause the runtime to add permissions that aren’t covered by
a policy, but they can cause the runtime to throw an exception and not load the code if it won’t
get the permissions it needs to run. To further secure things, the runtime may take away stan-
dard permissions from an assembly if it has indicated that it doesn’t need them.
The set of default security policies can be altered by administrators through two different tools:
the .NET Framework Configuration Tool (an MMC snap-in) and the Code Access Security
Policy Tool (a stand-alone executable: caspol.exe). Both of these are shipped with the
Framework and are documented in the MSDN framework documentation.
Beyond policies, developers have two ways of further securing their applications: code access
security and role-based security.
accessed by a particular body of code. Code access security can operate at a more granular
level than simple security policies in that it can be enforced at the class level or at the class
member level, in addition to the assembly level.
Code access security checks are said to happen declaratively or imperatively, and there are two
mirrored sets of permission classes that allow this to happen. For imperative security, you
would use the core permission class specific to the resource you are interested in. For example,
if you want to specify security for file I/O, you would use the FileIOPermission class. For
declarative security, you would use the permission attribute classes. Each permission class has
a counterpart permission attribute class for use with declarative security. The declarative coun-
terpart to the FileIOPermission class is the FileIOPermissionAttribute class.
These permission attributes can be applied against assemblies, classes, and class members.
Note that the constructor accepts a SecurityAction enumeration value. Selecting one security
action type over another will affect the results of the code access security check.
Member Description
Assert Instructs the runtime to only examine the current caller of the
code to see if he has sufficient privileges to access the indicated
resource. Asserts happen at runtime.
Demand Instructs the runtime to require all callers higher in the call stack
to have correct permissions to access the indicated resource.
Demands happen at runtime.
Deny Indicates that no caller can access the specified resource, even if C
he has the correct permissions.
.NET SECURITY
InheritanceDemand Indicates that any class which inherits from the current class
MODELS
C
MODELS
.NET SECURITY
1084
ioPermission.LinkDemand()
.
.
.
End Sub
The runtime would throw an exception on the call to LinkDemand if the appropriate permis-
sions were not available; thus, the rest of the code in the method would not run.
C
.NET SECURITY
MODELS
1086
C
MODELS
.NET SECURITY
1088
APPENDIXES
Role-Based Security
Role-based security is a more traditional security model. Instead of being based on the concept
of code accessing a particular resource, it is based on the concept of limiting or allowing
actions as determined by a user’s identity and his membership in a role group. In .NET, a prin-
cipal object is used to encapsulate a user’s identity and indicate to which roles he belongs.
Principals
In a sense, a principal is a sort of proxy that interacts with the security system as a representa-
tion of a user. Principals work in conjunction with identity objects. Identity objects uniquely
identify users to the runtime.
There are three types of principals:
• Generic—These apply to users and roles that exist outside of the concept of Windows
users and roles. In the .NET Framework, these are represented by the GenericPrincipal
class in the System.Security.Principal namespace.
• Windows—These are a one-to-one map with Windows users and groups. These are rep-
resented by the WindowsPrincipal class—also in the System.Security.Principal
namespace.
• Custom—These are defined by an application. They have no default representation in the
class library, and need to be created and defined by each application that uses them.
pPermit.Demand()
.
.
.
End Sub
Joining Permissions
If your code needs to check for a blend of identities or roles, you can use the
PrincipalPermission.Union method to combine one permission object with another:
pPermit.Union(otherPermit).Demand
C
.NET SECURITY
MODELS
.NET Framework Base Data APPENDIX
D
Types
1092
APPENDIXES
FRAMEWORK
characters (fixed member set)
.NET
Value Range: 0 to ~2,000,000,000 characters
Assembly Linker utility, 944 Bitmap class, 346, 373 CharSet enumeration, 983
AssemblyKeyFileAttribute BizTalk Server 2000, 13-14 classes
class, 967 block scope, 34-35 abstract classes, 70-71
Assert method, 917-918 BooleanSwitch class, 914-915 AccessControlEntry class, 649
assertions, 917-918 brace matching, 46-47 AccessControlList class, 600,
attributes browers 650
Active Directory, 780-781 browser-to-server communica- Active Directory, 779-781
class attributes, 70-71 tion ActiveXMessengerFormatter
directories, 785-788 detecting browser infor- class, 600
auditing messages, 657-658 mation, 703-705 ArrayList class, 871, 878-883
authentication MyStatus Indicator, AssemblyKeyFileAttribute
basic authentication, 308 712-720 class, 967
credentials, 308-310 overview, 692 attributes, 70-71
digest authentication, 308 state management, AutoResetEvent class, 540
Kerberos authentication, 308 705-711 AxHost class, 84, 131-132
messages, 652-657 uplevel and downlevel, 703 BinaryMessageFormatter
NTLM authentication, 308 Brush class, 346 class, 600, 639-641
overview, 307 brushes BinaryReader class, 215,
HatchBrush class, 364-366 248-251
proxies, 311
LinearGradientBrush class, BinaryWriter class, 215,
AuthenticationManager class,
276 362-364 248-251
AuthenticationTypes enumera- SolidBrush class, 361 Bitmap class, 346, 373
tion, 788 TextureBrush class, 361-362 BooleanSwitch class, 914-915
Automatic Transaction building block services Brush class, 346
Processing, 808 calendar services, 13 Cache class, 694
AutoResetEvent class, 540 directory and search services, classes that replace VB6 ele-
AxHost class, 84, 131-132 13 ments, 36
identify services, 13 Clipboard class, 84, 100-102
B notification and messaging CollectionBase class, 888-892
services, 13 Constraint class, 723
backward compatibility,
overview, 12 ConstraintCollection class,
952-953
personalization services, 13 723
BCL (Base Class Library), 75-77
ConstraintException class, 723
behavioral changes in VB .NET,
36-39
C constructors, 71
C# overview, 12
ContainerControl class, 84, 92
Bézier curves, 353
Cache class, 694
ContextMenu class, 84, 107
Bézier splines, 354-355
calendars, 13, 935-936
Control class, 85, 91-92,
binary files, reading and writ-
125-128
ing, 248-251 callback functions, 986-988
CultureInfo class, 929-931
BinaryMessageFormatter class, CallingConventions enumera-
600, 639-641 tion, 984
Cursor class, 85
BinaryReader class, 215, caps (lines), 350-352
Cursors class, 85
248-251 cardinal splines, 353-354
CustomLineCap class, 345
BinaryWriter class, 215, Catch statements, 898-899
DataAdapter class, 723,
248-251 761-762
CCW (COM Callable Wrapper),
863
DataColumn class, 723
classes
1098
V Web development
client request/server response
WebRequest class, 275,
292-293, 303-304
validating XML documents, WebResponse class, 275,
managing user requests,
463-467 292-293, 304
695-703
ValidationEventArgs class, 413 WFP (Windows File Protection),
overview, 695
ValidationEventHandler dele- 954
System.Web namespace, 692,
gate, 465-467 Win32 API
694
variables accessing, 978-984
System.Web.Caching name-
declaring, 32-34 space, 693-694 callback functions, 986-988
scope, 34-35, 901 System.Web.Configuration classes, 67
threads namespace, 693 libraries, 979
accessing, 569-570 System.Web.Design.WebCont Windows 2000 operating sys-
scope, 571 rols namespace, 693 tems, 15
thread local storage, System.Web.Hosting name- Windows Clipboard, 84,
572-576 space, 693 100-102
thread-private variables, System.Web.Mail namespace, Windows coordinate system,
572 693 348
VB, evolution of, 30 System.Web.Security name- Windows File Protection (WFP),
VB .NET space, 693 954
behavioral changes, 36-39 System.Web.Services name- Windows forms
design goals, 30-31 space, 693 ActiveX controls, 131-132
object-oriented (OO) support, System.Web.Services.Configu borders, 97-98
32 ration namespace, 693 buttons, 98
overview, 11-12 System.Web.Services.Descript composite controls, 141-142
relationship to Visual Basic, ion namespace, 693 containers, 92
30 System.Web.Services.Discove controls, 92, 124-151
vector fonts, 157 ry namespace, 693
creating, 86-91
version control, 954 System.Web.Services.Protocol
docking controls, 130-131
versioning, 952, 959-960 s namespace, 693
EventLog control, 142-151
Visual Basic, evolution of, 30 System.Web.SessionState
Form class, 86-91
namespace, 693-694
Visual Basic .NET. See VB .NET instantiating, 91
System.Web.UI namespace,
Visual C++ .NET, 12 MDI forms, 93-96
693
Visual Studio .NET menus
System.Web.UI.Design name-
IDE, 39-47 checkmarks, 109-113
space, 693
overview, 10 context menus, 107
System.Web.UI.HtmlControls
VS .NET IDE, 39-47 defaults, 113-114
namespace, 693
System.Web.UI.WebControls events, 117-124
W namespace, 693 main menu, 103-106
W3C (World Wide Web Web exceptions, 297-298 MDI applications,
Consortium), 415 Web sites, World Wide Web 107-108
WatcherChangeTypes enumer- Consortium, 416 merging, 114
ation, 234 WebClient class, 275, 301-302 owner draw menu items,
Web applications WebException class, 275 115-116
MyStatus Indicator, 712-720 WebProxy class, 276, 311 separator bars, 109-110
qManager, 660-688 shortcuts, 109-112
state management, 705-711 submenus, 108-109
XSLT (XSL Transformations)
1121
xslcolonnumber, 498
xslcolonotherwise, 498
xslcolonoutput, 498
xslcolonparam, 499
xslcolonpreserve-space,
499
xslcolonprocessing-
instruction, 499
xslcolonsort, 499
xslcolonstrip-space, 499
xslcolonstylesheet, 499
xslcolontemplate, 499-500
xslcolontext, 499
xslcolontransform, 499
xslcolonvalue-of, 499
xslcolonvariable, 499
xslcolonwhen, 499
xslcolonwith-param, 499
handling exceptions, 515
LREs (literal result elements),
500
overview, 491-493, 499-500
resources, 501
result document, 494-496
source document, 493
sources of input, 511-512
style sheets, 494, 496-499,
506-511
System.Xsl namespace,
490-491
types of output, 513-514
XPath, 501-503
XsltArgumentList class, 491
XsltCompileException class,
491
XsltContext class, 491
XsltException class, 491, 515
XslTransform class, 491,
506-514