Richard Simon Mark Schmidt
Teach Yourself
Visual C++ .NET in
24
Hours
800 East 96th St., Indianapolis, Indiana, 46240 USA
Sams Teach Yourself Visual C++ .NET in 24 Hours Copyright © 2002 by Sams Publishing All rights reserved. No part of this book shall be reproduced, stored in a retrieval system, or transmitted by any means, electronic, mechanical, photocopying, recording, or otherwise, without written permission from the publisher. No patent liability is assumed with respect to the use of the information contained herein. Although every precaution has been taken in the preparation of this book, the publisher and author assume no responsibility for errors or omissions. Nor is any liability assumed for damages resulting from the use of the information contained herein. International Standard Book Number: 0-672-32323-0
Karen Wachs Jenny Watson
DEVELOPMENT EDITOR Clint McCarty
MANAGING EDITOR Charlotte Clapp
PROJECT EDITORS Heather McNeill Sheila Schroeder
INDEXER
First Printing: Month Year 02
ACQUISITIONS EDITORS
Bart Reed
Printed in the United States of America
03
Michael Stephens
COPY EDITOR
Library of Congress Catalog Card Number: 2001095794
04
ASSOCIATE PUBLISHER
Ginny Bess 01
4
3
2
1
PROOFREADER Andrea Dugan
Trademarks All terms mentioned in this book that are known to be trademarks or service marks have been appropriately capitalized. Sams Publishing cannot attest to the accuracy of this information. Use of a term in this book should not be regarded as affecting the validity of any trademark or service mark.
Warning and Disclaimer Every effort has been made to make this book as complete and as accurate as possible, but no warranty or fitness is implied. The information provided is on an “as is” basis. The authors and the publisher shall have neither liability nor responsibility to any person or entity with respect to any loss or damages arising from the information contained in this book or from the use of the programs accompanying it.
TECHNICAL EDITORS Bill Craun Mitch Denny
ILLUSTRATOR Steve Adams Laura Robbins
TEAM COORDINATOR Lynne Williams
INTERIOR DESIGNER Gary Adair
COVER DESIGNER Aren Howell
PAGE LAYOUT Octal Publishing, Inc.
Contents at a Glance Introduction
Part I Getting Started with Visual C++ .NET Hour 1
Using Visual C++ .NET
1
5 5
2
Special Features of Visual C++ .NET
17
3
Writing a Simple C++ .NET Program
25
PART II Understanding .NET Hour 4
39
Understanding the Basics of .NET
41
5
Understanding Managed Versus Unmanaged Code
53
6
Integrating with Other .NET Languages
59
PART III Implementing a User Interface Hour 7
69
Working with Windows Forms
71
8
Working with Resources
95
9
Programming with Graphics
105
Printing with .NET
121
10
PART IV Server Development Hour 11
137
Creating Web Services
139
12
Creating Web Services with ATL
155
13
Working with .NET Error Handling and Diagnostics
165
14
ATL Servers
179
PART V Language Features Hour 15
193
Attributes
195
16
Collections and Arrays
211
17
Interfaces
227
18
Events and Delegates
243
PART VI Advanced Programming Hour 19
261
Threading and Synchronization
263
20
Database Access
283
21
COM with .NET
307
22
Mixing Managed and Unmanaged Code
327
23
Control Class Libraries
343
24
Serialization
365
A
Quiz Answers
381
B
Visual Studio .NET IDE Reference
389
Index
397
Table of Contents Introduction
1
Audience and Organization ....................................................................................1 Conventions Used in This Book ............................................................................1 Get Started! ............................................................................................................2
Part I Getting Started with Visual C++ .NET Hour 1 Using Visual C++ .NET
5 5
Getting Familiar with the New IDE ......................................................................6 Application Types with Visual C++ .NET ............................................................10 Working with Solutions and Projects ..................................................................11 Compiling and Debugging ....................................................................................12 Summary ..............................................................................................................14 Q&A ......................................................................................................................15 Workshop ..............................................................................................................15 Quiz ................................................................................................................15 Hour 2 Special Features of Visual C++ .NET
17
Using the New Language Keywords ....................................................................18 Creating User-Defined Attributes ........................................................................20 Pragmas, Compiler, and Linker Features ............................................................20 Using New Pragmas ........................................................................................20 Including External Assemblies for Use ..........................................................22 Using New Compiler Options ........................................................................22 Using New Linker Options ............................................................................23 Summary ..............................................................................................................23 Q&A ......................................................................................................................24 Workshop ..............................................................................................................24 Quiz ................................................................................................................24 Hour 3 Writing a Simple C++ .NET Program
25
Building an MFC Application ..............................................................................26 Building a Managed .NET Framework Application ............................................29 Comparing the Differences ..................................................................................35 Summary ..............................................................................................................36 Q&A ......................................................................................................................36 Workshop ..............................................................................................................37 Quiz ................................................................................................................37
vi
Sams Teach Yourself Visual C++ .NET in 24 Hours
PART II Understanding .NET Hour 4 Understanding the Basics of .NET
39 41
Understanding the .NET Framework Namespaces ..............................................42 The System Namespace ..................................................................................42 The Microsoft Namespace ..............................................................................44 Commonly Used .NET Classes ............................................................................44 The System::Windows::Forms::Application Class ........................................44 The System::Windows::Forms::Form Class ....................................................45 The System::Threading::Thread Class ............................................................45 The System::IO::File and System::IO::FileInfo Classes ................................45 The System::Drawing::Graphics Class ..........................................................46 The System::Web::Services::WebService Class ..............................................46 The System::Data Classes ..............................................................................46 The System::Xml Classes ................................................................................46 The System::Web::UI::Page Class ..................................................................47 The System::ValueType Class ........................................................................47 Deploying .NET Applications ..............................................................................48 What Is the Common Language Runtime? ..........................................................49 Summary ..............................................................................................................50 Q&A ......................................................................................................................50 Workshop ..............................................................................................................51 Quiz ................................................................................................................51 Hour 5 Understanding Managed Versus Unmanaged Code
53
The Relationship Between Managed and Unmanaged Code ..............................54 Using Managed Code for Easy Memory Management ........................................55 Small Memory Allocations ..............................................................................55 Where Is the Destructor? ................................................................................55 Forcing the Garbage Collector to Take Action ..............................................56 Migrating Unmanaged Legacy Code ....................................................................56 Accessing Unmanaged C++ from .NET ........................................................56 Accessing Managed .NET Code from Unmanaged C++ Code ......................57 Summary ..............................................................................................................57 Q&A ......................................................................................................................57 Workshop ..............................................................................................................58 Quiz ................................................................................................................58 Hour 6 Integrating with Other .NET Languages
59
Integrating .NET Component Assemblies ............................................................60 Building the Project ..............................................................................................60 Creating the Application and Component Projects ........................................60 Defining the C++ Class ..................................................................................61
Contents
vii
Defining the C# Component Class ..................................................................64 Defining the Visual Basic Windows Form ......................................................64 Adding References to Components ................................................................65 Using the C# and C++ Classes ........................................................................66 Summary ..............................................................................................................67 Q&A ......................................................................................................................67 Workshop ..............................................................................................................68 Quiz ................................................................................................................68
PART III Implementing a User Interface Hour 7 Working with Windows Forms
69 71
Creating a Simple Windows Form ........................................................................72 Creating a New Application ............................................................................72 Creating the Windows Form ................................................................................74 Creating the Windows Form Class ..................................................................74 Initializing the Windows Form ........................................................................75 Completing the Application ............................................................................77 Making the Push Button Work ........................................................................78 Building an MDI Interface with Windows Forms ................................................................................................79 Creating a MDI Child Window ......................................................................80 Adding a Menu ................................................................................................83 Adding a Toolbar ............................................................................................88 Adding an About Dialog ................................................................................91 Compiling and Running the MDI Application ................................................93 Summary ..............................................................................................................93 Q&A ......................................................................................................................94 Workshop ..............................................................................................................94 Quiz ................................................................................................................94 Hour 8 Working with Resources
95
The .NET Managed Resource File ......................................................................96 Creating the .NET Resource File ........................................................................96 Integrating the Resource File ................................................................................97 Reading Resources at Runtime ..........................................................................100 Summary ............................................................................................................103 Q&A ....................................................................................................................104 Workshop ............................................................................................................104 Quiz ..............................................................................................................104
viii
Sams Teach Yourself Visual C++ .NET in 24 Hours
Hour 9 Programming with Graphics
105
Understanding GDI+ ..........................................................................................106 Discovering the New Features ......................................................................106 New Programming Methodology ..................................................................110 Building a Simple GDI+ Application ................................................................110 Using the .NET Framework ..........................................................................110 Using GDI+ in MFC ....................................................................................114 Removing Drawing Flicker ..........................................................................117 Summary ............................................................................................................119 Q&A ....................................................................................................................119 Workshop ............................................................................................................120 Quiz ..............................................................................................................120 Hour 10 Printing with .NET
121
Modifying the Toolbar and Menu ......................................................................122 Working with the PrintDocument Object ..........................................................128 Providing Simple Printing ............................................................................128 Selecting the Printer ......................................................................................132 Changing the Page Setup ..............................................................................133 Providing a Print Preview ..............................................................................134 Summary ............................................................................................................136 Q&A ....................................................................................................................136 Workshop ............................................................................................................136 Quiz ..............................................................................................................136
PART IV Server Development Hour 11 Creating Web Services
137 139
Overview of Web Services and .NET ................................................................140 Understanding the Web Service Infrastructure ............................................140 Creating a Simple Web Service ..........................................................................141 Creating the New Project ..............................................................................141 Compiling and Debugging the Default Web Service ....................................142 Changing the Web Service ............................................................................145 Using a Web Service Within an Application ......................................................149 Adding Web References ................................................................................149 Using a Web Reference ................................................................................150 Using Web Service Directories ....................................................................151 Discovering Web Services ............................................................................152 Summary ............................................................................................................153 Q&A ....................................................................................................................153 Workshop ............................................................................................................153 Quiz ..............................................................................................................153
Contents
Hour 12 Creating Web Services with ATL
ix
155
Creating an ATL Web Service Project ................................................................156 Uncovering the ATL Web Service Implementation ............................................157 Defining an ATL Web Service Interface ......................................................157 Implementing the Interfaces ..........................................................................158 Building a Test Application ................................................................................160 Summary ............................................................................................................162 Q&A ....................................................................................................................162 Workshop ............................................................................................................163 Quiz ..............................................................................................................163 Hour 13 Working with .NET Error Handling and Diagnostics
165
Understanding the .NET Error Handling Classes ..............................................166 Understanding the Exception Class ....................................................................167 Viewing the Stack Trace ................................................................................168 Using the InnerException Property ..............................................................169 Finally Processing ........................................................................................170 Overview of the Trace and Debug Classes ........................................................171 Adding Trace Statements ..............................................................................171 Asserting on Invalid Values ..........................................................................174 Creating and Using Custom Exceptions ............................................................174 Creating the MyException Class ..................................................................175 Using Custom Exceptions ............................................................................176 Summary ............................................................................................................176 Q&A ....................................................................................................................177 Workshop ............................................................................................................177 Quiz ..............................................................................................................177 Hour 14 ATL Servers
179
Why ATL Server? ..............................................................................................180 Creating an ATL Server Project ..........................................................................181 ATL Server Sequence of Events ........................................................................184 The Server Response File ..................................................................................185 Creating the NumberGuess Server Response File ............................................187 Implementing the Replacement Functions ........................................................188 Summary ............................................................................................................191 Q&A ....................................................................................................................192 Workshop ............................................................................................................192 Quiz ..............................................................................................................192
x
Sams Teach Yourself Visual C++ .NET in 24 Hours
PART V Language Features Hour 15 Attributes
193 195
A Step in the Right Direction: Attributes Simplify ATL Development ............196 Attributes and the Build Process ........................................................................198 Attribute Programming with ATL ......................................................................199 Creating an Attributed ATL Object ....................................................................202 Where’s the IDL File? ........................................................................................204 Creating the MFC Client to Access Your ATL Object ......................................206 Examining the Injected Attribute Code ..............................................................208 Summary ............................................................................................................209 Q&A ....................................................................................................................210 Workshop ............................................................................................................210 Quiz ..............................................................................................................210 Hour 16 Collections and Arrays
211
.NET Framework Collection Class Design ........................................................212 The ICollection Interface ..............................................................................212 The Enumeration Interfaces ..........................................................................213 The Collection Classes ......................................................................................214 System::Array ................................................................................................214 Enumerating Collections ..............................................................................216 The ArrayList Collection Class ....................................................................218 The Stack and the Queue ..............................................................................221 The Hashtable Collection Class ....................................................................223 Summary ............................................................................................................225 Q&A ....................................................................................................................225 Workshop ............................................................................................................226 Quiz ..............................................................................................................226 Hour 17 Interfaces
227
Interfaces Explained ..........................................................................................228 Implementing .NET Framework Interfaces ........................................................228 Implementing the StringStack Collection ....................................................231 Creating Your Own Interface ..............................................................................233 Declaring the Interface ..................................................................................234 Implementing the Interface ..........................................................................236 Creating the Client Application ..........................................................................238 Summary ............................................................................................................241 Q&A ....................................................................................................................242 Workshop ............................................................................................................242 Quiz ..............................................................................................................242
Contents
Hour 18 Events and Delegates
xi
243
The Unified Event Model ..................................................................................244 Unified Event Model Architecture ................................................................244 Working with Delegates ......................................................................................246 Creating and Handling Managed Events ............................................................255 Summary ............................................................................................................259 Q&A ....................................................................................................................259 Workshop ............................................................................................................260 Quiz ..............................................................................................................260
PART VI Advanced Programming Hour 19 Threading and Synchronization
261 263
Threading and Synchronization Explained ........................................................264 Creating Threads ................................................................................................265 The Running Thread State ............................................................................265 The WaitSleepJoin Thread State ..................................................................266 The Interrupting Thread States ......................................................................267 Suspending and Resuming a Thread ............................................................267 Creating the ThreadSynch Project ......................................................................267 Thread Synchronization ......................................................................................270 Creating Consumer and Producer Objects ........................................................274 Events and Timers ..............................................................................................277 Summary ............................................................................................................280 Q&A ....................................................................................................................280 Workshop ............................................................................................................281 Quiz ..............................................................................................................281 Hour 20 Database Access
283
Connected vs. Disconnected Clients ..................................................................284 Redesigning ADO for the .NET Framework ......................................................285 Creating the Authors ADO.NET Application ....................................................286 Connecting to the Data Source ..........................................................................293 Displaying and Navigating Through the Data ....................................................296 Inserting and Deleting Table Records ................................................................298 Summary ............................................................................................................303 Q&A ....................................................................................................................304 Workshop ............................................................................................................304 Quiz ..............................................................................................................305
xii
Sams Teach Yourself Visual C++ .NET in 24 Hours
Hour 21 COM with .NET
307
COM Interop Design ..........................................................................................308 Using COM Objects within .NET ......................................................................309 Creating the COM Object ............................................................................310 Creating the .NET Client ..............................................................................314 Using .NET Objects Within COM Projects ........................................................319 Summary ............................................................................................................324 Q&A ....................................................................................................................324 Workshop ............................................................................................................325 Quiz ..............................................................................................................325 Hour 22 Mixing Managed and Unmanaged Code
327
Unmanaged and Managed Code Together ..........................................................328 Unmanaged Code Can Introduce Memory Leaks ..............................................330 Platform Invocation ............................................................................................331 Using P/Invoke to Call the MessageBox Function ............................................332 Using P/Invoke for Custom Data Types ............................................................335 Specifying Specific Data Types for Marshaling ................................................339 Summary ............................................................................................................340 Q&A ....................................................................................................................340 Workshop ............................................................................................................340 Quiz ..............................................................................................................341 Hour 23 Control Class Libraries
343
Controls Within the .NET Framework ..............................................................344 Inheriting from an Existing Windows Form Control ....................................344 Inheriting from the UserControl Class ..........................................................345 Inheriting from the Control Class ................................................................345 Creating the Custom Control Project ................................................................345 Using Custom Controls in a Managed C++ Application ..................................348 Strong-Named Assemblies and the Global Assembly Cache ............................351 Using Custom Controls with C# .NET ..............................................................353 Stock Properties ..................................................................................................356 Creating Custom Control Properties ..................................................................357 Summary ............................................................................................................362 Q&A ....................................................................................................................363 Workshop ............................................................................................................363 Quiz ..............................................................................................................363 Hour 24 Serialization
365
Binary and XML Serialization ..........................................................................366 Creating the ObjectSerialization Class and Windows Form ..............................367
Contents
xiii
Serializing with Attributes ..................................................................................372 Customizing the Serialization Process ..............................................................374 Serializing and Deserializing Objects ................................................................375 Summary ............................................................................................................378 Q&A ....................................................................................................................379 Workshop ............................................................................................................379 Quiz ..............................................................................................................379 Appendix A Quiz Answers
381
Hour 1 ................................................................................................................381 Hour 2 ................................................................................................................381 Hour 3 ................................................................................................................382 Hour 4 ................................................................................................................382 Hour 5 ................................................................................................................382 Hour 6 ................................................................................................................382 Hour 7 ................................................................................................................382 Hour 8 ................................................................................................................383 Hour 9 ................................................................................................................383 Hour 10 ..............................................................................................................383 Hour 11 ..............................................................................................................383 Hour 12 ..............................................................................................................384 Hour 13 ..............................................................................................................384 Hour 14 ..............................................................................................................384 Hour 15 ..............................................................................................................384 Hour 16 ..............................................................................................................385 Hour 17 ..............................................................................................................385 Hour 18 ..............................................................................................................385 Hour 19 ..............................................................................................................386 Hour 20 ..............................................................................................................386 Hour 21 ..............................................................................................................386 Hour 22 ..............................................................................................................387 Hour 23 ..............................................................................................................387 Hour 24 ..............................................................................................................387 Appendix B Visual Studio .NET IDE Reference
389
Source Window ..................................................................................................389 Outlining ........................................................................................................390 Task List Shortcuts ........................................................................................390 Reducing Window Clutter ..................................................................................391 Docking ........................................................................................................391 Auto-Hiding ..................................................................................................392 Preset Layouts and Developer Profiles ........................................................392 New Developer Studio Windows ..................................................................393
xiv
Sams Teach Yourself Visual C++ .NET in 24 Hours
Help System ........................................................................................................394 Integrated Help ..............................................................................................394 Dynamic Help ................................................................................................395 Comment Web Pages ....................................................................................395 Index
397
About the Author Richard J. Simon has been a pioneer in new technology since 1985 when he started a consulting company to develop PC solutions for businesses. As an early Windows developer, he developed using client/server technologies that are now common place in the industry. Richard has been on the leading edge of Internet technologies and n-tier development using early alpha versions of Microsoft technology and has been actively using .NET technologies since early 2000. Richard if a former CTO of over eight years for a software development company that developed and marketed applications to fortune 1000 companies. He currently is the Co-Founder and CEO of MillenniSoft, Inc. and an established author of several Windows programming books and has tech edited several other books on Windows development and technologies. Mark Schmidt has been a software engineer at Hewlett-Packard for the past 3 years. Mark began his career writing magazine articles for Fawcette Technical Publications. His articles have appeared in Visual C++ Developer’s Journal, Visual Basic Programmer’s Journal, and Visual Studio Magazine. In January 2001, Mark spoke at the VSLive! Developer’s Conference in San Francisco. In that talk, Mark outlined his solution for advanced customizable user interfaces based on the Active Template Library (ATL). Mark is on a constant path to learn the latest and greatest technologies, and his recent work within the .NET world has kept him busier than ever. Feel free to contact him at
[email protected].
Dedication To my wife, Cher, for all her love and support. It takes a special person to put up with someone like me.
Acknowledgments One of the most influential people in my life has been my mother. Thank you for always believing in my abilities and never ever doubting me. You alone are responsible for instilling in me a drive to succeed. This book wouldn’t have been possible without my wife and children (Jordan, Jake, Jonah, and Mallorie). They are my inspiration and my life. Thank you to my coworkers and especially my manager at HP, Kathy Morgan, for allowing me to grow through external projects such as this. A big thank you to my in-laws, Chuck and Kris. Their love and support through the years would take more than a lifetime to pay back. Thank you, Damon. You’re the only one who truly understands the strangeness of my mind. Finally, Parkside Church deserves my thanks for serving as my second family and providing an atmosphere of love and friendship, truly showing how blessed they all are.
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, 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 due to 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:
Michael Stephens Sams Publishing 800 East 96th Street Indianapolis, IN 46240 USA
Introduction Microsoft Visual C++ has enjoyed half a decade as the leading C++ tool. Even now as Microsoft rolls out new technologies such as C# and the .NET Framework and puts new twists on mature technologies such as Visual Basic, C++ remains the language of choice for many serious developers. Whether you plan to become a C++ programmer or just need to have a working proficiency with Visual C++, this book is designed to get you started in the right direction.
Audience and Organization This book is targeted toward those who have some exposure to C++ programming and who are interested in learning Visual C++ .NET. When you finish the last lesson, you should be familiar with the Visual C++ environment and have a good understanding of how to write, build, debug, and deploy C++ programs using Visual C++ .NET.
Conventions Used in This Book This book uses several conventions to help you prioritize and reference the information it contains: • Tips highlight information that can make your C# programming more effective. • Cautions focus your attention on problems or side effects that can occur in specific situations. • Notes provide useful sidebar information that you can read immediately or circle back to without losing the flow of the topic at hand. In addition, this book uses various typefaces to help you distinguish code from regular English. Code is presented in a monospace font. Placeholders—words or characters used temporarily to represent the real words or characters you would type in code—are typeset in italic monospace. Some code statements presented in this book are too long to appear on a single line. In these cases, a line-continuation character is used to indicate that the following line is a continuation of the current statement.
2
Sams Teach Yourself Visual C++ .NET in 24 Hours
Get Started! So now it’s time to dive in headfirst. The first lesson provides a gentle introduction to Visual C++ .NET and outlines the way it differs from earlier versions of Visual C++. By the time you get to Chapter 3, you’ll be ready to build your first Visual C++ .NET program.
PART I Getting Started with Visual C++ .NET Hour 1 Using Visual C++ .NET 2 Special Features of Visual C++ .NET 3 Writing a Simple C++ .NET Program
HOUR
1
Using Visual C++ .NET Visual C++ .NET is Microsoft’s newest version of Visual C++ and is part of Visual Studio .NET (VS .NET). It combines the previous features of Visual C++ with the new features of the .NET Framework, giving you the best of both worlds. Visual C++ .NET is currently the only .NET-capable language that allows you to mix traditional Win32 and Microsoft Foundation Classes (MFC) applications with the new .NET applications. The new Visual Studio .NET environment for Visual C++ .NET is somewhat different from previous versions of Visual Studio. Several new features are combined with a new look, and some features you might have used before are no longer available, because they were replaced or made obsolete. In this hour you will learn: • What new features were added to the IDE and which ones are no longer supported • The types of applications Visual C++ .NET can create • How to compile and debug your applications • How to work with solutions and projects
6
Hour 1
Getting Familiar with the New IDE Microsoft’s Visual Studio .NET combines all the .NET languages, which include Visual Basic .NET (VB .NET), Visual C# .NET (VC# .NET), and Visual C++, and the features of Visual InterDev into one environment. It is possible to have a project from each language open simultaneously and build them all with a single menu selection. This is a radical departure from the different environments that Visual Basic and Visual C++ had with previous versions of Visual Studio. Microsoft has produced an environment that allows the developer to work seamlessly with all the .NET technologies. The new Integrated Development Environment (IDE) makes heavy use of rich content with Web browser–type functionality. Beginning with the start page shown in Figure 1.1 and continuing on to the integrated online help shown in Figure 1.2, HTML is used for the interface wherever possible. FIGURE 1.1 Visual Studio .NET IDE startup page.
With the Visual Studio .NET IDE, you can build about any type of application you might require. You can build console applications, Windows applications, Web applications, servers, services, custom controls, and so on. Just as you would expect, the entire IDE is customizable and allows you to position your windows as docked or floating, customize the menus, and write macros, similar to the previous version of Visual Studio. The difference is the way documents are organized and how dockable windows act.
Using Visual C++ .NET
FIGURE 1.2 Visual Studio .NET integrated help.
First, the Multiple Document Interface (MDI) previously used for Visual Studio has been changed dramatically and replaced with a tabbed interface, shown in Figure 1.3, that allows you to switch between your open documents by selecting a tab. This interface can be somewhat frustrating if you are used to the MDI interface; however, it does have its advantages. If you really want the MDI interface, shown in Figure 1.4, Microsoft has made it available through an option you can select in the Options dialog. FIGURE 1.3 Visual Studio .NET tabbed interface.
7
1
8
Hour 1
FIGURE 1.4 Visual Studio .NET with a more traditional MDI interface.
A nice feature added to the IDE is the ability to make the Help window (and startup page) floating or dockable. Right-clicking the tab of this window displays a menu with options that make the window floating or dockable. This way, you can have your Help window floating on a separate monitor to give yourself more room for editing. You also have the ability to put two files side by side, shown in Figure 1.5, or one over the top of the other for editing in two files more efficiently. This is done by selecting the New Horizontal or Vertical tab group from the right-click menu or from the Window menu on the main menu bar. Another new and very useful feature, especially when screen real estate is at a premium, is the Autohide feature for docked windows. Making your Help Contents, Index, Class Viewer, and other windows hidden automatically allows you to conserve space. You can easily access the windows when you need them by hovering the mouse over the appropriate tab on the edge of the IDE window, as shown in Figure 1.6.
Using Visual C++ .NET
FIGURE 1.5 Vertical tab groups in Visual Studio .NET.
FIGURE 1.6 Autohide feature with Help Contents window in Visual Studio .NET.
9
1
10
Hour 1
Application Types with Visual C++ .NET You can create several new application types with Visual C++ .NET. Most of them are the same as the previous version of Visual C++; however, there are a few new ones that support creating applications with the .NET Framework Software Development Kit (SDK). The following list shows the application types you can create with Visual C++ .NET: • ATL project. Create a new Active Template Library (ATL) project. Based on the Component Object Model (COM), ATL allows you to create COM objects that can be used by any COM-enabled programming language such as Visual Basic, Delphi, and Visual Basic Script (VBScript). • Custom Wizard. Create a new application wizard for Visual Studio .NET. A project of this type can be a real timesaver if you tend to create several projects that rely on the same boilerplate code. • Extended stored procedure. Create an extended stored procedure for SQL Server. This is a procedure written in C that is callable from SQL Server. • MFC application. Create a new Windows application using the MFC library. The MFC library is a collection of C++ classes that make using the WIN32 application programming interface (API) much easier. It supports various features such as window management, Internet programming, file I/O, and extended data types, to name just a few. • MFC DLL project. Create a new dynamic link library (DLL) that uses the MFC library. • MFC ActiveX Control project. Create a new ActiveX control using the MFC library. • MFC ISAPI Extension project. Create a new Internet Server extension DLL using MFC. This allows you to add functionality to your Web server using the Internet Services API (ISAPI) and MFC. • Win32 project. Create an application using the Win32 API. You can create a console application, a Windows application, a DLL, or a static library with this type of project. Creating a project of this type, however, tends to be more difficult because most tasks will have to be done without the use of the built-in wizards. • ATL Server project (new). Create a new Internet Information Services (IIS) Web server extension using ATL. Among the many features ATL Server provides (such as access to cryptography, mail, and message queuing on the server), ATL Server lets you create and define your own markup tags on Web pages. When these markup tags are encountered by the Web server, they are replaced by output from your ATL Server object.
Using Visual C++ .NET
• ATL Server Web Service project (new). Create a new Web service using ATL. Web services are designed to exchange data between two systems on different networks using Extensible Markup Language (XML). • Managed C++ application (new). Create a managed Windows application using C++ and the .NET Framework SDK. A managed application simply means that the application runs within the environment of the .NET runtime engine. • Managed C++ class library (new). Create a managed class library using C++ and the .NET Framework. • Managed C++ Web service (new). Create a managed Web service with C++ and the .NET Framework. Throughout the lessons in this book, you will typically use the new project types that support the .NET Framework because most lessons are focused on the new .NET features rather than the project types availablein previous versions of Visual C++.
Working with Solutions and Projects One of the most notable changes in Visual Studio .NET from previous versions is how projects are managed and organized. Previous versions of Visual Studio allowed you to add several projects to a workspace, but they all had to be independent projects and written in Visual C++. Visual Studio .NET, however, recognizes several project types, including Visual Basic and C#, all of which can be mixed within what is known as a solution. A solution is a replacement for the workspace and does not have a bias for one language or another. Several projects can be loaded and navigated with the Solution Explorer window, shown in Figure 1.7. The solution shown in Figure 1.7 actually contains five different projects and a miscellaneous file. The five projects include a Web service written in VB .NET, a Web site written in ASP.NET with C#, a couple of Visual C++ projects, and a Web Setup Windows Installer project. By using a single command, Ctrl+Shift+B, or the menu item Build, Build Solution, all the projects in the solution are built with the appropriate compilers. Dependencies between the projects are taken into account in deciding the order of the build process. All solution settings, including the addition of another project to a solution, are done through the Solution Explorer. By selecting a project in a solution, you can adjust that project’s settings. Selecting the solution and displaying its properties allows you to change the solution configuration. You can also add existing solutions and projects to a solution or create a new project in the solution. Adding one solution to another causes a merge of all the projects contained in the selected solution into the current solution.
11
1
12
Hour 1
FIGURE 1.7 Solution Explorer in Visual Studio .NET.
Compiling and Debugging There are not many differences between how you perform the build/debug process in Visual Studio .NET and previous versions of Visual Studio. There are, however, several internal differences that allow debugging across languages now that all the compilers are integrated. It is possible with the new debugger to debug Visual Basic, Visual C++, C#, the Managed Extensions for C++, script, and SQL. Perhaps the greatest change with the debugging process lies within the IDE itself. As mentioned before, any of the debugging windows in the IDE can now be hidden along the edges of the main window, and a simple hover of the mouse will bring them into view. Also, the four watch windows are now completely independent of one another, and you can finally have up to four different memory view windows, compared to the one you were limited to in the previous version of Visual Studio. One addition to the debugging process, which Visual C++ developers haven’t had but other languages in the Visual Studio family have had for a long time, is an Immediate window. The Immediate window allows you to change variables and execute simple commands during runtime while you are debugging. Other new features include the following: • Debugging applications written for the .NET Framework or Win32. Because solutions written with the .NET Framework may contain several projects with differing
Using Visual C++ .NET
programming languages, Visual Studio has integrated the debugging process to seamlessly transition from the debugging of one language to another without any extra work needed by the user. • Attaching to running applications on either the host or a remote computer. Although the concept of attaching to remote applications isn’t new within Visual C++, great work has gone into making this debugging process more robust. • Debugging multiple programs simultaneously by executing them from Visual Studio .NET. This is definitely a welcome addition to Visual Studio. If your solution contains multiple executables, debugging the interprocess communication between them will now be much easier. • Added Visual C++ runtime error checks, to name a few, are notification when assigning a long data type to a shorter one, the use of uninitialized variables, and stack corruption. In addition, you can customize the runtime error checks in a number of ways—one of which is routing the error information to a file or other destination. • Buffer security checks to detect buffer overruns. Rather than trying to figure out why your application has produced a General Protection Fault (GPF), the buffer security check will notify you and gracefully exit. • Setting breakpoints in DLLs that have not yet been loaded. The process of debugging DLLs that your application is dependent on has been made much easier. Simply place a breakpoint within the DLL code without needing to tell Visual Studio about any dependencies. The Configuration Manager, shown in Figure 1.8, allows you to manage all the configurations of each project currently loaded in Visual Studio .NET. You can determine which configuration to build for each project for each solution configuration. Configurations can be configured to selectively eliminate projects from the build. FIGURE 1.8 The Configuration Manager dialog in Visual Studio .NET.
13
1
14
Hour 1
As indicated earlier, you can build an entire solution with a single command. You can also selectively build any project within the solution by selecting it in the Solution Explorer and building it directly. As you can imagine, building a solution with several projects of different types can cause a lot of errors during the build process. Visual Studio .NET includes a nice feature known as the Task List, shown in Figure 1.9, that keeps track of all the errors that need to be resolved. In some languages, such as VB .NET, the Task List keeps an up-to-date list of items that need to be resolved. With Visual C++ .NET, the build process is required to produce the tasks. FIGURE 1.9 The Task List with errors in Visual Studio .NET.
Summary From the first moment you open Visual Studio .NET, you’ll be surprised at the vast number of changes from previous versions. This hour, you became familiar with some of these major new enhancements. With a completely redesigned user interface, you’ll see how these changes will increase your ability to get the job done quicker than before. However, if you don’t like these new changes, turning them off is a trivial matter. In addition to the regular application types present in older versions of Visual Studio, you learned about the new applications that are now possible by combining C++ with the .NET Framework. Microsoft has added several other features besides the ones discussed in this hour. You will find them as you continue to use the IDE. At this point, you should have a general idea of how the IDE is laid out and how to work with projects.
Using Visual C++ .NET
Q&A Q Is it possible to convert existing workspaces from previous Visual Studio versions into a solution within Visual Studio .NET? A Yes. By you opening the existing workspace, Visual Studio .NET will convert it into a solution for you so that you can start working with your old projects in the new environment. Q What if I don’t like the new IDE, can I still work with the old one? A You can’t use previous versions of Visual Studio to build and debug new .NET applications; however, you can customize the interface to look very similar to previous versions of Visual Studio with the MDI interface and window layout.
Workshop The Workshop provides quiz questions to help solidify your understanding of what was covered in this hour. Answers are provided in Appendix A, “Quiz Answers.”
Quiz 1. What is a solution? 2. Can Visual C++, Visual Basic, and C# projects all reside within the same solution? 3. How many steps does it take to build a solution with all three languages?
15
1
HOUR
2
Special Features of Visual C++ .NET With such a large change in the way Visual C++ applications are written within Visual Studio .NET, it should come as no surprise that Microsoft has added several language features to accommodate writing applications for the .NET framework while still maintaining the ability to create older Visual C++ application types. Through the addition of new language keywords, attributes, preprocessor directives, and new compiler and linker switches, the ability to create such differing styles of applications makes the task much easier. In this hour’s lesson, you will learn about the new special features for writing .NET applications in Visual C++ .NET. Specifically, in this hour you will learn: • New language keywords • Attributes and how they are defined • Pragmas that specify managed or unmanaged code
18
Hour 2
• Preprocessor directives • Compiler options • Linker options
Using the New Language Keywords Several new keywords has been added to the Visual C++ .NET compiler so that it can build .NET Framework applications. The reason for some of these additions is to maintain cross-language interoperability (CLI), a standard that Microsoft defined in order for applications written in separate programming languages to easily and transparently coexist with each other within the .NET Common Language Runtime (CLR). In order for all the languages to build CLS-compliant applications, there has to be a standard set of language features, which requires additional keywords to be added to each of the .NET languages. For example, the exception handling done in a .NET application is different from what is done in a standard C++ application. The CLR provides try/except/finally functionality, whereas C++ has try/catch. Although the functionality is basically the same, differences do exist. Therefore, new keywords are needed so the compiler can generate the common language calls for .NET. Table 2.1 shows a list of all these new keywords with a description of each. TABLE 2.1
New Keywords for the .NET Framework in VC++ .NET.
Keyword
Description
__abstract
Declares an abstract class that cannot be instantiated directly. It must first be derived from, and the new class must provide implementations for any pure virtual methods.
__box
Creates a copy of a __value class on the CLR heap.
__delegate
Declares a reference to a unique method of a managed class. This is typically used for callback functions, where a pointer to a function is required.
__event
Declares an event method in a managed class.
__finally
Declares a finally block for a try block. The finally block is called all the time before the except block when an exception is caught, and even if an exception is not thrown.
__gc
Declares a managed class type.
__identifier
Allows a C++ keyword to be used as an identifier. This is useful when you’re accessing external classes that may use a C++ keyword as an identifier.
__interface
Declares an interface.
Special Features of Visual C++ .NET
TABLE 2.1
19
continued
Keyword
Description
__nogc
Declares a native C++ class that is not garbage-collected and is allocated on the standard C++ heap. This keyword is not required because the compiler defaults to __nogc if __gc is not specified.
__pin
Prevents an object of a managed class from being moved in memory by the CLR during garbage collection.
__property
Declares a property member in a managed class.
__sealed
Prevents a class declared with the __gc specifier from being a base class or a method from being overridden in a derived class.
__try_cast
Performs a cast on a pointer or throws an exception if the cast fails.
__typeof
Returns the system type of a class, the value type, and so on.
__value
Declares a value type. This is similar to standard structures in C++.
Of the new keywords, the __gc keyword is probably the most noteworthy. By declaring a type with the __gc keyword, you activate the .NET Framework functionality, such as interoperability and garbage collection, for that type. The following code example shows how a class is declared with the __gc keyword: __gc class MyClass { private: int m_nValue; public: int GetValue() {return m_nValue;} };
It is also possible to declare arrays, pointers, and interfaces with the __gc keyword. Doing so specifies that the value works with the managed heap in the .NET Framework. For example, an array declared as shown here creates the array on the .NET heap and its memory is garbage-collected: Int32 myarray[] = __gc new Int32[10];
When a pointer is declared with the __gc keyword, it is then free to point into the .NET heap. Furthermore, once the pointer is declared, the compiler takes care of initializing the contents of its memory block to zero. Pointers are declared as shown in the following statement: Int32 __gc* pMyInt;
Other keywords that are useful and commonly used are __property, __event, and __finally. As you work through following sections, you will see uses for the other keywords shown in the Table 2.1.
2
20
Hour 2
Creating User-Defined Attributes Attributes provide several uses and advantages that you will learn about in a later hour’s lesson. Visual C++ .NET provides the ability to not only use the predefined attributes, but also to create your own. Using the attribute keyword, you can create your own attributes to use within your applications. The following code segment shows how to use the attribute keyword to declare a class as an attribute that you can apply to classes: [ attribute(Class) ] public __gc class MyAttrbute { public: MyAttribute() {...} //TODO: Define code for constructors MyAttribute( int nValue ) {...} };
Declaring an attribute that you can apply to method parameters is done by replacing the Class specifier with Parameter, as shown in the followingI~user-defined attributes;creating> code segment: [ attribute(Parameter) ] public __gc class MyAttribute {...}
Pragmas, Compiler, and Linker Features There are some other miscellaneous keywords and features added to the VC++ .NET compiler and linker that make writing code for the .NET Framework possible. These features include new pragma keywords, preprocessor directives, and compiler and linker flags.
Using New Pragmas Two pragma statements make it possible to write new .NET applications that utilize existing legacy code. The first pragma, managed, tells the compiler that the code enclosed within the pragma should be compiled as managed .NET code. This pragma is used when .NET Framework code appears within your legacy C++ applications. The second pragma, unmanaged, does the opposite: It tells the compiler the code within the pragma should be compiled as unmanaged or regular C++ code. When writing a .NET application in VC++ .NET, you can switch back to the standard C++ compiler for part of the application with this pragma. You will learn about the issues involved with mixing managed and unmanaged code within an application in a later hour’s lesson. The code in Listing 2.1 shows the use of the managed and unmanaged pragmas.
Special Features of Visual C++ .NET
LISTING 2.1 C++ .NET 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30:
21
Use of #pragma managed and #pragma unmanaged Within Visual
// must be compiled with /clr compiler option // for managed code #using // for use with the unmanaged code #include //Managed code by default void ManagedFunction(void) { System::Console::WriteLine(“Managed function.\n”); } // turn off managed code #pragma unmanaged void UnmanagedFunction(void) { printf(“Unmanaged function.\n”); } // turn managed code back on #pragma managed void main() { ManagedFunction(); UnmanagedFunction(); }
As you can see from this example, it is possible to create an application with mixed code and control the compiler with the managed and unmanaged pragmas. Another very useful pragma that you should use in virtually every application you create is once. This pragma placed at the top of any file tells the compiler to only load the file once. This is extremely useful in header files to keep them from being included more than once through redundant references. It eliminates the practice of using a definition as a tag and checking for that definition within the header. The following code shows an example of how you would previously enforce the inclusion of a file only once: #ifndef MY_INCLUDE #define MY_INCLUDE // The include file contents #endif
2
22
Hour 2
This code is now replaced with the #pragma code segment:
once
statement, as shown in the following
#pragma once // The include file contents
Including External Assemblies for Use Writing managed C++ code for the .NET Framework requires you to use other assemblies (usually DLLs) in order to provide the basic features of the .NET Framework. It is also common to include your own assemblies or third-party ones to provide additional functionality. Specifying what assemblies your application uses is done with the #using preprocessor directive. It is very similar to the #include directive, except that it doesn’t include a source file. Instead, it actually includes the definition of the assembly, known as the assembly manifest. This manifest describes the namespaces, types, enumerations, and methods defined for use within the assembly. After specifying which assemblies your code will access, you have to specify which namespaces within those assemblies you plan on using. This is accomplished with the using keyword. Without these declarations, anything you use from an included assembly must be fully referenced with the namespace. For example, if you use the MyComponents.DLL assembly, which includes a Simple namespace with a SimpleClass type, the fully qualified reference is MyComponents.Simple.SimpleClass. If your code references this type more than a few times, it is better to use a namespace. The following code shows an example of how to use the using keyword: #using #using #using using namespace System; using namespace System::ComponentModel; using namespace MyComponents::Simple;
Using New Compiler Options There are a few new compiler options for writing managed .NET code in Visual C++ .NET. However, only one option is required to actually compile an application as a .NET Framework application: /clr. The /clr option tells the compiler to compile the code for the common language runtime (CLR). All code within the project defaults to managed code unless you specify otherwise with the pragma described earlier.
Special Features of Visual C++ .NET
23
The /clr option also has an optional :noassembly directive that specifies that the assembly manifest that describes the assembly should not be inserted into the resulting output file. When an assembly manifest is not inserted into the file, the default file extension produced by the compiler is .netmodule, and it can only be used when the output file type is a DLL. The /NOENTRY linker option described in the next section must also be used with this directive. The other two new compiler options deal with the #using keyword described earlier. The first is the /AI option, which allows you to specify a search path for the files included in your application with the #using keyword. The second switch, /FU, replaces the #using keyword by telling the compiler to use a filename within the application. Therefore, you don’t have to explicitly use the files within your source. In order for this option to work, you must turn off the Force #using property in the compiler settings for the project.
Using New Linker Options The new linker options allow you to customize the linking of your managed .NET Framework modules. The first, /NOASSEMBLY, produces a module that is not an assembly by itself, but can be added to an assembly. This option is redundant with the /clr:noassembly directive described earlier and has the same effect. The /ASSEMBLYMODULE:filename linker option is used when you have a module that is not part of an assembly. This option binds the module to the specified assembly. Although the code within the assembly that the module is bound to cannot access the module code, any application that uses the resulting assembly can access everything within the assembly. The last link option, /ASSEMBLYRESOURCE:filename, allows you to bind a .NET Framework resource file to an assembly. These resources are then accessed with the System.Resources namespace classes. The resource file has a .resources extension and is generated by the Resource File Generator (RESGEN.EXE) or in the development environment.
Summary This hour you learned about the new special features in Visual C++ .NET that allow it to create managed code for the .NET Framework. Other new features have been added to the language, compiler, and linker to provide enhancements and support for 64-bit Windows development. However, they are not required for .NET development.
2
24
Hour 2
As you read through the other hours, you will work with the new language features you’ve become familiar with in this hour, and you will see how they fit into the bigger picture of creating .NET Framework applications and working with legacy Visual C++ applications.
Q&A Q Where do I get all the information on the new language features and the compiler and linker directives not required for .NET development? A Microsoft provides a complete list of the new compiler directives in its online help that ships with Visual Studio .NET. Search for “What’s New” in the online help to bring up a list of topics. Q What is an assembly? A An assembly is the finished result of a .NET project. It contains what is known as metadata, which describes data types such as structures or classes, version information, and internal resources such as graphics and/or sound files. This information is stored in what is called the assembly manifest. Q How do I enter command-line options to my project? A If you know what option you need, select Project, Properties from the main menu. Under the Configuration Properties heading, expand the C/C++ heading and select Command Line. You can add extra command-line options in the edit box titled Additional Options, located on the right. To specify additional linker options, follow the same steps but select the Linker heading instead of the C/C++ heading mentioned earlier.
Workshop The Workshop provides quiz questions to help solidify your understanding of what was covered in this hour. Answers are provided in Appendix A, “Quiz Answers.”
Quiz 1. What is the keyword to declare a class as managed? 2. Attributes work similarly to what other feature in C++? 3. What compiler directive is required to produce an application for the .NET Framework?
HOUR
3
Writing a Simple C++ .NET Program Inevitably every programming book and programming course has you create as your first application—a simple “Hello World” program. Therefore, this hour’s lesson is dedicated to writing your first simple application with Visual C++ .NET. The difference is, this lesson already assumes you know how to create a simple application and are at least somewhat experienced with the previous version of Visual C++. With that assumption, the lesson will take you through creating two applications: one with MFC and one using the .NET Framework. This allows you to compare the two methods of programming Windows applications with Visual C++ .NET. In this hour you will learn: • How to build a simple application with MFC • How to build a managed application with the .NET Framework • How to study and understand the differences between the two types of applications
26
Hour 3
Building an MFC Application Building an MFC application with Visual Studio .NET is very similar to the way it was done with previous versions of Visual Studio. The Application Wizard is available to allow you to customize your settings, although it has a different look. As usual with Visual Studio, first select the New, Project menu option to display the New Project dialog, as shown in Figure 3.1. Select the MFC application type and name the application HelloMFC. FIGURE 3.1 New Project Window in Visual Studio .NET for an MFC application.
Pressing the OK button in the New Project dialog brings up the MFC Application Wizard, shown in Figure 3.2. This wizard is no longer a linear process; you can now select the different sections on the left side of the wizard page and directly access the settings you want to change. The differences between this and the Application Wizard for MFC applications in the previous version of Visual Studio are mainly visual and navigational. Select the Application Type section in the wizard and change the settings to match those shown in Figure 3.3. These selections will result in creating a dialog-based application when you click the Finish button. The first thing you will see after dismissing the settings dialog is the dialog editor. If your dialog requires any controls, you can easily select them from the toolbox displayed on the left. For this project, however, we are just going to change the controls that are already present.
Writing a Simple C++ .NET Program
27
FIGURE 3.2 The MFC Application Wizard.
3 FIGURE 3.3 Dialog-based application settings for the HelloMFC application.
First of all, select the button labeled OK. On the right side of the IDE, you should see a window titled Properties. This is a departure from the old way of doing things in Visual C++, which required editing properties through small tool windows. Properties are now categorized and always displayed without having to select a menu item or click a toolbar button. Change the property value labeled Caption to Message by typing the new value in and pressing Enter. You should see the text on the button change as you do this. Now you need to change the control ID of the static text control. Select the static text control located in the middle of the dialog in the dialog editor and change the ID property to the value IDC_MESSAGE.
28
Hour 3
FIGURE 3.4 HelloMFC dialog template.
Assign a member variable to the static text control by right-clicking the control and selecting Add Variable from the context menu. The Add Member Variable Wizard, shown in Figure 3.5, is displayed. Edit the member variable properties, as shown, and finish the wizard. FIGURE 3.5 The Add Member Variable Wizard for the MFC application.
One final step in the dialog editor is to double-click the Message button to add a handler for when the button is clicked. This is a nice new feature added to Visual Studio .NET. When you double-click objects within the dialog editor and other form editors, the most
Writing a Simple C++ .NET Program
29
appropriate message map entry and method definition is added to your application. In this case, the ON_BN_CLICKED message map entry and the CHelloMFCDlg::OnBnClickedOK() method are added. Because this button is the OK button, you could have overridden the OnOK() method to handle the selection; however, this method works for all buttons and other control types. Change the OnBnClickedOK() method to what is shown in the following code to set the message text to a message: void CHelloMFCDlg::OnBnClickedOk() { m_MessageST.SetWindowText(“Hello from the world of MFC.”); }
Compile and run the HelloMFC application by selecting the Debug, Start menu item from the menu bar or by pressing the F5 key. Clicking the Message button within the application displays your message, as shown in Figure 3.6. FIGURE 3.6 Executing the HelloMFC application.
If you are already familiar with building MFC applications in previous versions of Visual Studio, the HelloMFC application should have been simple for you to create within Visual Studio .NET. What you’ve created, however, is an unmanaged C++ project. This application has nothing to do with .NET and, as such, does not run within the .NET runtime. Any memory allocation, versioning issues, and other things that benefit .NET applications will have to be done manually using this type of project. To run in the .NET runtime, you need a managed C++ application.
Building a Managed .NET Framework Application Switching gears to the .NET Framework and building the same type of application as a managed C++ application is a bit different, as you will see. The first step is the same: Select to build a new project from the main menu, only this time select Managed C++ Application as the type and name the application HelloNET. Notice that you don’t receive any wizard to ask what settings you would like to have in your application, as you did with the MFC application. The C++ .NET applications are generated with a script that builds a basic application framework that is a console “Hello
3
30
Hour 3
World” application. One difference between Visual C++ .NET and the other .NET languages is that there is no form editor for Visual C++ .NET to edit Windows Forms, the basis of building user interfaces in .NET. Although it may seem daunting at first having to hand-code the user interface, you’ll find that working with the .NET Framework Forms classes is rather intuitive.
If you want to learn C# also, you can actually use a dummy application in C# that does provide the Windows Form editor to build your forms and then port the code into VC++ .NET. You have to know how to do the porting, but it isn’t that difficult with a little practice—and it saves time on designing your forms.
The next step is to declare a class that represents the form for the application. Select Project, Add Class from the main menu. In the list of available templates in the dialog that is displayed, select the Generic C++ Class template and click the Open button. Name the class CHelloNETForm and specify the base class Form, the .NET Framework’s class found in the System::Windows::Forms namespace, which provides the Windows Form functionality. You may get an error message explaining that Visual Studio .NET cannot find the Form base class. Click Yes to continue adding the class. Performing the previous steps sets up a new file and skeleton code for your class. The next steps will transform your generic class into a managed C++ .NET class. Click the Class View tab next to the Solution Explorer tab on the right side of the IDE window. Expand the project tree and locate the Classes item and expand that also. You should now see your CHelloNETForm class. Right-click the class and select Add, Add Variable from the context menu. You will now be presented with the Add Variable dialog. Set the access to protected, the variable type to Button*, and the variable name to m_pbtnMessage. Then click Finish. Add another Button* variable named m_pbtnDone and a Label* variable named m_pstMessage. The variables you just added are .NET Framework classes within the Forms namespace. Controls, however, need to have a container object to hold them. Add another variable of type System::ComponentModel::Container* with the name m_pComponents and the same protected access level. Now that you’ve added the member variables, its time to add some member functions. Right-click the CHelloNETForm class again, but this time select Add, Add Function. Enter void as the return type, InitForm as the function name, and an access level of protected. Click Finish to close the dialog.
Writing a Simple C++ .NET Program
31
The last step to finish the design of the class is to add the necessary elements that are not supported by wizards within the IDE. Using Listing 3.1 as a guide, add the appropriate using statements and add the __gc keyword immediately preceding the class keyword. This indicates to the compiler that the class is managed by the .NET Framework and its memory manager. When you are finished, you’re HelloNETForm.h file should look similar to Listing 3.1. LISTING 3.1 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23:
Transforming a Generic C++ class into Managed Code
#pragma once #using #using #using #using using namespace System; using namespace System::Windows::Forms; __gc class CHelloNETForm : public Form { public: CHelloNETForm(void); ~CHelloNETForm(void); protected: Button* m_pbtnMessage; Button* m_pbtnDone; Label* m_pstMessage; System::ComponentModel::Container* m_pComponents; void InitForm(); };
Now it’s time to fill in the functions you just created. Refer to Listing 3.2 as you read through this section. To begin with, try and compile your project. One thing you’ll notice is that the compiler complains that NULL is undefined. One great addition to Visual C++ .NET is that when you’re adding member variables to a class like we did earlier, the generated code also includes class initializers for these variables. In this case, because we declared pointers, the generated code set these variables to NULL. However, it didn’t add the appropriate #include statement. To fix this problem, include the header file stdlib.h at the top of the HelloNETForm.cpp file. Your program should now compile with zero errors and zero warnings.
3
32
Hour 3
Begin by filling in the constructor code. Because the member variables you added are simply pointers, you need to create the objects before you can use them. Therefore, create the form control container member variable (m_pComponents) by using the C++ keyword new. The remaining line in the constructor calls the InitForm function. In the InitForm() function, create the three controls, one by one, and set the properties appropriately. After all the controls are created, they are added to the form with the Form::Controls->Add() method. The order in which the controls are added has an effect on the tab order if the tab order isn’t specified for the controls. In this case, it is specified and therefore doesn’t matter. LISTING 3.2 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34:
Creating the Form
#include “stdafx.h” #include “hellonetform.h” #include #using CHelloNETForm::CHelloNETForm(void) : m_pbtnMessage(NULL) , m_pbtnDone(NULL) , m_pstMessage(NULL) , m_pComponents(NULL) { m_pComponents = new System::ComponentModel::Container(); // Initialize the Form InitForm(); } CHelloNETForm::~CHelloNETForm() { } void CHelloNETForm::InitForm() { // Allocate the m_pbtnMessage = m_pbtnDone = m_pstMessage =
controls new Button(); new Button(); new Label();
SuspendLayout(); // Initialize all control properties // // Message Button
Writing a Simple C++ .NET Program
LISTING 3.2 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66: 67: 68: 69: 70: 71: 72: 73: 74: 75: 76: }
33
continued // m_pbtnMessage->Location = System::Drawing::Point(240, 8); m_pbtnMessage->Name = “Message”; m_pbtnMessage->TabIndex = 0; m_pbtnMessage->Text = “Message”; m_pbtnMessage->add_Click( new System::EventHandler( this, &CHelloNETForm::OnMessageClick ) ); // // Done Button // m_pbtnDone->Location = System::Drawing::Point(240, 40); m_pbtnDone->Name = “Done”; m_pbtnDone->TabIndex = 1; m_pbtnDone->Text = “Done”; m_pbtnDone->add_Click( new System::EventHandler( this, &CHelloNETForm::OnDoneClick ) ); // // Message Label // m_pstMessage->Location = System::Drawing::Point(8, 8); m_pstMessage->Name = “Label”; m_pstMessage->Size = System::Drawing::Size(192, 40); m_pstMessage->TabIndex = 2; m_pstMessage->Text = “”; // Set form properties and add controls to form // // Form1 // AutoScaleBaseSize = System::Drawing::Size(5, 13); ClientSize = System::Drawing::Size(322, 87); Controls->Add( m_pstMessage ); Controls->Add( m_pbtnDone ); Controls->Add( m_pbtnMessage ); FormBorderStyle = System::Windows::Forms::FormBorderStyle::FixedDialog; Name = “HelloNET”; Text = “HelloNET”; ResumeLayout(false);
In order for your form to respond to user events, you’ll need to capture the button click events. Events that were previously handled with the MFC message map are handled quite differently in the .NET Framework. Events are handled by delegates within your
3
34
Hour 3
class. A delegate is quite similar to a C/C++ function pointer. You assign delegates to handle object events with the following statement: m_pbtnMessage->add_Click( new System::EventHandler( this, &CHelloNETForm::OnMessageClick ) );
Every time the Message button is clicked, the CHelloNETForm::OnMessageClick() method is called. In your project, you will add two event handlers to the two buttons. Event handlers must have a specific set of parameters, much in the same way they did for MFC message map handlers. For most events, pointers to a generic Object and an EventArgs object are passed as parameters. The generic Object pointer is a pointer to the object that caused the event, whereas the EventArgs object contains information specific to the event. Following the instructions given earlier, add two member functions. The first function is named OnMessageClick, and the second function is named OnDoneClick. Both functions have a void return type, protected access level, and two parameters: Object* source and EventArgs* e. Listing 3.3 shows the final version of the HelloNETForm.h file. LISTING 3.3 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25:
Adding Event Handler Declarations
#pragma once #using #using #using #using using namespace System; using namespace System::Windows::Forms; __gc class CHelloNETForm : public Form { public: CHelloNETForm(void); ~CHelloNETForm(void); protected: Button* m_pbtnMessage; Button* m_pbtnDone; Label* m_pstMessage; System::ComponentModel::Container* m_pComponents; void InitForm(); void OnMessageClick( Object* source, EventArgs* e); void OnDoneClick( Object* source, EventArgs* e); };
Writing a Simple C++ .NET Program
35
Now open the HelloNETForm.cpp file. The two functions you just added will be at the bottom of the file. In OnMessageClick, set the m_pstMessage Text property to “Hello from the .NET World.” For the OnDoneClick function, call the function Close, which shuts down the application. The two functions should appear similar to the following: 1: void CHelloNETForm::OnMessageClick( Object* source, EventArgs* e ) 2: { 3: m_pstMessage->Text = “Hello from the .NET World.”; 4: } 5: 6: void CHelloNETForm::OnDoneClick( Object* source, EventArgs* e ) 7: { 8: Close(); 9: }
The final change to the application is to have the main() function create and run the new Windows Form class. Make the changes shown in the following code segment: #include “helloNETform.h” int _tmain(void) { Application::Run(new CHelloNETForm()); return 0; }
With those final additions, the application is ready to compile and run. This is the same as with the MFC application. Simply press the F5 key or select the Debug, Start menu command. The resulting Visual C++ .NET application should have the same appearance as the MFC application created earlier.
Comparing the Differences Although the process of building the MFC application was more automated with wizards and form designers, the resulting code of the two applications shows that the MFC application is much more complex. If you take into consideration that the .NET application has no resource file to describe the Windows Form and remove the form definition from the comparison, the .NET application is significantly smaller and even easier to read. Looking at a few of the major differences between the applications shows a fundamental distinction in the way a .NET application is developed versus how an MFC/Win32 application is developed. The first main difference is that an MFC Windows application always starts with a CWinApp derivative. The InitInstance() method is overridden and provides the startup initialization for the application. By contrast, the .NET application doesn’t require an
3
36
Hour 3
application class. The .NET application’s entry point is the main() function, whereas an MFC/Win32 application’s entry point is a WinMain() function encapsulated within the MFC library. Another major difference you should notice while looking at both applications is that the .NET application does not delete anything it allocates with new. This is because the .NET Framework frees all objects once they are no longer referenced. This is done by the garbage collector automatically. This eliminates the problems of memory leaks in your applications. Finally, the way that events are handled is quite different between the two applications. With MFC, a message map entry is added to the class’s message map, which maps an event or Windows message to a class function. In the .NET Framework, there is no message map; therefore, each object has events associated with it to which you can attach an event handler that is called when the events occur. For a further comparison of the two applications, look at the code for the applications on the accompanying CD and compare them in more detail. In the end, you should find the .NET application cleaner and easier to work with than the MFC application.
Summary In this hour you created two applications that perform the same task. One application was created with MFC and the other with the .NET Framework. Both application display a hello message when a push button is clicked. You also learned about the differences between the two application implementations. Being familiar with how things are implemented in .NET as compared to MFC will help you in making better decisions when the time comes to convert legacy applications to the .NET platform.
Q&A Q Is using Windows Forms the only way to produce a user interface within .NET? A Windows Forms represent windows of all types, not just dialogs. They can be MDI frame windows, pop-up windows, tool windows, and so on. They are the only means within the .NET Framework to represent a Windows-based user interface. Q Is it possible to create a dialog in MFC and use it from a .NET application? A Yes, it is possible to do so. In fact, in Hour 5, “Understanding Managed Versus Unmanaged Code,” there is a lesson on mixing managed (.NET Framework) code with unmanaged (MFC/Win32) code that will help you understand how this is done.
Writing a Simple C++ .NET Program
37
Workshop The Workshop provides quiz questions to help solidify your understanding of what was covered in this chapter. Answers are provided in Appendix A, “Quiz Answers.”
Quiz 1. What is the base class for a Windows Form? 2. How is a delegate related to an event? 3. How are events different in MFC versus the .NET Framework?
3
PART II Understanding .NET Hour 4 Understanding the Basics of .NET 5 Understanding Managed Versus Unmanaged Code 6 Integrating with Other .NET Languages
HOUR
4
Understanding the Basics of .NET The .NET Framework is a massive set of classes, structures, enumerations, and so on, organized into a set of namespaces and based on a common language runtime. Understanding those namespaces and how the common language runtime works will give you a better understanding of the capabilities of the .NET Framework and how to create applications, controls, and services with Visual C++ .NET. In this hour, you will learn more about the .NET Framework and the common language runtime (CLR). You will also learn more about the architecture and how memory is managed. Specifically, in this hour you will learn: • The major .NET Framework namespaces • The commonly used .NET classes • How applications are packaged for .NET • How the common language runtime works
42
Hour 4
Understanding the .NET Framework Namespaces If you have browsed through online documentation for the .NET Framework, you’ve noticed that it is organized hierarchically. This is similar to the way the MFC library is organized; however, the organization of the .NET Framework is not entirely based on class derivation as it is with the MFC library. The .NET Framework organization begins with the top-level namespaces System and Microsoft, which contain other namespaces, classes, value types, interfaces, delegates, and enumerations. The use of namespaces provides organization of related code elements without the use of derivation. For example, the System::Windows::Forms namespace contains the Form class, which implements the Windows Form, as well as the control classes to use with Windows Forms. This use of namespaces is used throughout the .NET Framework.
The System Namespace The System namespace is the root namespace for the majority of the .NET Framework. It includes all classes and structures that represent the base value types, such as Array, Byte, Object, Int32, String, and so on. It also contains other namespaces that contain classes for Windows Forms, network communications, file I/O, threads, and so on. Additionally, the System namespace includes other classes to deal with system-level functionality. It also contains other miscellaneous value types or structures, delegates for handling events, The System namespace is the root namespace for the majority of the .NET Framework. some commonly used enumerations, and interface definitions.
Basic Types in .NET Applications can use the The System namespace is the root namespace for the majority of the .NET Framework. classes and structures that provide the basic type support in all .NET applications directly or they can use their native types, which are mapped to the .NET Framework types when compiled. Having the value types defined in the .NET Framework provides a common base that all .NET languages use so they can work with each other without problems missing data types. When you write managed .NET code in Visual C++ .NET, you can mix and match the usage of the .NET types and the native C++ types; however, it is better to choose one or the other and stick with it for clarity. Table 4.1 lists the .NET value types within the System namespace that represent native C++ types.
Understanding the Basics of .NET
TABLE 4.1
43
Native C++ Types and Their .NET Framework Representations
C++ Type
.NET Framework Value Type
char
Byte
signed char
SByte
short
Int16
int/long
Int32
__int64
Int64
unsigned short
UInt16
unsigned int/long
UInt32
unsigned __int64
UInt64
float
Single
double
Double
bool
Boolean
wchar_t
Char
There are other value types, including Decimal, Object, IntPtr, UIntPtr, and String, that don’t have native C++ alternatives and are therefore used directly whichever direction you choose for your applications.
What Else Is in the System Namespace? The System namespace includes classes for dealing with arrays, system status, controlling the application, and so on. There are too many classes to cover in this hour’s lesson. However, you can always refer to online help for a list of classes that are within a namespace—the help is organized by namespaces, which makes it easy to find what you need. There are also delegate definitions within the System namespace to handle events. The most commonly used of those delegates is the EventHandler delegate. Additionally there are also delegate definitions for asynchronous operations and unhandled exceptions. Enumerations within the System namespace include DayOfWeek, PlatformID, TypeCode, and other global enumerations. These enumerations can be used anywhere within managed code. Additionally, the System namespace includes interface definitions and other namespaces that also include classes, structures, interfaces, enumerations, and delegates. For more information on the namespace hierarchy, refer to the reference included with this book or the online help shipped with Visual Studio .NET.
4
44
Hour 4
The Microsoft Namespace The Microsoft namespace includes other namespaces that are related directly to Microsoft’s languages and some Win32 features. The Microsoft namespace isn’t as vast and encompassing as the System namespace; in fact, it doesn’t directly have any classes. The Microsoft namespace includes other namespaces for specific support of Microsoft’s languages, such as CSharp, JScript, VisualBasic, and VSA (Visual Studio for Applications). There isn’t much use for these namespaces unless you are trying to compile source code, in which case these namespaces give you access to the compilers. The other Microsoft namespace is Win32. This namespace is more useful and gives you access to the Registry and system-level events. For the most part, a .NET application shouldn’t use the Registry to store information, but you may find it necessary to retrieve information from the Registry when dealing with legacy applications. Overall, it is possible to write applications for the .NET Framework and never have a need for the classes found within the Microsoft namespaces.
Commonly Used .NET Classes Literally hundreds of classes make up the .NET Framework, many of which are support classes for the more widely used classes. However, a few classes stand out in the .NET Framework as ones often used in building .NET applications. Again, refer to the online help shipped with Visual Studio .NET for a complete reference to these and all the classes in the .NET Framework.
The System::Windows::Forms::Application Class The Application class provides methods and properties to manage an application. It is analogous to the CWinApp class in MFC because it has methods to start and stop an application, process Windows messages, and properties to get information about the application. Unlike with the CWinApp class, you do not derive a class for your application from the Application class. Instead, all the members, methods, and properties are static and therefore callable without a created instance. A common use of the Application class is running an application by providing the message loop for a Windows Form. You can also exit the application or current thread with the Application class’s methods.
Understanding the Basics of .NET
45
If your application requires processing of Windows messages, you can also use the Application class to add message filters to the application’s message pump.
The System::Windows::Forms::Form Class The Form class provides the foundation for displaying any window within your application. It is capable of creating a standard, borderless, floating window. The Form class also supports creating a multiple-document interface (MDI) frame and child windows. The Form class is similar to the CWnd class in MFC in that it is the basis of all windows; however, the Form class is not the base class for controls. Using the properties of the Form class, an application can determine the appearance, size, color, and other features of the window or dialog. In addition to the properties, methods are provided to manipulate a form. In order to create a new window, you must derive a class from the Form class and provide an implementation that sets the properties, adds any required controls, and handles the events for the window. The Form class is not designed to be used directly.
The System::Threading::Thread Class Creating and running threads within the .NET Framework is managed through the Thread class. This class has all the methods necessary to manage a thread, similar to the MFC equivalent CWinThread class. The .NET Thread class is more complete than the MFC CWinThread class in that it also includes methods for working with thread data, or thread local storage (TLS), and thread execution.
The System::IO::File and System::IO::FileInfo Classes The File class is another class where all methods are static and can therefore be called without having an instance of the class. Because the File class performs security checks on all methods each time you use one of these methods, it can cause performance issues when heavily used. You can solve this by using the related FileInfo class, which provides methods and requires that you instantiate it in order to work. Because an instance of the FileInfo class is used, the security checks are only performed once. The FileInfo class is similar to the CFile class in the MFC library because they both provide file I/O support and require an instance of the class to work. The File class in the .NET Framework is very useful for quick-and-easy file operations that don’t occur often or within processes.
4
46
Hour 4
The System::Drawing::Graphics Class The Graphics class encapsulates Microsoft’s new graphics APIs, known as GDI+. Drawing graphics within an application is a much easier task with GDI+ and the Graphics class because you no longer work with Device Context (DC) handles and the issues involved with them. GDI+, and hence the Graphics class, also provides a more robust API with advanced capabilities for gradient shading and other graphic effects that were difficult to perform before GDI+. There isn’t a corresponding class within the MFC library because GDI+ has its own class library that is mirrored with the Graphics class in the .NET Framework.
The System::Web::Services::WebService Class Classes implement a Web Service by deriving from the WebService class, which gives them access to the ASP.NET objects. These objects include Application, Session, User, and Context, which are useful when you’re writing Web Services. Creating managed Web Services is one of the primary uses Microsoft envisions for the Visual C++ .NET compiler. Therefore, Microsoft has made Web Services simple to write, and the WebService class is an integral part of that development.
The System::Data Classes Storage and retrieval of data is important in today’s connected world. The System::Data classes form what is known as ADO.NET, the obvious progression of the familiar ADO technology into the .NET environment. However, with the addition of Web Services, Microsoft has updated the technology to allow for data transfers using a common file format (XML) and Internet protocol (HTTP). The major class within the System::Data namespace is the DataSet class. Using DataSet as the starting point, one or more DataTable objects are created within that set. Furthermore, the DataSet object can also contain data constraints using the Constraints class and data relationships using the DataRelations classes. The tables mentioned earlier can be created with something known as a data adapter. Currently, two data adapters are installed with Visual Studio .NET: the OLE DB data adapter (OleDbDataAdapter), provided by the OLD DB .NET Data Provider, and the SQL data adapter (SqlDataAdapter), provided by the SQL Server .NET Data Provider.
The System::Xml Classes If you’ve spent even just a small amount of time reading through .NET documentation, you’ve probably noticed the prevalence that XML has within various portions of the
Understanding the Basics of .NET
47
framework. In fact, XML is at the heart of Web Services because it is the data format used to transport information over networks. In previous versions of Visual C++, if you planned on incorporating XML into your applications, a separate download was required to get the necessary header files and libraries. With Visual Studio .NET, everything you need is already built in. The XmlReader class provides the necessary logic to support the reading in of XML files. One difference between this class and previous downloadable versions of MSXML (the Microsoft version of an XML parser) is that it doesn’t use the same design paradigms. In other words, rather than being solely a DOM parser or solely a SAX parser, it is a combination of both. Although you are able to select and manipulate nodes similar to a DOM parser (also known as the pull model of parsing), it is a forward-only reader similar to the SAX parser. The System::XML classes also contain an XmlWriter class designed to easily write XML data. Like the XmlReader class, XmlWriter also implements a forward-only method for XML data. XmlWriter outputs data that conforms to the W3C Extensible Markup Language 1.0 Second Edition recommendation.
The System::Web::UI::Page Class Although Web Forms are created in an ASPX file that resides on the IIS server, the logic behind the Web Form is provided by the Page class. Writing the logic for a Web Form involves writing a DLL file that is used with the ASPX file. The DLL provides a class derived from the Page class that implements the logic of the ASP.NET Web Form. When the user first visits a Web Form, the ASPX file is compiled with the DLL file and, in essence, becomes a compiled program that produces HTML output for the user to view. The Page class is the Web equivalent of the Form class for Windows Forms. It provides all the information and support needed to program a robust Web user interface with Web Forms.
The System::ValueType Class All structures and enumerations in the .NET Framework are implicitly derived from the ValueType class. All structures and enumerations that you define within your application are also implicitly derived from the ValueType class. The derivation is enforced by the compiler and is not explicitly defined within the declaration. For example, when you declare a structure or an enumeration, the compiler automatically derives those types from the ValueType class.
4
48
Hour 4
Deriving from the ValueType class provides a common set of functionality for all value types within the .NET Framework. The Equals, GetType, and ToString members are the primary functionality provided by the ValueType class besides giving all value types within the .NET Framework a common base.
Deploying .NET Applications Deploying applications written with the .NET Framework is much easier than deploying applications written with MFC. In addition to having a built-in Windows Install builder within Visual Studio .NET, the actual installation of a project is made simple by the use of assemblies and assembly manifests that describe the contents and dependencies.
Obviously, because the .NET runtime is not included with all versions of Windows, you will have to ship and install it on a user’s system. This will probably be a single-setup application similar to other technologies releases by Microsoft, which will make installing the runtime easy. However, with a technology as big as .NET is, you should expect the file to be somewhat large.
An assembly in the .NET Framework is any EXE or DLL file that implements managed code. The assembly manifest describes the version information, dependencies for the assembly with version information, and type information on what the assembly exports for use. Some of the benefits of deploying .NET applications and the use of assemblies are as follows: • Applications are isolated and produce no impact on existing installed applications because DLL conflicts are eliminated through the use of assemblies. Several DLLs can coexist with the same basic name; however, they can be different versions. The .NET application or component knows how to load the appropriate matching component. • Components are private by default and are not installed where they are visible to other applications. • Code sharing is explicit and is not the default, which forces the using application to have the code references within its assembly. • Installing applications is as simple as copying the files to a computer. Registry settings aren’t needed for a .NET application to function. • Creating an installer is quicker since integration with the Windows Installer for advertising, publishing, repairing, and install-on-demand features is included right out of the box.
Understanding the Basics of .NET
49
• Enterprise deployment of applications, including the use of Active Directory, makes installation across the network much simpler. • Incremental downloads and caching keep the downloaded software to the minimum required for the accessed components. Assemblies can describe a single file or a set of related files. An assembly manifest can describe several unlinked but related files as a single assembly or a single linked file, such as an EXE file, that contains all the information needed to execute. Assemblies can also describe component DLL files that other assemblies can use. These component assemblies can be private to the user or global, meaning several applications and components can use a single component assembly. In order to make sure there is a single location for global assemblies, the .NET Framework defines a global assembly cache that contains all deployed shared assemblies. Figure 4.1 shows the global assembly cache viewed with Windows Explorer. FIGURE 4.1 The global cache assembly viewed in the Windows Explorer.
What Is the Common Language Runtime? The .NET Framework runtime environment is known as the common language runtime, or CLR. All .NET applications are executed by the CLR, which provides memory management and manages the overall execution of the application. Any code written that executes within the CLR is known as managed code. Therefore, all .NET Framework code is managed because it executes within the CLR.
4
50
Hour 4
Having a managed execution environment has many benefits, including cross-language integration, cross-language exception handling, better security, versioning and deployment, simple component interaction, and debugging. The CLR provides another benefit for developers: a common runtime base that all .NET languages are based on. This allows any .NET component or module to work with other .NET components regardless of the language in which they were written. All .NET languages that produce code for the CLR generate code according to the common language specification (CLS). This specification dictates how code is generated so that it is all done the same across all languages. When a .NET language compiles managed code, it actually is precompiling the code into another language that is the common language for all .NET compilers. This language is called the Microsoft Intermediate Language (MSIL). This language is compiled by the just-in-time (JIT) compiler at runtime when an application is first executed. Using a JIT compiler allows the .NET application to be compiled and optimized for the machine on which it is executing. The .NET Framework is capable of using code from all different languages because, by the time the CLR uses the code, the code is resolved into a common base language with all the same types and methods for handling errors, events, user interface, and so on.
Summary In this hour you got a better picture of how the .NET Framework is laid out and how it works. You learned about some of the major classes within the .NET Framework and what their MFC relatives are. You also learned how the benefits of the .NET Framework make deploying applications easier and how applications, components, and even source code written in different languages can all work together within the same environment. By now, you should have a better concept of what .NET really is and what benefits it provides the developer and the end user. As you continue through the upcoming hours, you will learn more about the concepts introduced in this hour and how they are put to use in application development.
Q&A Q Because there is a common language to which all .NET compilers ultimately resolve, is it possible to have .NET languages other than what Microsoft provides or even to write code in MSIL directly?
Understanding the Basics of .NET
51
A Actually, there are several other .NET compilers being developed and released. COBOL, FORTRAN, and even Java compilers have been released (or will be released soon) that produce .NET applications. It is possible for anyone to create his or her own language and compiler as long as it produces the MSIL code to the correct standards. It is also possible to write .NET applications directly in MSIL if you’re inclined to do so. Q In the past, generally Visual C++ has been better at performance than other languages, such as Visual Basic. Is there a performance difference with using one .NET language over another? A The performance of all .NET languages is exactly the same because they all resolve to the same machine code. It is possible that a .NET language compiler produces poor MSIL code; however, in theory, the languages should be all the same. Therefore, there is no advantage in writing managed code in one language over another. However, Visual C++ is the only language that has the ability to mix managed and unmanaged code within the same application, which gives it an advantage over the other languages.
Workshop The Workshop provides quiz questions to help solidify your understanding of what was covered in this chapter. Answers are provided in Appendix A, “Quiz Answers.”
Quiz 1. Is there a difference between using basic .NET types or native C++ types? 2. Where are shared assemblies deployed? 3. How many files does an assembly represent? 4. What is the execution platform on which a .NET application runs?
4
HOUR
5
Understanding Managed Versus Unmanaged Code Visual C++ .NET is currently the only language compiler that is capable of producing code for the .NET Framework as well as traditional Win32 code. It is possible to produce applications using two different SDKs and standards within the same compiler as separate applications, or even within the same application. This feature is what sets Visual C++ .NET apart from all other .NET language compilers and is what makes it a powerful compiler to use when you need the best of both worlds. In this hour, you will learn more about what managed code written for the .NET Framework is and how it compares to unmanaged code written with the Win32 SDK and MFC. Specifically, in this hour you will learn: • The relationship between managed and unmanaged code • Memory management in managed code • How to control the garbage collection of memory • The steps to migrating legacy unmanaged code
54
Hour 5
The Relationship Between Managed and Unmanaged Code Unmanaged code is what you have been writing with Visual C++ and MFC. Therefore, unmanaged code is the same type of application you are already used to. Managed code, on the other hand, is code that is written and compiled for the .NET Framework and runs with the help of the CLR. Managed and unmanaged code have a common relationship in that they both resolve down to using the Win32 API at their lowest state. The differences arise in how the source code is written and structured as well as how the applications that are built run within the environments for which they were created. Figure 5.1 shows the relationship between managed and unmanaged code within a single executable. FIGURE 5.1 The relationship between managed and unmanaged code.
Unmanaged Objects
MFC
ATL
Managed Objects
C
.NET Framework
Win32 API
Typically unmanaged Visual C++ applications are written with either the MFC, ATL, or C runtime library or a mixture of more than one of these libraries. Ultimately, all unmanaged code written for the Windows platform uses the Win32 API to work with the operating system. In contrast, all managed code is written for the .NET Framework and is based solely on the .NET Framework. Therefore, it must use the classes provided through the .NET Framework for development and does not access the Win32 API directly. Anytime code outside the .NET Framework is accessed, that portion of code is run as unmanaged. For example, a managed class written for the .NET Framework can use an unmanaged class.
Understanding Managed Versus Unmanaged Code
55
However, when a program is executing code within the unmanaged class, all the features of managed code are not available because it is running in unmanaged mode without the help of the CLR.
Using Managed Code for Easy Memory Management Writing code for the .NET Framework provides the benefit of automatic memory management. This means that you allocate the memory you need with the new operator, but you don’t have to worry about freeing the memory. An internal garbage collector within the CLR determines when memory is no longer referenced and cleans it up when necessary. The garbage-collection feature keeps the memory clean while the application is executing, which frees you from writing the code to manage the memory allocations and deleting allocated memory when it is no longer needed.
Small Memory Allocations Typically in unmanaged code, it is bad practice to allocate small amounts of memory or classes repeatedly and then delete them. The memory becomes fragmented, and the overhead for the application is too high and brings down the performance of the application. To combat this problem, developers had to use objects conservatively by reusing them when possible, especially when they might be allocated and deleted multiple times during the operation of the application. The managed memory within the .NET Framework doesn’t have those issues with shortterm objects. Objects that are created and go out of scope frequently are optimized by the memory manager and reused when possible. Furthermore, even with repeated allocating and freeing of memory, the runtime has the ability to defragment and optimize free memory within the runtime environment. Therefore, it is entirely acceptable to use short-term memory objects in the .NET Framework to simplify your code, and the application won’t suffer with a performance hit.
Where Is the Destructor? With all the great features, there is a downside that causes object-oriented C++ developers some issues. With the new memory manager, you have no idea when an object may be destructed because it happens with the garbage collector. Therefore, you can’t clean up a class or perform automatic features within the destructor of the class. The class destructor is not directly callable, and there is no delete operator to call the destructor for you as there is in unmanaged C++ code.
5
56
Hour 5
The solution is to provide a Dispose() method within the class definition. However, the Dispose() method is not called automatically, which means you have to add code to call Dispose() for each class that needs to be cleaned up when it isn’t being used. When the garbage collector is ready to remove a class from memory, it does call the Finalize() method if one is defined within the object. Then, the object is finally deleted from memory.
Forcing the Garbage Collector to Take Action In general, you should leave the garbage collector alone and let it decide when it’s appropriate to reclaim memory. However, sometimes it is a good idea in an application to force the garbage collector to remove unused classes from memory. This is often the case when your application has just finished a memory-intensive process that created several objects that are no longer needed now that the process is complete. Forcing the garbage collector to clean up the memory can increase your application’s performance in such instances. By placing a call to the GC.Collect() method, your application indicates to the garbage collector that it needs to clean up the memory now rather than later. Calling this method too often, however, can cause performance issues within your application. Therefore, be careful when calling it, and only do so when you need to clean up a lot of memory that is no longer needed.
Migrating Unmanaged Legacy Code Inevitably there will come a time when you need to migrate legacy code to the .NET Framework. You have several options as to how to proceed, including migration, and you will have to decide which is the best method for each circumstance. Performing the migration in steps allows you to take smaller steps without having to redevelop the entire application all at once, which is not generally acceptable in real-world situations. If all your legacy code is already in C++, the managed extensions will help you make a smooth transition to the .NET platform, because you can mix the unmanaged code with the managed code as you transition it. This allows you to take one component at a time and transition it to the .NET Framework while taking full advantage of the managed code. You can write thin managed wrappers for your legacy code in order to make working with the unmanaged code as seamless as possible.
Accessing Unmanaged C++ from .NET Managed extensions allow you to use a C++ class from any .NET language by writing a simple wrapper for the C++ class using the managed extensions. With a wrapper, any of the .NET languages can use the unmanaged C++ class because the wrapper provides a
Understanding Managed Versus Unmanaged Code
57
fully managed interface and acts as a mapper between the .NET language and the unmanaged C++ class.
Accessing Managed .NET Code from Unmanaged C++ Code Accessing managed code from unmanaged code can be done by directly calling the managed class from C++ or by using the built-in Component Object Model (COM) support in the .NET Framework. Either method has its advantages and disadvantages, and choosing the right one will depend on your particular application. The COM interface is flexible and callable from other components, whereas calling the managed classes directly provides better performance and integration. Because the Visual C++ compiler translates data, pointers, exceptions, and instruction flow between the managed and unmanaged portions of your application transparently, combining managed and unmanaged C++ code within a single executable is relatively simple.
Summary Understanding the differences between managed and unmanaged code will allow you to decide how to migrate your legacy applications to the .NET Framework as well as see what is best to leave as legacy unmanaged C++ code. With the Visual C++ .NET compiler, you have the option of deciding which code is best implemented in the managed environment and what code is best left unmodified. Once you decide what code should be rewritten, you can rewrite it in stages, without taking on a rewrite of the entire application.
Q&A Q Is it possible to use an unmanaged DLL from a .NET language without the source code to the DLL? A With Visual C++ .NET, you can wrap the DLL with a managed .NET class wrapper. This will allow you to use that DLL from any of the .NET languages, as if it was just another managed class. Q Is there a performance hit using the .NET Framework memory manager as opposed to the unmanaged memory manager with MFC? A Actually the .NET Framework memory manager is much more efficient in allocating classes and structures than the memory manager in the MFC library. MFC
5
58
Hour 5
classes can cause the heap to become fragmented with no automatic maintenance to defragment it. The .NET Framework memory manager is kept clean and efficient and uses dead objects when possible to maximize performance.
Workshop The Workshop provides quiz questions to help solidify your understanding of what was covered in this hour. Answers are provided in Appendix A, “Quiz Answers.”
Quiz 1. When is an managed object destroyed? 2. How many .NET languages can mix managed and unmanaged code? 3. What is the feature of the CLR that keeps memory clean?
HOUR
6
Integrating with Other .NET Languages One of the most useful features of the .NET Framework is the ability to write code in a wide range of languages and bring it all together in a seamless fashion. Additionally, Visual C++ .NET can bridge the gap to unmanaged code and make it usable within other .NET languages. In this hour, you will learn how to integrate with other .NET languages by building a simple application that uses Visual Basic, C#, and both managed and unmanaged Visual C++. Specifically, in this hour you will learn: • How to create a Visual Basic .NET Windows Form application • How to create a C# assembly • How to create a C++ assembly with both managed and unmanaged code • How to use the C# and C++ assemblies in a Visual Basic .NET application
60
Hour 6
Integrating .NET Component Assemblies As you learned earlier, the .NET languages all compile to a common language known as the Microsoft Integrated Language, or MSIL. This is what allows .NET languages to easily share classes between assemblies as if they were all written in the same language. Because these languages are all precompiled to MSIL, there is really no difference between a DLL written in VB, C#, C++, and any other .NET language. In order to use classes from other languages within a .NET application, it is necessary to build an assembly that contains those classes. An assembly is usually a DLL file that contains classes that are publicly available for use by other applications and components. Once assemblies are built that contain the classes in each of the languages, the application references those assemblies, which makes the namespaces and classes available for use within the application. The references are stored within the assembly of the application and tells it where to find the components at runtime.
Building the Project When building an application with multiple languages, you need to build a new project for each language, because a different .NET language compiler is involved with each language. The Solution Explorer makes dealing with multiple projects of different languages easy and keeps them all within the same solution. An advantage of having multiple projects that use different compilers within a solution is that the build process can be performed on all the projects at one time with just a single step.
Creating the Application and Component Projects The first step in building a .NET application that utilizes multiple programming languages is to decide what the primary language will be for the application. This is the language that will ultimately produce the executable file. For this project, it makes sense to pick either C# or Visual Basic because both of those languages have better Windows Form support than Visual C++, and this application is a Windows Form application. Because it is a toss-up between VB and C#, this application will use Visual Basic as the primary language. Use the File, New, Project menu item to create a Visual Basic Windows application named IntegratedApp, as shown in Figure 6.1.
Integrating with Other .NET Languages
61
FIGURE 6.1 The New Project dialog for IntegratedApp in Visual Basic .NET.
You should now have an empty Visual Basic .NET Windows application with a blank Windows Form in the Form Designer. The next step is to create the two components in C# and C++. From the Solution Explorer, right-click the solution and select Add, New Project from the context menu. For the project type, select the Visual C# Projects item. Then select Class Library in the templates section and give it the name CSLibrary, as shown in Figure 6.2. FIGURE 6.2 The Add New Project dialog for CSLibrary in Visual Basic .NET.
6 Finally, repeat the process of creating a class library for C++ by selecting to create a new managed C++ class library from the Add New Project dialog and name it CPPLibrary.
Defining the C++ Class Open the file CPPLibrary.h, where you will find a namespace declaration with a generic class definition. Writing a class library DLL is very simple with the .NET Framework;
62
Hour 6
any namespaces and public classes are automatically made available to any user of the component library. The project wizard already created a sample class for you, so all you have to do is simply change the given name to a more appropriate name for our application. Change the class from the name Class1 to CPPClass. The next step is to add a member function. In the Class View, find the CPPClass definition within the CPPLibrary project. Right click the class and select Add, Add Function. In the Add Function dialog displayed, set the return value to String* and the function name to GetMsg. Then click Finish. You should now be looking at the C++ header file once again, and you’ll notice the new function within the class. Instead of returning NULL, however, it returns a string instead, as shown on line 14 of Listing 6.1. LISTING 6.1 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17:
The CPPClass Definition in CPPLibrary
// CPPLibrary.h #pragma once using namespace System; namespace CPPLibrary { public __gc class CPPClass { public: String* GetMsg() { return( “Hello from managed C++” ); } }; }
At this point, CPPClass has one method, GetMsg(), that returns a pointer to a String object containing a message. Adding an unmanaged class, CPPUnmanagedClass, to the namespace is done by adding the code shown in Listing 6.2. LISTING 6.2 1: 2: 3: 4: 5: 6: 7:
The CPPUnmanagedClass Definition in CPPLibrary
// CPPLibrary.h #pragma once using namespace System; namespace CPPLibrary
Integrating with Other .NET Languages
LISTING 6.2 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29:
63
continued
{ #pragma unmanaged class CPPUnmanagedClass { public: LPCTSTR GetMsg() { return( “Hello from unmanaged C++” ); } }; #pragma managed public __gc class CPPClass { public: String* GetMsg() { return( “Hello from managed C++” ); } }; }
Because other .NET languages cannot call unmanaged code directly, you need to wrap the usage with managed code. Therefore, creating a new method in CPPClass that is managed and uses CPPUnmanagedClass would allow other .NET languages to effectively use the unmanaged code. Adding the method GetUnmanagedMsg(), as shown in Listing 6.3, provides the necessary wrapper. LISTING 6.3 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14:
The GetUnmanagedMsg() Declaration in CPPClass
using namespace System; namespace CPPLibrary { #pragma unmanaged class CPPUnmanagedClass { public: LPCTSTR GetMsg() { return( “Hello from unmanaged C++” ); } };
6
64
Hour 6
LISTING 6.3
continued
15: #pragma managed 16: 17: public __gc class CPPClass 18: { 19: public: 20: String* GetMsg() 21: { 22: return( “Hello from managed C++” ); 23: } 24: 25: String* GetUnmanagedMsg() 26: { 27: CPPUnmanagedClass* pUMObj = new CPPUnmanagedClass; 28: String* strMsg = pUMObj->GetMsg(); 29: 30: return( strMsg ); 31: } 32: }; 33: }
Finally, you need to include the windows.h file into the project so it can compile the unmanaged code. The include statement shown here is added to the stdafx.h file in the project: #include
Defining the C# Component Class The C# class is much more compact than the C++ class. Defining a similar method to return a message in the C# class is done by modifying the class1.cs file in the project you created earlier. First of all, as you did in the C++ project, change the class name to something more appropriate. Add the following class definition to the CSLibrary namespace, either manually or using the Add New Function dialog, and you’re finished with the C# class library: public class CSharpClass { public String GetMsg() { return( “Hello from C#” ); } }
Defining the Visual Basic Windows Form Using the Windows Form designer in Visual Basic makes it quick and easy to define a Windows Form, unlike doing it manually in Visual C++. The type of form you are
Integrating with Other .NET Languages
65
creating for this application simply has a text area for the message to be displayed and buttons that will call the appropriate components when clicked. Start by using the Form designer and dragging the appropriate controls to the form, as shown in Figure 6.3. Also use the property dialog for each of the controls and name the controls as shown. Name the buttons bntCS, btnCPP, btnUnmanagedCPP, and btnDone. Name the message text label stMessage. FIGURE 6.3 A Windows Form in the Visual Basic .NET form designer.
Adding References to Components Adding a reference to each of the component libraries is necessary in order to use the components within the Visual Basic application. Using the Solution Explorer, right-click each of the components and build them as shown in Figure 6.4. FIGURE 6.4 Building components from the Solution Explorer.
6
Once you have built the components, use the right-click menu and select the references in the IntegratedApp project. Then select Add Reference from the menu to display the Add Reference dialog shown in Figure 6.5. Your components won’t be shown in this dialog; therefore, use the Browse button to open the Select Component dialog, find the DLL files that were built for the components, and add them.
66
Hour 6
FIGURE 6.5 The Add Reference dialog.
Once you’ve added the references for both the C# and C++ components, you can use the classes in the Visual Basic project.
Using the C# and C++ Classes When a user clicks the buttons on the Windows Form you designed earlier, you can use the classes in the appropriate components to get a message and display it in the stMessage control. Adding the appropriate event handlers for each of the buttons is done by double-clicking each of the buttons on the form in the Windows Form editor. As you double-click these buttons, appropriate event handlers are added to handle each button’s Click event. Add the code shown in Listing 6.4 to use the classes, display the messages, and close the application when the user clicks the Done button. LISTING 6.4 Event Handler Code in IntegratedApp Using Components Written in C# and C++ 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13:
Private Sub btnDone_Click( ByVal sender As System.Object, ByVal e As System.EventArgs) _ Handles btnDone.Click Close() End Sub Private Sub btnCPP_Click( ByVal sender As System.Object, ByVal e As System.EventArgs) _ Handles btnCPP.Click Dim msgClass As CPPLibrary.CPPClass = New CPPLibrary.CPPClass() stMessage.Text = msgClass.GetMsg() End Sub
Integrating with Other .NET Languages
LISTING 6.4 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26:
67
Continued
Private Sub btnUnmanagedCPP_Click ( ByVal sender As System.Object, ByVal e As System.EventArgs) _ Handles btnUnmanagedCPP.Click Dim msgClass As CPPLibrary.CPPClass = New CPPLibrary.CPPClass() stMessage.Text = msgClass.GetUnmanagedMsg() End Sub Private Sub btnCS_Click( ByVal sender As System.Object, ByVal e As System.EventArgs) _ Handles btnCS.Click Dim msgClass As CSLibrary.CSharpClass = New CSLibrary.CSharpClass() stMessage.Text = msgClass.GetMsg() End Sub
Notice how even the unmanaged code is used in the same fashion as the managed code written in C++ and C#. This is because once the unmanaged code is wrapped by managed C++ code, VB only interfaces with the managed C++ code, and it doesn’t matter how it is executed behind the scenes. You can now build the application and run it by pressing the F5 key or selecting the Debug, Start menu item from the menu bar. If you watch the output window within the debugger, you will notice that when the application starts, the components are not loaded. As you click each button on the dialog, the appropriate component is loaded when used for the first time. This feature speeds up the loading of applications if the code is broken into class libraries.
Summary In this hour you built an application that uses all the different .NET language compilers shipped with Visual Studio .NET within a single application. This Visual Basic application used class libraries created in C# and C++. The C++ component even used unmanaged code within its class to show the integration features.
Q&A Q What happens if a DLL is missing at runtime? A The .NET Framework has a very advanced method of keeping track of the DLLs an application needs and knows how to find them at load time. If a DLL is simply missing, the application will continue to execute as long as the DLL is not needed.
6
68
Hour 6
Once a class from that DLL is needed, though, the application will provide an error saying it cannot load the DLL. To demonstrate this, rename one of the DLLs you built in this hour’s project and run the application.
Workshop The Workshop provides quiz questions to help solidify your understanding of what was covered in this hour. Answers are provided in Appendix A, “Quiz Answers.”
Quiz 1. How is code packaged from other languages when used within a .NET application? 2. Does it matter which language is used as the main application language? 3. When are the DLLs loaded when used by an application?
PART III Implementing a User Interface Hour 7 Working with Windows Forms 8 Working with Resources 9 Programming with Graphics 10 Printing with .NET
HOUR
7
Working with Windows Forms Windows Forms are a new concept for Visual C++ developers. They represent the user interface for standard Windows applications written with the .NET Framework. Applications no longer construct the user interface directly by creating windows using the standard Win32 APIs or MFC equivalents. Instead, the .NET Framework supplies the higher Windows Forms API, which provides a complete set of objects for building application user interfaces. In this hour’s lesson, you will learn how to create and work with Windows Forms using Visual C++ .NET. Specifically, in this hour you will: • Create and display a basic Windows Form • Create an MDI frame window with Windows Forms • Create a menu and toolbar for the MDI frame window • Create and display an MDI child window with controls • Create and use a simple dialog box
72
Hour 7
Creating a Simple Windows Form Using Visual C++’s Application Wizard to create a simple dialog-based application using MFC is a relatively easy task. Just by setting a few configuration options, the wizard creates a dialog template and associated source code for the class that uses that template. An instance of the class is then used as the main window for the application. Creating the same simple application with Visual C++ .NET in managed code—and thus using Windows Forms—is not as simple. Because there are no resource files to define templates for dialogs and there is no integrated form designer that can be used with Visual C++ .NET, you must create Windows Forms using source code instead. However, both Visual Basic .NET and C# do have a form designer for Windows Forms. In fact, due to the similarities in syntax between C# and C++, you may even find it easy to design the interface using the C# form designer and port the code to C++. In practice, until Microsoft provides a form designer, you will most likely develop your interface in C# or Visual Basic .NET and use those classes in your C++ applications. However, this hour’s lesson will teach you how to create Windows Forms in C++. After completing this lesson, you will also understand how to create Windows Forms in VC++ .NET.
Creating a New Application Select the File, New, Project menu item to display the New Project dialog. Select to create a new managed C++ application and name it SimpleWindowsForm. This will create a basic managed C++ application. Open the SimpleWindowsForm.cpp file in the editor, and you will see the code shown in Listing 7.1, which is generated for you. Compiling the application at this stage produces nothing more than a “Hello World” console application. LISTING 7.1 SimpleWindowsForm.cpp—File Content After First Creating the Application 1: 2: 3: 4: 5: 6: 7: 8: 9: 10:
// This is the main project file for VC++ application project // generated using an Application Wizard. #include “stdafx.h” #using #include using namespace System;
Working with Windows Forms
LISTING 7.1 11: 12: 13: 14: 15: 16: 17:
73
continued
// This is the entry point for this application int _tmain(void) { // TODO: Please replace the sample code below with your own. Console::WriteLine(S”Hello World”); return 0; }
One of the first things you see in this file, following the #include statement, is a #using preprocessor directive. Whenever you need to use a class within a namespace, you have to direct the compiler to the location of the file that contains this namespace. In Listing 7.1, you are notifying the compiler that you will be using certain namespaces and classes within the mscorlib.dll file. Now that the file containing the namespaces you need has been imported, you can then zero in on which namespaces you actually plan on using. Therefore, line 9 contains the using keyword for the System namespace. The next piece of code, starting on line 12, may look a little confusing if you’ve never programmed in a Unicode environment. Unicode allows you to create applications that can be compiled to run on a single-byte system (so called because all characters are only 1 byte) or compiled to run on a double-byte system (where a single character is comprised of 2 bytes). Therefore, when Listing 7.1 is compiled, the compiler will replace the _tmain function with main if compiled for single-byte systems or wmain if compiled for doublebyte character systems. A question you may be asking is, what does this _tmain function do? Every executable application compiled for Windows has to have at least one entry point that the operating system can call in to upon the initial loading of the program. If the application runs within the console, it has a main (or _tmain) entry point. However, if your application uses a Graphical User Interface (GUI), the main entry point for that application is WinMain.
You can easily change your application to run without the console window. Include the windows.h header file following the tchar.h file’s include line and change line 12 in Listing 7.1 to read as follows: int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpszCmdLine, int nCmdShow )
7
74
Hour 7
Creating the Windows Form The first step in writing a Windows Form application is to include the appropriate .NET Framework components that support the Windows Forms. Add the following lines to the SimpleWindowsForm.cpp file after the #using statement: #using #using #using using namespace System; using namespace System::Drawing; using namespace System::Windows::Forms;
As was mentioned earlier, these statements import the portions of the .NET Framework needed and also indicate which namespaces you will use when declaring the Windows Form class. System.DLL is a component that includes all the system-level .NET Framework classes. System.Drawing.DLL is a component that deals with coordinates and other drawing capabilities in .NET. The coordinates’ classes are what you need for setting up a Windows Form. The final component, System.Windows.Forms.DLL, includes the support for the Windows Forms’ classes.
Creating the Windows Form Class The next step in creating a Windows Form is to actually declare the managed Windows Form class. As you learned earlier, you need to declare the class with the __gc specifier to mark the class as managed. Declare a class named SimpleWindowsForm, as shown in Listing 7.2, with the typical constructor/destructor pairs. In order for this class to be used within your main function however, you must place the class declaration above the main function. LISTING 7.2 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11:
SimpleWindowsForm—Class Declaration
__gc class SimpleWindowsForm : public Form { public: SimpleWindowsForm(); virtual ~SimpleWindowsForm() {} }; SimpleWindowsForm::SimpleWindowsForm() { }
Working with Windows Forms
75
Initializing the Windows Form Because there’s not a resource file to define a dialog template to initialize the Windows Form and its associated controls, you must create and initialize them using C++ source code. The initialization of the Windows Form occurs when the class is first constructed; otherwise, the form would display with nothing. It is generally good practice to create an InitForm() method that is called from within the constructor of the class, as Listing 7.3 shows. This allows you to easily find the form properties because they reside within a single function. The constructor may contain code for initializing member variables or calling other functions, so its best to not have to search for your form’s properties through all the other code that’s present. LISTING 7.3 Members 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21:
SimpleWindowsForm—Class Declaration with Basic Methods and
__gc class SimpleWindowsForm : public Form { protected: void InitForm(); public: SimpleWindowsForm(); virtual ~SimpleWindowsForm() {} };
SimpleWindowsForm::SimpleWindowsForm() { // Initialize the Windows Form InitForm(); }
void SimpleWindowsForm::InitForm() { }
The code shown in Listing 7.3 is the basic class for any Windows Form you create throughout the rest of this hour and in the coming hours. From this point, depending on how the form is initialized, the Windows Form can be anything and contain anything of which it is capable.
7
76
Hour 7
This form is going to be a simple Windows Form with a “Hello World” message and an OK push button. First, you need to declare the class members to store pointers to the Windows Form controls. Therefore, add the following declarations to the SimpleWindowsForm class: Label* m_pHelloWorld_L; Button* m_pOK_B;
In order to immediately know what type of control a member variable represents, you can use the naming scheme for member variables, as shown in the previous code snippet. The L for the m_pHelloWorld_L member variable means that the variable is a label, and likewise, the B after the m_pOK_B variable means that it is a Button. Creating and initializing the controls within the Windows Form is straightforward enough; however, the screen layout is a matter of trial and error. All the coordinates and sizes are in pixels; therefore, you need to determine the right location and size for each control and initialize its properties. Listing 7.4 shows the modified InitForm() method, which creates and initializes the controls. Figure 7.1 shows the resulting Windows Form. LISTING 7.4 The SimpleWindowsForm::InitForm()—Method Definition with Windows Form Control Initialization 1: void SimpleWindowsForm::InitForm() 2: { 3: // Allocate controls 4: m_pHelloWorld_L = new Label; 5: m_pOK_B = new Button; 6: 7: // m_pHelloWorld_L Initialization 8: m_pHelloWorld_L->Location = Point(8, 20); 9: m_pHelloWorld_L->TabIndex = 1; 10: m_pHelloWorld_L->TabStop = false; 11: m_pHelloWorld_L->Text = S”Hello World!”; 12: m_pHelloWorld_L->Size = System::Drawing::Size(100, 16); 13: 14: // m_pOK_B Initialization 15: m_pOK_B->Location = Point(100, 50); 16: m_pOK_B->TabIndex = 2; 17: m_pOK_B->TabStop = true; 18: m_pOK_B->Text = “OK”; 19: m_pOK_B->Size = System::Drawing::Size(70, 20); 20: 21: // Add controls to the Form 22: Controls->Add( m_pHelloWorld_L ); 23: Controls->Add( m_pOK_B ); 24:
Working with Windows Forms
LISTING 7.4 25: 26: 27: 28: }
77
continued // Initialize Form attributes Size = System::Drawing::Size(200, 110); Text = “Simple Form”;
As you can see from the code in Listing 7.4, every aspect of each control and the form itself is controlled through the class properties of the control and form. Therefore, you can easily generate forms dynamically at runtime. Also, in case you are wondering where the objects are deleted, note that the class is managed by the common language runtime (CLR). Therefore, deleting the objects you create is not necessary.
Completing the Application At this point, you have created a class that represents a Windows Form, but the application doesn’t know how to display the form yet, so you cannot run the application and see the form. This is accomplished through a simple change to the main() function. Instead of displaying a string to the console window, you need to tell the .NET Framework to run the Windows Form you just created. When you change the main() function to resemble the one shown in Listing 7.5, the .NET Framework will start a message loop for the SimpleWindowsForm object passed in as the parameter to the static Application::Run() method. LISTING 7.5
Modifications to the main() Function to Run the Application with a
SimpleWindowsForm Object int _tmain(void) { Application::Run( new SimpleWindowsForm() ); return 0; }
That’s it. There’s no message loop to code and no registering of window classes. Compile the application and run it to see the dialog application shown in Figure 7.1. FIGURE 7.1 The displayed Windows Form defined by the SimpleWindowsForm
class.
7
78
Hour 7
Making the Push Button Work After running the application, you are probably wondering why the OK push button doesn’t work. It is because there is no code to handle the push button in the application. Unlike MFC applications, where the OK and Cancel push buttons are handled by the CDialog class, the .NET Framework doesn’t recognize these buttons from others. Push buttons and other controls in the .NET Framework provide events when they are clicked or some other event occurs. Instead of capturing those events with a message map, as you do in MFC, you need to handle the events by binding them to a class method by passing a function pointer to the button control that it calls when the event you wish to handle occurs. This function pointer is also known as a delegate in the .NET Framework. Start by adding the following protected method declaration to the SimpleWindowsForm class: void OnOK(Object* pSender, EventArgs* pEventArgs);
Then add the following statement to the InitForm() method after the other m_pOK_B-> statements: m_pOK_B->add_Click( new EventHandler( this, OnOK ) );
This statement ties the Click event of the push button to the event handler OnOK(). Other events for controls are added in the exact same way, except there are other methods to represent each event. For example, add_DoubleClick() adds an event handler for the double-clicking of a control. You can also tie more than one event to a single event handler. The OnOK() method is defined with the following statements, which call the Close() method. This method closes the Windows Form, and control is passed back to the main() function. Here’s the code: void SimpleWindowsForm::OnOK(Object* pSender, EventArgs* pEventArgs) { // Close the Windows Form Close(); }
Now that all the necessary code is in, you can build your project and run the application. If everything works correctly, you should be able to click the OK button, which closes the form.
Working with Windows Forms
79
Building an MDI Interface with Windows Forms Many traditional desktop applications that deal with documents provide a multiple document interface (MDI) to allow the user to work with multiple documents simultaneously. The MDI interface is implemented in MFC using the CMDIFrameWnd and CMDIChildWnd classes. Using Windows Forms, all the functionality for all window types, including the MDI frame window and child windows, is provided in the System::Windows::Forms::Form class. Creating an MDI interface with Windows Forms starts with the same steps you followed to create the SimpleWindowsForm in the previous section. Start by creating a C++ managed application named MDIWindowsForms and copy the code from Listing 7.3 into the new MDIWindowsForms.cpp file. As was mentioned before, the class declaration needs to be inserted above your _tmain function. In order to avoid any confusion, you should rename the class and its constructor and destructor from SimpleWindowsForm to MDIWindowsFrame. This application will also use the same namespaces as the SimpleWindowsForm project, so add the appropriate #using statements to import the DLLs and reference the same namespaces with the using keyword. As you did in the SimpleWindowsForm project, insert the code to launch your Windows Form from the _tmain function. In order for a Windows Form to become an MDI frame window, you must set a single property, IsMdiContainer, in the Form class to True. Once that property is set in the InitForm() method, the Windows Form acts as an MDI frame window or container for MDI child windows. Your MDIForms.cpp file should be similar to Listing 7.6. LISTING 7.6 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14:
Creating the Skeleton Code for an MDI Windows Form
// This is the main project file for VC++ application project // generated using an Application Wizard. #include “stdafx.h” #using #include #include #using #using #using using namespace System;
7
80
Hour 7
LISTING 7.6 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49:
continued
using namespace System::Drawing; using namespace System::Windows::Forms; __gc class MDIWindowFrame : public Form { protected: void InitForm(); public: MDIWindowFrame (); virtual ~MDIWindowFrame () {} }; MDIWindowFrame::MDIWindowFrame () { // Initialize the Windows Form InitForm (); } void MDIWindowFrame::InitForm() { // Initialize Form attributes Size = System::Drawing::Size(200, 110); Text = “MDI Window Frame”; // this form will be a MDI window IsMdiContainer = true; } // This is the entry point for this application int _tmain(void) { Application::Run( new MDIWindowFrame() ); return 0; }
Creating a MDI Child Window For the purposes of this example, there is only one type of MDI child window. The MDI child window is a window with a rich-text edit control and appears as a simple text editor window. Adding all the features that would normally go into such an application is beyond the scope of this hour’s lesson. However, you will understand how to create an MDI child window. To create the MDI child window, you will have to create another class for it. As with the other Windows Forms in this hour, the MDI child window form starts with the same
Working with Windows Forms
81
basic class shown in Listing 7.3. Change the class name from SimpleWindowsForm to MDIChildForm throughout the class definition, making sure you change the constructor and destructor names also. Add a protected declaration for a System::Windows::Forms::RichTextBox object pointer named m_pRichTextBox to the class definition. In the InitForm() method, this member pointer is assigned to a new RichTextBox object and then initialized, as shown in Listing 7.7. LISTING 7.7
The InitForm() Method for the MDIChildForm Class
1: void MDIChildForm::InitForm() 2: { 3: m_pRichTextBox = new System::Windows::Forms::RichTextBox; 4: 5: m_pRichTextBox->Anchor = (AnchorStyles) 6: (AnchorStyles::Top | 7: AnchorStyles::Bottom | 8: AnchorStyles::Left | 9: AnchorStyles::Right ); 10: 11: m_pRichTextBox->Name = “RichTextBox”; 12: m_pRichTextBox->Size = get_ClientSize(); 13: 14: m_pRichTextBox->TabIndex = 0; 15: 16: Controls->Add( m_pRichTextBox ); 17: 18: // Initialize Form attributes 19: Text = “Untitled”; 20: }
This form takes advantage of a nice feature of Windows Forms—the ability to anchor controls to one or more sides of a sizeable window. This allows you to write forms that are sizeable and maintain orderly control layout. The size of the control is initially set to the client size of the form and then anchored so that as the client area is sized, the control is sized to match. Once the new control object is added to the form, it is complete and ready to use as an MDI child window. Because most MDI applications create a single child window when they are launched, this application will do the same. In order to create the child window, you must first declare and create an instance of the MDIChildForm class you created earlier. Once this has been done, you have to associate
7
82
Hour 7
the parent window (MDIWindowFrame) with this new child to ensure proper communication between the two windows. MDIChildForm, because it derives from the Form class, contains a member variable called MdiParent, which is used to make this association. MdiParent in this case is your main MDIWindowFrame class instance, referenced through something called the this pointer. Once that association has been made, you call the Show function provided by the Form base class for your child window. Listing 7.8 demonstrates how to create, associate with a parent window, and show the initial MDI child window. LISTING 7.8
Revised InitForm Method to Display an Initial MDI Child Window
void MDIWindowsFrame::InitForm() { AutoScaleBaseSize = System::Drawing::Size(5, 13); ClientSize = System::Drawing::Size(292, 273); // Add the MDI client area control to the form Controls->Add( new System::Windows::Forms::MdiClient ); MDIChildForm* pChild = new MDIChildForm; pChild->MdiParent = this; pChild->Show(); // this form will be a MDI window IsMdiContainer = true; }
Compile your project now and run it. If everything is working correctly, you should see the main window and a single child window, similar to Figure 7.2. FIGURE 7.2 The MDI Windows Form application with an initial MDI child window.
Working with Windows Forms
83
Adding a Menu Because this is supposed to be a Multiple Document Interface application, you need to add a way to create another child window within the main window. You’ll do this by first adding a menu to the main Windows Form. Adding a menu to any Windows Forms is similar to how the controls were added to the SimpleWindowsForm. Without a resource file to describe the menu, it has to be built by allocating menu items and adding them to a menu bar and setting all attributes for each menu item through the source code. Because this code can get large, it is a good idea to create a separate method called from the InitForm() method to create the menu. This keeps the InitForm() method tidy. Therefore, the first step is to add a protected method, CreateMenu(), to the MDIWindowFrame class. You can add the method manually, making sure to add both the declaration and the definition of the method, or you can use the Add Method Wizard. Using the Class View, navigate to the MDIWindowFrame class, right-click it, and select Add, Add Function. In the dialog that is displayed, enter void for the return type and set the function name to CreateMenu. There will be no parameters for this function, so click Finish to add the method to the class. Because this is a simple MDI application, the menu will only contain a few basic menu items. For each menu item, you need to allocate a new MenuItem class. Although it is not necessary to keep a pointer to each menu item allocated within the class, it is useful to have if you modify any of the menu items at runtime. Listing 7.9 shows the MDIWindowFrame class declaration with the declared MainMenu and MenuItem members. LISTING 7.9 The MDIWindowFrame Class with Declared Member Data for the Menu and Menu Items 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17:
__gc class MDIWindowFrame : public Form { protected: MainMenu* MenuItem* MenuItem* MenuItem* MenuItem* MenuItem* MenuItem* MenuItem* MenuItem* MenuItem* MenuItem* protected:
m_pMainMenu; m_pFileMenu; m_pFileSeparator; m_pFileNewMenu; m_pFileExitMenu; m_pWindowMenu; m_pWindowCascadeMenu; m_pWindowTileMenu; m_pWindowCloseMenu; m_pHelpMenu; m_pHelpAboutMenu;
7
84
Hour 7
LISTING 7.9
continued
18: void InitForm(); 19: void CreateMenu(); 20: 21: public: 22: MDIWindowFrame(); 23: virtual ~MDIWindowFrame() {} 24: };
The menu is created in the InitForm() method. However, instead of placing all the menu-initialization code in InitForm(), you’ll create a separate method, CreateMenu(), that is called from InitForm(). This allows you to keep things better organized. As you can see in Listing 7.10, the menu-initialization code in the CreateMenu() method is somewhat lengthy. LISTING 7.10 The CreateMenu() Method, Which Initializes the Main Menu Within the MDIWindowForm Class 1: void MDIWindowFrame::CreateMenu() 2: { 3: // Allocate menu items 4: m_pMainMenu = new System::Windows::Forms::MainMenu; 5: m_pFileMenu = new System::Windows::Forms::MenuItem; 6: m_pFileSeparator = new System::Windows::Forms::MenuItem; 7: m_pFileNewMenu = new System::Windows::Forms::MenuItem; 8: m_pFileExitMenu = new System::Windows::Forms::MenuItem; 9: m_pWindowMenu = new System::Windows::Forms::MenuItem; 10: m_pWindowCascadeMenu = new System::Windows::Forms::MenuItem; 11: m_pWindowTileMenu = new System::Windows::Forms::MenuItem; 12: m_pWindowCloseMenu = new System::Windows::Forms::MenuItem; 13: m_pHelpMenu = new System::Windows::Forms::MenuItem; 14: m_pHelpAboutMenu = new System::Windows::Forms::MenuItem; 15: 16: // 17: // MainMenu 18: // 19: MenuItem* pMenuItems[] = new MenuItem*[3]; 20: pMenuItems[0] = m_pFileMenu; 21: pMenuItems[1] = m_pWindowMenu; 22: pMenuItems[2] = m_pHelpMenu; 23: m_pMainMenu->MenuItems->AddRange( pMenuItems ); 24: 25: 26: // 27: // FileMenu 28: //
Working with Windows Forms
LISTING 7.10 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66: 67: 68: 69: 70: 71: 72: 73: ); 74: 75:
85
continued
pMenuItems = new MenuItem*[3]; pMenuItems[0] = m_pFileNewMenu; pMenuItems[1] = m_pFileSeparator; pMenuItems[2] = m_pFileExitMenu; m_pFileMenu->MenuItems->AddRange( pMenuItems ); m_pFileMenu->Index = 0; m_pFileMenu->Text = “&File”; // FileNewMenu m_pFileNewMenu->Index = 0; m_pFileNewMenu->Text = “&New”; m_pFileNewMenu->add_Click( new EventHandler( this, FileNew_Click ) ); // Separator m_pFileSeparator->Index = 1; m_pFileSeparator->Text = “-”; // FileExitMenu m_pFileExitMenu->Index = 2; m_pFileExitMenu->Text = “E&xit”; m_pFileExitMenu->add_Click( new EventHandler( this, FileExit_Click ) ); // // WindowMenu // pMenuItems[0] = m_pWindowCascadeMenu; pMenuItems[1] = m_pWindowTileMenu; pMenuItems[2] = m_pWindowCloseMenu; m_pWindowMenu->MenuItems->AddRange( pMenuItems ); m_pWindowMenu->Index = 1; m_pWindowMenu->MdiList = true; m_pWindowMenu->Text = “&Window”; // WindowCascadeMenu m_pWindowCascadeMenu->Index = 0; m_pWindowCascadeMenu->Text = “&Cascade”; m_pWindowCascadeMenu->add_Click( new EventHandler( this, WindowCascade_Click ) ); // WindowTileMenu m_pWindowTileMenu->Index = 1; m_pWindowTileMenu->Text = “&Tile”; m_pWindowTileMenu->add_Click( new EventHandler( this, WindowTile_Click )
// WindowCloseMenu
7
86
Hour 7
LISTING 7.10 76: 77: 78: 79: 80: 81: 82: 83: 84: 85: 86: 87: 88: 89: 90: 91: 92: }
continued
m_pWindowCloseMenu->Index = 2; m_pWindowCloseMenu->Text = “Close &All”; m_pWindowCloseMenu->add_Click( new EventHandler( this, WindowCloseAll_Click ) ); // // HelpMenu // m_pHelpMenu->MenuItems->Add( m_pHelpAboutMenu ); m_pHelpMenu->Index = 2; m_pHelpMenu->Text = “&Help”; m_pHelpAboutMenu->Index = 0; m_pHelpAboutMenu->Text = “&About...”; m_pHelpAboutMenu->add_Click( new EventHandler( this, HelpAbout_Click ) );
Starting with the main menu bar, you have to instantiate a new instance of the appropriate class for each element in the menu. The main menu bar is instantiated as a MainMenu class. You then instantiate each menu item, starting with the top-level menu items and working down into each of the submenus. Even the separator in the File menu is a menu item that you instantiate. After you create each of the MenuItem classes, you add them to the appropriate parent, which is the main menu for the top-level menu items and the toplevel menu item for the submenu items. Each MenuItem object has an event handler assigned to it with the add_Click() method for handling the Click event. Anytime the user selects a menu item with the mouse or keyboard, the assigned handler for the Click event is called. The first thing you need to do to add the event handlers to the class is to declare them in the class declaration. Each event takes two parameters—a pointer to an Object and a pointer to an EventArgs object. Add the event handler declarations to the protected area of the class declaration using the following code snippet as a guide: void void void void void void
FileExit_Click( Object* pSender, EventArgs* pArgs ); FileNew_Click( Object* pSender, EventArgs* pArgs ); HelpAbout_Click( Object* pSender, EventArgs* pArgs ); WindowCascade_Click( Object* pSender, EventArgs* pArgs ); WindowCloseAll_Click( Object* pSender, EventArgs* pArgs ); WindowTile_Click( Object* pSender, EventArgs* pArgs );
Listing 7.11 shows the definitions of the each of the menu item event handlers.
Working with Windows Forms
LISTING 7.11 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: ) 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: ) 26: 27: 28: 29: 30: 31: 32: 33: 34: 35:
87
The MenuItem Object’s Click Event Handlers for the Main Menu
void MDIWindowFrame::FileExit_Click( Object* pSender, EventArgs* pArgs ) { // Close the application Close(); } void MDIWindowFrame::FileNew_Click( Object* pSender, EventArgs* pArgs ) { MDIChildForm* pChild2 = new MDIChildForm; pChild2->MdiParent = this; pChild2->Show(); } void MDIWindowFrame::WindowCascade_Click( Object* pSender, EventArgs* pArgs { LayoutMdi( MdiLayout::Cascade ); } void MDIWindowFrame::WindowTile_Click( Object* pSender, EventArgs* pArgs ) { LayoutMdi( MdiLayout::TileHorizontal ); } void MDIWindowFrame::WindowCloseAll_Click( Object* pSender, EventArgs* pArgs { int nCount = MdiChildren->get_Length(); for( int i = 0; iClose(); } void MDIWindowFrame::HelpAbout_Click( Object* pSender, EventArgs* pArgs ) { }
The last thing to do before you can test your menu is to add the menu to the main Windows Form. When you add controls to a Windows Form, certain events are fired based on layout changes. However, during the initial stages of building the form, this can produce undesirable results. You can temporarily suspend firing these events by calling the SuspendLayout function at the beginning of the InitForm function. To turn layout events back on, simply call ResumeLayout before you return from the function.
7
88
Hour 7
Adding a menu to a form is a little different from adding other types of controls. Because forms usually have menus associated with them, the Forms class contains a Menu property. To associate a menu to the form, it’s a simple matter of setting that property to point to the menu you created with CreateMenu, the m_pMainMenu variable. Listing 7.12 shows the revised InitForm function. LISTING 7.12
The InitForm() Method with Menu Assignment
1: void MDIWindowFrame::InitForm() 2: { 3: SuspendLayout(); 4: 5: CreateMenu(); 6: 7: // Associate main menu control with the Form 8: Menu = m_pMainMenu; 9: 10: AutoScaleBaseSize = System::Drawing::Size(5, 13); 11: ClientSize = System::Drawing::Size(292, 273); 12: 13: // Add the MDI client area control to the form 14: Controls->Add( new System::Windows::Forms::MdiClient ); 15: 16: IsMdiContainer = true; 17: Name = “MDIWindowFrame”; 18: Text = “MDI Application”; 19: 20: ResumeLayout(false); 21: }
The main menu is assigned to any Windows Form window by setting the Form.Menu property to the MainMenu class you instantiated and initialized with the menu items in the CreateMenu() method. In order for the MDI child windows to display, you need to add a MdiClient control to the Controls array, as shown on line 18. Your menu should now be fully functional. Compile your application and run the executable. Selection File, New from the menu bar will create another MDI child window. You can also put the windows in a cascade or tile layout by using the Window menu.
Adding a Toolbar The process of creating a toolbar is similar to how the menu was created. The main toolbar control is created and then each of the toolbar elements is created as a new object and added to the toolbar control. Again, as with the menu, this is best done in the toolbar’s own method. Therefore, declare and create a new method named CreateToolbar(), where you will create the toolbar with the code shown in Listing 7.13.
Working with Windows Forms
89
LISTING 7.13 The CreateToolbar() Method Definition Showing the Creation of the Toolbar Control 1: void MDIWindowFrame::CreateToolbar() 2: { 3: m_pMainToolbar = new System::Windows::Forms::ToolBar; 4: m_pFileNewButton = new System::Windows::Forms::ToolBarButton; 5: m_pToolbarImages = new 6: System::Windows::Forms::ImageList(); 7: 8: // 9: // MainToolbar 10: // 11: m_pMainToolbar->Appearance = 12: System::Windows::Forms::ToolBarAppearance::Flat; 13: m_pMainToolbar->AutoSize = false; 14: m_pMainToolbar->Buttons->Add(m_pFileNewButton); 15: 16: m_pMainToolbar->DropDownArrows = true; 17: m_pMainToolbar->ImageList = m_pToolbarImages; 18: m_pMainToolbar->Name = “MainToolbar”; 19: m_pMainToolbar->ShowToolTips = true; 20: m_pMainToolbar->Size = System::Drawing::Size(292, 25); 21: m_pMainToolbar->TabIndex = 1; 22: 23: m_pMainToolbar->add_ButtonClick( 24 new ToolBarButtonClickEventHandler( this, ToolbarButton_Click ) ); 25: 26: // FileNewButton 27: m_pFileNewButton->ImageIndex = 0; 28: m_pFileNewButton->ToolTipText = “Create new MDI Child”; 29: 30: // ToolbarImages 31: m_pToolbarImages->ColorDepth = 32: System::Windows::Forms::ColorDepth::Depth8Bit; 33: 34: m_pToolbarImages->ImageSize = System::Drawing::Size(16, 16); 35: m_pToolbarImages->Images->Add( 36: new System::Drawing::Icon( “FileNew.ico” ) ); 37: 38: m_pToolbarImages->TransparentColor = 39: System::Drawing::Color::Transparent; 40: }
The toolbar control is created by instantiating a new ToolBar class. ToolBarButton objects are added to the toolbar to represent the buttons in the toolbar control. In this case, there is a single toolbar button. This toolbar control has a flat appearance and tooltips. These properties are set during the initialization of the toolbar control. Toolbars are also associated with an ImageList
7
90
Hour 7
object to provide the images for the toolbar buttons. The images are indexed by the order they are added to the ImageList object. The index is assigned to the toolbar button’s ImageIndex property to identify which icon to use for the toolbar button. The ImageList object itself is initialized by setting its image size, color depth, and transparent color properties. Using the Add() method, you can add images to the ImageList object. In this case, only a single icon image is added. Looking back at Listing 7.13, notice that the toolbar has an event, ButtonClick, that a handler is assigned to with the add_ButtonClick() method. Any time a button on the toolbar is clicked by the user, this event is raised and the handler is called. Listing 7.14 shows the definition of the handler for the ButtonClick event. LISTING 7.14
The Toolbar ButtonClick Event Handler
1: void MDIWindowFrame::ToolbarButton_Click( Object* pToolbar, ToolBarButtonClickEventArgs* pArgs ) 2: { 3: //Check to see which button was pressed 4: if (m_pMainToolbar->Buttons->IndexOf(pArgs->Button) == 0 ) 5: { 6: MDIChildForm* pChild = new MDIChildForm; 7: 8: pChild->MdiParent = this; 9: pChild->Show(); 10: } 11: }
Because the toolbar in this example only has a single button, the check to see which button was clicked is not necessary. However, it shows what code is required to check which button is clicked when there is more than one button on the toolbar. To determine which button was clicked, you use the Button property of the ToolBarButtonClickEventArgs parameter as an index into your toolbar’s Buttons collection. The value returned from the IndexOf function of the Buttons collection tells you which button was clicked. Because there is only one button on the toolbar, the value will always be 0. Finally, the following statements are added to the InitForm() method to call the CreateToolbar() method and add the toolbar control to the Windows Form: CreateToolbar() Controls->Add( m_pMainToolbar );
Working with Windows Forms
91
Adding an About Dialog Finally, all applications need to have an About dialog. This sample application is no exception. Therefore, you will learn how to create a modal dialog from a Windows Form. As with the other Windows Forms up to this point, start with the class shown in Listing 7.3 but this time change the class name to AboutDialog. The About dialog has a message and an OK button, so declare protected Label and Button pointer member data items within the AboutDialog class. In order to handle the OK button’s Click event, also declare a protected event handler named OnOK(), as shown in Listing 7.15. LISTING 7.15 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14:
The AboutDialog Class Definition
__gc class AboutDialog : public Form { protected: Label* m_pAbout_L; Button* m_pOK_B; protected: void InitForm(); void OnOK(Object* pSender, EventArgs* pEventArgs); public: AboutDialog(); virtual ~AboutDialog() {} };
The AboutDialog Windows Form is initialized in the InitForm() method, just like the other Windows Forms in this example. This form is very similar to the one you worked with in the first example in this hour in that it only has a label and button control on the form. Listing 7.14 shows the InitForm() method for the AboutDialog class. LISTING 7.16 Definitions 1: 2: 3: 4: 5: 6: 7: 8: 9: 10:
The AboutDialog Class’s InitForm() and OnOK() Method
AboutDialog::AboutDialog() { InitForm(); } void AboutDialog::InitForm() { // Allocate controls m_pAbout_L = new Label; m_pOK_B = new Button;
7
92
Hour 7
LISTING 7.16 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42:
continued
// m_pAbout_L Initialization m_pAbout_L->Location = Point(8, 20); m_pAbout_L->TabIndex = 1; m_pAbout_L->TabStop = false; m_pAbout_L->Text = S”MDI Windows Form”; m_pAbout_L->Size = System::Drawing::Size(120, 16); // m_pOK_B Initialization m_pOK_B->Location = Point(100, 50); m_pOK_B->TabIndex = 2; m_pOK_B->TabStop = true; m_pOK_B->Text = “OK”; m_pOK_B->Size = System::Drawing::Size(70, 20); m_pOK_B->add_Click( new EventHandler( this, OnOK ) );
// Add controls to the Form Controls->Add( m_pAbout_L ); Controls->Add( m_pOK_B ); // Initialize Form attributes Size = System::Drawing::Size(200, 110); Text = “About”; FormBorderStyle = FormBorderStyle::FixedDialog; MaximizeBox = false; MinimizeBox = false; } void AboutDialog::OnOK(Object* /*pSender*/, EventArgs* /*pEventArgs*/) { Close(); }
The new Label object is assigned a size and location along with the contents to display in the About dialog. The Button object is assigned a size and location also, along with a handler for the Click event. When the user clicks the OK button, it will call the OnOK() method and close the dialog, as expected. There are a few initialization settings for the Windows Form that make it have the characteristics of a dialog. First, the FormBorderStyle property is set to a fixed dialog border so that it isn’t sizeable. Additionally, the minimize and maximize boxes are removed from the form by setting the corresponding properties to False. The About dialog is displayed from the Help, About menu item’s Click event handler, as shown earlier in Listing 7.8. A new instance of the AboutDialog class is created, and the ShowDialog() method is called.
Working with Windows Forms
93
Compiling and Running the MDI Application At this point, the application is ready to compile and run. When it is compiled and executed, you can open multiple MDI child windows and use the Window menu items to adjust them. Selecting the Help, About menu item displays the About dialog as expected (see Figure 7.3). FIGURE 7.3 The MDI Windows Form application with all types of windows being displayed.
As stated earlier, the source code for this example can be found in its entirety on the Sams Web site for your convenience.
Summary In this hour you built two applications using four different types of Windows Forms. You built a dialog-based application that used a form with controls as the main application window, and you created an MDI application that used Windows Forms for the MDI frame, MDI child, and About dialog. In the examples, you learned how to initialize controls, including menus and toolbars, for use with Windows Forms and how to handle events from the menu and toolbar when the user selects an option. Although there is much more to Windows Forms that cannot be covered in an hour’s lesson, you should now have a foundation upon which you can start working with Windows Forms in your applications.
7
94
Hour 7
Q&A Q Can any Windows Form have a menu and/or toolbar? A Because a toolbar is nothing more than another control, any Windows Form can have a toolbar. However, the client area for the window will not be as easy to keep separated as it is with an MDI application. Even in Windows applications written with MFC, any window can have a menu, and that’s the case with Windows Forms, except that only the form can have a menu, not the controls. However, the only real use for a menu that meets usability standards is as the main window of the application. Q Are there any resource files used in .NET applications or am I required to ship a set of image files with my application? A There is an actual .NET resource file that’s used to store application information, including binary images and values. This file is an XML file with the extension .resx, and it’s compiled with the resgen.exe program. More information on this is covered in the next hour.
Workshop The Workshop provides quiz questions to help solidify your understanding of what was covered in this hour. Answers are provided in Appendix A, “Quiz Answers.”
Quiz 1. What is the difference between a Windows Form that is used as a normal form and one that is used as an MDI container? 2. Are dialogs and menus defined in resource files? 3. What is the class for the menu? 4. How are images assigned to toolbar buttons?
HOUR
8
Working with Resources Up to this point, you have been placing literal strings within your source code. However, once you start thinking of deploying your application and possibly translating these strings into different languages, hard-coding strings like this will require more work in the long run. To alleviate this, your application can use data that comes directly from a resource file. However, this resource file isn’t just limited to strings. Resource files can contain any number of different types of data that your application may need, such as image files, icons, cursors, version information, and so on. In this hour, we are going to look at a new type of resource format used by the .NET framework: the managed resource. If you wish to use the older resource script files that were used by the older versions of Visual Studio, Microsoft has not changed any of that functionality, and you are free to do so. Using managed resources carries with it the advantage of sharing resources across assembly boundaries.
96
Hour 8
In this hour you will learn: • How to use the ResEditor application to create .NET resource files • How to incorporate the resource file into your project • How to access resources at runtime
The .NET Managed Resource File With the .NET Framework, Microsoft has devised a solution for handling application resources that can be used within all the .NET languages. Visual C++ .NET is no exception. Rather than using the older resource scripts (.rc files), as was used with previous versions of Visual C++, the .NET Framework uses an XML-based file format with a file extension of .resx. The obvious advantage of using an XML-based format over the .rc file format is demonstrated by the number of tools available for XML files. XML is easy to parse, it is self-describing when used with a schema, and an abundance of third-party XML editors are available. The process of creating a .NET resource file, however, is a little more difficult. Even though the Visual Studio .NET IDE does have XML editing capabilities, creating a managed resource file with this method is highly error prone and difficult. For simple projects with a few strings, it might be easier, but once you start embedding image files you will need another tool to get the job done. Rather than referencing an external image file from the resource file, the .NET Framework can read base-64 encoding. Base-64 encoding is a method that converts raw bits, as seen in an image file, into readable ASCII characters that can easily be included within an XML file. Even though the Visual Studio .NET IDE does not have a tool for easily creating .resx files, a utility is supplied in one of the .NET Framework examples that ships with Visual Studio .NET. This utility is named ResEditor.exe and will prove to be quite valuable in creating managed resource files.
Creating the .NET Resource File Before you can use the ResEditor utility, you have to build the executable from the C# source files. The utility is located in the .NET Framework SDK tutorials folder in the resourcesandlocalization\reseditor directory. You can use the supplied batch (.bat) file to build the application. During this hour you’re going to create a managed C++ project using a single Windows Form and two controls—a Label control and a PictureBox control. To begin, open the
Working with Resources
ResEditor utility. At the bottom of the dialog, select System.String under the Add group box and type in a value of IDS_HELLO in the edit box next to it. To add the string, click the Add button. This adds the string ID to the resource file, but you still have to set the actual string data. Click the IDS_HELLO item you just added and enter the string you want to use for your application. The next step is to create and add an image to the resource file. Create a bitmap file of any color depth and size before you begin this step. Using the same procedure as before, select System.Drawing.Bitmap from the Add group box and give it the ID IDB_PICTURE. Click the IDB_PICTURE item you just added. This time you should see a small browse button where the data should go. Click this button and browse to the bitmap item you just created. Your results should look similar to Figure 8.1. FIGURE 8.1 Creating .NET managed resources using ResEditor.
To create the .resx file, click File, Save As on the menu bar. The default file format to save as is a .resources file. A .resources file is a binary format that is embedded within an assembly during the linking step of the build process. If you save in this format, you will not be able to open the file in an editor and make any changes. Therefore, set the file format to .resx by changing the Save As Type group box. Save the file to the location where you plan on creating your project.
Integrating the Resource File In this step, you’re going to create the managed C++ application and integrate the resource file you created earlier into the build cycle. To start, open Visual Studio .NET and create a new managed C++ application.
97
8
98
Hour 8
The next step is to add your resource file into the project. Click Project, Add Existing Item from the main menu. By default, the File Open dialog that is displayed is set to only display Visual C++ source files. To find your resource file, you will have to change it to look at all files. Browse to the .resx file you created earlier and click Open. Your file should now be displayed in the Solution Explorer. Open the file by doubleclicking it. You should now be in XML editing mode, similar to Figure 8.2. Clicking the XML button located at the bottom-left corner of the main IDE window will display the XML file in text mode. FIGURE 8.2 Editing the .resx file in Visual Studio .NET.
In order to gain access to the resources available in the .resx file, it must be compiled into a binary format (the .resources file format mentioned earlier). The resource compiler for the .NET Framework is named resgen.exe. To set up the build process to automatically compile the .resx file for you, right-click the .resx file in the Solution Explorer and select Properties. The dialog that is displayed is used to set the custom build step for the file. Select the Custom Build Step item on the left side of the dialog. For the Command Line property, enter the following line: resgen $(InputName).resx $(InputName).resources
By doing this, you are invoking the resgen resource compiler. The arguments to the compiler include the input file, which in this case is the .resx file you created, and the output file you want the file to be compiled to. The $(InputName) macro shown in the
Working with Resources
preceding line of code is simply the name of the file without the extension. The only advantage of using this macro is to eliminate any errors when typing in the filename of your resource file. The last step in this dialog is to fill in the Outputs property. This value should be the same as the second command-line argument shown earlier. Figure 8.3 shows the dialog box with proper values. FIGURE 8.3 Creating the resource file custom build step.
The last step in integrating your resource file is to link the compiled resource file into your assembly. Right-click your project name in Class View and select Properties from the context menu. In the dialog that is displayed, select the Input option underneath the Linker heading on the left side. For the Embed Managed Resource File property, set the value to the filename of the compiled resource file. Figure 8.4 shows the results. FIGURE 8.4 Linking the compiled resource file to your assembly.
99
8
100
Hour 8
Reading Resources at Runtime At this point, you’ve created a resource file, created the custom build step to create the binary resource file, and set up the project to link in that file to your built assembly. The final step this hour is to use those resources from managed C++ code. Included within the .NET Framework is a class named ResourceManager that’s specifically designed to work with application resources. In addition to being able to read resources at runtime, ResourceManager is also culture aware. By you specifying a language and sublanguage (collectively known as a culture) when using the ResourceManager class, it has the ability to select the proper resource based on that culture. If one is not found, it returns the default resource instead. One clear advantage that this brings is the ability to easily change the language for a user interface. In this hour, however, we are just going to focus on using the ResourceManger class to retrieve the default resource. The two main functions within the ResourceManager class that you will be using are the GetString function and the GetObject function. The GetString function, as its name implies, retrieves the value of a string resource given its string ID. The GetObject function, on the other hand, retrieves other types of data within the resource file, such as images, cursors, icons, and so on. Because the GetObject function is generic and can retrieve differing types of data, you have to cast it to the specific data type you want when assigning it to a variable. This is done by using the dynamic_cast C++ operator, which is used to change data from one type to another. The first step, before we can start using the resources, is to create a WinForm with two controls: a Label control and a PictureBox control. Next, using the same methodology as previous lessons, create an InitForm function to handle the initialization of the form. Your header file should look similar to Listing 8.1. LISTING 8.1 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13:
The Header File for a Basic WinForm That Uses Resources
#pragma once #using using namespace System; // required dlls for WinForms #using “System.dll” #using “System.Windows.Forms.dll” #using “System.Drawing.dll” // required namespaces for WinForms using namespace System::ComponentModel; using namespace System::Windows::Forms;
Working with Resources
LISTING 8.1 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27:
continued
using namespace System::Drawing; __gc class WinForm: public Form { public: WinForm(); ~WinForm(); void InitForm(); protected: System::ComponentModel::Container* m_Components; Label* m_Label; PictureBox* m_Picture; };
In the InitForm function, create the Label control, the PictureBox control, and the control container. Set the properties as shown in Listing 8.2. However, you’ll need to set the PictureBox control’s size based on the bitmap size you created when you made your resource file. You can do this either by opening the image in the Microsoft Windows Paint program and selecting Image, Attributes from the main menu to get the width and height or by opening your .resx file in the ResEditor utility and expanding the image resource to display information about the image. LISTING 8.2 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21:
101
Creating the Label and PictureBox Controls
#include “StdAfx.h” #include “winform.h” #using WinForm::WinForm(void) { InitForm(); } WinForm::~WinForm(void) { Form::Dispose(); } void WinForm::InitForm() { SuspendLayout(); // create component container m_Components = new System::ComponentModel::Container();
8
102
Hour 8
LISTING 8.2 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: }
continued // create the label control m_Label = new Label(); m_Label->Location = Point(10, 30 ); m_Label->TabIndex = 1; m_Label->TabStop = false; m_Label->Size = System::Drawing::Size(200, 16); // create the PictureBox Control m_Picture = new PictureBox(); m_Picture->Location = Point( 10, 50 ); m_Picture->TabIndex = 2; m_Picture->TabStop = false; m_Picture->Size = System::Drawing::Size(183, 102); // add the controls to the container Controls->Add( m_Label ); Controls->Add( m_Picture ); // form properties ClientSize = System::Drawing::Size(205, 200); FormBorderStyle = System::Windows::Forms::FormBorderStyle::FixedDialog; Name = S”Hour 8”; Text = S”Hour 8”; ResumeLayout();
Now that you have a basic WinForm set up that contains a Label and a PictureBox control, it’s time to use the resources you created earlier to set the controls’ data. In order to use the ResourceManager class discussed earlier, you must first create an instance of the class using parameters to the constructor to tell it which resource you wish to use. The resource name is the name of the output file without the .resources extension that is created when your .resx file is compiled. For this hour’s lesson, the name of the .resources file is NETResources. The second parameter to the constructor is the location of this resource. As you’ll recall from earlier in the lesson, the resource is linked to the assembly as an embedded managed resource file. Therefore, the location is not an actual path but an assembly class pointer that is found by calling the GetExecutingAssembly function. Immediately following the SuspendLayout function call within the InitForm function of your WinForm class, insert the following code: // create resource manager Resources::ResourceManager * pResources =
Working with Resources
new Resources::ResourceManager (S”NETResource”, Reflection::Assembly::GetExecutingAssembly());
Now that you’ve created an instance of the ResourceManager class, you can use it to retrieve the string and image resources. The property for the Label control that you want to change is the Text property. Use the GetString function provided by the ResourceManager class to set this property, as shown here: // get label text from resource m_Label->Text = pResources->GetString(S”IDS_HELLO”);
Setting the Image property for the PictureBox control, however, isn’t as straightforward. As mentioned earlier, the only other function available to retrieve resources using the ResourceManager class is the GetObject function. Because this function returns an Object* value, you will need to cast that return value to an Image object. This is done using the dynamic_cast operator, as the following code demonstrates: // get image data for PictureBox from resource m_Picture->Image = dynamic_cast (pResources->GetObject(S”IDB_PICTURE”));
The last thing to do to finish the project is to make sure you launch your WinForm from the application’s main function, as shown in Listing 8.3. LISTING 8.3 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13:
Launching the WinForm
#include “stdafx.h” #include “WinForm.h” #using #include using namespace System; // This is the entry point for this application int _tmain(void) { Application::Run(new WinForm()); return 0; }
Summary Working with managed resources is a radical departure from the old resource script method employed in previous versions of Visual C++. Although the process may seem more involved until the toolset matures within the VS .NET IDE, the advantages to using
103
8
104
Hour 8
managed resources can make up for this in the long run. If you’re planning on sharing resources between programming languages, using managed resources will greatly simplify the process. In this hour, you learned how to use managed resources and how to access them using the .NET Framework. You used the ResEditor tool to simplify the creation of the .resx file, which was later added to the build cycle and embedded within your assembly. Finally, you learned how simple it is to use the ResourceManager class to access those resources at runtime.
Q&A Q If I can only embed one .resources file into an assembly, does that mean I have to put every language resource into one file? A No, it doesn’t. The ResourceManager class has a CreateFileBasedResourceManager function that supports the reading of .resources files. You can use multiple .resources files in different languages. Q I know I can create resources manually using an XML editor or the ResEditor utility. Are there any other ways? A Yes, there is another way. You can also programmatically create resources using the ResourceWriter class. In fact, this is exactly the way the ResEditor.exe tool reads and writes resources.
Workshop The Workshop provides quiz questions to help solidify your understanding of what was covered in this hour. Answers are provided in Appendix A, “Quiz Answers.”
Quiz 1. Why would you want to include a .resx file in your project rather than just including the already compiled .resources file? 2. What is an advantage of using managed resources compared to unmanaged resources? What are some disadvantages? 3. Can you use the GetObject function within the ResourceManager class to retrieve a string resource?
HOUR
9
Programming with Graphics Visual C++ .NET has a new graphics API, GDI+, for use in developing graphically advanced applications. This new graphics API provides features to your applications that were not available previously, except through thirdparty vendors. These features include antialiasing, multiple graphic file formats, transparent brushes and pens, and many other features. The GDI+ API is available to traditional unmanaged C++ applications and is also available through the .NET Framework. In this hour’s lesson, you will learn how to write applications using both MFC and the .NET Framework with the GDI+ API. Specifically, in this hour you will: • Review the GDI+ API and its architecture • Create a .NET application that performs simple GDI+ painting • Create the equivalent MFC application using GDI+ • Optimize the painting with GDI+ to remove flicker
106
Hour 9
Understanding GDI+ GDI+ is the successor to the GDI API, which has been available in Windows operating systems since the beginning. As GDI was a subsystem in previous versions of Windows, GDI+ is a subsystem within Windows XP that is responsible for displaying graphical information on screens and printers. Outside the functional similarities of the two APIs, GDI+ has an entirely new API that consists of a set of C++ classes for unmanaged code and a set of .NET classes for managed code. Each set of classes expose the entire set of GDI+ functionality. In addition to the new API that GDI+ has created, GDI+ also serves as a wrapper around the original GDI subsystem. Some functions have been improved and optimized, whereas functions that needed no improvements were simply wrapped with GDI+ methods instead.
Although GDI+ is a new feature of Windows XP, it is also available on Windows 2000 when you load the component updates that come as part of Visual Studio .NET. You may also download a redistributable file for Windows NT 4 Service Pack 6, Windows 98, and Windows Me.
Besides the new API interface and the aforementioned new features, GDI+ does away with the device context and the necessity to program with a device context. With GDI+ you, as a programmer, can focus on what you want to display and not be concerned about the details of a particular device. This goes a step beyond the GDI API, which provided a level of abstraction from the hardware layer but never delivered complete abstraction. If the functionality that you require can only be attained by using the older methods, you can do so because that is still supported.
Discovering the New Features As indicated earlier, GDI+ provides many new features that were not available in the GDI API. Many of these features were possible through advanced GDI techniques; however, many developers could not write the necessary code and resorted to buying thirdparty graphics libraries.
Gradient Brushes The first of the new features is gradient brushes. GDI+ now supports the use of linear and path gradient brushes. You can think of a brush much in the same way you think of a paintbrush. A brush can be any color (limited, of course, by the system bit depth) and, when used on the screen, paints certain objects using the color it is currently set to. Gradient brushes extend the single color brush by allowing you to start with one color,
Programming with Graphics
107
end with a different color, and interpolate all the color values in between those starting and ending colors. Gradient brushes can be used to draw lines, curves, and paths. The linear gradient brush changes color gradually in a linear line as you move across the shape. For example, if you have a horizontal gradient brush, the color on the far-left side is the starting color, and the colors gradually change as you move from left to right, where the color used on the far right is your ending color. Because this lesson doesn’t have color pictures, it is difficult to show an example; however, if you are familiar with gradients, this should make sense. The path gradient is harder to explain and even harder to show without color pictures. When you fill a shape with a path gradient brush, you have several options on how the colors change as they relate to the shape. However, all the options are relative to the path that makes up the shape. For example, one option is to have a center color and an edge color so that the color changes from the edge color along the path to the center color as it moves away from the path.
Cardinal Splines A spline is a geometric object that defines a curve between two or more points in which the curve is influenced by some other value, such as another point or a tension value. A cardinal spline is a type of spline in which a series of points is defined and, given a tension value that controls the amount of line curvature, the points are connected so that no sharp edges occur, as shown in Figure 9.1. FIGURE 9.1 A cardinal spline drawn with the DrawCurve()
method in GDI+.
The actual point indicators in the drawing were drawn separately for illustration purposes and are not part of the DrawCurve() implementation.
Persistent Paths A path, as it relates to graphics, is a combination of lines and curves. Paths are extremely useful when it comes to drawing shapes and filling the interior portions of those shapes, and they can even be used to create clipping regions (regions in which no drawing is done). With GDI, a path that was created belonged to the device context. However, once
9
108
Hour 9
the path was drawn, it was lost. With GDI+, paths are treated separately and therefore can be used over and over again to draw within GDI+.
Transformations with the Matrix Transformations in GDI+ are described with a Matrix object. The Matrix object provides easy transformations, such as rotations and scaling, for GDI+ objects that have a Transform() method. The Transform() method receives a Matrix object that defines the transformation or transformations to apply to the object every time it is drawn. For example, if a Path object has had the Transform() method called with a Matrix object, it is transformed every time it is drawn.
Scalable Regions GDI+ expands the region functionality provided by GDI. A region in GDI could only have a translation transformation applied to it because its coordinates were stored relative to the actual device coordinates. GDI+, on the other hand, stores regions in world coordinates. This allows you to use any of the three transformations on this region: rotation, scaling, or translation. As mentioned earlier, these transformations are applied using the Matrix object.
Alpha Blending A very nice feature built in to GDI+ is advanced alpha blending. The AlphaBlend() function was available in later versions of GDI; however, it was essentially a BitBlt() function with alpha-blending capabilities. Alpha blending in GDI+ is much more advanced, with the ability to create transparent colors that you can use to create brushes and pens. For example, instead of using the AlphaBlend() function on a bitmap to make it appear transparent, with GDI+ you can simply draw the portions of the image transparently by using brushes and pens that are transparent.
Recoloring GDI+ also provides support for adjusting image colors. One or more colors in an image can be changed to other colors. You can also adjust a color’s intensity relative to another color, adjust the brightness or contrast of all colors, and convert the image colors to grayscale. Recoloring is done by providing an ImageAttributes object when drawing images. The ImageAttributes object stores a color matrix and other information that determines how the colors are changed.
Color Correction Color correction within GDI+ provides the ability to correct the displayed colors to represent the true colors as closely as possible. This feature is similar to the Image Color Matching (ICM) feature already found as part of the Windows Win32 API.
Programming with Graphics
109
Antialiasing A feature most developers did without when developing with GDI was antialiasing (or the smoothing of images). The algorithms for antialiasing are somewhat complex to provide a good result. With GDI+, it is a simple option to turn on, and everything drawn is antialiased for the best presentation possible, given the resolution and color depth of the device. In case you aren’t familiar with antialiasing, it is a method of smoothing out the “jaggies” on graphic images. For example, on a diagonal line, you can see the jagged edges as the line passes from one pixel row to the next. With antialiasing, additional pixels are colored with variations of the true line color to produce an illusion of the line being smooth. This is a very common technique used in video games and is even used within Windows extensively to provide an easy-to-read user interface. Figure 9.2 provides an example of text drawn with and without antialiasing. FIGURE 9.2 Text drawn with and without antialiasing.
Not Antialiased Antiliased
Metadata Metadata is often used within images to determine their features. For example, a digital camera may store camera settings with a picture in the metadata. GDI+ allows you to read and write metadata in image files. An application can then use the metadata to adjust itself based on image settings stored in the metadata.
Graphics Containers Containers in GDI+ provide a useful way for beginning a section of drawing code that changes GDI+ properties specifically for the next block of drawing instructions. When those instructions are finished, ending the container restores GDI+ to its original state before the container was created. An application can also nest containers while drawing with GDI+. In other words, complex drawing logic can nest additional GDI+ settings by creating additional containers within a container. For example, an application could draw a rectangle and then begin a container. Within that container, the application would set the clipping region to the inside of the rectangle. At this point, anything drawn is clipped to the rectangle. Once the drawing is done, the application can end the container and the clipping region, and any other settings made within the container are restored to previous settings.
9
110
Hour 9
This saves you from constantly writing code to set the GDI+ settings back to where they were when you are done drawing a portion of an image.
New Programming Methodology If you have written code using GDI, you are familiar with device contexts and how they store the information about the capabilities of a particular device. The device context is the basis for all GDI calls and is required for GDI to know the device with which you are working. Any time an application used GDI, it first started by retrieving a handle to the device context, which then was used as the first argument in each of the GDI functions that interacted with the device context to produce an image or change settings. With GDI+ you don’t have handles or device contexts. All the functionality is encapsulated within a C++ Graphics object that provides methods to perform GDI+ operations. The Graphics object is the main object within GDI+. However, there are several other supporting classes for pens, brushes, and so on. The Graphics object, as the device context, is associated with a particular device. The object manages the attributes for the device and applies them as drawing occurs through the Graphics object. The way pens, brushes, fonts, and so on are handled with GDI+ during the drawing operation is another difference in programming with GDI+. With GDI, you selected objects with the SelectObject() function into the device context and then performed the GDI operations that used the newly selected objects. With GDI+, you don’t select objects into the Graphics object; instead, you simply pass them as parameters to the appropriate methods. There is one drawback to this method—performance. Passing more parameters into GDI+ methods does have an impact on performance; however, for most applications it won’t be noticeable.
Building a Simple GDI+ Application Any application that is going to do special painting with GDI+, as is the case with GDI, most likely does it whenever a window needs painting. The techniques using Visual C++ .NET with managed and unmanaged code are very similar, as this section shows. Both applications paint a rectangle with a circle in the middle that is sized based on the window client area.
Using the .NET Framework As usual, writing a VC++ .NET Windows application starts with a Windows Form class that is used as the main form for the application. Because this application doesn’t serve any real purpose other than to demonstrate the GDI+ capabilities, the form is defined with no controls, as shown in Listing 9.1.
Programming with Graphics
LISTING 9.1 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46:
111
GraphicsWindowsForm Windows Form Class Declaration
#include “stdafx.h” #include #using #using #using #using using namespace System; using namespace System::Drawing; using namespace System::Windows::Forms; __gc class GraphicsWindowsForm : public Form { protected: System::ComponentModel::Container* m_pComponents; protected: void InitForm(); public: GraphicsWindowsForm(); virtual ~GraphicsWindowsForm() {} }; GraphicsWindowsForm::GraphicsWindowsForm() { // Initialize the Windows Form InitForm(); } void GraphicsWindowsForm::InitForm() { m_pComponents = new System::ComponentModel::Container(); // Add Initialization Code //........................ // Initialize Form attributes Text = “Graphic Form”; } int _tmain(void) { // TODO: Please replace the sample code below with your own. Console::WriteLine(S”Hello World”); return 0; }
9
112
Hour 9
In order to perform the painting, the first step is to add an event handler for the Paint event in the GraphicsWindowsForm class and provide an implementation. Add the following event handler declaration to the GraphicsWindowsForm class: void PaintHandler( Object* sender, PaintEventArgs* e );
In order for your paint handler function to be called, you must wire it up to the main class by providing a function pointer, also known as a delegate, to the Paint event. In the InitForm function, add the following call to wire your event handler to the Paint event: add_Paint( new PaintEventHandler(this, PaintHandler ));
Providing the implementation of the PaintHandler method is where GDI+ comes into play. For the purposes of this example, GDI+ is used to paint a rectangle with an ellipse in the middle. Listing 9.2 shows the definition of the OnPaint() method. LISTING 9.2
OnPaint() Implementation Within the GraphicsWindowsForm Class
1: void GraphicsWindowsForm::PaintHandler(Object* sender, PaintEventArgs* e) 2: { 3: System::Drawing::Rectangle rcRect = get_ClientRectangle(); 4: 5: // Resize the rectangle to be 10 pixels smaller all the way around 6: rcRect.Inflate( -10, -10 ); 7: 8: // Fill the background with the appropriate color 9: e->Graphics->FillRectangle( 10: new SolidBrush (Color::FromKnownColor( KnownColor::Control)), 11: get_ClientRectangle() ); 12: 13: // Draw the rectangle and circle 14: e->Graphics->DrawRectangle( new Pen(Color::Black, 5), rcRect ); 15: e->Graphics->DrawEllipse( new Pen(Color::Red, 1), rcRect ); 16: }
Compiling and running this example creates a Windows Form that looks like the one shown in Figure 9.3. Try resizing the window of the application. You will see a real problem, as shown in Figure 9.4. The window is not painting correctly when it is resized. What could make this happen? The client area is not entirely invalid when it is resized, and only the invalid portion of the client is being painted. The rest of the client area is left untouched for performance reasons.
Programming with Graphics
113
FIGURE 9.3 GDI+ drawing a rectangle and ellipse within a Windows Form using VC++ .NET.
9 FIGURE 9.4 Resizing problem with painting the client area and invalid regions.
For some drawing, this is fine. However, when a rectangle and ellipse are being resized as the window is sized, this problem is the result. In order to solve the problem, add a delegate to handler the Resize event and invalidate the entire client area with the Invalidate() method. Do this by adding the following protected declaration in the GraphicsWindowsForm class: void ResizeHandler( Object* sender, EventArgs* e );
The ResizeHandler method is then defined with the following implementation: void GraphicsWindowsForm::ResizeHandler( Object* sender, EventArgs* e ) { Invalidate(); }
The last step in creating the Resize event handler is to notify your Form class that you wish to receive Resize events. In the InitForm function, following your recent addition of the Paint handler, add the following line: add_Resize( new EventHandler( this, ResizeHandler ));
114
Hour 9
Now, running the application produces a result that is what you would expect. As the window is resized, the rectangle and ellipse are resized and painted. The entire client area is invalid now, which causes the entire client area to be repainted as the window is sized.
Using GDI+ in MFC In order to compare the same functionality in Visual C++ .NET using unmanaged code and MFC with the Graphics object, you first need to create a new single document MFC application named UnmanagedGraphics. You can turn off the toolbar, status bar, and other nice application features if you choose. They are not needed for this example.
Creating and Setting Up the Project When using GDI+ in unmanaged code, you need to include the header file, gdiplus.h, and you need to initialize the GDI+ system with a call to GdiplusStartup(). When the application finishes, you need to call GdiplusShutdown() to shut down the GDI+ subsystem. The best place to make these calls is in the InitInstance() and ExitInstance() methods within the CUnmanagedGraphicApp class. First open the stdafx.h file and add the following statement to include gdiplus.h. #include
Then you need to modify the CUnmanagedGraphicApp class to add an override for the ExitInstance() method by adding the following public method declaration: virtual int ExitInstance();
Also add the following protected member data item to store the GDI+ token value used to shut down GDI+: ULONG_PTR
m_gdiplusToken;
Finally, you need to modify the InitInstance() method and define the ExitInstance() method, as shown in Listing 9.3. LISTING 9.3 InitInstance() and ExitInstance() Methods Modified to Work with GDI+ 1: BOOL CUnmanagedGraphicsApp::InitInstance() 2: { 3: CWinApp::InitInstance(); 4: 5: // Initialize GDI+ 6: Gdiplus::GdiplusStartupInput gdiplusStartupInput; 7: Gdiplus::GdiplusStartup( &m_gdiplusToken, &gdiplusStartupInput, NULL );
Programming with Graphics
LISTING 9.3 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21:
115
continued // Standard initialization return TRUE;
}
int CUnmanagedGraphicsApp::ExitInstance() { // Shutdown GDI+ Gdiplus::GdiplusShutdown( m_gdiplusToken ); return( CWinApp::ExitInstance() ); }
The final step in setting up an MFC application to use GDI+ is to change the project’s link option to include the gdiplus.lib file, as shown in the project settings dialog in Figure 9.5. FIGURE 9.5 Adding the gdiplus.lib to the linker options of UnmanagedGraphics application.
Painting the View with GDI+ With MFC’s document/view architecture, the area to focus on in the application to paint the client area of a single document application is the CUnmanagedGraphicsView class. The application wizard already provides the OnDraw() method, so all that is left is to add the GDI+ code to draw the rectangle and ellipse. Before you begin, however, the Project Wizard has commented out the pDC parameter in the parameter list. Because you will be using that parameter, you will need to uncomment it. Listing 9.4 shows the CUnmanagedGraphicsView::OnDraw() method implementation.
9
116
Hour 9
LISTING 9.4 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 39:
OnDraw() Method Implementation with GDI+ in an MFC Application
using namespace Gdiplus; void CUnmanagedGraphicsView::OnDraw(CDC* pDC) { CUnmanagedGraphicsDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); // Get client rectangle size CRect rcClientRect; GetClientRect( &rcClientRect ); // Create a GDI+ rectangle the same as rcClientRect Rect rcClient( rcClientRect.left, rcClientRect.top, rcClientRect.Width(), rcClientRect.Height() ); Rect rcRect; // Create a GDI+ rectangle 10 pixels smaller in all dimensions rcRect = rcClient; rcRect.Inflate( -10, -10 ); // Initialize the GDI+ Graphics object from the device context handle Graphics* Graphics = Graphics::FromHDC( pDC->m_hDC ); // Initialize the color object for the brush Color clrBrush; clrBrush.SetFromCOLORREF( ::GetSysColor( COLOR_WINDOW ) ); // Declare SolidBrush Pen Pen
the Brush, and two pens to draw with oBrush( clrBrush ); oPen1( Color::Black, 5 ); oPen2( Color::Red, 1 );
// Draw the contents of the window Graphics->FillRectangle( &oBrush, rcClient ); Graphics->DrawRectangle( &oPen1, rcRect ); Graphics->DrawEllipse( &oPen2, rcRect ); // Release the device context handle from GDI+ Graphics->ReleaseHDC( pDC->m_hDC ); }
The last thing you need to do is add a using statement for the GDI+ namespace, GdiPlus. After the #include statements in the same file as your OnDraw function, add the following line: using namespace Gdiplus;
Programming with Graphics
117
Compiling and running the application shows the window in Figure 9.6. The application written with MFC doesn’t have the same painting issue as the .NET application; therefore, you don’t have to invalidate the window on a resize operation. MFC will automatically call the appropriate painting methods in response to events that may have an effect on the client portion of a window. In this case, a Resize event would make portions of the client area invalid, so MFC counteracts this by making the appropriate function call into your OnDraw event handler.
9 FIGURE 9.6 Unmanaged MFC application using GDI+.
As you can see, the .NET managed version of this application was much easier to write, even without the wizard to create all the source code for you, as is the case with the MFC application.
Removing Drawing Flicker Up to this point, all graphical drawing has been done directly to the device through GDI+. As you size the application window, you will notice there is a flicker in the window as the background is painted and then the image is painted. The image is even painted one element at a time—first the rectangle, then the ellipse. A more advanced way of performing the drawing is to use a technique that actually draws on a bitmap and then displays the bitmap with a single statement. This method has been used for years by GDI developers to eliminate flicker. How is it done with GDI+? Using the managed application for this example, the first step to flicker-free painting is to turn off the Windows Form painting of the background. This is done by overriding the OnPaintBackground() method and implementing it to do nothing, as shown in the following code segment: void GraphicsWindowsForm::OnPaintBackground( PaintEventArgs* pPaintArgs ) { // Do nothing }
118
Hour 9
With the OnPaintBackground() method doing nothing, the background of the window is effectively left unpainted. The same is accomplished in normal window development by setting the window class’s background brush to NULL when registering the window class. The next step is to change the way the drawing occurs. Instead of drawing directly to the Graphics object contained in the PaintEventArgs object, you need to create a new Graphics object from a new Bitmap object. All the drawing occurs on the new Graphics object. The Graphics object is a generic object used for a variety of drawing operations. It contains all the device context information you need when drawing, which means you don’t have to worry about it, as was the case when using regular GDI. When the drawing is complete, the Bitmap object is drawn to the real Graphics object that is contained in the PaintEventArgs object. Listing 9.5 shows the new implementation of the OnPaint() method. LISTING 9.5
Avoiding Flicker by Drawing to a Bitmap First
1: void GraphicsWindowsForm::PaintHandler(Object* sender, PaintEventArgs* e ) 2: { 3: // Create a bitmap image and create a new Graphics object from it. 4: System::Drawing::Bitmap* bmp = new System::Drawing::Bitmap( ClientRectangle.get_Width(), 5: ClientRectangle.get_Height(), 6: pPaintArgs->get_Graphics() ); 7: Graphics* bmpGr = Graphics::FromImage( bmp ); 8: 9: System::Drawing::Rectangle rcRect = get_ClientRectangle(); 10: 11: rcRect.Inflate( -10, -10 ); 12: 13: // Draw on the bitmap image 14: bmpGr->FillRectangle( new SolidBrush( Color::FromKnownColor( KnownColor::Control) ), 15: get_ClientRectangle() ); 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: }
bmpGr->DrawRectangle( new Pen( Color::Black, 5 ), rcRect ); bmpGr->DrawEllipse( new Pen( Color::Red, 1 ), rcRect ); // Draw the bitmap onto the real Graphics object pPaintArgs->Graphics->DrawImageUnscaled( bmp, 0, 0 ); // Clean up bmpGr->Dispose(); bmp->Dispose();
Programming with Graphics
119
When this version of the application is compiled and run, you will see that there is no flicker as the window is sized. This is because the entire window content, including the background, is constructed in memory. Rather than first drawing an object and then drawing another object, and so on, you are drawing one single object each time you repaint. The flicker you initially saw was the different objects being drawn at different times.
9 This is a tip for the more advanced GDI developers who know how to deal with memory device contexts and compatible bitmaps. It is possible using unmanaged code and GDI+ to create a memory device context and select a compatible bitmap into the device context as you would if you were going to draw on the bitmap with GDI. Instead of using GDI, pass the device context off to GDI+ and use the advanced drawing techniques to draw on the bitmap. Once you are finished with GDI+, release the device context and call the BitBlt() function to display the bitmap on the real device context.
Summary In this hour you built two applications using the GDI+ API. The first was a managed application using the .NET Framework and its built-in support for GDI+. The second was a traditional MFC application that used GDI+ for its drawing. You learned how to work with GDI+ to perform painting and also learned an easy optimization technique to remove excessive flicker when painting. Although you didn’t use much of the GDI+ API in this hour, you will use it more in the upcoming hours.
Q&A Q Is GDI+ faster than GDI? A The answer depends on what you are trying to accomplish. Currently, based on my experience, GDI+ is not faster than GDI. Even when compared in Windows XP, GDI outperforms GDI+. If raw performance is necessary, you may have to use GDI. However, GDI+ does provide some powerful features, and it should speed up later as new optimized hardware is developed.
120
Hour 9
Q Do I have to use GDI+ to draw in the .NET Framework or can I use GDI? A GDI+ is the only API provided through the .NET Framework for graphics. It is possible to write unmanaged code to use GDI if it is important to do so. However, GDI+ is the way to draw in the future, and your efforts should focus toward using it.
Workshop The Workshop provides quiz questions to help solidify your understanding of what was covered in this hour. Answers are provided in Appendix A, “Quiz Answers.”
Quiz 1. What is the primary class for GDI+? 2. Does GDI+ have a SelectObject() function equivalent? If it does, what is it? 3. What unique feature does GDI+ provide alpha blending that the AlphaBlend() function in GDI doesn’t provide? 4. How do you keep a Windows Form from painting its background?
HOUR
10
Printing with .NET Printing is a common feature that most desktop applications implement in some form. Some applications produce simple printouts, whereas other applications such as word processors and graphic editors produce WYSIWYG output. When you’re writing a managed Visual C++ .NET application, whatever you are printing is handled through the .NET Framework’s printing classes and GDI+. In this hour’s lesson, you will modify the MDIWindowsForms application from Hour 7, “Working with Windows Forms,” to add printing capabilities using GDI+. In this hour you will: • Add a new toolbar button and menu option to the application • Create a print preview option that shows a document in WYSIWYG format before it is printed • Display a printer dialog that allows the user to choose which printer to use • Use GDI+ to produce the output on the printer device
122
Hour 10
Modifying the Toolbar and Menu This hour’s lesson starts with the MDIWindowsForms application you worked on in Hour 7, “Working with Windows Forms.” You may also download the source code from the Sams Web site. Before getting into the printing functionality, we’ll add a menu option and toolbar button to the application for the print feature. The Print, Page Setup, and Print Preview menu items and Print toolbar button are standard for applications that have printing features. Listing 10.1 indicates which new method and members need to be added to the MDIWindowFrame class declaration. The additions that need to be added to the code from Hour 7 are shown in bold typeface. LISTING 10.1 The MDIWindowFrame Class Changes to Add the Print Menu Item and Toolbar Button 1: __gc class MDIWindowFrame : public Form 2: { 3: protected: 4: 5: // Printing Support 6: MenuItem* m_pFileSeparator1; 7: MenuItem* m_pFileSeparator2; 8: MenuItem* m_pFilePrintMenu; 9: MenuItem* m_pFilePrintPreviewMenu; 10: MenuItem* m_pFilePageSetupMenu; 11: 12: // Created during hour 7 13: MainMenu* m_pMainMenu; 14: MenuItem* m_pFileMenu; 15: MenuItem* m_pFileNewMenu; 16: MenuItem* m_pFileExitMenu; 17: MenuItem* m_pWindowMenu; 18: MenuItem* m_pWindowCascadeMenu; 19: MenuItem* m_pWindowTileMenu; 20: MenuItem* m_pWindowCloseMenu; 21: MenuItem* m_pHelpMenu; 22: MenuItem* m_pHelpAboutMenu; 23: 24: ToolBar* m_pMainToolbar; 25: ToolBarButton* m_pFileNewButton; 26: ImageList* m_pToolbarImages; 27: System::ComponentModel::Container* m_pComponents; 28: 29: // Printing Support 30: void FilePrint_Click( Object* pSender, EventArgs* pArgs ); 31: void FilePrintPreview_Click( Object* pSender, EventArgs* pArgs ); 32: void FilePageSetup_Click( Object* pSender, EventArgs* pArgs ); 33:
Printing with .NET
LISTING 10.1
123
continued
34: // Added during hour 7 35: void FileExit_Click( Object* pSender, EventArgs* pArgs ); 36: void FileNew_Click( Object* pSender, EventArgs* pArgs ); 37: void HelpAbout_Click( Object* pSender, EventArgs* pArgs ); 38: void WindowCascade_Click( Object* pSender, EventArgs* pArgs ); 39: void WindowCloseAll_Click( Object* pSender, EventArgs* pArgs ); 40: void WindowTile_Click( Object* pSender, EventArgs* pArgs ); 41: void OnToolbarButtonClick( Object* pToolbar, 42: ToolBarButtonClickEventArgs* pArgs ); 43: 44: protected: 45: void InitForm(); 46: void CreateMenu(); 47: void CreateToolbar(); 48: 49: // Printing Support 50: void PrintForm( MDIChildForm* pChildForm ); 51: 52: public: 53: MDIWindowFrame (); 54: virtual ~MDIWindowFrame () {} 55: };
In order to enable the user to print, you have to add the appropriate menu items. In Listing 10.1, you added variables from the MenuItem class that will be used to add onto the menu already present. With these menu items, you will be able to change settings, view a print preview of the document, and print the document on the printer. Starting on line 30, you added three event handler declarations. As you’ll recall from Hour 7, once the user clicks a menu item, the delegate (event handler) that is assigned to that menu item is called to perform the necessary menu functions. The last item that was added, line 50, will be the function that is called to print the document on the user’s printer. The next step is to add the new menu items to the existing main menu. Because you started with a modular design and placed all the menu-creation logic within the CreateMenu function, you will simply add onto this function to include the new items. Modifying the CreateMenu() method to add the new printing menu items is done by allocating new MenuItem classes and adding them to the File menu. For a review on how to add menu items and how to create event handlers for them, see Hour 9, “Programming with Graphics.” The modifications to the File menu include the three print functions that you will add to the application, as indicated in Listing 10.2.
10
124
Hour 10
LISTING 10.2 Modifications in the CreateMenu() Method to Add the Print Menu Item to the File Menu 1: void MDIWindowFrame::CreateMenu() 2: { 3: // Allocate printing support menu items 4: m_pFileSeparator1 = new MenuItem; 5: m_pFileSeparator2 = new MenuItem; 6: m_pFilePrintMenu = new MenuItem; 7: m_pFilePrintPreviewMenu = new MenuItem; 8: m_pFilePageSetupMenu = new MenuItem; 9: 10: // allocate menu items 11: m_pMainMenu = new MainMenu; 12: m_pFileMenu = new MenuItem; 13: m_pFileNewMenu = new MenuItem; 14: m_pFileExitMenu = new MenuItem; 15: m_pWindowMenu = new MenuItem; 16: m_pWindowCascadeMenu = new MenuItem; 17: m_pWindowTileMenu = new MenuItem; 18: m_pWindowCloseMenu = new MenuItem; 19: m_pHelpMenu = new MenuItem; 20: m_pHelpAboutMenu = new MenuItem; 21: 22: // 23: // MainMenu 24: // 25: MenuItem* pMenuItems[] = new MenuItem*[3]; 26: pMenuItems[0] = m_pFileMenu; 27: pMenuItems[1] = m_pWindowMenu; 28: pMenuItems[2] = m_pHelpMenu; 29: m_pMainMenu->MenuItems->AddRange( pMenuItems ); 30: 31: // 32: // FileMenu 33: // 34: pMenuItems = new MenuItem*[7]; 35: pMenuItems[0] = m_pFileNewMenu; 36: pMenuItems[1] = m_pFileSeparator1; 37: pMenuItems[2] = m_pFilePageSetupMenu; 38: pMenuItems[3] = m_pFilePrintMenu; 39: pMenuItems[4] = m_pFilePrintPreviewMenu; 40: pMenuItems[5] = m_pFileSeparator2; 41: pMenuItems[6] = m_pFileExitMenu; 42: m_pFileMenu->MenuItems->AddRange( pMenuItems ); 43: 44: m_pFileMenu->Index = 0; 45: m_pFileMenu->Text = “&File”; 46: 47: // FileNewMenu
Printing with .NET
LISTING 10.2
125
continued
48: m_pFileNewMenu->Index = 0; 49: m_pFileNewMenu->Text = “&New”; 50: m_pFileNewMenu->add_Click( 51: new EventHandler( this, FileNew_Click ) ); 52: 53: // Separator1 54: m_pFileSeparator1->Index = 1; 55: m_pFileSeparator1->Text = “-”; 56: 57: // FilePageSetupMenu 58: m_pFilePageSetupMenu->Index = 2; 59: m_pFilePageSetupMenu->Text = “Page &Setup...”; 60: m_pFilePageSetupMenu->add_Click( 61: new EventHandler( this, FilePageSetup_Click ) ); 62: 63: // FilePrintMenu 64: m_pFilePrintMenu->Index = 3; 65: m_pFilePrintMenu->Text = “&Print...”; 66: m_pFilePrintMenu->add_Click( 67: new EventHandler( this, FilePrint_Click ) ); 68: 69: // FilePrintPreviewMenu 70: m_pFilePrintPreviewMenu->Index = 4; 71: m_pFilePrintPreviewMenu->Text = “Print Pre&view...”; 72: m_pFilePrintPreviewMenu->add_Click( 73: new EventHandler( this, FilePrintPreview_Click ) ); 74: 75: // Separator2 76: m_pFileSeparator2->Index = 5; 77: m_pFileSeparator2->Text = “-”; 78: 79: // FileExitMenu 80: m_pFileExitMenu->Index = 6; 81: m_pFileExitMenu->Text = “E&xit”; 82 m_pFileExitMenu->add_Click( 83: new EventHandler( this, FileExit_Click ) ); 84: 85: // Window and Exit menu continued… 86: }
Finally, a new toolbar button needs to be added for printing. This is done by adding a new toolbar button as well as a new image to the associated toolbar image list in the CreateToolbar() method. On line 5 of Listing 10.3, create a new ToolBarButton and assign it to your m_pFilePrint member variable. This member variable should be declared in your class declaration as a protected member type. The next step is to add the button to the main toolbar. This can be seen on line 12, which adds the toolbar button to
10
126
Hour 10
the main toolbar’s Buttons collection. The next step is to define which image the toolbar button is going to use and what the tooltip is when the user hovers his mouse over it. These lines were added starting at line 29. The last step is to add the printer icon image to the image list that is associated with the toolbar. The source code for this hour, which can be downloaded from the Sams Web site, contains the two icons you need. The icons for this application are 8-bit (256 color) icons that are 16 pixels wide and 16 pixels high. The code to add the icon to the image list starts on line 33 of Listing 10.3. LISTING 10.3 Modifications to the CreateToolbar() Method to Add the Print Toolbar Button 1: void MDIWindowFrame::CreateToolbar() 2: { 3: m_pMainToolbar = new ToolBar; 4: m_pFileNewButton = new ToolBarButton; 5: m_pFilePrintButton = new ToolBarButton; 6: m_pToolbarImages = new ImageList(); 7: 8: // MainToolbar 9: m_pMainToolbar->Appearance = ToolBarAppearance::Flat; 10: m_pMainToolbar->AutoSize = false; 11: m_pMainToolbar->Buttons->Add(m_pFileNewButton); 12: m_pMainToolbar->Buttons->Add(m_pFilePrintButton); 13: 14: m_pMainToolbar->DropDownArrows = true; 15: m_pMainToolbar->ImageList = m_pToolbarImages; 16: m_pMainToolbar->Name = “MainToolbar”; 17: m_pMainToolbar->ShowToolTips = true; 18: m_pMainToolbar->Size = System::Drawing::Size(292, 25); 19: m_pMainToolbar->TabIndex = 1; 20: 21: m_pMainToolbar->add_ButtonClick( 22: new ToolBarButtonClickEventHandler( this, OnToolbarButtonClick)); 23: 24: // FileNewButton 25: m_pFileNewButton->ImageIndex = 0; 26: m_pFileNewButton->ToolTipText = “Create new MDI Child”; 27: 28: // FilePrintButton 29: m_pFilePrintButton->ImageIndex = 1; 30: m_pFilePrintButton->ToolTipText = “Print”; 31: 32: // ToolbarImages 33: m_pToolbarImages->ColorDepth = ColorDepth::Depth8Bit; 34: m_pToolbarImages->ImageSize = System::Drawing::Size(16, 16); 35: m_pToolbarImages->Images->Add( 36: new System::Drawing::Icon( “FileNew.ico” ) ); 37:
Printing with .NET
LISTING 10.3 38: 39: 40: 41: 42: 43: }
127
continued
m_pToolbarImages->Images->Add( new System::Drawing::Icon( “FilePrint.ico” ) ); m_pToolbarImages->TransparentColor = System::Drawing::Color::Transparent;
Handling the new Print toolbar and menu option is done with the FilePrint_Click() method and the modifications to the OnToolbarButtonClick() method shown in Listing 10.4. You may have noticed a different layout to OnToolbarButtonClick than the one that was presented in Hour 7. Because you are starting to add more buttons to the toolbar, you would have to create several if statements to accommodate them all. This hour, you’ll consolidate the if statements into a switch statement for easier readability. Both methods call the PrintForm() method, which is where the printing logic is implemented. The Print Preview and Page Setup menu items are handled with the FilePrintPreview_Click() and FilePageSetup_Click() methods, respectively, which are defined later in the hour. LISTING 10.4 The OnToolbarButtonClick() and FilePrint_Click() Method Implementations 1: void MDIWindowFrame::OnToolbarButtonClick 2: ( Object* pToolbar, ToolBarButtonClickEventArgs* pArgs ) 3: { 4: //Check to see which button was pressed 5: switch( m_pMainToolbar->Buttons->IndexOf(pArgs->Button) ) 6: { 7: case 0: // File New 8: { 9: MDIChildForm* pChild = new MDIChildForm; 10: pChild->MdiParent = this; 11: pChild->Show(); 12: } 13: break; 14: 15: case 1: // File Print 16: { 17: PrintForm( dynamic_cast(get_ActiveMdiChild()) ); 18: } 19: break; 20: 21: default: 22: break; 23: } 24: }
10
128
Hour 10
LISTING 10.4
continued
25: void MDIWindowFrame::FilePrint_Click ( Object* pSender, EventArgs* pArgs ) 26: { 27: PrintForm( dynamic_cast(get_ActiveMdiChild()) ); 28: }
The PrintForm() method is called with the current active MDI child window pointer, which is cast to the MDIChildForm class type. The reason a dynamic_cast is used in this case is due to it being an upcast. In other words, get_ActiveMdiChild returns a pointer to a Form object, which the MDIChildForm class derives from. So, to cast from a base class to a derived class, use the dynamic_cast operator. With a pointer to the MDI child form, the printing logic is able to retrieve the contents of the window to produce the printed text. You may want to compile your application now to make sure everything is working correctly. The new Print menu functions and Print toolbar button, of course, do not currently do anything, but the goal here is to make sure they show up correctly.
The first thing you should do before implementing the event handler is to first make sure it is wired correctly and receiving events. One common technique for debuggin event handling is the so-called MessageBox method. Within the event handler functions, you can use the MessageBox function to display a quick message. Here’s an example of calling the MessageBox function: MessageBox( NULL, “In EventHandlerFunction”, “My App”, MB_OK );
Working with the PrintDocument Object The System::Drawing::Printing::PrintDocument class is the basis for all document printing within the .NET Framework. It manages the document page printing, settings for the document (such as margins), and the Graphics object for the printing device. However, to use the class, you have to do more than just call the function. After the function is called, your application must then handle various events that are fired by using delegate functions within your class.
Providing Simple Printing As you start working with printing code, you might see some similarities with using GDI or GDI+. The reason for this is that the hardware that each of these methods use is also
Printing with .NET
129
similar. GDI+ uses the monitor to render graphics, whereas printing uses a printer which prints onto a piece of paper. If you had to display a rectangle on the screen, why should the method be any different for displaying a rectangle on the printed page? With printing, of course, there are a few extra things that you need to work with, such as paper sizes, color depth, and margins. Underneath the hood is an object called the device context. There is a certain device context for a video card and a certain device context for a printer. Once your application obtains access to that device context, you can call the same GDI+ functions regardless of whether you’re using a printer or a display adapter. Initially providing the printing functionality to simply send the contents of the MDIChildForm to the printer is done by creating a PrintDocument object and adding the printing functionality to the PrintForm() method. The PrintDocument object provides events for when printing begins and ends as well as for when a page is printed. At a minimum, an application handles the PrintPage event to provide the basic printing content of the report. For this application, you’re going to create a PrintDocument object as a member variable in your class. Add a variable named m_pPrintDocument to the protected section of your class declaration. The best place to create this object and hook up the event handler is within the InitForm function. Add the following code to your InitForm function: m_pPrintDocument = new PrintDocument; m_pPrintDocument->add_PrintPage(new PrintPageEventHandler(this,OnPrintPage));
The PrintForm() method receives the MDIChildForm that is currently active. In order to print the contents of the form, a method needs to be defined to retrieve the text contents. This is easily done by adding the following public method declaration and implementation to the MDIChildForm class: String* GetPrintText() { return( m_pRichTextBox->get_Text() ); }
The PrintForm() method calls the PrintDocument object’s Print() method to start the printing, as shown in Listing 10.5. You’ll notice that the Print method is inside a try block and is followed immediately by a catch block with an error message. If you don’t have a printer installed or you have a printer installed but it is currently unavailable, you will get an exception and your program will cease to function. By adding the try/catch block, you can avoid the application fault and continue. Error handling like this will be covered in Hour 13, “Working with .NET Error Handling and Diagnostics.” The printing process is then started, and the OnPrintPage() method is called to handle the PrintPage event raised from the PrintDocument object.
10
130
Hour 10
LISTING 10.5
The PrintForm() Method Implementation
1: void MDIWindowFrame::PrintForm( MDIChildForm* pChildForm ) 2: { 3: // Make sure that there was a form provided 4: if ( !pChildForm ) 5: return; 6: 7: // Get the text stream to print 8: m_sPrintText = pChildForm->GetPrintText(); 9: m_nPrintPos = 0; 10: 11: // Initialize the font to use for printing 12: m_pPrintFont = new Drawing::Font( “Arial”, 10 ); 13: 14: // Cause the document to be printed 15: try 16: { 17: m_pPrintDocument->Print(); 18: } 19: catch(Exception* /* e */ ) 20: { 21: MessageBox(NULL, “Printing Failed”, “”, MB_OK ); 22: } 23: }
The m_sPrintText member stores the contents of the window that is being printed. The m_nPrintPos member stores the position within the string that is currently being printed. Finally, the m_pPrintFont member is the font used for all the printing. These members are declared as protected members of the MDIWindowFrame class with the following statements added to the class declaration: String* Drawing::Font* int
m_sPrintText; m_pPrintFont; m_nPrintPos;
The OnPrintPage() method is defined in Listing 10.6 and shows how to process the text contained in the m_sPrintText member for multiple lines. The code doesn’t account for a single line being too long, but that could be added with word-wrapping logic. LISTING 10.6
The OnPrintPage() Method Implementation
1: void MDIWindowFrame::OnPrintPage( 2: Object* pSender, PrintPageEventArgs* pArgs ) 3: { 4: Graphics* gr = pArgs->get_Graphics(); 5:
Printing with .NET
LISTING 10.6 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: needed 37: 38: 39: 40: 41: 42: 43: 44: }
131
continued
int int int int
nPos; nYPos = pArgs->MarginBounds.get_Top(); nCount = 0; lpp = pArgs->MarginBounds.get_Height() / (int)m_pPrintFont->GetHeight(gr);
// While there are more lines to print and the end of the page // has not been reached, keep printing. while( m_nPrintPos < m_sPrintText->get_Length() && nCount < lpp ) { // Find the first line break nPos = m_sPrintText->IndexOf( “\n”, m_nPrintPos ); // If no line break, set to end of line if ( nPos < 0 ) nPos = m_sPrintText->get_Length()-1; // If anything needs to be printed, do it if ( nPos > m_nPrintPos ) gr->DrawString( m_sPrintText->Substring (m_nPrintPos, nPos-m_nPrintPos), m_pPrintFont, Brushes::Black, (float)pArgs->MarginBounds.get_Left(), (float)nYPos, new StringFormat ); // Increment the print position and line number m_nPrintPos = nPos+1; nCount++; nYPos += (int)m_pPrintFont->GetHeight(gr); } // If more lines are available to print, indicate that another page is if ( m_nPrintPos < m_sPrintText->get_Length() ) pArgs->HasMorePages = true; else { pArgs->HasMorePages = false; m_nPrintPos = 0; }
Compiling and running the application at this point provides basic printing functionality without the page setup, print preview, or printer selection. The output is simply sent to the default system printer.
10
132
Hour 10
Selecting the Printer Allowing the user to select which printer to use when printing is done by using the PrintDialog class, which displays the common dialog shown in Figure 10.1. By tying the PrintDocument object to the PrintDialog object, any settings the user makes in the Print dialog are reflected in the PrintDocument object. Listing 10.7 shows the modifications required in the PrintForm() method to display and use the PrintDialog object. FIGURE 10.1 The Print common dialog.
LISTING 10.7 The PrintForm() Modifications to Use the PrintDialog Object to Allow the User to Select a Printer 1: void MDIWindowFrame::PrintForm( MDIChildForm* pChildForm ) 2: { 3: // Make sure that there was a form provided 4: if ( !pChildForm ) 5: return; 6: 7: // Get the text stream to print 8: m_sPrintText = pChildForm->GetPrintText(); 9: m_nPrintPos = 0; 10: 11: // Initialize the font to use for printing 12: m_pPrintFont = new Drawing::Font( “Arial”, 10 ); 13: 14: PrintDialog* pPrintDlg = new PrintDialog; 15: 16: // Assign the PrintDocument object to the PrintDialog 17: pPrintDlg->Document = m_pPrintDocument; 18: 19: // If the user selected OK, print 20: if ( pPrintDlg->ShowDialog() == DialogResult::OK ) 21: { 22: try 23: { 24: m_pPrintDocument->Print();
Printing with .NET
LISTING 10.7 25: 26: 27: 28: 29: 30: 31: }
133
continued } catch(Exception* /* e */ ) { MessageBox(NULL, “Printing Failed”, “”, MB_OK ); }
}
Changing the Page Setup Displaying a Page Setup dialog, as shown in Figure 10.2, allows the user to change the page setup for printing. The user can change the page orientation and margins along with the paper size. On line 17 of Listing 10.7, you see that the PrintDialog has a property named Document that is a PrintDocument object. This is set to the PrintDocument object that you are using for your application. By doing this, any changes that a user makes within the Printer Setup dialog will change the various properties within your PrintDocument object. FIGURE 10.2 The Page Setup common dialog.
Displaying the Page Setup dialog is done with a PageSetupDialog object. The settings are stored in a PageSettings object that is then passed to the PrintDocument object before printing so it can adjust the pages accordingly. Because the user selections for the page settings should be for the life of the program, the PageSettings object is declared as protected in the class definition of the MDIWindowFrame class with the following statement: PageSettings*
m_pPageSettings;
10
134
Hour 10
Add a statement to the InitForm() method to initialize the m_pPageSettings member to 0. Doing so provides a method to determine whether the user has set the page settings or the default page settings should be used. When the user first displays the Page Setup dialog, the m_pPageSettings member is initialized and used. Listing 10.9 shows the implementation of the OnPageSetup() method, which is called when the user selects the Page Setup menu item. LISTING 10.9
The OnFilePageSetup() Method Implementation
1: void MDIWindowFrame::OnFilePageSetup( Object* pSender, EventArgs* pArgs ) 2: { 3: PageSetupDialog* pPageSetupDlg = new PageSetupDialog; 4: 5: // If the user hasn’t set pages setting yet, initialize the object 6: if ( !m_pPageSettings ) 7: m_pPageSettings = new PageSettings; 8: 9: pPageSetupDlg->PageSettings = m_pPageSettings; 10: pPageSetupDlg->ShowDialog(); 11: }
Before you display the Printer Setup dialog when you want to print, you need to first initialize its properties with those found in the Page Setup dialog. The PrintDocument class has a property named DefaultPageSettings that you set from your m_pPageSettings variable. Therefore, before the setup dialog is shown, the properties that the user had chosen earlier get reflected into the Printer Setup dialog. Add the following code to the PrintForm method immediately preceding the code for launching the Printer Setup dialog box: // Assign the page settings if present if ( m_pPageSettings ) m_pPrintDocument->DefaultPageSettings = m_pPageSettings;
Providing a Print Preview The Print Preview option is the final option missing from this application. The .NET Framework provides virtually all the functionality for the print preview in the PrintPreviewDialog object. As was mentioned earlier when the similarities were discussed with display adapters and printers, you’ll notice these similarities when working with the Print Preview methods and with the methods used to print. Rather than print to a physical page, the Print Preview window simply routes the drawing calls to the display adapter instead. In other words, the underlying Graphics object contained by the PrintDocument object is shared between the print previewer and the physical printer. Listing 10.10 shows the OnFilePrintPreview() method implementation.
Printing with .NET
135
LISTING 10.10 The OnFilePrintPreview() Method Implementation to Provide a Print Preview Dialog 1: void MDIWindowFrame::OnFilePrintPreview( Object* pSender, EventArgs* pArgs ) 2: { 3: MDIChildForm* pChildForm = 4: dynamic_cast(get_ActiveMdiChild()); 5: 6: // If now child window is active, nothing to print. 7: if ( !pChildForm ) 8: return; 9: 10: // Perform the same initialization as PrintForm() 11: m_sPrintText = pChildForm->GetPrintText(); 12: m_nPrintPos = 0; 13: m_pPrintFont = new Drawing::Font( “Arial”, 10 ); 14: 15: if ( m_pPageSettings ) 16: m_pPrintDocument->DefaultPageSettings = m_pPageSettings; 17: 18: PrintPreviewDialog* pPrintPreviewDlg = new PrintPreviewDialog; 19: 20: pPrintPreviewDlg->Document = m_pPrintDocument; 21: pPrintPreviewDlg->ShowDialog(); 22: }
The functionality prior to displaying the Print Preview dialog is basically the same code as the PrintForm() method. All the same setup has to occur for the print preview to work. Compiling and running the application now provides the Print Preview dialog shown in Figure 10.3 when the user selects the Print Preview menu option. FIGURE 10.3 The Print Preview dialog.
10
136
Hour 10
Summary In this hour you modified the MDIWindowsForms application to give it printing capabilities for page setup, printer selection, and print preview. This hour’s lesson gave you an overall overview of how to implement printing functionality commonly found in modern applications.
Q&A Q Can the Print Preview dialog be used as an MDI child window? A
is derived from the Form class, and as you know, any form has the potential of being an MDI child window. Another option is to use PrintPreviewControl, which is the print preview portion of the Print Preview dialog. This class can be used directly to provide whatever interface you desire. PrintPreviewDialog
Q What part is GDI+ and what part is the .NET Framework in this example? A The only parts of the example that involve GDI+ are the use of the Graphics object, the call to the DrawString() method, and the font. Everything else is part of the .NET Framework.
Workshop The Workshop provides quiz questions to help solidify your understanding of what was covered in this hour. Answers are provided in Appendix A, “Quiz Answers.”
Quiz 1. What object provides the holding location for page settings? 2. What event is raised when a new page is generated during the printing process? 3. What is the main object used for handling printing in the .NET Framework?
PART IV Server Development Hour 11 Creating Web Services 12 Creating Web Services with ATL 13 Working with .NET Error Handling and Diagnostics 14 ATL Servers
HOUR
11
Creating Web Services One of the biggest reasons to use the .NET Framework to create applications in Visual C++ .NET over using MFC is the .NET Framework’s support for Internet technologies. Even though MDC does provide support for various Internet-related tasks through the WinInet library and ISAPI classes, the .NET Framework expands on these and makes many more technologies available. The .NET Framework makes writing Web-based technology very easy with the provided classes. Web Services are especially well suited to Visual C++ .NET because you can do everything other .NET languages can do, and you can also wrap legacy code with .NET managed classes and make them Web Services for use in Web development. The only caveat to creating Web Services with Visual C++ .NET is that it tends to be more difficult than other .NET languages because C++ itself is a more difficult language to master. In this hour, you will learn about the features included in the .NET Framework for writing Web Services with Visual C++ .NET. In this hour you will: • Review the Web Service support in the .NET Framework • Write a simple .NET Web Service • Use the new Web Service as part of an application • Debug and test the Web Service
140
Hour 11
Overview of Web Services and .NET Web Services are ASP.NET programmable entities that provide particular functionality and are accessible to any other systems through the use of the Simple Object Access Protocol (SOAP), a technology that uses Internet standard XML and HTTP. Web Services are based on the XML standard and other Internet standards in order to provide an interface that is usable not only by other .NET applications but also applications written in other languages and on other platforms. A Web Service can either be used internally by a single application or exposed to be used externally over the Internet. By using an XML-based protocol as the fundamental communication method, the technology gap between systems is eliminated. Developers can create applications that use many different Web Services together, similar to using objects together. One of the main characteristics of a Web Service is its high level of abstraction. The implementation and usage of the Web Service is done through an XML-based protocol. Therefore, the client and server need only deal with that technology and understand the inputs, outputs, and location provided by the Web Service.
Understanding the Web Service Infrastructure Web Services that reside on a Web server are addressed via URLs. Clients of Web Services can discover what Web Services are available, dynamically discover their interfaces, and then use the Web Services, all without being specifically programmed to work with those Web Services. Essentially a Web Service has the same generic capabilities that a Web browser has with a Web site. The Web browser isn’t developed to use a specific Web site, but when it is pointed to one, it knows how to adapt to the content and display it appropriately. A Web Service is similar in that the client does not have to be programmed to know everything about a Web Service. Instead, the client discovers the Web Service’s interface and then uses it appropriately. Web Services must be loosely coupled, use an open standard for communication, and use a universal data format. Rather than using a binary protocol to send and receive data, Web Services uses text-based XML, which in turn leads to several advantages. First of all, it is much easier to debug by reading XML than it is to parse through chunks of binary data. Secondly, by using standard technologies, namely HTTP and XML, third-party vendors can easily create their own implementations for different platforms. Web Services also use an infrastructure that provides a discovery mechanism, a service description to describe how the services should be used, and a standard format with which to communicate. Figure 11.1 shows a representation of the Web Service infrastructure.
Creating Web Services
FIGURE 11.1 Web Service infrastructure and communications.
141
Return Service Response (XML)
Request Service
Return Service Description (XML)
Request Service Description
Request Discovery Document (XML)
Return Discovery Document
Link to Discovery Document
Locate Service
Web Service Client
Web Service Directory Service such as UDDI http://www.uddi.org
Web Service
Creating a Simple Web Service Visual Studio .NET provides a template for creating a new Web Service using managed C++ and the .NET Framework. The template provides the basic foundation for writing a Web Service. However, once the base Web Service code is created, the rest of the implementation is up to you.
Creating the New Project You start creating a new Web Service by selecting the New, Project menu item from the main menu and selecting the Managed C++ Web Service option, as shown in Figure 11.2. FIGURE 11.2 The New Project dialog with the Managed C++ Web Service project selected.
11
142
Hour 11
Name the new Web Service WebService and select the OK button to create the Web Service project. Table 11.1 lists the files created and their descriptions. TABLE 11.1
Managed C++ Web Service Files Created by Visual Studio’s Template
Filename
Description
WebService.cpp
The main Web Service source file.
WebService.asmx
The Web Service description file that describes the service. This file will only contain a reference to the Web Service class.
WebService.h
The header file for the Web Service source file. This file includes the class declaration of the Web Service.
WebService.vsdisco
The discovery file for the Web Service.
Web.config
A Web configuration file that controls how the Web Service is treated on the Web server.
Global.asax
A Web Service description file that describes the Global class within the Web server.
Global.asax.h
The Global class declaration and definition. The Global class is derived from HttpApplication and provides entry points for when the application starts and ends, when a session begins and ends, and when a request begins and ends.
AssemblyInfo.cpp
This file contains the custom attributes for the Web Service assembly. Attributes are a new feature within Visual Studio .NET that allow common pieces of code to be substituted with replacement tags. You will learn more about attributes in Hour 16.
Stdafx.cpp
A common C++ file for any global classes. By default, there isn’t anything defined other than the include file stdafx.h.
Stdafx.h
A common include file for the WebService project. Includes the .NET Framework components required for the Web Service.
Compiling and Debugging the Default Web Service Compiling and running the default Web Service builds a “Hello, World!” Web Service. The building of a Web Service differs, however, in the building of a regular application. It also includes the deployment of files onto your local Web server (IIS). After your Web service DLL has been built, a temporary XML document is created listing the files that will be deployed to your IIS Web publishing directory. The files that are published and needed to use the Web service include WebService.asmx, Global.asax, Web.config, and WebService.vdisco. Furthermore, the DLL that contains your actual Web Service is placed in a subdirectory named bin. Once the temporary XML file is built, the build
Creating Web Services
143
process then invokes the Web deployment tool named VCDeploy.exe, which performs the necessary file operations to place your Web Service files onto your local Web server. All this happens by default without you having to make any necessary project setting changes, leaving you to concentrate on your code instead. It is worth looking at to see how Visual Studio deals with debugging the Web Service. Open the WebService.cpp file and place a breakpoint on the return value statement, as shown in Listing 11.1. LISTING 11.1 WebService.cpp—MyCPPWebService HelloWorld() Method Implementation 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21:
#include “stdafx.h” #include “WebService.h” #include “Global.asax.h” namespace WebService { // WEB SERVICE EXAMPLE // The HelloWorld() example service returns the string “Hello, World!”. // To test this web service, ensure that the .asmx file in the deployment //path is set as your Debug HTTP URL, in project properties and press F5. String __gc* MyCPPWebService::HelloWorld() { // TODO: Add the implementation of your Web Service here return S”Hello World!”; } };
After you set the breakpoint, pressing F5 will compile the Web Service and start the debugger. Once the Web Service is started, the page shown in Figure 11.3 is displayed in your Web browser. It shows the Web Service name and the available operations for the Web Service. In this case, only the HelloWorld operation is available, which directly relates to the only method provided in the MyCPPWebService class. Notice the URL in the address bar. It points to the WebService.asmx file, which contains the information for the Web Service. The .asmx file is similar to the .aspx file of an ASP.NET application, except it is named differently to distinguish between Web Services and Web applications.
11
144
Hour 11
FIGURE 11.3 The Web browser interface for testing MyCPPWebService.
Click the HelloWorld hyperlink in the Web browser to show the next page in the Web browser, as shown in Figure 11.4. This Web page shows the actual SOAP that is used to communicate with the Web Service. It shows both the POST and the response when the HelloWorld operation is called. FIGURE 11.4 The Web browser interface for the HelloWorld operation showing SOAP used to execute the Web Service method.
Creating Web Services
145
The Invoke button on the Web page actually executes the HelloWorld operation and triggers the debug breakpoint that you set. Continuing with the Web Service reveals the XML results, as shown in Figure 11.5. Notice that the string returned is the same string returned from the C++ code in Listing 11.1. The .NET Framework takes care of all the mess dealing with XML to format the request from the client and the response from the Web Service. FIGURE 11.5 The Web browser showing XML output from invoking the HelloWorld method of the Web Service.
Changing the Web Service Now that you’ve seen how a Web Service works, it is time to change it into a Web Service with a little more substance. Instead of a simple HelloWorld() method, the Web Service will have a new Web Service method to return the current system date and time in a formatted string. Additionally, another Web Service method will calculate the area of a rectangle and return the results. Change the MyCPPWebService class definition to add the CurrentDateTime() and RectangleArea() methods, as shown in Listing 11.2. LISTING 11.2 WebService.h—MyCPPWebService Declaration with CurrentDateTime() and RectangleArea() Methods 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15:
#using using namespace System; using namespace System::Web; using namespace System::Web::Services; namespace WebService { public __gc class MyCPPWebService : public WebService { public: [System::Web::Services::WebMethod] String __gc* HelloWorld();
11
146
Hour 11
LISTING 11.2 16: 17: 18: 19: 20: 21: 22: }
continued [System::Web::Services::WebMethod] String __gc* CurrentDateTime(); [System::Web::Services::WebMethod] double RectangleArea( double dWidth, double dHeight ); };
The CurrentDateTime() declaration is similar to the HelloWorld() declaration in that it returns a String and doesn’t take parameters. However, the RectangleArea() method is different in that it returns a Double and takes two Double parameter values. Each declaration has the System::Web::Services::WebMethod attribute in front of it to signify that the method is accessible via the Web Service. Any methods that do not have this attribute are not accessible. Of course, these functions currently don’t do anything because they haven’t been defined. Open the WebService.cpp file. Add the two functions immediately following the HelloWorld function. To get the current date and time, you can use the .NET Framework structure System::DateTime. This structure defines a property named Now that returns the current date and time object whose member variables correspond to the exact time that function was called (that is, the current date and time at that moment). After that is done, you can simply convert it to a String object and return it. The Rectangle method is a little simpler—simply return the result of multiplying the two parameters. Your code should look similar to Listing 11.3. LISTING 11.3 Methods 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15:
Defining the Rectangle and CurrentDateTime Web Service
#include “stdafx.h” #include “WebService.h” #include “Global.asax.h” namespace WebService { String __gc* MyCPPWebService::HelloWorld() { return S”Hello World!”; } String __gc* MyCPPWebService::CurrentDateTime(void) { return System::DateTime::get_Now().ToString(); }
Creating Web Services
LISTING 11.3
147
continued
16: 17: double MyCPPWebService::RectangleArea( double dWidth, double dHeight ) 18: { 19: return dWidth*dHeight; 20: } 21: };
Looking at the SOAP generated to handle a Web Service method such as RectangleArea(), you would see the following: POST /WebService/WebService.asmx HTTP/1.1 Host: localhost Content-Type: text/xml; charset=utf-8 Content-Length: length SOAPAction: “http://tempuri.org/RectangleArea” double double HTTP/1.1 200 OK Content-Type: text/xml; charset=utf-8 Content-Length: length double
As you can see, the block shown for the POST shows the two parameters, dWidth and dHeight, as Double. The result is shown in the HTTP block as RectangleAreaResult and is also Double.
11
148
Hour 11
When you run the Web service this time, you will see three methods you can invoke: the original HelloWorld() method and the CurrentDateTime() and RectangleArea() methods. Selecting the CurrentDateTime hyperlink shows the XML results, as displayed in Figure 11.6. FIGURE 11.6 The Web browser showing XML output from the CurrentDateTime()
Web Service method.
When you select the RectangleArea hyperlink, the Web browser opens a form, shown in Figure 11.7, for you to enter values for the parameters required by the RectangleArea() method. When you click the Invoke button, the RectangleArea() method is called on the Web Service and the values are passed in as parameters. The result is calculated and returned in the same way as the other methods. FIGURE 11.7 The RectangleArea() parameter input form for testing.
Creating Web Services
149
Using a Web Service Within an Application One of the advantages of Web Services is that they can provide functionality that other applications and even other Web Services can use to build new solutions. You can use Web Services published by other organizations, or you can use your own organization’s Web Services. Either way, your use of the Web Service is the same. In order to demonstrate using a Web Service within an application, create a new managed C++ application named WebServiceApp. The Visual Studio .NET template creates a console application that will work for this test application. You could just as easily use the Web Service from a Windows Form–based application in the same manner.
Adding Web References Adding a Web reference allows an application to use a Web Service. Adding a Web reference adds proxy classes for the Web Service classes that the application accesses. For example, the Web Service you just created has the MyCPPWebService class. The proxy class has the same name and has stubs in place of the methods. When you use the proxy class, it formats the appropriate request, sends it to the Web Service, and then waits for a response. When the response is received, the proxy class interprets the response and returns it as the return value of the corresponding proxy class method. Use the Solution Explorer or the Project, Add Web Reference menu item to display the dialog shown in Figure 11.8. This dialog allows you to select a Web service from a UDDI provider or from your local Web server or another Web server, for which you must provide the URL in the address bar. Because the Web Service you want is on your local Web server, select the hyperlink that says “Web References on Local Web Server”. Once you select the hyperlink, the URL http://localhost/default.vsdisco file is displayed. You will see a list of linked reference groups on the right side. One of these links should be http://localhost/WebService/WebService.vsdisco. Select this hyperlink to display the Web Service you created earlier. Figure 11.9 shows the Web Service and how it is displayed. Selecting the Add Reference button adds this Web Service to your new application. When the Web Service is added, Visual Studio builds a proxy class in C# and compiles it automatically. It then also adds the appropriate header file, WebService.h, which loads the WebService.dll component file just compiled. Finally, an XML file named WebService.wsdl is added to the project. It is used to regenerate the C# source file that creates the WebService.dll file.
11
150
Hour 11
FIGURE 11.8 The Add Web Reference dialog in Visual Studio .NET.
FIGURE 11.9 WebService on localhost shown in the Add Web Reference dialog.
Using a Web Reference After adding the Web reference to the application, you only need to include the WebService.h file to make the MyCPPWebService proxy class available for use. The WebService.h file contains the necessary code for importing the metadata of your Web Service by using the #using keyword on the proxy DLL that was built when you added the Web reference.
Creating Web Services
151
Open the WebServiceApp.cpp file and add the statements shown in Listing 11.4 to the main() function. This creates an instance of the MyCPPWebService proxy class. You use it to communicate with the Web Service. LISTING 11.4 WebServiceApp.cpp—Additions to the main() Function to Use the MyCPPWebService Web Service Proxy Class 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18:
#include “stdafx.h” #include “WebService.h” #using #include using namespace System; // This is the entry point for this application int _tmain(void) { MyCPPWebService* pWebService = new MyCPPWebService; Console::WriteLine(pWebService->CurrentDateTime()); Console::WriteLine(pWebService->RectangleArea( 10.5, 20.25 )); return 0; }
Compiling and executing the application results in a console window that displays the current date and time and the area calculation. When you instantiate the MyCPPWebService proxy class, a connection to the referenced Web Service is verified automatically.
Using Web Service Directories Web Service directories provide a central location to register Web Services so that clients of other organizations can discover and use remote Web Services. Web Service directories, such as UDDI (Universal Description, Discovery, and Integration), provide this service. Think of a Web Service directory as a search engine for Web Services available for public use. Without the Web Services directories, it would be impossible to find a Web Service on the Internet, just as it would be impossible to find a Web site without a search engine. UDDI specifications define a standard way to publish and discover information about Web Services. Businesses and developers can use the UDDI registry to programmatically locate information about Web Services exposed by other organizations.
11
152
Hour 11
It is not a requirement that any exposed Web Service be registered with a directory. However, as with a Web site, where you have to know the URL, any user of the Web Service would have to know the URL to find the discovery file for the Web Service. For more information on UDDI, you can go to the Web site www.uddi.org.
Discovering Web Services After the client knows where the discovery file is for a Web Service (either by using a Web Service directory or by some other means), the client can ask for the discovery document or documents. A discovery document describes the Web service using XML and the Web Services Description Language (WSDL) standard. The file generally has a .disco or .vsdisco extension and is not necessary if the client already knows the location of the service description file. The Web Service discovery file contains URLs to the Web Service description documents that you wish to expose for usage. The following is an example of a discovery file for a Web Service:
In the preceding Web Service discovery file, a client is given a URL of http:// MyWebServer/UserName.asmx?WSDL when it connects to discover the Web Services. The tag determines which Web Services are exposed in the discovery file. When you’re creating a new Web Service with the Visual C++ .NET Web Service Wizard, a discovery file is created. This discovery file turns on the automatic discovery of Web Services and therefore does not need to explicitly name the Web Services. The only issue with this method is that all Web Services are discovered and exposed. Here is an example of such a discovery file:
The tag excludes those directories from the discovery process. Therefore, you can overcome the issue of dynamic discovery by adding the directory of the Web Service that you do not want discovered and exposed.
Creating Web Services
153
Summary In this hour, you learned how to create and use Web Services with Visual C++ .NET. Creating a Web Service is very similar to creating a normal class, except it is derived from System::Web::Services::WebService and the exposed methods have the appropriate attributes described in this hour’s lesson. Once you deployed your Web service, you were able to test it through the Web browser. You were also able to use it in the WebServiceApp application through a proxy class that was built when you added a Web reference. Web Services supply a powerful feature for providing remote functionality to other users with an open interface that is not platform dependent. The .NET Framework is designed to make creating and using Web Services as easy as possible, as this hour’s lesson demonstrates.
Q&A Q How do I make my Web Service available to the public through the UDDI registry? A The information and process is available on the UDDI Web site (www.uddi.org). Briefly, you need to register your business with a UDDI registry. This lets you publish Web Services to that UDDI registry under your business name. Q Can I use Web Services developed in other .NET languages and can those languages use the .NET services I create? A Yes. In fact, anyone can use the Web Services you create, and they don’t even have to use .NET to access them. Any programming environment that can communicate with industry standard XML and SOAP over HTTP can access and use your Web Service.
Workshop The Workshop provides quiz questions to help solidify your understanding of what was covered in this hour. Answers are provided in Appendix A, “Quiz Answers.”
Quiz 1. What does UDDI stand for? 2. What is the base class for a Web Service? 3. What class type does a .NET application use to access a Web Service? 4. How is a Web Service added to an application?
11
HOUR
12
Creating Web Services with ATL Although creating Web Services with the .NET Framework is easy and the resulting Web Service is robust, this is not the only way to create a Web Service with Visual C++ .NET. With the new features in the Active Template Library (ATL), you can create a Web Service that can easily take advantage of unmanaged code without the need to bridge between managed and unmanaged code. In fact, after you get past the extra work required to code the interface of an ATL Web Service, the implementation of the Web Service functionality is easier if you are trying to use legacy unmanaged code and third-party unmanaged libraries. The basic principle of how the Web Service works is the same; however, there are some differences that make using the Web Service a little different from a Web Service written with the .NET Framework. In this hour you will learn: • How to create a simple Web Service with ATL • How to review the implementation of a Web Service in ATL
156
Hour 12
• How to add a Web reference to an ATL Web Service • How to use the ATL Web Service in a .NET application
Creating an ATL Web Service Project Creating an ATL Server Web Service is a simple process using the wizard provided with Visual Studio .NET. Selecting the New, Project menu item displays the New Project dialog. Select the ATL Server Web Service option, as shown in Figure 12.1, and name the project ATLWebService. FIGURE 12.1 The New Project dialog with the ATL Server Web Service project selected.
The ATL Server Project Wizard is displayed when you select to create the project. This wizard lets to set the options for the project before the source code is generated. Figure 12.2 shows the wizard’s overview page. From this page, you can select the other wizard pages from the titles on the left. FIGURE 12.2 The ATL Server Project Wizard’s overview page.
Creating Web Services with ATL
157
Selecting the Project Settings page displays the wizard page shown in Figure 12.3. For this project, select the Generate Combined DLL option, as shown in Figure 12.3. This keeps the Web Service DLL and the ISAPI extension DLL the same. FIGURE 12.3 The Project Settings page of the ATL Server Project Wizard.
The other pages in the wizard provide further configuration options that are not necessary for this project. Therefore, click the Finish button, and the project is created.
Uncovering the ATL Web Service Implementation At this point in the project, the wizard has generated a functional ATL Web Service. The Web Service is similar to the one created in the last hour’s lesson in that it simply exposes a HelloWorld() interface. However, the implementation is totally different from the Web Service created with the .NET Framework. As with the .NET implementation, the result from building the project is a DLL file and a discovery file (.disco). The project also places a basic HTML file on the Web server for testing the Web Service.
Defining an ATL Web Service Interface Instead of being class based, where all you have to do is mark a method as a Webmethod, the ATL implementation of a Web Service works by declaring interfaces for which you then create a class that implements them. These interfaces are the exposed Web Service interfaces a user of the Web Service is able to work with. For example, look at the default project file, ATLWebService.h, the wizard built and you will see an interface, as shown in Listing 12.1, for HelloWorld.
12
158
Hour 12
LISTING 12.1
ATLWebService.h—Interface Declaration in ATL Web Service
1: namespace ATLWebServiceService 2: { 3: // all struct, enum, and typedefs for your webservice should 4: // go inside the namespace 5: // IATLWebServiceService - web service interface declaration 6: // 7: [ 8: uuid(“B81B30EC-7C78-48A2-856E-7C4A756611CE”), 9: object 10: ] 11: __interface IATLWebServiceService 12: { 13: // HelloWorld is a sample ATL Server web service method. 14: // It shows how to declare a web service method and its 15: // in-parameters and out-parameters 16: [id(1)] HRESULT HelloWorld([in] BSTR bstrInput, [out, retval] BSTR *bstrOutput); 17: // TODO: Add additional web service methods here 18: };
Looking at the HelloWorld interface declaration, you can see the parameters for the interface. The first parameter is for input and is marked [in] to designate it as input only. The second parameter is actually not a parameter that you need to pass to the interface; it is the return value. Because all interfaces return an HRESULT, the return value for the interface is declared as a parameter with [out, retval]. An interface declaration can only have one parameter declared this way, and it is the last parameter defined. Declaring new interfaces is done by simply adding additional statements to the __interface section. For example, to add the two interfaces that were implemented in the last hour’s lesson, you would add the following statements after the HelloWorld interface declaration: [id(2)] HRESULT CurrentDateTime([out, retval] BSTR* bstrOutput); [id(3)] HRESULT RectangleArea([in] double dWidth, [in] double dHeight, [out, retval] double* dResult);
As you declare more interfaces to the Web Service, you simply give them new IDs and declare them using the same type of syntax. One issue you should be aware of when declaring your interfaces is this: All return value parameters must be declared as pointers.
Implementing the Interfaces You implement the interfaces with the CATLWebServiceService class the wizard generated for you when it created the project. The implementation of the HelloWorld interface is shown in Listing 12.2.
Creating Web Services with ATL
159
LISTING 12.2 ATLWebService.h—A CATLWebServiceService Class Declaration That Implements the IATLWebServiceService Interface 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17:
class CATLWebServiceService : public IATLWebServiceService { public: // This is a sample web service method that shows how to use the // soap_method attribute to expose a method as a web method [ soap_method ] HRESULT HelloWorld(/*[in]*/ BSTR bstrInput, /*[out, retval]*/ BSTR *bstrOutput) { CComBSTR bstrOut(L”Hello “); bstrOut += bstrInput; bstrOut += L”!”; *bstrOutput = bstrOut.Detach(); return S_OK; } }; // class CATLWebServiceService
As you can see, the implementation is somewhat more complex than the implementation in the .NET example. However, this HelloWorld implementation actually builds a message based on a value passed as a parameter. If the implementation is successful, the method should return S_OK; otherwise, an error should be returned. Another aspect of the implementation to notice is the [soap_method] attribute. This indicates that the method is called using the Simple Object Access Protocol (SOAP), which is required for a Web Service. Implementing the other two interfaces, CurrentDateTime and RectangleArea, is done in a similar manner as the HelloWorld implementation, as shown in Listing 12.3. LISTING 12.3 ATLWebService.h—The CurrentDateTime and RectangleArea Implementations 1: class CATLWebServiceService : public IATLWebServiceService 2: { 3: public: 4: ... 5: [ soap_method ] 6: HRESULT CurrentDateTime(/*[out, retval]*/ BSTR *bstrOutput) 7: { 8: CComBSTR bstrOut( L”Today”); 9: 10: CTime time = CTime::GetCurrentTime(); 11:
12
160
Hour 12
LISTING 12.3 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: };
continued bstrOut = time.Format(“%A %B %d, %Y
%I:%M:%S %p” );
*bstrOutput = bstrOut.Detach(); return S_OK; } [ soap_method ] HRESULT RectangleArea(/*[in]*/ double dWidth, /*[in]*/ double dHeight, /*[out, retval]*/ double* dResult) { *dResult = dWidth*dHeight; return S_OK; } // class CATLWebServiceService
This is the final step necessary before the ATL Web Service can be compiled, which will deploy the Web Service to your local Web Service. When it is deployed, the DLL, the discovery file (.disco), and an HTML file that serves as the description of the Web Service are copied to your Web server. Unlike the .NET Web Service, which has an ASP.NET test wrapper generated to test the Web Service, the ATL Web Service doesn’t have a built-in way to test itself. Therefore, you have to build a test application that uses the Web Service.
Building a Test Application A test application for an ATL Web Service doesn’t have to be anything complex or even have a user interface. All you are interested in is testing the Web Service interface to make sure you get the results you are expecting. A simple managed C++ .NET console application works for testing and is very easy to build. First, use the New, Project menu item to create a new managed C++ application named ATLWebServiceApp. This will build a simple console .NET application. Adding a Web reference to the new application is the same as it was in the last hour’s lesson, except this time the discovery file is a .disco file instead of a .vsdisco file. Select Project, Add Web Reference from the menu bar to display the Add Web Reference dialog. Click the hyperlink to display Web Services that are on your local machine and then select the ATLWebService you just created, as shown in Figure 12.4, and click the Add Reference button to build the proxy class to use in your test application.
Creating Web Services with ATL
161
FIGURE 12.4 Adding an ATLWebService
Web reference.
Open the ATLWebServiceApp.cpp file, modify the _tmain() function, and then include the WebService.h file, as shown in Listing 12.4. LISTING 12.4 ATLWebServiceApp.cpp—The main() Function Modifications to Use the ATLWebServiceService Proxy Object 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19:
#include “stdafx.h” #include “WebService.h” #using #include using namespace System; // This is the entry point for this application int _tmain(void) { ATLWebServiceService* pWebService = new ATLWebServiceService; Console::WriteLine( pWebService->HelloWorld( “You” ) ); Console::WriteLine( pWebService->CurrentDateTime() ); Console::WriteLine( pWebService->RectangleArea( 12.5, 10.25 ) ); return 0; }
12
162
Hour 12
This creates a new instance of the ATLWebServiceService proxy class that points to the ATL Web Service you created. Compiling the application and running it displays the console window shown in Figure 12.5. As you can see, each of the ATL Web Service methods worked and returned the correct results. FIGURE 12.5 Console window showing the results of using the ATL Web Service.
As you add additional interfaces to the Web Service, you will have to delete the Web reference and the files it created and then add the Web reference again in order for it to generate the proxy class correctly.
Summary In this hour, you learned how to create and use an ATL Web Service. Although the process is somewhat different in ATL, the resulting Web Service is equally flexible for use as demonstrated with the test application you created. ATL Web Services have the advantage of being able to use unmanaged code efficiently and can even mix MFC code if needed. Third-party libraries don’t have to be wrapped by managed class wrappers as they do in a .NET Web Service.
Q&A Q Is there a performance advantage using ATL for a Web Service over .NET? A If no issues exist with using unmanaged code and the .NET Web Service is all managed, there are minimal performance differences. However, the first time a .NET Web Service is used, loading the .NET Framework does take longer for the initial response from the Web Service. Using ATL, this is not an issue, and the first use is very responsive. ATL will have some performance advantages over the .NET Web Service. The bottom line is this: Performance shouldn’t be the deciding factor in determining which way to write a Web Service.
Creating Web Services with ATL
163
Q Why doesn’t my ATL Web Service show up in the Add Web Reference dialog when I select the hyperlink to see my local Web Services? A Because the default .vsdisco file that Visual Studio .NET creates in your Web root does an auto-discovery for Web Services, and it only looks for other .vsdisco files. It doesn’t look for .disco files (which is the file extension that an ATL Web Service has). You can manually change the .vsdisco file to explicitly reference your ATL Web Service if necessary, or you can directly reference it as you did in today’s lesson.
Workshop The Workshop provides quiz questions to help solidify your understanding of what was covered in this hour. Answers are provided in Appendix A, “Quiz Answers.”
Quiz 1. How does an ATL Web Service expose its methods? 2. What type of method do you create in the ATL class to support the Web Service calls? 3. What is the return type for all exposed ATL Web Service methods? 4. How do you specify the return value that is returned to the Web Service user? 5. What limitation does the return value type have for an ATL Web Service method?
12
HOUR
13
Working with .NET Error Handling and Diagnostics When you write managed C++ applications with the .NET Framework, error handling is managed by a set of .NET classes. The syntax for exception handling is similar to unmanaged C++. However, the .NET Framework provides a rich set of classes to provide strong exception handling and diagnostic capabilities. In this hour’s lesson, you will learn how to use the .NET Framework’s error handling and diagnostics to build applications that handle errors gracefully. Also, for when an unexpected error does occur, you will learn how to display debugging information that will help track down the error. In this hour you will: • Add structured exception handling to an application • Add trace statements to an application
166
Hour 13
• Use assertion code to test for valid values • Display a stack dump when an error occurs • Create a custom exception error class
Understanding the .NET Error Handling Classes An exception occurs when some major error happens within your application. You may have heard the term General Protection Fault (GPF). This is a result of not handling an exception, resulting a fatal crash of your application. However, don’t jump to the conclusion that you should start putting exception-handling code everywhere. An exception should be the last step you perform when all else fails. Your error-handling code should be designed well enough so as to avoid an exception altogether. For instance, if you allocate an area of memory to be accessed through a pointer, you should first check to see whether the memory allocation succeeded rather than trying to dereference the pointer and catching the exception when it occurs. In other words, you should catch errors within your application so that an exception never occurs. Exception handling and debugging with the .NET Framework is very similar to using the exception classes provided in the MFC library. However, the exception handling in the .NET Framework is more robust and provides even a higher level of detail than the MFC library provides. The .NET Framework is highly protected, using exception handling to guard against errors. However, that also requires you to correctly handle the exceptions generated by the .NET Framework. If you fail to do so, your application will terminate with an unhandled exception when an exception is generated. Debugging statements in your .NET applications are also different than in MFC. Instead of macros, such as ASSERT, TRACE, and so on, the .NET Framework provides classes that give similar functionality and even expanded capabilities. The .NET Framework also provides a means to turn debugging statements on and off dynamically at runtime. The classes you’ll be primarily interested in include the System::Exception class and its derivatives and the System::Diagnostics::Debug and System::Diagnostics::Trace classes. Additional classes in the System::Diagnostics namespace are also useful and worth looking at. For example, there are classes for performance monitoring, event logs, process management, and so on.
Working with .NET Error Handling and Diagnostics
167
Understanding the Exception Class Exceptions generally occur in .NET applications in response to abnormal conditions or other conditions that are not expected during the execution of the application. The common language runtime (CLR) provides an object-oriented exception model that relies on the Exception class and its derivatives. A .NET application handles exceptions in a very similar manner as MFC applications— by defining a protected block of code with the try keyword followed by one or more catch statements. When an exception condition occurs, the CLR or the application throws an exception, which is represented by an Exception class derivative. The exception is handled by the first catch statement in the exception-handling chain that handles the same exception class. For example, the following code segment shows a try..catch block with multiple catch statements: int Calculate( Size* pSize1, Size* pSize2, int nDivisor ) { try { return( ((pSize1->get_Height() * pSize1->get_Width()) + (pSize2->get_Height() * pSize2->get_Width())) / nDivisor ); } catch ( DivideByZeroException* e ) { ... } catch ( NullReferenceException* e ) { ... } catch ( Exception* e ) { ... } }
Each of the catch statements in the preceding example looks for a different type of exception. The first one is looking for a DivideByZeroException, which occurs when a division operation with a value of zero is performed. The second catch is looking for the NullReferenceException, which occurs when null pointers are accessed. The final catch will catch any other exception, because all exceptions are ultimately derived from the Exception class. The Exception class is the exception base class; however, exceptions are broken into two main categories with their own base classes that are derived from Exception. The first is the SystemException, which is the base class for all predefined CLR exception classes.
13
168
Hour 13
The second is the ApplicationException, which is the base class for user-defined and application exception classes. Where the MFC library uses the CException class as its exception base class and has several derivative exceptions, the .NET Framework has even more predefined exceptions. There are too many predefined exceptions to list in this hour’s lesson; however, you can find information on all the exceptions in the online help provided with Visual Studio .NET. The Exception class provides a number of properties to help identify the code location, the type, the help file, and the reason for the exception. The following properties are available as part of the Exception class: StackTrace, InnerException, Message, HelpLink, HResult, Source, and TargetSite.
Viewing the Stack Trace The Exception class has the StackTrace property, which carries a stack trace that can be used to determine where the error occurs in the code. The StackTrace property is a String that lists all the called methods as well as the line numbers in the source file where the calls are made. Figure 13.1 shows the sample output from the StackTrace property when the code in Listing 13.1 is executed. LISTING 13.1 DivByZeroException.cpp—An Example of Using StackTrace to Show Where an Error Occurred 1. 2. 3. 4. 5. 6: 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21.
// This is the main project file for VC++ application project // generated using an Application Wizard. #include “stdafx.h” #include #using using namespace System; void Calculate( int nDiv ) { try { // Force the DivideByZeroException to be thrown Console::WriteLine(10/nDiv); } catch (DivideByZeroException* e) { Console::WriteLine( “A divide by zero occurred:” ); Console::WriteLine( e->get_StackTrace() );
Working with .NET Error Handling and Diagnostics
LISTING 13.1 22. 23. 24. 25. 26. 27. 28. 29. 30.
169
continued
} } // This is the entry point for this application int _tmain(void) { Calculate( 0 ); return 0; }
FIGURE 13.1 Sample StackTrace output when the divide-by-zero exception occurs.
The StackTrace property provides an exact location of the line of code that caused the exception. This includes the function name, filename, and line number of the statement.
Using the InnerException Property When you’re handling exceptions, it is often useful to capture a series of related exceptions. Once inside an exception handler, it’s possible to throw yet another exception. The result is a series or a chain of exceptions. The InnerException property of the Exception class points to the original or outermost exception in the chain inside the current catch block. For example, if you catch an exception, such as the DivideByZeroException, as shown in Listing 13.1, you can then throw an ArgumentException that has its InnerException property set to the original DivideByZeroException. The use of the InnerException property is shown in Listing 13.2. LISTING 13.2 Property
DivByZeroException.cpp—Demonstrated Use of the InnerException
1: void Calculate( int nDiv ) 2: { 3: try
13
170
Hour 13
LISTING 13.2 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 20: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32:
continued { // Force the DivideByZeroException to be thrown Console::WriteLine(10/nDiv); } catch (DivideByZeroException* e) { Console::WriteLine( e->get_Message() ); throw( new ArgumentException( “Divide by Zero”, e ) ); }
} // This is the entry point for this application int _tmain(void) { try { Calculate( 0 ); } catch( ArgumentException* e ) { Console::WriteLine( e->InnerException->get_StackTrace() ); } return 0; }
Not all predefined exception classes allow you to set the InnerException property. It can only be set on the constructor of the exception; therefore, the exception class you use will determine whether you can use the InnerException property.
Finally Processing When writing try..catch blocks, you’ll sometimes need to have additional code that is executed regardless of an exception occurring or the code executing correctly. This is often the case when there is critical clean-up code that must execute. For instance, if your application opens a file or other type of resource within a function and an exception is thrown, if the file is not subsequently closed within the finally block, it will remain open until the user actually reboots his system. Exception handling in the .NET Framework provides the optional __finally block, which you can define for these situations. Defining the __finally block is done by using the finally keyword at the end of a try..catch block, as shown in the following code segment:
Working with .NET Error Handling and Diagnostics
171
int _tmain(void) { try { Calculate( 0 ); } catch( ArgumentException* e ) { Console::WriteLine( e->InnerException->get_StackTrace() ); } __finally { Console::WriteLine( “Program Ending.” ); } return 0; }
Defining a __finally block forces the statement to be written to the console even if an unhandled exception occurs.
Overview of the Trace and Debug Classes The .NET Framework provides two essentially identical classes—Trace and Debug—for use in debugging applications and providing error checking while developing applications. The only difference between the two classes is that the Debug class code will only run if you build your application in debug mode. Whenever you build a release version of your application, any code that references the Debug class is removed. The Trace class, on the other hand, always places executable code in the application regardless of whether the application is built in debug or release mode. The Trace class is especially useful in debugging release-only issues. To use either the Trace or Debug class, you need to define either the TRACE or DEBUG symbol, create a new Listener object, and add Trace or Debug calls to your code.
Adding Trace Statements Trace statements are useful in determining the flow of execution within an application without stepping through the application with a debugger. This is especially important if you are debugging an application and the timing of the execution is critical. A trace statement produces the equivalent result as MFC’s TRACE macro. You can use either the Trace::WriteLine() or Debug::WriteLine() static method to output a trace statement in your code. One thing to keep in mind is that you need to make the statements as meaningful as possible. If you place several Debug statements in your application, some possibly within
13
172
Hour 13
loop statements, the amount of information you may get could be large. Another possibility is if you have a base class and several derived classes, and the base class issues a Debug statement, you have to use the debugger to find out which derived class has caused the Debug statement to be issued. Within your application, you can use several predefined macros to aid in creating meaningful statements. Some of these macros include the __LINE__ macro, which signifies the current line number in the source file. Likewise, the __FILE__ macro can be used to get the current name of the source file. One of the most useful macros available is the __FUNCTION__ macro, which is used to get the current function name. The example shown in Listing 13.3 uses the Debug class to output trace statements. When the application is compiled for debug mode, the trace statements are displayed; when it is compiled for release mode, the statements are not displayed. The reason, as stated earlier, is because the Debug class doesn’t produce code in release mode. If you change the class to Trace, the statements are produced in both debug and release modes. LISTING 13.3 DivByZeroException.cpp—The Debug Class Used to Display Trace Statements to the Console Window 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27:
#using #using using namespace System; using namespace System::Diagnostics; void Calculate( int nDiv ) { // Indent the trace statement for clarity Debug::Indent(); Debug::WriteLine( “Entering Calculate()\n” ); try { // Force the DivideByZeroException to be thrown Console::WriteLine(10/nDiv); } catch (DivideByZeroException* e) { Console::WriteLine( e->get_Message() ); throw( new ArgumentException( “Divide by Zero”, e ) ); } Debug::WriteLine( “Leaving Calculate()” ); }
Working with .NET Error Handling and Diagnostics
LISTING 13.3
173
continued
28: int _tmain(void) 29: { 30: // Add a listener that sends output to the console window. 31: Debug::Listeners->Add( new TextWriterTraceListener( 32: System::Console::get_Out() ) ); 32: 33: Debug::WriteLine( “Entering Main()\n” ); 34: 35: try 36: { 37: Calculate( 0 ); 38: } 39: catch( ArgumentException* e ) 40: { 41: Console::WriteLine( e->InnerException->get_StackTrace() ); 42: } 43: __finally 44: { 45: Console::WriteLine( “Program Ending.” ); 46: } 47: 48: Debug::WriteLine( “\nLeaving Main()” ); 49: 50: return 0; 51: }
When you execute the application in debug mode, you’ll notice that the trace statements are displayed on the console window, as shown in Figure 13.2. FIGURE 13.2 Trace statements created with the Debug class.
13
Note that the Leaving Calculate() trace statement was never displayed because the ArgumentException was thrown, which caused the function to immediately exit. This is a good example of how trace statements work. You can tell what portion of the source code was never executed based on the trace statements.
174
Hour 13
Asserting on Invalid Values Using the Assert() method of the Debug and Trace classes is essentially the same as the ASSERT macro in MFC. The value within the parameter of the Assert() method is evaluated, and if it is false, the Assert() method displays a message. The best use of the Assert() method is to determine whether a value is what you expect it should be. If it isn’t, the Assert() method’s message alerts you. This is a very effective method of catching hidden problems when you are unit-testing an application. Add an Assert() statement to the Calculate() method shown in Listing 13.3 after the two Debug statements. The Assert() statement checks for an invalid nDiv value with the following statement: Debug::Assert( nDiv != 0, “nDiv must not be equal to 0” );
After the example is compiled and run, a message box is displayed, as shown in Figure 13.3. FIGURE 13.3 Assertion Failed message box with options to abort, retry, or ignore.
When an assertion fails, you have the option to quit the application immediately, enter into the debugger at the location the assertion failed, or ignore the assertion and continue executing.
Creating and Using Custom Exceptions Creating custom exceptions is useful for when you need to attach more information to an exception than what is provided by the predefined exception classes or when there is a situation in your application that is not covered by a predefined exception class. This is often the case when you are building custom control or class libraries and you need to generate exceptions that are unique to your components. Any custom exception class that you build should be derived from the ApplicationException class or at least the Exception class. However, this is not required. In fact, you can throw an exception with any class. This is not
Working with .NET Error Handling and Diagnostics
175
recommended, though, because you do lose some of the basic features that the Exception class provides. When designing a custom exception class, you should keep in mind that the exception information should not be tied to the AppDomain or even the computer. For example, you should not design an exception that is reliant upon a file that resides on a computer. The application that catches the exception may not be running on the same computer and would not have access to the file. All information in the exception class should be able to stand on its own no matter what AppDomain or computer handles the exception.
Creating the MyException Class Creating a simple exception starts with declaring a managed class that derives from the ApplicationException class, as shown in Listing 13.4. LISTING 13.4 DivByZeroException.cpp—Declaration of the MyException Custom Exception Class 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13:
public __gc class MyException : public ApplicationException { private: int m_nValue; public: MyException( int nValue, String* sMsg ) : ApplicationException( sMsg ) { m_nValue = nValue; } __property int get_Value() { return( m_nValue ); } };
Your exception class should provide a constructor or multiple constructors, where all allowed values for the exception are passed as parameters. You should not require the user of the exception class to allocate the class, set properties, and then throw the exception. It is cumbersome to use and not a good practice for exception coding. The MyException class has a constructor that receives nValue and sMsg parameters. The sMsg parameter relates directly to the same parameter in the ApplicationException class and is passed directly into that class. The nValue parameter is part of the custom exception and is saved in a private member. For any member that you add to your exception class, you should provide a property to retrieve the value. In this case, the get_Value() property is added to return the m_nValue member.
13
176
Hour 13
Using Custom Exceptions Using custom exceptions is really no different from using one of the predefined exception classes. You still use the throw keyword to generate the exception, except for custom exceptions you simply allocate your exception class instead of a predefined exception. Listing 13.5 shows a simple function that uses the MyException class by throwing and catching the exception. LISTING 13.5 DivByZeroException.cpp—A Function That Uses the MyException Class by Throwing and Catching the Exception 1: void UseMyException(int nValue) 2: { 3: try 4: { 5: nValue = nValue / 3; 6: 7: if ( nValue < 10 ) 8: throw(new MyException( nValue, “Value less than 10.” ) ); 9: 10: if ( nValue > 20 ) 11: throw(new MyException( nValue, “Value greater than 20.” ) ); 12: 13: Console::Write( “Value is equal to: “ ); 14: Console::WriteLine( nValue ); 15: } 16: catch (MyException* e) 17: { 18: Console::WriteLine( e->get_Message() ); 19: Console::Write( “Value is equal to: “ ); 20: Console::WriteLine( e->get_Value() ); 21: } 22: }
As you can see in the listing, throwing and catching the custom exception is identical to using a predefined exception class. The catch determines that the exception is of the MyException class and handles the exception appropriately. Although this example is simple, you can see how using custom exceptions is easy. You can use the same methods for more complex applications, control libraries, and components, where you need custom exceptions.
Summary In this hour, you learned how to use the .NET Framework’s exception classes for handling errors that occur in the your applications. You also learned how to use the Trace
Working with .NET Error Handling and Diagnostics
177
and Debug classes to provide information while running an application to aid in debugging. Finally, you created a custom exception class and used it within a simple function. The features you learned about in this hour will give you an understanding of how the exception handling is done in managed C++ code and help you to write more robust applications.
Q&A Q If I am using both managed and unmanaged code, can I catch exceptions generated from either managed or unmanaged code? A It doesn’t matter where the exception is thrown, if there is a try..catch block to handle the exception correctly. For example, if unmanaged code is called from managed code that is protected with a try..catch, the managed code will catch any exceptions it is looking for that are thrown by the unmanaged code. Q If I am using the Trace class, how do I turn off trace statements at runtime and turn them back on? A The .NET Framework provides the TraceSwitch class, which you can use not only to turn off trace statements but to turn them on with a filter for what level of trace statements you want to see. This is the elaborate solution; the other method is to simply remove the TraceListener from the Listeners collection.
Workshop The Workshop provides quiz questions to help solidify your understanding of what was covered in this hour. Answers are provided in Appendix A, Quiz Answers.
Quiz 1. What is the base class for all exceptions in the .NET Framework? 2. What is the namespace that provides diagnostic classes such as the Trace and Debug classes? 3. When is the finally code block called? 4. What exception does the InnerException property contain?
13
HOUR
14
ATL Servers The Active Template Library (ATL) Server is a new technology within Visual C++ .NET not present in previous versions of Visual Studio. ATL Server is a server-based technology that runs on the Internet Information Services (IIS) Web server platform. It plays a dual role within the .NET Framework, having the ability to create Web Services, as you saw in Hour 11, “Creating Web Services,” as well as Web applications, the topic of this hour. ATL Server uses a tag-replacement methodology similar to the way ASP.NET functions. In other words, a certain tag (otherwise known as a replacement tag, consisting of a string of characters enclosed within two curly braces on each end) is encountered by the Web server (IIS), and rather than that tag being passed directly to the rendering stream, it is handed off to an extension DLL. This extension DLL then replaces the tag with its own content, which is then passed down to be rendered on the client Web browser.
180
Hour 14
In this hour you will learn: • How to create an ATL Server project and what options are available for your project • What a server response file (SRF) is and the syntax necessary to interact with your extension DLL • How to specify new replacement tags within your extension DLL and how to render to the response stream
Why ATL Server? Before we begin our discussion on the internals of ATL Server and how to use it, you’ll need an understanding of why you would want to use ATL Server in the first place. A common question being asked with this new technology involves another similar technology: ASP.NET. If both technologies involve the whole notion of “tag replacement,” and ASP.NET is widely used and documented, why would someone even want to consider using ATL Server, which is still in its infancy and theoretically more difficult to use? The whole concept of the .NET Framework involves giving you, the developer, the tools necessary to get the job done. There are some aspects of ATL Server that outweigh any advantages you would receive by using ASP.NET. However, there are also many reasons why you should choose ASP.NET over ATL Server. Why use ATL Server? First of all, no other language within the .NET Framework can beat the performance of C++. In some instances, other languages can come close, but if you plan on writing serious CPU-intensive code, for instance, then using ATL Server within the confines of the C++ programming language will give you that extra performance boost you’re looking for. Second, ATL Server provides support for debugging and diagnostics. ATL Server, through an option when you create your project, can automatically integrate with the Performance Monitor on the server. This can give you rich information not seen by a script-driven language such as ASP. Lastly, ATL Server supports various other technologies, such as cookies, form processing, thread management, cryptography, and so on. As you follow along in this hour, keep these issues in mind. By finding the differences and similarities between two languages, you’ll be able to make better decisions down the road when it comes time to design your application.
ATL Servers
181
Creating an ATL Server Project For this hour’s lesson, you’re going to go a little beyond the customary Hello World example. By the end of the lesson, you will have created the famous number-guessing game, where the object is to guess the magic number randomly generated by the server in the smallest amount of guesses possible. To begin with, open the Project Wizard dialog by selecting New, Project from the File menu. Select Visual C++ Projects from the Project Types list and click the ATL Server Project template. Give your project the name NumberGuess and click OK. The first thing you’ll see is the ATL Server Project Wizard dialog, as shown in Figure 14.1. FIGURE 14.1 The ATL Server Project Wizard.
Although you’re going to use all the default settings for this project, it helps to know what the available options are for your ATL Server project. Click the hyperlink titled Project Settings on the left side of the dialog. These options, shown on Figure 14.2, control what binary files are created and how they are deployed. Each ATL server needs at least two components to function correctly. The first is the Web application DLL. This DLL contains the tag-replacement logic and is the main component where you will perform most of your ATL Server actions. The second component is the ISAPI extension DLL. The ISAPI extension DLL is responsible for interacting with the IIS Web server. This DLL handles incoming client requests from IIS and delegates them to the appropriate tag handler within your Web application DLL. These two main components, however, do not need to reside in different DLLs. By checking
14
182
Hour 14
the box labeled Generate Combined DLL, you can place both components within the same DLL. The last option available to you on this page is the Deployment Support option. This option is responsible for placing all of the necessary files for your Web application into an IIS virtual root folder on your local machine. It also registers your extension DLL so that IIS forwards client requests appropriately. FIGURE 14.2 The Project Settings dialog for ATL Server.
Figure 14.3 shows the next page of options: Server Options. The first three options available under the Additional Support Options heading deal with data caching. Data caching provides enhanced performance when dealing with differing types of data. ATL Server can cache the following types of data: • Binary Large Objects (BLOBs), which are large areas of allocated memory used for various reasons, such as for binary data for an image file • Files that are temporary and will be deleted upon release of the file-caching object (IFileCache) • OLE DB data source connections on a per-thread basis The remaining options on this page allow your ATL Server application to create performance counters to be viewed through the system’s Performance Monitor application. With this enabled, you can view real-time statistics of client requests passing to your application from IIS. Checking the Browser Capabilities Support option will allow you to gain information about the client browser you are rendering to, provided that the user has made this information available in the security settings. This will allow you to tailor any HTML and scripting code to make sure your application renders correctly on different browser platforms.
ATL Servers
183
FIGURE 14.3 The server options available in ATL Server.
The third page of available options, Application Options, changes behavior within your application DLL (see Figure 14.4). The first option, Validation Support, inserts a function within your application that verifies the data received as query parameters and form variables. The second option, Stencil Processing Support, is important because it provides the tag-replacement logic necessary for your ATL Server project. If you want your ATL server to be accessible as a Web Service, you can select the third check box, and the necessary code will be inserted to do this. The last two options on this page insert culture information into the server response file (SRF). The SRF will be covered later in this hour. FIGURE 14.4 The application options available in ATL Server.
Finally, the last page of options for your ATL Server project aids you when creating your Web application. You can have the wizard insert comments telling you to fill in the missing pieces in the generated code. The second check box tells the wizard to generate
14
184
Hour 14
attributed code (attributes are covered in the next hour), and the last option inserts debugging support. Figure 14.5 shows the Developer Support Options dialog. FIGURE 14.5 Options for developer support within your ATL Server project.
ATL Server Sequence of Events As mentioned earlier, three main pieces make up any ATL Server project: the Web application DLL, the ISAPI extension DLL, and the server response file. These three pieces can be seen using the Solution Explorer within the IDE, as shown in Figure 14.6. Each of these pieces plays a certain role within the overall architecture of ATL Server. FIGURE 14.6 The Solution Explorer view showing the three main components of the ATL Server project.
The first action to happen in this sequence of events is the client request for an SRF file. Once IIS receives this request, its first job is to load any DLLs that are registered for this file type and are located in the virtual directory specified by the URL passed to it. The ISAPI extension DLL you created will register itself to handle SRFs as long as the
ATL Servers
185
Deployment Support option is checked when you create your project using the ATL Server Project Wizard. At this point, control has passed from IIS to your ISAPI extension DLL. The next step is to check the server response file cache. When an SRF file is loaded for the first time, it is parsed and any information related to that SRF, such as what DLLs are required, is saved in this cache for subsequent performance improvements should that SRF be needed again. Once the SRF has been loaded and its DLL references have been determined, the ISAPI extension DLL loads the main application DLL required for the SRF. After the DLL is loaded, the ISAPI extension asks the Web application DLL for certain configuration parameters by calling the GetFlags function provided by the IRequestHandler interface that your Web application DLL implements. It follows this function call with another implemented function in IRequestHandler, called InitializeHandler, to perform any necessary initialization. It’s important to note that so far in this sequence, none of the code you will write has even been touched yet. In other words, the Project Wizard code that was generated for you handles all these tasks, allowing you to concentrate on the main logic of your application. Finally, the SRF is parsed (if it hasn’t already been parsed and isn’t already in the server response file cache). Static content is sent back to IIS along the HTTP response stream. Dynamic data, which is represented as tags within the SRF, is replaced with whatever content is sent by your Web application DLL tag replacement handler.
The Server Response File You should now be at a point where you understand how all the different pieces of ATL Server work together to render content back to the client, so now its time to get into the code and implement the necessary logic for your number-guessing game. The best place to start is with the server response file (SRF). The SRF is a combination of regular HTML and ATL Server replacement tags. Each tag is contained within beginning and ending double curly braces. Using Solution Explorer, open the NumberGuess.srf file contained within the NumberGuess solution. The Visual Studio .NET IDE is registered to treat server response files as regular HTML files. SRFs, however, are dynamic in nature, so what you see when you are designing the Web page is not what you’ll see when it’s rendered by the client Web browser. At the bottom-left area of the main IDE window, you can see two buttons, labeled Design and HTML, on a toolbar. These buttons control how the file is displayed within the IDE when you are working with it. By default, the SRF opens in Design View and shows
14
186
Hour 14
what the file will look like when rendered. However, as just mentioned, it doesn’t have the ability to render the dynamic content, so you’ll see the ATL Server tags instead of the content, which will be sent down the response stream by your tag-replacement function. Click the HTML button to edit the file in straight text mode. What you’ll see is the mixing of regular HTML tags and the double curly brace ATL Server tags. Your server response file should appear similar to Listing 14.1. LISTING 14.1 1: 2: 3: 4: 5: 6: 7: 8: 9: 10:
ATL Server Project Wizard–Generated Server Response File
{{// use MSDN’s “ATL Server Response File Reference” to learn about SRF files.}} {{handler NumberGuess.dll/Default}} This is a test: {{Hello}}
You can use several different ATL Server tags within the SRF. The full list of available tags is provided in Table 14.1. TABLE 14.1
ATL Server Replacement Tags
Tag
Description
codepage
The codepage tag within an SRF is used when the SRF is parsed. This is required if any characters within the file are not within the normal ANSI character set.
comment
Any data within this tag is a comment and is not rendered on the client Web browser.
handler
The handler tag specifies the name of the Web application DLL and the name of the request handler. There can only be one handler tag per SRF. Any additional handlers must be specified as subhandlers.
include
This tag inserts the rendered content of another SRF at the current position of the SRF that uses this tag.
locale
The locale tag signifies that any content rendered after that point in the file should support the locale that is specified.
replacement
The replacement tag is a customized tag that is replaced with content from the request handler within your Web application DLL. It can contain either a replacement function call or rendering instructions, such as conditional statements.
subhandler
The subhandler tag defines an additional request handler. Unlike the handler tag, there can be multiple subhandler tags within an SRF.
ATL Servers
187
Creating the NumberGuess Server Response File The user interface of your number-guessing game will consist of a simple text entry box, a Submit button, a Restart button, and plain text. The same SRF will be used throughout the user’s session. This means that the same SRF will be used to play the game, display error messages, restart the game, and finally show the congratulations message when the user guesses the number correctly. To begin with, overwrite the current contents of your NumberGuess.srf file with the code from Listing 14.2. LISTING 14.2 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35:
Creating the NumberGuess.srf File
{{ // This file implements the classic guess the number game }}
{{handler NumberGuess.dll/Default}}
{{if UserGuessedCorrectly}} Congratulations you guessed the correct number in {{GetNumGuesses}} steps!
{{else}}
{{GetLastError}}
Enter a number between 1 and 100:
14
188
Hour 14
LISTING 14.2 36: 37: 38: 39: 40: 41: 42: 43: 44: 45:
continued
{{endif}}
By looking at the structure based on the ATL Server tags, you can see the basic flow of the Web application. Starting on Line 18, you see the following conditional: {{ if UserGuessedCorrectly }}
Replacement tags can contain either if or while loop conditional statements such as this one. The function UserGuessedCorrectly, in case you haven’t guessed, is a function you will create within your Web application DLL. It returns either True or False based on the value of the UserGuess form variable on line 32. If the user does guess the correct value, the text congratulating the user is displayed and any rendering instructions after the {{else}} replacement tag is ignored. However, if the user doesn’t guess correctly, the SRF’s contents past the {{else}} tag is rendered. In this case, the SRF tells the Web application to display the last error it encountered if there was one and then display the standard HTML for the input box and Submit and Restart buttons.
Implementing the Replacement Functions The last step is to implement the actual games logic within the main request handler class. One nice feature added to the editing of SRFs is the ability to right-click a replacement tag and have the replacement function automatically added for you within the request handler class. Therefore, right-click the UserGuessedCorrectly tag and select Add Tag Handler from the context menu. Continue this same process and add tag handlers for GetNumGuesses and GetLastError. Before you implement the replacement functions, you’ll need a few member variables to keep track of state within your application. Using the Class Viewer, right-click the
ATL Servers
189
CNumberGuessHandler class within the NumberGuess project and select Add, Add Variable from the context menu. Set the member access level to private, set the variable type to int, and give it the variable name m_iMagicNumber. This is the number the user will try to guess. Repeat the process and add another private integer with the name m_iNumGuesses. Finally, add one more variable with the same access level, set the type to TCHAR?, and give it the name m_szLastError.
The first replacement function you are going to implement is the function responsible for determining whether the user has guessed the number correctly: the UserGuessedCorrectly function. First, you need to get the value the user entered from the input box. This value will be passed in as a form variable. Anytime you need to get details about the request from the client, you can use the member variable m_HttpRequest, which is a CHttpRequest object. This variable is created in a base class from which you are deriving. To get the value of form variables, you simply create a local variable of type CHttpRequestParams and assign it the result of a call to the CHttpRequest member function GetFormVars. This will give you a collection of all the form controls and their associated values. To get at the input box control’s value, you can use the GetField member function provided by your local CHttpRequestParams variable, passing it the name of the form variable you are requesting. Take caution: The function can return a NULL value if the form value doesn’t exist. This happens the first time the page is loaded because the form hasn’t been submitted yet. You can, however, use this to your advantage. You need a good place to generate the random number that the user is trying to guess. The function ValidateAndExchange may be a tempting place to put this code, but the comment that says to add initialization code is a little misleading. The ValidateAndExchange function is called each time your SRF is loaded. This means that it is loaded each time the user clicks the Submit button. If you place your random number–generation code there, a random number is generated each and every time the user tries to guess. In other words, the user’s chances of guessing the number correctly would be virtually zero. Therefore, the UserGuessedCorrectly function should check the return value of the GetField function call. If the value is not NULL, it should check to see whether it is between the required range of 1 to 100 and, if not, set the last error member variable to a proper value and return HTTP_S_FALSE. If the number is within the range, you should check to see whether it is lower, higher, or exactly the same as your randomly generated magic number. If it is the same, HTTP_SUCCESS should be returned so that the conditional in the SRF evaluates to True and the congratulations statement is rendered. Your code should look similar to Listing 14.3.
14
190
Hour 14
LISTING 14.3 Function 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47:
Implementation of UserGuessedCorrectly Tag Replacement
[ tag_name(name = “UserGuessedCorrectly”) ] HTTP_CODE OnUserGuessedCorrectly(void) { // Get form fields collection from the request const CHttpRequestParams& theFields = m_HttpRequest.GetFormVars(); if( theFields.Lookup( “UserGuess”) != NULL ) { int iGuess = _ttoi( theFields.Lookup( “UserGuess” )); if( iGuess < 1 || iGuess > 100 ) { _tcscpy( m_szLastError, “You must enter a number \ between 1 and 100.” ); return HTTP_S_FALSE; } // increment number of steps m_iNumGuesses++; if( iGuess < m_iMagicNumber ) { _tcscpy( m_szLastError, “Your guess is too low.” ); return HTTP_S_FALSE; } if( iGuess > m_iMagicNumber ) { _tcscpy( m_szLastError, “Your guess is too high.” ); return HTTP_S_FALSE; } // user guessed correctly return HTTP_SUCCESS; } else { // user has started a new game srand( (unsigned)time( NULL ) ); m_iMagicNumber = (rand()%100)+1; // reset number of guesses m_iNumGuesses = 0; return HTTP_S_FALSE; } }
ATL Servers
191
Now it’s time to finish implementing the two remaining replacement functions: OnGetLastError and OnGetNumGuesses. OnGetLastError is responsible for rendering the last error message the user has encountered. In order to send content down the response stream to render on the client’s browser, you use the m_HttpResponse member variable, which is an instance of CHttpResponse. Rendering content on the client uses a streambased method of rendering. Therefore, you use the stream operator SetValue( pArray->SetValue( pArray->SetValue( pArray->SetValue( pArray->SetValue(
S”Doug”, 0 ); S”Heather”, 1 ); S”Kevin”, 2 ); S”Julie”, 3 ); S”Daniel”, 4 ); S”Lynne”, 5 );
// print array statistics Console::WriteLine( “\tVariable: pArray” ); Console::WriteLine( “\tType: {0}”, pArray->GetType()->ToString() ); Console::WriteLine( “\tCount: {0}”, pArray->Length.ToString() ); } // This is the entry point for this application int _tmain(void) { Console::WriteLine(); TestArray(); }
The next step is to display some statistics about the collection. This is meant to show how the different classes represent the elements they contain and to allow you to make simple comparisons. Because all the collection classes you are using inherit from the base Object class, you can display the type of the collection by calling the GetType method implemented within the Object class. This can be seen on line 26 of Listing 16.1. To display a count of the number of elements within the array, you can access the Length property implemented by the Array class, as shown on line 27 of Listing 16.1.
Enumerating Collections Before you start working with the other collection types, you’ll need to add a function that enumerates through all of the collection elements and displays their values in the console. As mentioned earlier, most collections implement the IEnumerable interface. This interface only contains one method, which simply returns an instance of a different object. This new object is the real workhorse of collection enumeration. To get the enumerator object, you call the GetEnumerator method within the collection class, which then returns an IEnumerator object.
Collections and Arrays
The IEnumerator object is implemented by the collection class itself because the collection only knows how it is internally organized. For those not familiar with interfaces and COM, this can be somewhat confusing, especially if you’ve done a lot of inheritance programming in the past. Each collection class returns an interface named IEnumerator with a call to GetEnumerator, but the internal implementation of each of these objects is different. In other words, if an object declares that it inherits from an interface, it is up to that object to actually implement the methods that the interface contains. More technically, the interface methods within an interface definition are pure virtual functions that must be overridden if an object is to inherit that interface. The IEnumerator interface contains two methods and one property, as mentioned earlier. However, even though the internal implementation of these functions and this property is different among collection classes, the way a client uses the enumerator stays the same no matter what the collection is. For this hour, you are going to create a function that prints out the values within a collection in the console. Because I mentioned that enumerating over collections is the same, this one function can be used by all the collection classes you will be working with, except for one, which will be explained later. Underneath the TestArray function, create a function named PrintCollection that returns void and takes a single pointer to an IEnumerable object. The code for this function can be seen in Listing 16.2. To enumerate over the collection, you first need to call the GetEnumerator function to obtain a pointer to the IEnumerator object. Once you have this object, you can then enumerate over all of the values within the collection by calling MoveNext on the enumerator object to advance to the next value in the collection. You need to be aware of a couple things when using enumerator objects. First of all, before you can access any of the values within the collection through the enumerator object, you must always call MoveNext first. Before that function is called, the enumerator is in an unknown state and will throw an exception. Also, once the MoveNext function returns a false value, you cannot access the collection values unless you reset the enumerator by calling the Reset function. To print out the variables within the collection, use a while loop that uses the value returned from the MoveNext function as its conditional statement. If the MoveNext function returns true, you can access the element within the collection. This is done by using the Current property implemented by the collection. After you have added the PrintCollection function, call it from within your TestArray function to print the array values using your array variable name as the parameter. The finished code for the TestArray function and the PrintCollection function can be seen in Listing 16.2.
217
16
218
Hour 16
LISTING 16.2 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32:
Enumerating and Outputting the Values of a Collection
void TestArray() { // create Array with 5 elements Array* pArray; pArray = Array::CreateInstance( __typeof(String), 6); // populate array pArray->SetValue( S”Doug”, 0 ); pArray->SetValue( S”Heather”, 1 ); pArray->SetValue( S”Kevin”, 2 ); pArray->SetValue( S”Julie”, 3 ); pArray->SetValue( S”Daniel”, 4 ); pArray->SetValue( S”Lynne”, 5 ); Console::Write( “\tValues: “ ); PrintCollection( pArray ); // print array statistics Console::WriteLine( “\tVariable: pArray” ); Console::WriteLine( “\tType: {0}”, pArray->GetType()->ToString() ); Console::WriteLine( “\tCount: {0}”, pArray->Length.ToString() ); } void PrintCollection( IEnumerable* pIEnum ) { System::Collections::IEnumerator* pEnum = pIEnum->GetEnumerator(); while ( pEnum->MoveNext() ) { Console::Write( “{0} “, pEnum->Current ); } Console::WriteLine(); }
After you build and run your application, your output should look similar to Figure 16.1.
The ArrayList Collection Class The Array object you just learned about is good for a wide variety of applications and is fast when you known which index you need to access. However, the main disadvantage of using an Array object is the fixed size. With an array, once it is created, you cannot increase or add other elements anywhere in the array, nor can you remove any elements. The .NET Framework contains a different type of collection that has the same indexbased lookup performance while still allowing you to add and delete members from the collection.
Collections and Arrays
219
FIGURE 16.1 The results of creating an array and enumerating its values.
16 The ArrayList collection, like the Array class, implements the IList interface. However, the Array class is a fixed size. If you were to call the IList function Add on the Array object, a System.NotSupportedException would be thrown and the exception message would tell you that the collection is a fixed size. However, the ArrayList lets you add and remove elements at will while still maintaining fast lookups using an index value. Of course, with the advantages also come the disadvantages. Although the ability to insert and remove elements is a good advantage, if you perform many of these operations repeatedly and the size of the array fluctuates, the performance factor starts to look a little less attractive. This is due to the way an array list is implemented internally. When you declare an ArrayList object, a predetermined number of memory blocks are set aside to be used when you start adding elements. Once you get to the point where the array is filled, more blocks of memory need to be created to hold additional values. For example, if you create an ArrayList object, 16 elements in the array are actually created but not indexable. Once you add the 17th element to the array list, another 16 blocks is allocated. The other disadvantage, which is somewhat related to the repeated memory allocations, concerns memory storage. Once you add another element, which causes more blocks to be allocated, the array remains at that size regardless of how many elements are later removed. For example, if you added 100 elements, 112 blocks in the array are created. If you then removed 99 of those elements, the array size still remains at 112, even though you may never use these blocks again. Figure 16.2 shows how an ArrayList object is internally organized and how it expands its capacity to allow for more elements. Add another function to CollectionTest.cpp, named TestArrayList(), that accepts zero parameters and returns void. To create an array list, simply declare a pointer variable of type ArrayList and assign to it a new instance of an ArrayList object using the new keyword. To add elements to an array list, call the member function Add with a single parameter that’s the type of object you wish to add. If you need help, Listing 16.3 demonstrates how this is done.
220
Hour 16
FIGURE 16.2 The internal structure of the ArrayList collection.
1
2
3
4
ArrayList with 4 elements and a capacity of 8
1
2
3
4
5
6
7
8
9
Same ArrayList with 9th element added causing memory allocation to increase capacity
LISTING 16.3
Working with the ArrayList Class
1: void TestArrayList() 2: { 3: // create array list 4: ArrayList* pArrayList = new ArrayList(); 5: 6: // populate array list 7: pArrayList->Add(S”Doug”); 8: pArrayList->Add(S”Heather”); 9: pArrayList->Add(S”Kevin”); 10: pArrayList->Add(S”Julie”); 11: pArrayList->Add(S”Daniel”); 12: pArrayList->Add(S”Lynne”); 13: 14: // print arraylist statistics 15: Console::WriteLine( “\tVariable: pArrayList” ); 16: Console::WriteLine( “\tType: {0}”, pArrayList->GetType()->ToString() ); 17: Console::WriteLine( “\tCount: {0}”, pArrayList->Count.ToString() ); 18: Console::WriteLine( “\tCapacity: {0}”, pArrayList->Capacity.ToString() ); 19: Console::Write( “\tValues:” ); 20: PrintCollection( pArrayList ); 21: }
To view some of the statistics of an ArrayList object, you can copy the same code you used for the TestArray function, making sure to change the name of the variable you are using. However, there is one other property that ArrayList has that Array does not: the Capacity property. The capacity of an array list denotes the number of elements the array can hold before it has to allocate more. By default, the capacity is set to 16, but you are free to change that value by calling set_Capacity, which can be useful if you have any idea how large the array list may become. Because the ArrayList class implements the IEnumerable interface, you can use the function you created earlier to enumerate through all the elements in the collection. Also, take note that the array list will only allow you to enumerate
PrintCollection
Collections and Arrays
221
through the defined values. In other words, even thought the capacity of an array list is more than the number of elements you have added, they are not available because they have not been defined.
The Stack and the Queue If you’ve ever taken college-level computer science classes, you probably had to implement either of the two classic data structures: the stack or the queue. The reason that these two collection classes are placed under the same heading is due to their similarity. A stack is implemented like it sounds—as a stack of objects. Take, for instance, a stack of blocks that you have built. You have one block in your hand and place it on the stack of other blocks. Now, what is the first block you have to take off to get to the other blocks? Obviously, it’s the last one you just placed on the stack. This is known as a Last-In FirstOut (LIFO) structure. A queue is just the opposite. To visualize a queue, think of a line of patrons outside of a movie theatre. If you’ve ever had to stand in line, you know that the first person there will be the first person to get served. This is known as a First-In First-Out (FIFO) structure. Both data structures are implemented as a list but do not implement the IList interface. The reason for this is that stacks and queues are not indexable and therefore cannot have random access to all the elements within that collection. Instead, you add and remove elements one by one. Figure 16.3 shows a graphical representation of a stack and a queue. FIGURE 16.3 The stack and the queue.
Item Added
Item
Item Added
Item Removed
Item
Item
Item
Item
Item
Item Item
Item Item Removed
Item
Stack
Queue
When an element is added, it is placed on the top of the stack. When it is removed, it is also removed from the top of the stack
When an element is added, it is placed on the top or back of the queue. When it is removed, it is removed from the top or front of the queue
16
222
Hour 16
The processes of adding and removing items from a stack and queue are similar. The only difference is in the name of the functions. With a stack, you add objects by pushing them with the function Push, and you remove them by calling Pop. However, with a queue, you add items onto the queue by calling Enqueue and remove items with Dequeue. Create two functions, named TestStack and TestQueue, where both functions take zero parameters and do not have a return value. Using the Push function for the stack and the Enqueue function for the queue, add items onto the appropriate structures. As before, also output the type of the collection and the number of items in the collection. You can use Listing 16.4 as a guide. The last thing to do is to call the PrintCollection function for the stack and the queue. Compile and run your application and take note of the way the values in each collection are displayed. If everything has worked correctly, you’ll notice that the stack collection is in reverse order. This is due to the nature of the stack, as described earlier. The last item you added is actually the first element in the list, and it’s the item that will be removed when you call the Pop function. LISTING 16.4
Creating the Stack and the Queue
1: void TestStack() 2: { 3: // create stack 4: Stack* pStack = new Stack(); 5: 6: // populate stack 7: pStack->Push( S”Doug” ); 8: pStack->Push( S”Heather” ); 9: pStack->Push( S”Kevin” ); 10: pStack->Push( S”Julie” ); 11: pStack->Push( S”Daniel” ); 12: pStack->Push( S”Lynne” ); 13: 14: pStack->Pop(); 15: 16: // print stack statistics 17: Console::WriteLine( “\tVariable: pStack” ); 18: Console::WriteLine( “\tType: {0}”, pStack->GetType()->ToString() ); 19: Console::WriteLine( “\tCount: {0}”, pStack->Count.ToString() ); Console::Write( “\tValues:” ); PrintCollection( pStack ); } void TestQueue() { // create queue Queue* pQueue = new Queue();
Collections and Arrays
LISTING 16.4
223
continued
// populate queue pQueue->Enqueue( S”Doug” ); pQueue->Enqueue( S”Heather” ); pQueue->Enqueue( S”Kevin” ); pQueue->Enqueue( S”Julie” ); pQueue->Enqueue( S”Daniel” ); pQueue->Enqueue( S”Lynne” ); // print queue statistics and values Console::WriteLine( “\tVariable: pQueue” ); Console::WriteLine( “\tType: {0}”, pQueue->GetType()->ToString() ); Console::WriteLine( “\tCount: {0}”, pQueue->Count.ToString() ); Console::Write( “\tValues:” ); PrintCollection( pQueue ); }
The Hashtable Collection Class Whereas most collections so far have resembled that of a list, the hash table collection does not. Instead, a hash table is known as a dictionary collection. Dictionaries are ordered as one large collection of key and value pairs. In other words, if you were to look up a word in a real dictionary, you would find that it is associated with a definition. In this example, the key is the word and the value is the definition. The Hashtable class within the .NET Framework allows you to associate any type of managed object with another managed object. If you have ever browsed through the online .NET Framework documentation, you may have noticed that many objects have a function named GetHashCode. This function is designed so that the object can be placed within a dictionary collection, such as a hash table. The GetHashCode function returns a pseudo-unique integer that identifies a key. The integer is considered “pseudo-unique” because implementing a hash function is more of an art than an exact method. In other words, given a large amount of keys, it is possible that at least two of the keys will have the same hash code. This hash code is used as an index into the hash table. The advantages of using a hash code coupled with the hash table is that lookups are much faster than what’s possible by searching value by value in other collections. Create a function within your main file called TestHashtable that also takes zero parameters with no return value (void). To add elements to the Hashtable collection, you call the Add function, giving it an object for the key and an object for the value. For this example, associate each value with an integer key. Listing 16.5 shows how this is done.
16
224
Hour 16
LISTING 16.5
Creating and Adding Items to a Hash Table
1: void TestHashTable() 2: { 3: Hashtable* pHashTable = new Hashtable(); 4: 5: pHashTable->Add( __box(1), S”Doug” ); 6: pHashTable->Add( __box(2), S”Heather” ); 7: pHashTable->Add( __box(3), S”Kevin” ); 8: pHashTable->Add( __box(4), S”Julie” ); 9: pHashTable->Add( __box(5), S”Daniel” ); 10: pHashTable->Add( __box(6), S”Lynne” ); 11: 12: // print Hashtable statistics and values 13: Console::WriteLine( “\tVariable: pHashTable” ); 14: Console::WriteLine( “\tType: {0}”, pHashTable->GetType()->ToString()); 15: Console::WriteLine( “\tCount: {0}”, pHashTable->Count.ToString() ); 16: Console::Write( “\tValues: “ ); 17: PrintDictionaryCollection( pHashTable ); 18: }
You may be wondering why the integers in Listing 16.5 are surrounded by the __box keyword. Because the Hashtable class expects a managed object for the key, you have to convert the integer to a managed type so that it can work within the .NET runtime. The __box keyword copies a value object into a managed object.
If you were to call the PrintCollection function for the Hashtable object you just created, you might notice some peculiarities. Instead of outputting the values you placed within the hash table, the values that get outputted are instead the names of a .NET Framework object type: the System::Collections::DictionaryEntry object. Each element within a hash table first gets placed into a DictionaryEntry object, along with its associated key, before it is placed in the collection. Therefore, when you enumerate the values, you are actually retrieving the DictionaryEntry objects instead of the value objects you placed in the collection. Therefore, create another function named PrintDictionaryCollection with a single IEnumerable* parameter. contains two properties you can access to get the hash table element’s key and value. These properties are Key and Value, respectively. Therefore, rather than outputting the Current property as you did before, output the Key and the Value properties instead. This is shown in Listing 16.6. DictionaryEntry
Collections and Arrays
LISTING 16.6
225
Enumerating and Printing Values Within a Dictionary Collection
1: void PrintDictionaryCollection( IEnumerable* pIEnum ) 2: { 3: IDictionaryEnumerator* pEnum = 4: dynamic_cast(pIEnum->GetEnumerator()); 5: while ( pEnum->MoveNext() ) 6: { 7: Console::Write( “{0}={1} “, pEnum->Key, pEnum->Value ); 8: } 9: }
Summary Collections are a tremendous help when dealing with large amounts of similar data. Without them, you would have to literally create a new variable for each piece of data, which of course would be nearly impossible. The .NET Framework contains many predefined collections you can use. This hour covered the collections used most frequently, but as you dig deeper into the .NET documentation, you will find many more specialized collection classes. Due to the nice design of the .NET Framework collection classes, you’ll find that working with these different collections is quite similar, which frees you from having to learn new methods all the time. The key to selecting a certain collection class is knowing the advantages and disadvantages of each one. Although an array might not be a good choice for one application, it might later prove to be the best collection choice for a different application. In other words, there is no collection class that is better than the other. It all depends on the requirements of the application you are creating.
Q&A Q What if none of the collections does what I need it to do? A You are free to make your own collection class. As mentioned earlier, the collection classes implement certain collection interfaces, such as ICollection, IEnumerable, and IList. Simply create a new class that implements these interfaces. Q Is there a linked list collection class? A Surprisingly, no. The reason is that C++ is the only .NET language that supports the use of pointers—something that linked lists use. Because the .NET Framework works across different languages, there is no way to implement this type of collection.
16
226
Hour 16
Workshop The Workshop provides quiz questions to help solidify your understanding of what was covered in this hour. Answers are provided in Appendix A, “Quiz Answers.”
Quiz 1. Why would you want to choose an Array object over an ArrayList object? Why would you choose an ArrayList object over an Array object? 2. The Array class internally allocates contiguous blocks of memory. Does the stack and queue do the same? Why or why not? 3. What happens when you add a hash table element that has the same key to an element already in the hash table?
HOUR
17
Interfaces When you’ve always done something the same way using the same methods, it can be quite difficult to switch gears and think about the solution in a different way. Unless you are a seasoned COM programmer, interface-based programming can be a radical switch from the way you logically design and organize your code. Interfaces play a crucial role in an object-oriented programming language. So much so that some debates have arisen in which many have argued that if a programming language does not support the use of interfaces, it’s not an object-oriented programming language. In this hour you will learn: • What an interface is and why it’s useful • How to implement some of the defined interfaces within the .NET Framework • How to define and implement your own interfaces • How to implement interface inheritance • How to use interfaces within a client application
228
Hour 17
Interfaces Explained In the last hour, you worked with collections, and the term interface was used often without any deep discussion on what an interface is or does. An interface provides a contract between itself and the object that will implement it. It’s similar to the concept of class declaration and class definition. In other words, if you create a class declaration within a header file but do not implement any of the functions within the source file (.cpp file) and then try to compile, the compiler will inform you that the member function definitions are missing. More specifically, you told the compiler you were going to make a class with certain member functions but then failed to do so. Similarly, if you create a class and say that it implements a certain interface but doesn’t implement any of the interface methods or properties, you’ve broken that contract. If a class implements an interface, it must implement all the methods and properties of that interface. So, what makes an interface different from a regular class declaration? First of all, an interface doesn’t have to be implemented by a class. A structure (struct) can just as well implement an interface. Second, an interface is meant to be used by different programming languages. This is why you see an interface within the .NET Framework documentation that can be used with C#, Visual Basic, or C++. This being the case, an interface must support data types that can be used (or at least mapped by the compiler) with any programming language that supports interfaces. Lastly, a separation exists between the interface and the implementation. For instance, when a class is declared, there is one implementation for it. When an interface is created, there can be different implementations for that interface. You saw this during the last hour. There is a single interface, ICollection, but several implementations, as seen in all the different collection classes that implement that interface.
Implementing .NET Framework Interfaces For this exercise, you’re going to add on to last hour’s project and create your own collection class. In case you don’t have the project anymore, you can create a new project. Click New, Project from the File menu within the IDE. Select Visual C++ Project from the list of project types and select Managed C++ Application from the list of templates. Give your project the name CollectionTest and click OK to create the project. The collection you will create is simply a specialized derivation of the stack collection that only accepts strings rather than any System::Object-based objects. To begin, create a new managed C++ class. The easiest way to do this is to select Project, Add Class from the main menu. Select the Generic C++ Class list item and click Open. Give your class
Interfaces
229
the name StringStack and click Finish to create the class. Because we are working within the .NET Framework, you’ll have to convert the generic class that was created into a managed C++ class. Open the StringStack.h file and import the mscorlib.dll file with the #using statement. Next, declare the namespaces you will be using with the using keyword. The namespaces that this class needs are System and System::Collections. Finally, add the __gc keyword in front of the class keyword within your class. Your code should now appear similar to Listing 17.1. LISTING 17.1 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13:
Creating the StringStack Managed Class
#using #include #include using namespace System; using namespace System::Collections; public __gc class StringStack { public: StringStack(void); ~StringStack(void); };
Implementing an interface is very similar to deriving a class from a base class. In other words, to implement an interface, you use inheritance. In order to create a collection class that supports enumeration, you must implement at least two collection interfaces: ICollection and IEnumerable. This is done by creating a comma-delimited list following the class name declaration, separated by a colon. The following code snippet shows the StringStack class implementing the two collection interfaces: public __gc class StringStack : public ICollection, public IEnumerable
Before you begin the implementation of the StringStack class, create an instance of the object in the CollectionTest.cpp file. The first step is to include the StringStack header file. Next, create a function named TestStringStack above the _tmain function. The TestStringStack should take no parameters and have a return type of void. Within the TestStringStack function, declare a StringStack pointer and assign to it a new instance of a StringStack object. Finally, call the TestStringStack function from the _tmain function. Your CollectionTest.cpp file should appear similar to Listing 17.2.
17
230
Hour 17
LISTING 17.2 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23:
Creating the StringStack Object
#include “stdafx.h” #using #include #include #include “StringStack.h” using namespace System; void TestStringStack() { // create stack StringStack* pStringStack = new StringStack(); } // This is the entry point for this application int _tmain(void) { TestStringStack(); return 0; }
Now would be a good time to compile your application. If you’ve followed the directions so far, any errors you see are supposed to be there. You learned earlier that an interface is like a contract. When a class specifies that it implements an interface but fails to add the methods and properties of that interface, it has broken that contract, causing the compiler to complain. Within the Visual Studio .NET IDE are two ways to view build errors. The first way is through the Task List window. This window, shown in Figure 17.1, displays the official error message and doesn’t contain any extra information the compiler may give you. To view any extra information, open the Output window to view the data the compiler has emitted. If you don’t see either of these two windows, you can click Views, Other Windows and then select either Task List or Output to view the respective window. The error within the Task List window is error C2259, which mentions that you cannot instantiate the StringStack abstract class. Looking at the Output window tells you the reason. After the text of the error, the compiler tells you that several pure virtual functions were not defined. The pure virtual functions are the interface functions you must implement within your class.
Interfaces
231
FIGURE 17.1 The Task List window showing an interface error.
Task List
Implementing the StringStack Collection Because your class implements two separate interfaces, you have to implement the pure virtual functions each interface contains. Of course, you have no idea what they are, which is why you have to refer to documentation. If you look up ICollection and IEnumerable within the .NET Framework documentation, you’ll find the methods and properties you need to implement listed.
Sometimes you might have to use an interface that, for some reason, does not have any documentation, or the documentation isn’t available at the time. As mentioned, the compiler gives you rich information on what pure virtual functions you failed to implement. The compiler even goes as far as to tell you the exact signature of the interfaces. You can use the information from the compiler to ensure that you implement the correct interface methods.
An interface can contain zero or more methods and zero or more properties. A property is similar to a member variable within a class or structure. A client using that interface and a property within that interface can set the property to a certain value and also get the value of that property. In the documentation for the ICollection interface, the
17
232
Hour 17
properties are listed first, followed by the methods. One of the properties listed is the IsSynchronized property. Properties, although similar to member variables, are not set or retrieved like member variables; rather functions are used to do so. Therefore, when you implement a property for an interface within Visual C++ .NET, you need to preface the property with either get_ or set_. The IsSynchronized property, therefore, can translate into two functions within your class: get_IsSynchronized and set_Is Synchronized. However, the documentation states that this is a read-only property, which means there is no set_IsSynchronized function. Using the documentation or the compiler output as a guide, add the necessary function declarations for the ICollection interface and the IEnumerable interface. Your StringStack.h file should appear similar to Listing 17.3. LISTING 17.3 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28:
Declaring the ICollection and IEnumerable Interface Methods
#pragma once #using #include #include using namespace System; using namespace System::Collections; public __gc class StringStack : public ICollection , public IEnumerable { public: StringStack(void); ~StringStack(void); // ICollection properties int get_Count(); Object* get_SyncRoot(); bool get_IsSynchronized(); // ICollection methods void CopyTo( Array* pDest, int iIndex ); // IEnumerable method IEnumerator* GetEnumerator(); };
The final step is to now implement the functions for the StringStack class. Because you are creating a stack class, you will use the standard stack functions Push and Pop.
Interfaces
233
However, the Stack class within the .NET Framework accepts any object derived from System::Object, but your class is specialized and only accepts String objects. Therefore, your Push function takes a single String parameter, whereas your Pop function returns a String object. Also, because you are creating a stack, you can create a private Stack variable and pass most of the function calls to that private variable, thus freeing yourself from having to implement a stack data structure. All you need to pay close attention to is that the necessary casting is performed as you convert from String to Object, and vice versa. Because we want to concentrate on interfaces this hour, we will forgo the discussion on how to implement the functions themselves. The code for the StringStack implementation can be found on the Sams Web site.
Creating Your Own Interface Now that you’ve gotten a good feel for how to implement an interface that has already been defined, you are now going to create your own interface, implement it with a class, and finally use that implementation within a client application. The first step is to create the necessary projects you will need. You will be creating two projects that both reside within the same solution. Click New, Project from the File menu, select Visual C++ Project from the list of project types, and select Managed C++ Class Library. Give your project the name DogLib and click OK to create the project. This project is where you will implement the interface and associated class, which will then be compiled into a .NET assembly DLL. To create the client application that will utilize the library, select New, Project from the File menu. Once again, select Visual C++ Project from the list of project types and select the Managed C++ Application template. Give your project the name DogClient. Before you click OK to create the project, click the radio button Add to Solution, which adds the project to the same solution as the library you just created. The last housekeeping step before you start coding is to change the output directory properties for each of the projects you just created. Using Class View, right-click each project and select Properties. The property page that you need to update is the first page displayed, so change the OutputDirectory property as shown in Figure 17.2, making sure you do this for each project. This will place the final binaries for each project in the same directory.
17
234
Hour 17
FIGURE 17.2 Changing project properties to place binary files in a common directory.
Declaring the Interface The class library you are going to create will implement three interfaces using a single class. These interfaces will consist of an IDog base interface and two interfaces that derive from this base: IPoodle and IBulldog. If you haven’t already guessed it, this is the reason for the strange project names. The first step is to change the class name from the default wizard-generated class name, Class1, to a more appropriate class name, CDog. Open the DogLib.h file and make that change. Interfaces are declared using the new Visual C++ keyword __interface. You’ll notice as you start designing and declaring the methods your interface contains that this is very similar to creating the declaration for a class. The first interface to create is the IDog interface. With Visual C++ 6.0 and ATL, interfaces were declared using an Interface Definition Language (IDL) file. However, with Visual Studio .NET, you can declare an interface in the same file as the implementation class. The IDog interface will contain one method and two properties. However, as mentioned earlier, the properties actually translate into two separate functions each: one to set the property’s value and one to retrieve the value. These are known informally as the getter and the setter. The method that the IDog interface supports returns a String value and has the name Bark. Because managed interfaces can use regular .NET Framework data types, you don’t have to use any of the automation-compatible data types, as are used in Microsoft Interface Definition Language (MIDL) declarations in COM and ATL. The first property the IDog interface contains is the NumberOfFleas property. The data type for this property is the .NET value type Int32, and since it is a read-only property,
Interfaces
235
the client cannot give the dog more fleas. Therefore, this property only contains a getter function. Because properties are actually functions, you must preface the property function with the __property keyword. This does a couple of things for you. The most important is that it creates what is known as a pseudo data member. This pseudo data member allows a client using the property to use it more like a value variable rather than dealing with the getter and setter functions. In other words, you define the interface property using a get or set function, such as get_NumberOfFleas, but a client using the interface can simply refer to it as NumberOfFleas. Based on the context in which the property is used, the proper mapping to the appropriate interface method is performed. The __property keyword is also used by the IDE within the Object Browser window, within the Class View window, and with IntelliSense. In other words, when you type the name of an interface within the IDE and use the member access operator ->, IntelliSense will display the list of member functions, but instead of just showing the getter and setter functions (which it does do), it will also just display the name of the property in the list. Figure 17.3 shows the IntelliSense drop-down list for the IDog interface. FIGURE 17.3 IntelliSense can detect when an interface has a property based on the __property keyword.
We’ve already
talked about the first property, NumberOfFleas. The second property the IDog interface contains is the Name property, and unlike the NumberOfFleas property, it contains a getter and a setter function. The final interface code contained within the DogLib namespace for the IDog interface is shown here:
17
236
Hour 17
public __gc __interface IDog { String* Bark(); property void set_Name( String* sName ); property String* get_Name(); property Int32 get_NumberOfFleas(); };
The next two interfaces are specialized interfaces that inherit from the IDog interface. Interface inheritance is similar in syntax to class inheritance in C++. In other words, following the name of the interface, a comma-delimited base interface list is specified, separated from the derived interface name by a colon. Because a poodle probably has more than the average number of fleas, the IPoodle interface will override the IDog property NumberOfFleas. Furthermore, because a poodle sounds different from the average dog when it barks, it also overrides the Bark method. A poodle, however, can be named anything, just like any other dog. Therefore, the IPoodle interface doesn’t need to override the Name property. The IBulldog interface contains the same method and property as the IPoodle interface. The IPoodle and IBulldog interfaces appear as shown here: public __gc __interface IPoodle : public IDog { String* Bark(); __property Int32 get_NumberOfFleas(); }; public __gc __interface IBulldog : public IDog { String* Bark(); property Int32 get_NumberOfFleas(); }
Implementing the Interface It’s time to implement your interface. This step is quite similar to the interface implementation you did for the StringStack collection. The CDog class will implement all three interfaces. Therefore, create a base interface list from which your class inherits by placing the interfaces within a comma-delimited list, separated from the class name by a colon. As mentioned earlier, if you were to compile the library now, you would receive several errors because none of the interface methods and properties are implemented. When a client creates an instance of the class (rather than obtaining a pointer to a specific interface, which will be discussed later this hour) and calls the member function Bark, which interface does this Bark method belong to? Each of the three interfaces contains a Bark method, and without some way of delineating which Bark method the client needs, an ambiguity exists. This is solved by using fully qualified method names within the implementation. A fully qualified name consists of the interface name and the
Interfaces
237
method name, separated by the C++ scope resolution operator (::). Therefore, for the Bark method and the NumberOfFleas getter function, you will have to create three implementations for each, corresponding to each of the three interfaces. However, because there is only one Name property, you can create just one implementation function that will be used by all three interfaces. Listing 17.4 shows the implementation of the CDog class. LISTING 17.4 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41:
Implementation Class for the Three Interfaces
// DogLib.h #pragma once using namespace System; namespace DogLib { public __gc __interface IDog { String* Bark(); property void set_Name( String* sName ); property String* get_Name(); property Int32 get_NumberOfFleas(); }; public __gc __interface IPoodle : public IDog { String* Bark(); property Int32 get_NumberOfFleas(); }; public __gc __interface IBulldog : public IDog { String* Bark(); property Int32 get_NumberOfFleas(); }; public __gc class CDog : public IDog , public IPoodle , public IBulldog { public: // shared interface members void set_Name( String* sName ) { m_sName = sName; } String* get_Name() { return m_sName; } // IDog interface String* IDog::Bark() { return “Woof Woof!”; } Int32 IDog::get_NumberOfFleas(){ return 20; }
17
238
Hour 17
LISTING 17.4 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: }
continued // IPoodle interface String* IPoodle::Bark(){ return “Yap Yap!”; } Int32 IPoodle::get_NumberOfFleas() { return 100; } // IBulldog interface String* IBulldog::Bark(){ return “Grrr Grrr!”; } Int32 IBulldog::get_NumberOfFleas() { return 5; } private: String* m_sName; };
Even though you are currently unable to test the functionality of your interfaces and implementation, it would be a good time to compile the solution and remove any errors that are within the code.
Creating the Client Application Now that your interfaces and their associated implementation are finished, you can finish the client application. The application is simply going to exercise the interface implementation by creating objects and displaying information on the console. To begin with, you’re going to create two objects and compare them. Therefore, in the DogClient project, open the DogClient.cpp file and create a function named CreateDogs with no parameters and a void return type. Your interface implementation contains three interfaces, which means a client can ask for any of these interfaces when it creates an instance of your class. When you ask for a single interface, you are only allowed to call the interface methods and access the interface properties of that single interface. The process is similar to dynamically creating a class object, but rather than using a pointer to the class, you use a pointer to the interface you wish to use. So, how does the compiler know at runtime which methods you can call and which ones you can’t? Internally, when you create the class object, each interface has what is called a virtual function table (or vtable). This is a table of function pointers within the actual class implementation. Therefore, if you were to obtain an IDog interface, all the interface methods and properties within the IDog vtable would point to the IDog methods in the implementation class.
Interfaces
239
Create two objects and assign them to an IPoodle and IBulldog interface pointer. Next, give each of the objects a name by using the name property. You can use the pseudo data member Name rather than the set_Name interface method. Finally, output the results of each object’s Bark method and the Name and NumberOfFleas properties. The CreateDogs function should appear similar to the following: 1: void CreateDogs() 2: { 3: IPoodle* pPoodle = new CDog; 4: IBulldog* pBulldog = new CDog; 5: 6: pPoodle->Name = “Fifi”; 7: pBulldog->Name = “Butch”; 8: 9: Console::WriteLine( “The poodle named {0} says {1} and has {2} fleas.”, 10: pPoodle->Name, pPoodle->Bark(), __box(pPoodle->NumberOfFleas) ); 11: Console::WriteLine( “The bulldog named {0} says {1} and has {2} fleas.” 12: ,pBulldog->Name, pBulldog->Bark(), __box(pBulldog->NumberOfFleas) ); 13: }
Make sure you call the CreateDogs function from your _tmain function. Compile your application and execute it. If everything has worked correctly, your output should appear similar to Figure 17.4. FIGURE 17.4 Output of the client application using two separate interfaces.
The final exercise for this hour is designed to show some interesting properties of interfaces. Create another function named CreateTransformingDog with no parameters and a void return type. Rather than creating the CDog class and retrieving an interface pointer, create a class pointer to the class instead. Interfaces exhibit an interesting property known as transitivity. The transitivity property says that given three elements, if the first and second elements are related and the second and third elements are related, then the first and third elements also maintain that same
17
240
Hour 17
relationship. An example best explains how this works with interfaces: If you can get to object B from object A, and you also can get to object C from object A, then you should also be able to obtain object C from object B. In the interface implementation you created, if you can get an IPoodle interface pointer from a CDog pointer, and you can get a IBulldog pointer also from a CDog pointer, then you should be able to obtain an IBulldog pointer from an IPoodle pointer because they are both related to a CDog object. Listing 17.5 shows the CreateTransformingDog function with several different ways to obtain interface pointers. LISTING 17.5
Demonstration of the Transitive Properties of Interfaces
1: void CreateTransformingDog() 2: { 3: CDog* pDog = new CDog(); 4: 5: // uncomment to see ambiguous error message 6: //pDog->Name = “Ralph”; 7: 8: // transform dog into a generic dog 9: IDog* pGenDog = pDog; 10: 11: // give it a name 12: pGenDog->Name = “Ralph”; 13: 14: // print out stats 15: Console::WriteLine( “The dog named {0} says {1} and has {2} fleas.”, 16: pGenDog->Name, pGenDog->Bark(), __box(pGenDog->NumberOfFleas) ); 17: 18: // transform dog into a poodle 19: IPoodle* pPoodle = pDog; 20: 21: Console::WriteLine( “The poodle named {0} says {1} and has {2} fleas.”, 22: pPoodle->Name, pPoodle->Bark(), __box(pPoodle->NumberOfFleas) ); 23: 24: // transform poodle into a bulldog 25: IBulldog* pBulldog = dynamic_cast(pPoodle); 26: Console::WriteLine(“The bulldog named {0} says {1} and has {2} fleas.”, 27: pBulldog->Name, pBulldog->Bark(), __box(pBulldog->NumberOfFleas) ); 28: 29: // rename the original dog to see effect on other interface pointers 30: pGenDog->Name = “Bowser”; 31: 32: Console::WriteLine( “The dog named {0} says {1} and has {2} fleas.”, 33: pGenDog->Name, pGenDog->Bark(), __box(pGenDog->NumberOfFleas) ); 34: Console::WriteLine( “The poodle named {0} says {1} and has {2} fleas.”, 35: pPoodle->Name, pPoodle->Bark(), __box(pPoodle->NumberOfFleas) ); 36: Console::WriteLine(“The bulldog named {0} says {1} and has {2} fleas.”, 37: pBulldog->Name, pBulldog->Bark(), __box(pBulldog->NumberOfFleas) ); 38: }
Interfaces
241
You can obtain an interface pointer from the original class object, as shown on line 9. This isn’t any different from what you did earlier. On line 19, you see the process of obtaining an interface pointer from an interface pointer it is derived from. This is just a simple assignment statement. However, when converting between unrelated interface pointers (in this case, the IPoodle and IBulldog interfaces), you must use dynamic casting. This is seen on line 25. Finally, because all you are doing is obtaining different interfaces for the same object, any change that affects a property implemented by a single method for all interfaces can be seen through all interface pointers. This is demonstrated on line 30. Once again, make sure you call the CreateTransformingDog function from _tmain and compile your application. Your output should look similar to Figure 17.5. FIGURE 17.5 Output of the client application demonstrating the transitive property of interfaces.
Summary Interfaces are an important feature in any object-oriented programming language. Using interfaces forces you to concentrate more on design in the early stages of application development. Because a clear distinction exists between the interface and the implementation, the concept of data hiding is much more prevalent than the traditional class-based programming paradigms in the past. The .NET Framework is built using a wide variety of interfaces. By utilizing an interface design, the framework classes can grow without you having to design different ways of performing similar tasks. In other words, new implementations are added while the interfaces remain the same. This hour taught you how to use some of these .NET Framework interfaces. You also learned how to add interfaces to your own application and how a client can use the interfaces of your object. One thing is for certain: Using interfaces is possibly one of the best design decisions you can make when creating a new application.
17
242
Hour 17
Q&A Q What happened to all the interface programming constructs used in the past? Where’s the GUID? Where’s IUnknown? A You can still program using interfaces within ATL the same way as before. This hour concentrated on using managed interfaces that run within the common language runtime of the .NET Framework. The .NET Framework and Visual C++ .NET take care of all the COM details for you, so you can concentrate more on the implementation details. Q Can I implement one of the interfaces created this hour using C# .NET or VB .NET instead? A Yes, you can. Once the class library is compiled, you can use and/or implement those interfaces from any .NET language.
Workshop The Workshop provides quiz questions to help solidify your understanding of what was covered in this hour. Answers are provided in Appendix A, “Quiz Answers.”
Quiz 1. In what cases would you use interfaces as opposed to regular class inheritance? Why would you use class inheritance instead of interfaces? 2. Can you add an implementation for any of the .NET Framework interfaces? 3. Can an interface contain private or protected methods and/or properties? 4. Can an interface contain data members instead of properties? 5. Can you obtain an interface pointer from one object and then cast it to the same interface type but from a different object?
HOUR
18
Events and Delegates Events play a crucial role within the computer industry. Events within a computer can range from the high-level task of clicking a button, all the way to the low-level interrupts thrown by the computer processor. Within the last several years, events within the Windows family of operating systems have consisted largely of window message and the associated message handlers. When ATL finally arrived on the scene, programmers were introduced to connection points between two or more COM objects. Visual C++ .NET continues the logical progression of events to the next level by creating the Unified Event Model, which greatly simplifies the creation and handling of events within COM objects, managed classes, and even native C++. In this hour you will learn: • How the Unified Event Model is architected • How to create and use delegates • How to create events and handle them within managed and native C++
244
Hour 18
The Unified Event Model In the past, there have been different ways to create and handle events. For example, callback functions are used pervasively within the WIN32 API. A lot of functions require a function pointer that is to be used as the callback function. However, this function has to be a static member function within your class, which means you don’t have access to any of the data members of the current class instance. In other words, the this pointer within your static callback function is undefined. There have been a few workarounds for this, such as using global functions or variables, and sometimes, if you’re lucky, the function that requires a callback function pointer might have another parameter to which you can assign the this pointer to be passed into the callback function when it is called. As you can see, such workarounds can contribute to bad programming practices, especially if the scale of your project reaches large proportions. Another way of handling events has been the use of connection points within the Component Object Model (COM) with the help of Active Template Library (ATL) classes. However, the process of creating an event can be complicated, even for a seasoned veteran. Add to this the complexity of ensuring automation compatibility by using only automation-compatible data types, and the process can become a daunting task. The Unified Event Model within Visual C++ .NET was designed and implemented to handle some of these issues. There are still callback functions and connection points, but now you have another option in the form of an easy-to-use and well-designed model for event handling. First, we’ll look at the basic architecture of the Unified Event Model and define some of the new attributes and keywords it contains. This will help you better understand some of the programming lessons later in this hour.
Unified Event Model Architecture The centerpiece of the Unified Event Model is the delegate. Formally, a delegate is a class that wraps and controls access to a method with a specific signature. This method can either be an instance or a static function pointer. This fact alone solves the problem with using callback functions within C++, which require static function pointers, thus denying access to any instance member variables within the class. A delegate is declared with the __delegate keyword, and it generally prefaces a function signature, as the following code shows: 1: __delegate int HelloWorld();
However, even though a delegate shares many similarities with a function pointer, and the keyword is placed within the context of a function, a delegate itself is actually a C++
Events and Delegates
245
class. You’ll learn more about the use of the __delegate keyword and the internal structure of the delegate class later this hour. Events contain two main components: a sender and a receiver. These are also sometimes known as a source and a sink, respectively. The sender object is specified using the event_source attribute. This attribute is applied preceding the class declaration and is also used to specify whether the source object is a COM component, a managed C++ class, or a native C++ class. Each method used as an event is preceded by the __event attribute. An event is simply a method within an event source object that is used to generate or fire events back to any connected clients. The following code shows a native C++ class that acts as an event source and contains a single event: 1: 2: 3: 4: 5: 6: 7: 8:
[event_source(native) class CSource { public: CSource(void); ~CSource(void); __event void MyEvent( int iParam ); }
The connected clients are known as the event receivers. An event receiver, much like the event source object, is prefaced with the event_receiver attribute and also contains a parameter specifying the type of class of the event receiver. Within the event receiver object are specific methods with the same signature as an event source event method, as shown in the following code: 1: 2: 3: 4: 5: 6: 7: 8:
[event_receiver(native)] class CSink { public: CSink(void); ~CSink(void); void MyEventHandler( int iParam ); }
In order to connect and disconnect an event source method with an event receiver event handler, you use the __hook and __unhook methods. Internally, these attributes add the event receiver’s function pointers to a list within the event source object. When the event source object generates an event, it references its internal table of function pointers and synchronously calls each of these event handler methods. Figure 18.1 provides a general overview of how event source and event receiver objects communicate to form the event connections.
18
246
Hour 18
FIGURE 18.1 Event communication between one source object and multiple event receivers.
Event Receiver
Event Handler
Event Source
Event
Event Receiver
Event Handler
Event Function Pointer Event Function Pointer Event Function Pointer
Event Receiver
Event Function Pointer Event Handler
Event Receiver
Event Handler
Working with Delegates Now that you know the basic infrastructure of delegates and how they relate to event handling, you are now going to create an application that uses delegates. This will give you a basic understanding on how the event attributes create the injected code for events using the delegate classes. For this first lesson, you are going to create a Windows Form application that contains four buttons and a label control to view the results of calling delegates. Create a new project by clicking New, Project from the File menu. Select Visual C++ Project from the list of project types and select Managed C++ Application from the list of available templates. Give your project the name DelegateTest and click OK to create the project. Because this is a Windows Form application, you will need to create a Windows Form. If you created the SimpleWindowsForm project from Hour 7, “Working with Windows Forms,” you can use that as a base. Listing 18.1 contains the base Windows Form code that creates a Windows Form with four buttons and one label that you can use to create the user interface. LISTING 18.1
DelegateTest Windows Form Code
1: #include “stdafx.h” 2: 3: #using
Events and Delegates
LISTING 18.1 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51:
247
continued
#include #using #using #using using namespace System; using namespace System::Drawing; using namespace System::Windows::Forms; __gc class SimpleWindowsForm : public Form { protected: void InitForm(); void Invoke_Click(Object* pSender, EventArgs* pEventArgs); void Bind1_Click(Object* pSender, EventArgs* pEventArgs); void Bind2_Click(Object* pSender, EventArgs* pEventArgs); void BindBoth_Click(Object* pSender, EventArgs* pEventArgs); Label* Button* Button* Button* Button*
m_pOutput_L; m_pInvoke_B; m_pBind1_B; m_pBind2_B; m_pBindBoth_B;
public: SimpleWindowsForm(); virtual ~SimpleWindowsForm() {} }; SimpleWindowsForm::SimpleWindowsForm() { InitForm(); } void SimpleWindowsForm::InitForm() { // Allocate controls m_pOutput_L = new Label; m_pInvoke_B = new Button; m_pBind1_B = new Button; m_pBind2_B = new Button; m_pBindBoth_B = new Button; // m_pOutput_L Initialization m_pOutput_L->Location = Point(8, 8); m_pOutput_L->TabIndex = 1; m_pOutput_L->TabStop = false;
18
248
Hour 18
LISTING 18.1 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66: 67: 68: 69: 70: 71: 72: 73: 74: 75: 76: 77: 78: 79: 80: 81: 82: 83: 84: 85: 86: 87: 88: 89: 90: 91: 92: 93: 94: 95: 96: 97: 98: 99: 100:
continued m_pOutput_L->Text = S”Click a bind button”; m_pOutput_L->Size = System::Drawing::Size(336, 23); // m_pInvoke_B Initialization m_pInvoke_B->Location = Point(272, 40); m_pInvoke_B->TabIndex = 2; m_pInvoke_B->TabStop = true; m_pInvoke_B->Text = “Invoke”; m_pInvoke_B->add_Click( new EventHandler( this, Invoke_Click ) ); // add bind button 1 m_pBind1_B->Location = Point(8, 40); m_pBind1_B->TabIndex = 2; m_pBind1_B->TabStop = true; m_pBind1_B->Text = “Bind1”; m_pBind1_B->add_Click( new EventHandler( this, Bind1_Click ) ); // add bind button 2 m_pBind2_B->Location = Point(96, 40); m_pBind2_B->TabIndex = 2; m_pBind2_B->TabStop = true; m_pBind2_B->Text = “Bind2”; m_pBind2_B->add_Click( new EventHandler( this, Bind2_Click ) ); // add both bind button m_pBindBoth_B->Location = Point(184, 40); m_pBindBoth_B->TabIndex = 2; m_pBindBoth_B->TabStop = true; m_pBindBoth_B->Text = “BindBoth”; m_pBindBoth_B->add_Click( new EventHandler( this, BindBoth_Click ) ); // Add controls to the Form Controls->Add( m_pOutput_L ); Controls->Add( m_pInvoke_B ); Controls->Add( m_pBind1_B ); Controls->Add( m_pBind2_B ); Controls->Add( m_pBindBoth_B ); // Initialize Form attributes Size = System::Drawing::Size(365, 105); Text = “Delegate Test”;
} void SimpleWindowsForm::Invoke_Click(Object* pSender, EventArgs* pEventArgs) { } void SimpleWindowsForm::Bind1_Click(Object* pSender,
Events and Delegates
LISTING 18.1 101: 102: 103: 104: 105: 106: 107: 108: 109: 110: 111: 112: 113: 114: 115: 116: 117: 118: 119: 120:
249
continued EventArgs* pEventArgs)
{ } void SimpleWindowsForm::Bind2_Click(Object* pSender, EventArgs* pEventArgs) { } void SimpleWindowsForm::BindBoth_Click(Object* pSender, EventArgs* pEventArgs) { } // This is the entry point for this application int _tmain(void) { Application::Run( new SimpleWindowsForm() ); return 0; }
As mentioned, this Windows Form contains four buttons. Two of the buttons, Bind1 and Bind2, will be used to select which delegate will be called. The third button, BindBoth, is used to support what is known as multicasting, which is the method an event source uses when it needs to notify multiple event receivers of an event. This will be explained a little later in the hour. The fourth button does the actual function call on the delegate. The last control is a label that will help you visualize how the delegates are working. Compile your application to make sure everything is working correctly. You should see a window similar to the one in Figure 18.2 when you run the application. FIGURE 18.2 A Windows Form showing the layout of the buttons and label control for the DelegateTest application.
The general procedure you will implement consists of a single delegate and two functions. When you click one of the Bind buttons, it will bind the delegate to one of those two functions. Once that delegate is bound a function, any time the delegate method is called, it is routed to the bound function.
18
250
Hour 18
In your class declaration, create the declarations for the two functions you will bind. The return type is void, and they don’t accept any parameters. You can call them BoundFunction1 and BoundFunction2. The next step is to create the declaration for the delegate you will use. This is simply a function with the same signature of the methods you created earlier, with the only difference being the __delegate attribute preceding the return type. The last data member you need to add is a pointer to the delegate function you just declared. Listing 18.2 shows the final class declaration. Because the functions and the member variables will only be used within the class and not by any external objects, you can place all the declarations within the private section of your class declaration. LISTING 18.2 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26:
Final Delegate Class Declaration
__gc class SimpleWindowsForm : public Form { protected: void InitForm(); void Invoke_Click(Object* pSender, EventArgs* pEventArgs); void Bind1_Click(Object* pSender, EventArgs* pEventArgs); void Bind2_Click(Object* pSender, EventArgs* pEventArgs); void BindBoth_Click(Object* pSender, EventArgs* pEventArgs); Label* Button* Button* Button* Button*
m_pOutput_L; m_pInvoke_B; m_pBind1_B; m_pBind2_B; m_pBindBoth_B;
public: SimpleWindowsForm(); virtual ~SimpleWindowsForm() {} private: void BoundFunction1(); void BoundFunction2(); delegate void BoundFunction(); BoundFunction* m_pBoundFunction; };
Before you can compile your application to make sure everything is working correctly, you’ll need to add the two function definitions for BoundFunction1 and BoundFunction2. For now, just create an empty body for the functions. Now you’re going to take a look at what happens just by adding that single __delegate attribute. In Class View, right-click your project and select Properties from the context menu. In the list displayed on the left side of the dialog, expand the C/C++ heading and select Output Files. On the right side,
Events and Delegates
251
drop down the combo box labeled Expand Attributed Source and select the Yes option. Figure 18.3 shows the project properties dialog with the correct setting for the Expand Attributes Source property. FIGURE 18.3 Changing project properties to expand the attributed source.
With the Expand Attributed Source property enabled, the compiler will create an external source file with the merged contents of your source and the injected code placed by the compiler. Build your application and then open the file DelegateTest.mrg.cpp. Look for the line in the file that shows the delegate declaration you created earlier. If you look at the code immediately following that, you’ll see some interesting things. As mentioned earlier, even though a delegate behaves like a function pointer, it is actually a class. In this case, the class is named BoundFunction, which is also the name of the delegate function you created. Delegates are derived from the System::MulticastDelegate class within the .NET Framework. This class defines four functions. The first is the overloaded constructor. This constructor accepts a pointer to System::Object and IntPtr. Even though the type IntPtr sounds like a pointer to an integer (which wouldn’t be a bad guess), it is actually a pointer value type whose size is the size of an integer on the machine it is running on. In other words, IntPtr is similar to a typeless pointer such as void* and can point to any memory address limited to the machine size of an integer. For the delegate constructor, IntPtr will be a pointer to the delegate function. The next function within the MulticastDelegate class is the Invoke function. Rather than the delegate being called by name, it is called using this Invoke function. The Invoke method performs a synchronous call on the delegate method. In other words, once you call Invoke, the Invoke function will not return until the delegate method has returned. Following this function are two functions: BeginInvoke and EndInvoke. These two functions are designed to support asynchronous function calls. When BeginInvoke is called,
18
252
Hour 18
the common language runtime (CLR) will queue the function call using a thread from the available threads within the thread pool and will immediately return before the function is called. This can be advantageous if the delegate function performs long calculations, and it frees you from having to worry about any threading issues. For this lesson, however, we are just going to focus on the synchronous Invoke method. The next step is to add the event-handling code for the various “button click” events generated. When you click one of the bind functions, the delegate member variable is created and bound to one of the two bind functions, BoundFunction1 or BoundFunction2. As mentioned, the __delegate attribute creates a delegate class for you. The name of that delegate class is the same as the private delegate member variable you created. Even though that member variable appears like a function pointer, it is in fact a pointer to the class created when the __delegate attribute is expanded. The delegate class constructor accepts two parameters. If you want the delegate to point to a static member function within your class, the first parameter to the class constructor is a null value, whereas the second parameter is a pointer to the static member function. The format to specify a static member function within a class for this parameter is Class::Function, and because it needs to be a pointer, you must precede it with the C++ address-of operator—the ampersand. For this lesson, we will be using an instance member function, which means the constructor parameters are different compared to the previous method. The first parameter is a pointer to the instance class that contains the function you are going to bind. You need to pass it the current instance of the class, which in C++ is always maintained in C++ with the this pointer. The second parameter is a pointer to the function you are going to bind. The following is the code for the event handlers for the first two buttons in your application: 1: 2: 3: 4: 5: 6: 7: 8: 9:
void SimpleWindowsForm::Bind1_Click(Object* pSender, EventArgs* pEventArgs) { m_pBoundFunction = new BoundFunction( this, BoundFunction1 ); } void SimpleWindowsForm::Bind2_Click(Object* pSender, EventArgs* pEventArgs) { m_pBoundFunction = new BoundFunction( this, BoundFunction2 ); }
If you have a background in Visual C++ 6, you’ll notice that the naming scheme for events has changed. In technologies such as Microsoft Foundation Classes (MFC), events were prefaced with the word On followed by the event name,
Events and Delegates
253
such as OnClick. Within the .NET Framework, however, the naming scheme was designed to easily associate an event with the object that fired it. This is accomplished by prefacing the event name with the object that fired that event. For instance, if a button named Button1 contains a Click event, the event handler would be named Button1_Click.
The third button, labeled BindBoth, is going to be used to demonstrate the multicasting capability of delegates. Multicasting is the ability to bind several functions to a single delegate. Every time another function is bound to a delegate, the function pointer is placed into a linked list. Once the delegate is invoked, each function is called in the order in which it was bound to the delegate. For this example, you are going to bind both of the bind functions to the delegate. The first step is to create two delegate pointers and create an instance of the delegate class for each of the bound functions. The Delegate class within the .NET Framework contains a function named Combine that combines a delegate with another. Therefore, call the Combine function using the two delegate variables, as shown in this code snippet: 1: void SimpleWindowsForm::BindBoth_Click(Object* pSender, 2: EventArgs* pEventArgs) 3: { 4: BoundFunction* pBound1 = new BoundFunction( this, BoundFunction1 ); 5: BoundFunction* pBound2 = new BoundFunction( this, BoundFunction2 ); 6: 7: m_pBoundFunction = dynamic_cast 8: (Delegate::Combine(pBound1,pBound2)); 9: }
Now that the delegate has the ability to bind to one or both of the member functions, you must now implement the code to invoke the delegate. In the event handler for the Invoke button, first clear the label control by setting its Text property to an empty string. Finally, call the Invoke function of the delegate variable. Because a user may inadvertently click Invoke before binding any of the functions to the delegate, you should first check to make sure the delegate has been created. If it has been created, then the pointer is valid and not null. The event handler for the Invoke button is shown here: 1: void SimpleWindowsForm::Invoke_Click(Object* pSender, EventArgs* pEventArgs) 2: { 3: // clear label text 4: m_pOutput_L->Text = “”; 5: 6: if( m_pBoundFunction ) 7: m_pBoundFunction->Invoke(); 8: }
18
254
Hour 18
The final step for your application is to implement the functions called by the delegate class. The functions will simply set the label’s Text property, signifying which function is being called. However, in the case of a multicast delegate, if the second function in the list is called and overwrites the label’s text, you have no way of knowing whether the first function was ever called. Fortunately, there is a solution. The MulticastDelegate class, which your delegate class is derived from, contains a member function called GetInvocationList. This function returns an array of delegates for each function bound to the main delegate. Because this is an array, you can query the Count property to get the number of functions the main delegate is bound to. Therefore, if there is more than one function bound to the delegate, you should concatenate the string you wish to output to the current string displayed in the label control. The bound function implementations are shown here: 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17:
void SimpleWindowsForm::BoundFunction1() { if( m_pBoundFunction->GetInvocationList()->Count > 1 ) m_pOutput_L->Text = String::Concat(m_pOutput_L->Text, S” BoundFunction1 Called! “); else m_pOutput_L->Text = “BoundFunction1 Called!”; } void SimpleWindowsForm::BoundFunction2() { if( m_pBoundFunction->GetInvocationList()->Count > 1 ) m_pOutput_L->Text = String::Concat(m_pOutput_L->Text, S” BoundFunction2 Called! “); else m_pOutput_L->Text = “BoundFunction2 Called!”; }
You are now finished with the DelegateTest application. After you compile and run your application, bind the delegate to the member functions by clicking one of the bind buttons. You can then click the Invoke button to call the delegate’s Invoke method. If you click the BindBoth button and then click Invoke, your application should appear as shown in Figure 18.4. FIGURE 18.4 Output from utilizing a multicast delegate.
Events and Delegates
255
Creating and Handling Managed Events Now that you understand the basics behind the use of delegates, you’re going to explore the subject a little deeper to see how delegates can be used for event handling. For this lesson, a simple console application will be fine. Click New, Project from the File menu and create a new managed C++ application and name it ManagedEvents. Then click OK to create the project. For this application, you will create two managed classes: the CSource class, which will be responsible for firing events, and the CSink class, which will catch and handle these events as they are fired. First, you’ll work on the CSource class. A class that is responsible for creating and firing events is known as the event source. To specify that a class is the source for events, you use the event_source attribute. This attribute accepts a single parameter and, as such, must be enclosed with bracket symbols. The parameter tells the compiler what type of class it should expect. Here are the three possible values for this parameter: •
native.
Signifies that the class is a native nonmanaged C++ class
•
COM.
•
managed.
Tells the compiler that the class uses the Component Object Model Used for any C++ class that uses managed extensions
For this lesson, you are obviously creating a managed C++ class. Therefore, preceding the name of the class declaration, you must add the [event_source(managed)] attribute, as shown in Listing 18.3. LISTING 18.3 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17:
Creating the Event Source and the Event Receiver Classes
#include “stdafx.h” #using #include using namespace System; [event_source(managed)] __gc class CSource { public: CSource(void){} ~CSource(void){} }; [event_receiver(managed)] __gc class CSink
18
256
Hour 18
LISTING 18.3 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28:
continued
{ public: CSink(void){} ~CSink(void){} }; // This is the entry point for this application int _tmain(void) { return 0; }
Of course, an event source wouldn’t be any good if there weren’t any objects to handle the events that are fired. The event_receiver attribute is used to specify that a class is going to handle events from an event source. This attribute also accepts a parameter. This parameter is the same as the event_source parameter, which for the application you are creating is the managed parameter. The event_receiver attribute for the CSink class can be seen on line 8 of Listing 18.3. The application you are creating is simply going to fire an event, and the event handler will output a string to the console signifying that the event has been handled. To mark a function within the event source to be designated as an event, you simply preface the function declaration with the __event keyword. That’s all there is to it. Any time you are ready to fire that event, you call the function and preface the function call with the __raise keyword. Create a function called EventTest that accepts a pointer to a character string. Preface that function declaration with the __event keyword. Next, create a function named FireEvent with no parameters and a void return value. For the implementation of the FireEvent function, fire the event, as explained earlier, by using the __raise keyword. Believe it or not, the event source class is finished. If you’ve used connection points in the past with ATL, you’ll really appreciate the use of attributes to create events. Here’s the final class declaration for the CSource class: 1: 2: 3: 4: 5: 6: 7: 8: 9:
[event_source(managed)] __gc class CSource { public: CSource(void){} ~CSource(void){} void FireEvent() {
Events and Delegates
257
10: raise EventTest(“Hello from CSource\n\n”); 11: } 12: 13: event void EventTest( char* lpszOut ); 14: };
The next step is to implement the event handler and the code to associate and disassociate the event source with the event receiver. The CSink class will implement three functions. The first function is the event handler itself. This can be any function name as long as the signature is the same as the event being fired from the event source. Therefore, create a function named EventHandler that accepts a character string pointer and has a void return type. For the implementation, output a meaningful string to the console. This function can be seen in Listing 18.4. The next two functions will be used to associate the event with the event handler as well as to disassociate the event with the event handler. The process of associating the event and its handler is known as hooking. To hook these two objects together, you use the __hook keyword. The __hook keyword takes three parameters. The first parameter is the event and the class where that event is contained. This parameter takes the form &Class::EventName. The second parameter is a pointer to an already created instance of the source class. This parameter will be passed in from the _tmain method after the CSource class has been instantiated. Finally, the third parameter for the __hook attribute is the name of the receiver class and the associated event handler function name in the form &Class::EventHandlerName. As an option, you can use a fourth parameter that is a pointer to an instance of the event receiver class. If this is not specified, the default parameter is the instance of EventHandlerthe current receiver class by using the this pointer. The process of unhooking an event from an event handler is the same as hooking them together. The only difference is the use of the __unhook keyword instead of the __hook keyword. The final class implementation for the CSink class is shown in Listing 18.4. LISTING 18.4 1: 2: 3: 4: 5: 6: 7: 8: 9: 10:
Final Implementation of the Event Receiver Class
[event_receiver(managed)] __gc class CSink { public: CSink(void){} ~CSink(void){} void EventHandler( char* lpszOut ) { Console::WriteLine( “Sink handled event from source: “ );
18
258
Hour 18
LISTING 18.4 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: };
continued Console::WriteLine( lpszOut );
} void EnableEventHandling(CSource* pSource) {__ hook(&CSource::EventTest, pSource, &CSink::EventHandler); } void DisableEventHandling(CSource* pSource) {__ unhook(&CSource::EventTest, pSource, &CSink::EventHandler); }
All that remains is to finish the implementation of the _tmain function. The first step is to create an instance of a CSource object and a CSink object. Next, associate the event source’s event with the event receiver’s event handler by calling the EnableEventHandling member function implemented by the CSink class. After the association has been made, tell the event source to fire the event by calling the FireEvent method you created earlier. The last step is to disassociate the event and the event handler by calling the event receiver’s DisableEventHandling method. Your application is now finished. You may be asking yourself where delegates are involved here. After all, you haven’t used the __delegate keyword anywhere and there is no delegate class involved in any of the code you’ve written. This just goes to show you the power of attribute programming. As you did with the earlier project, open the project properties and enable the Expand Attributed Source property. Compile your application and open the managedevents.mrg.cpp file. Look for the declaration of your event, which is denoted by the use of the __event attribute. Immediately following this, you see the attribute provider–injected code. Here you see the familiar delegate class, which is the name of your event transformed into a class that derives from MulticastDelegate. Within that class you see the four functions mentioned earlier: the class constructor, Invoke, BeginInvoke, and EndInvoke. However, the injected code doesn’t stop there, as it did in the last project. This time a pointer to the delegate class is already created for you as a member variable. There are also functions for adding and removing functions from the delegate using the multicast feature of delegates, which is the same method you used to create them earlier using the Combine function. Finally, the __raise attribute is transformed into an Invoke function call for the delegate class. Within the event receiver, you can see some other familiar constructs. You can see how the event receiver class instantiates a new delegate and then passes it to the event source to add the event handler function to the main delegate, similar to the way you bound the function to the delegate upon a “button click” event earlier.
Events and Delegates
259
Once you compile and run your application, your output should appear similar to Figure 18.5. FIGURE 18.5 Output of a managed application using events.
Summary Visual Studio .NET has really ramped up event handling compared to previous versions of Visual Studio. Through the use of delegates, developers now have an easier way to encapsulate function pointers within class declarations than was possible before. You don’t have to deal with static or global functions to deal with callbacks. You no longer have to deal with the intricacies of connection points in COM. The Unified Event Model allows you to easily create, fire, and handle events, regardless of whether your application uses native C++ code, COM, or managed C++ code. This hour you learned some of the details behind delegates and how they work within a managed environment. By going under the hood to investigate how the attributes expand by looking at the injected code, you saw how the architecture is designed. This, in turn, can help tremendously in your design decisions. You also looked at how an event source is hooked to an event handler within an event receiver object. Little did you realize at the time that the method to perform this action was the same method you had investigated earlier with the delegate project. One thing is for certain: The use of attributes to handle delegates and events is a powerful toolset that you can use to your advantage when creating any future applications.
Q&A Q What’s the difference between events in managed code and events in native C++ code? A The only difference you have to worry about is changing the event_source parameter from managed to native. The rest is handled internally within the injected source code. Q Can I create an event receiver in C# that handles events fired from a C++ event source?
18
260
Hour 18
A As long as the event source is created using managed C++, you can. You should, however, ensure that any event parameters use .NET data types rather than regular C++ data types, as was used this hour.
Workshop The Workshop provides quiz questions to help solidify your understanding of what was covered in this hour. Answers are provided in Appendix A, “Quiz Answers.”
Quiz 1. How is event handling handled internally for a native C++ application? (Hint: Remove the __gc keywords and the namespace declarations from this hour’s lesson.) 2. How is a delegate related to an event? 3. How many function pointers can a delegate contain and how do you find out how many a certain delegate can call? 4. How would you go about implementing your own Invoke function within a delegate class?
PART VI Advanced Programming Hour 19 Threading and Synchronization 20 Database Access 21 COM with .NET 22 Mixing Managed and Unmanaged Code 23 Control Class Libraries 24 Serialization A Quiz Answers B Visual Studio .NET IDE Reference
HOUR
19
Threading and Synchronization You may have run into some applications that occasionally don’t respond to anything you do. In fact, if you even cover the application’s window with another application and then uncover it, you might be left with a white window. After a few seconds have elapsed, the application magically springs back to life. So, what happened? An application that exhibits this kind of behavior could benefit from using the technologies explained this hour: threading and synchronization. Threading means that while your main code is executing, another portion of your program is also running at the same time. In other words, once a thread is created, two places within your program are running in parallel. This could be done using multiple threads, in which case the application developer would create a thread for the user interface so that it remains responsive to the user while creating a background thread, also known as a worker thread, to perform the background tasks.
264
Hour 19
In this hour you will learn: • How to create and control threads using the .NET Framework • How to synchronize multiple threads using a monitor • About communication between threads using events
Threading and Synchronization Explained Some developers don’t realize it, but they have been using threading every time they create an application. Once a process is launched, the operating system creates a primary thread for that application. Regardless of the type of application, there is no way to create an application that contains no threads in it. This lesson deals with the procedure for creating multiple threads within a single process. There are many instances in which creating a multiple-threaded application is advantageous. In the preceding example, the advantage to creating a separate worker thread in addition to the primary thread, which handles the user interface, is that the user interface still remains responsive to the user and to various system messages that are passed to it. Figure 19.1 shows the results of creating a single-threaded application involved in a processor-intensive operation. You can see that the window contains a picture, but because the application is performing intensive work, it is unable to handle redrawing the picture when it is obscured by another window. FIGURE 19.1 Without separate threads for long operations, your application may not respond to user input.
The classic user interface thread and worker thread design is not the only application type that can take advantage of multithreading. Servers, for example, need to connect and communicate with a variety of different clients. If there were only a single thread running within the server, each client would need to wait for another client to finish before communication is possible with the server. With most servers, a single thread is responsi-
Threading and Synchronization
265
ble for listening for client connections. Once a client connection is established, it is placed within its own thread until the client disconnects. Therefore, while the client and server are busy communicating, the main thread is still available to listen for more client connections. Multithreading is not something that should be done, however, without proper design and planning. Each thread, regardless of how it is created and with what priority, still shares the same thing with all the other threads of the running process: the same address space. In other words, it’s possible that any global, static and instance variables are available to each thread and can be easily accessed. The problem lies in the fact that with parallel execution, one thread could be writing data to a variable while another is either trying to read that variable or, worse yet, trying to write to it. This is where synchronization comes into play. Synchronization refers to defensive coding practices that guarantee the safety of variables while multiple threads are executing. The underlying mechanism behind synchronization is the kernel object. Several kernel objects can be used within Windows. The most common include critical sections, mutexes, semaphores, and events. During this hour, you will be using some of these synchronization objects through the .NET Framework threading classes.
Creating Threads The System::Threading namespace contains all the classes needed to create a multithreaded application. Not only does it contain the class to create and manage a thread, but there are also classes designed to work with kernel objects, thread exceptions, and synchronization issues, such as deadlocks. The main class within the System::Threading namespace responsible for threads is the Thread class. Within the WIN32 API, when a thread was created, you had to supply a function pointer to the function that would be used for the thread’s main procedure. The Thread class’s constructor within the .NET Framework is similar, but rather than using a callback function, it uses a delegate. However, even though a thread is created and is associated with a delegate, the thread does not start. In order to start a thread, you must call the Thread class method Start.
The Running Thread State Once a thread is created, it can be placed in one of several states. The state of a thread lets you know what the thread is currently doing and can be retrieved using the ThreadState property. Once a thread is created, it is in the Unstarted state. In order to
19
266
Hour 19
kick off the thread, which in turn calls the delegate, you call the Start method, which places the thread in the Running state. Figure 19.2 shows the flowchart for the lifetime of a thread.
The WaitSleepJoin Thread State Once a thread has been started, it can be placed in a state known as the WaitSleepJoin state. Although oddly named, this state is used for the different ways a state is paused. The first is if the thread itself calls one of the wait functions. A wait function allows a thread to immediately stop execution at a certain point and only return when the object it is waiting for is signaled. These objects and the method each one uses to signal other objects will be explained later in this hour. By calling the Sleep function, you can stop thread execution for a certain amount of time (expressed in milliseconds). Once the time period is finished, the thread resumes execution. The third part of the WaitSleepJoin state is based on the Join function. The Join function is not called within the context of the current thread. Instead, it is called on a separate thread variable. Calling Join suspends the calling thread until the other thread has finished executing. This is similar to calling a wait function using another thread as an object, as opposed to the other kernel objects. FIGURE 19.2 A thread can be placed within one of several different thread states.
Unstarted
Start
Suspend
Wait, Sleep, or Join WaitSleepJoin
Running
Suspended Resume
Interrupt Signal
Abort or Return
Stopped
Threading and Synchronization
267
The Interrupting Thread States After a thread is placed in the WaitSleepJoin state, there are several ways to place it into another state. If a thread is waiting for an object, the object will be placed back into the Running state when that object has been signaled. Another way to place a thread back into the Running state while it is in the WaitSleepJoin state is to interrupt it using the Interrupt method. If this method is called, the thread stops waiting for an objects signal and begins execution following the point where it began waiting. You should be careful when interrupting a thread, however. If a thread is not in the WaitSleepJoin state when the Interrupt function is called, the Interrupt function will be queued, and the next time the thread enters the WaitSleepJoin state, it will be interrupted and resume execution without waiting.
Suspending and Resuming a Thread Analogous to the Interrupt function, which places a thread back into the Running state, is the Suspend function, which places a thread into a suspended or paused state. Once a Suspend request is made, the thread is placed into the SuspendRequested state. Once the thread responds to the Suspend request, it is then in the Suspended state. In order to place a thread out of the Suspended state, you can call Resume to once again place it in the Running state. A thread can stop execution in two ways. The first way is simply for the thread to return from its procedure. Once this occurs, the thread is then in the Stopped thread state. The other way a thread can be stopped is with a call to the Abort method. Once this method is called, the thread stops execution and is placed in the Stopped state. However, a thread may not stop execution immediately. The thread will only stop execution if it is in the Running state. In other words, if a thread is waiting for a signal from another object or is suspended, it will not stop executing until it is removed from either of those states.
Creating the ThreadSynch Project Now that you know the possible states a thread can be placed in, you will create a project that at first creates two threads. Later, you will add synchronization and a data-access simulation. Create a project by clicking New, Project from the File menu. Select the Visual C++ Project item from the list of project types and select the Managed C++ Application project template. Give your project the name ThreadSynch and click OK to create the project. This application is going to be a common design paradigm consisting of a producer object and a consumer object. The producer object, which consists of a single thread,
19
268
Hour 19
will create a single data object. It will then notify the consumer object, which is on a separate thread, that a block of data is ready. Once the consumer uses the object, it then notifies the producer that it is finished. This process then repeats a set number of times. The current task is to create the two threads and set the states to Running. Therefore, create two managed classes, called Producer and Consumer. For now, just create a default constructor for both of the classes. You can use Listing 19.1 as a guide while you follow along. As mentioned earlier, each thread has one method that is used as the main thread procedure. When the thread is entered, it is placed in the Running state. When the method returns, the thread is in the final Stopped state and cannot be restarted. The delegate method can be any name you wish and does not take any parameters and does not return any values. In both classes, create a method named ThreadRun that will be used as the delegate for the respective classes. Finally, just so you know the threads are being created and their delegates are being called, output some text to the console using Console::WriteLine. The next step is the actual creation and execution of the threads within your _tmain function. Creating a thread object is the same as creating any other instance of an object. Therefore, create a pointer to a Consumer class and a pointer to a Producer class and instantiate each one using the C++ keyword new, as shown on line 36 of Listing 19.1. This just creates the actual consumer and producer objects. In order to run them on separate threads, you have to create a thread object for each one, passing the Thread constructor a pointer to the delegate used within each class. The delegate name used for the Thread class is ThreadStart. LISTING 19.1 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16:
Creating a Multiple-Threaded Application
#include “stdafx.h” #using #include using namespace System; using namespace System::Threading; public __gc class Consumer { public: Consumer(){} void ThreadRun( ) { Console::WriteLine(“Consumer Thread Created” );
Threading and Synchronization
LISTING 19.1 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65:
269
continued }
}; public __gc class Producer { public: Producer(){} void ThreadRun() { Console::WriteLine( “Producer Thread Created” ); } }; // This is the entry point for this application int _tmain(void) { int nResult = 0; Producer* pProducer = new Producer(); Consumer* pConsumer = new Consumer(); Thread* pProdThread = new Thread(new (pProducer, &Producer::ThreadRun Thread* pconsThread = new Thread(new (pConsumer, &Consumer::ThreadRun try {
19 pProdThread->Start( ); pconsThread->Start( ); pProdThread->Join( ); pconsThread->Join( );
} catch (ThreadStateException* e) { Console::WriteLine(e->Message); nResult = 1; } catch (ThreadInterruptedException* e) { Console::WriteLine(e->Message); nResult = 1; } Environment::ExitCode = nResult; }
ThreadStart )); ThreadStart ));
270
Hour 19
Even though you’ve created the objects that will be used within the threads and you’ve created the actual thread objects, they are still in the Unstarted state. In order to place the threads in the Running state, you call the Start method on each of the Thread objects. On line 48 of Listing 19.1, you can see that the method Join is called on each thread. Calling Join will block the current thread, the main thread executing in the _tmain function, until each of the threads finishes. This is similar to calling the WIN32 API function WaitForSingleObject, but rather than you having to worry about thread handles, the .NET Framework wraps thread-controlling functions nicely for you within the Thread class. Like many other .NET Framework–based objects, the Thread class can throw exceptions to let the caller know that something out of the ordinary has happened. This application doesn’t catch every possible thread exception, but it does illustrate how to catch an exception if you try to restart a thread after it has either terminated or is already running. This exception is the ThreadStateException. The other exception, ThreadInterruptedException is thrown when another thread calls Interrupt on a thread that is in the WaitSleepJoin state. In order to verify that your threads are being created and started correctly, compile and run your application. Your output should appear similar to Figure 19.3. FIGURE 19.3 Output showing the creation and running of two separate threads.
Thread Synchronization Any time multiple threads are running within an application, you run the risk of accessing the same data at the same time. While one thread is writing data to a variable, another thread could also be doing the same. As you can see, this type of behavior is undefined and can lead to any number of problems. To solve this, you must use thread synchronization.
Threading and Synchronization
271
For this application you will use the .NET Framework class Monitor to control how your two threads synchronize with each other. The Monitor class is similar to using critical sections in which once a lock is obtained on an object, no other thread can obtain the lock until it released by the original thread. To obtain a lock on an object, you can use the static member function Enter found within the Monitor class. The parameter to this function is a managed class. Once the lock has been obtained on that class, no other thread will be able to obtain the lock until the Exit method is called, contained within the Monitor class. There are two ways to obtain a lock on an object. In the first method, which was just explained, Enter will wait for the object to be released and then attempt to lock the object. However, there is no guarantee that the next time the object is released, the thread wishing to obtain the lock will acquire it. The other method for obtaining a lock is by using the TryEnter function. This is similar to using Enter, but if an object is already locked, the TryEnter function will immediately return without trying to obtain the lock later. This application will create a custom data object that has the ability to both read and write data. The object, however, will use synchronization to prevent reading and writing at the same time by using methods within the Monitor class. To begin with, create a managed class named IntDataBlock. This class will have two functions in addition to the default constructor. The first function is used to write the data. However, because this is a simulation, no actual data will be written. Give this function the name WriteData. It accepts an integer parameter named nData and does not return a data type. The second function is used to read the data just written. This function is named ReadData and accepts no parameters but returns an integer. You can use Listing 19.2 to aid you when you are entering in these functions. Because this is a multithreaded application and the act of reading data infers that data has actually been written first, you have to prevent the case where data might be read before it is written. To do this, you will need to create a Boolean member variable that is initialized to false so that the data class in guaranteed to write before it reads. Create a private Boolean variable and initialize it to false within the default constructor. So, what happens if the ReadData function is called before the WriteData function and the ReadData has already acquired the lock on the IntDataBlock object? The Monitor class contains a method to temporarily suspend the lock acquisition and give control of the lock to the next object in the waiting queue. Therefore, if the lock is first obtained within the ReadData class, ReadData will relinquish control of the lock to the WriteData method, which is trying to acquire it by calling the Monitor method Wait. You can see how this works by looking at line 50 of Listing 19.2.
19
272
Hour 19
LISTING 19.2 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47:
Creating a Multithread-Aware Data Object
#include “stdafx.h” #using #include using namespace System; using namespace System::Threading; public __gc class IntDataBlock { public: IntDataBlock() { m_nLastData = 0; m_bIsReading = false; } void WriteData( int nData ) { Monitor::Enter( this ); { Console::WriteLine( “Entered Write Monitor” ); if( m_bIsReading ) { Console::WriteLine( “Write Waiting” ); Monitor::Wait( this ); } m_nLastData = nData; Console::Write(“Writing: {0}...”, __box(nData)); Console::Write(“Done\n”); m_bIsReading = true; Console::WriteLine( “Write Pulsing” ); Monitor::Pulse(this); } Console::WriteLine( “Leaving Write Monitor” ); Monitor::Exit( this ); } int ReadData() { Monitor::Enter( this ); { Console::WriteLine( “Entered Read Monitor” ); if( !m_bIsReading )
Threading and Synchronization
LISTING 19.2
273
continued
48: { 49: Console::WriteLine( “Read Waiting” ); 50: Monitor::Wait( this ); 51: } 52: Console::WriteLine(“Reading: {0}\n”, __box(m_nLastData)); 53: 54: m_bIsReading = false; 55: Console::WriteLine( “Read Pulsing” ); 56: Monitor::Pulse( this ); 57: } 58: Console::WriteLine( “Leaving Read Monitor” ); 59: Monitor::Exit( this ); 60: 61: return m_nLastData; 62: } 63: 64: private: 65: int m_nLastData; 66: bool m_bIsReading; 67: };
The ReadData function, at this point, has acquired the lock and because nothing has been written, it has called the Wait method. Therefore, control of the lock now resides with the WriteData method. Because the object is writing, and therefore the m_bIsReading flag is false, WriteData can perform its simulated data write. Because the WriteData method is finished with the lock, it seems only natural to just release the lock using the Exit method. However, the ReadData method is currently in a waiting state. Simply releasing the lock will not signal to the ReadData method that the lock has been released. To signal a thread that is waiting for a lock, you call the Monitor function named Pulse. This will place the thread that is waiting for the lock in the ready queue. Once the thread that is holding the lock finally releases the lock, the waiting thread will then gain control of it. This back-and-forth passing of control of the lock will guarantee that you are not writing data at the same time you are reading it. When creating an application using multiple threads, you need to be aware of all the possible execution sequences. In the sample code trace just covered, we used an example in which the ReadData method could obtain the lock first, but this isn’t guaranteed. In fact, the WriteData method could obtain the lock first. Due to this, simply using the Enter and Exit methods within the Monitor class may not be good enough. This is the reason for using the Wait and Pulse methods and the bool flag to control the sequence of events.
19
274
Hour 19
This process of ensuring that objects aren’t being concurrently accessed is known as thread safety and is a topic that should not be taken lightly. Any time you decide to create a threaded application, you need to inspect the code thoroughly to remove any concurrent data-access issues. However, it is possible to take this to an extreme by using thread-safe objects and procedures too much. Keep in mind that as you add thread-safe code, you will start running into performance issues. The first solution you should investigate is to avoid even having to use thread-safety constructs altogether. Try to find a solution that works in a threaded environment without relying on kernel objects or some of the .NET objects created for enforcing thread safety. In some cases, you can’t avoid having to use thread-safe helper functions or objects. If that’s the case, use them as minimally as possible. Don’t design your application with the mindset that you will make every object thread safe, even if you don’t use that object across thread boundaries. Only ensure data-access safety for those objects that will be used by multiple threads.
Creating Consumer and Producer Objects Now that you understand how the thread will be synchronized by encapsulating the synchronization logic within the data object, you will add the code necessary for the producer and consumer objects. As you’ll recall, the delegates within each of these classes will be doing all the work. Before you implement the delegates, you need to create another constructor for each of the classes. Because this lesson is meant to show synchronization, each class will work with the same data object. This single IntDataBlock object will be created within the _tmain function and passed to the consumer and producer objects through each of their overloaded constructors. Create an overloaded constructor for each of the classes that accepts a pointer to an IntDataBlock object and assigns a private member variable to the constructor parameter. Also, change the _tmain function by creating an IntDataBlock instance and passing them to the producer and consumer classes rather than using the default constructors for each class. You can use Listing 19.3 to help you.
Delegates are used extensively throughout the .NET Framework. In order for a delegate to have access to data that it can use, you can follow the example this hour that places the delegate function within an actual object. By doing this, you gain the added benefit of object-oriented programming with delegates, which otherwise isn’t possible by using a single global function instead.
Threading and Synchronization
275
LISTING 19.3 Implementing the Overloaded Constructors and Delegates for the Producer and Consumer Classes 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46:
public __gc class Consumer { public: Consumer(){} IntDataBlock* m_pDataBlock; Consumer( IntDataBlock* pDataBlock ) { m_pDataBlock = pDataBlock; } void ThreadRun( ) { for(int i=1; iWriteData( i ); } } }; public __gc class Producer { public: IntDataBlock* m_pDataBlock; Producer( IntDataBlock* pDataBlock ) { m_pDataBlock = pDataBlock; } void ThreadRun() { int nData; for(int i=1; iReadData( ); } } }; // This is the entry point for this application int _tmain(void) { int nResult = 0;
19
276
Hour 19
LISTING 19.3 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66: 67: 68: 69: 70: 71: 72: 73: 74: 75: 76: 77: 78: }
continued IntDataBlock* pData = new IntDataBlock( ); Producer* pProducer = new Producer( pData ); Consumer* pConsumer = new Consumer( pData ); Thread* pProdThread = new Thread (new ThreadStart(pProducer, &Producer::ThreadRun )); Thread* pconsThread = new Thread (new ThreadStart(pConsumer, &Consumer::ThreadRun )); try { pconsThread->Start( ); pProdThread->Start( ); pProdThread->Join( ); pconsThread->Join( ); } catch (ThreadStateException* e) { Console::WriteLine(e->Message); nResult = 1; } catch (ThreadInterruptedException* e) { Console::WriteLine(e->Message); nResult = 1; } Environment::ExitCode = nResult;
The code to actually produce and consume data is quite simple because all the data logic is contained within the data object. To produce data, simply create a for loop with a set number of iterations and for each iteration, call the WriteData function passing the loop index as the parameter. For the consumer object, simply create another loop and call the ReadData function on the IntDataBlock variable. You can now compile your application. It may be a good idea to place Console::WriteLine calls throughout various places in your code. This will help you to see how the synchronization works and what the sequence of events is as data is produced and consumed. Your output should appear similar to Figure 19.4.
Threading and Synchronization
277
FIGURE 19.4 Output of a multiplethreaded producer/ consumer application with synchronization.
Events and Timers You used the Monitor class and its various methods to control data synchronization. Another way you could have achieved this is with the Event kernel object. Events were covered in Hour 18, “Delegates and Events,” but the events discussed here are operating system events. When an event is created, it is placed in a non-signaled state. You can then open the event within a different area of code, such as another thread. Once you obtain a handle to the event, you can then wait for it to be signaled. Once the event is signaled, the object waiting can then proceed. Rather than restructuring the lesson you just created, you are just going to use events and a timer to simulate a long write operation. The code you are going to add will be placed within the IntDataBlock object and will be called when a write operation occurs. To begin, add a private member variable. This is a pointer to the ManualResetEvent class, which is in the .NET Framework. The two main types of kernel object events are ManualResetEvent and AutoResetEvent. The difference lies in how each of these events is reset. Once an object is signaled, it remains in the signaled state. Therefore, any other object that tries to wait for that event will immediately return because the event is signaled and there is no need to wait. Therefore, an event can be placed back into a nonsignaled state by resetting it. ManualResetEvent is reset manually by the developer; AutoResetEvent is automatically reset after the event is signaled. For this application, it doesn’t matter which type of event you use because the use of the event is pretty minimal. Create a private function within the IntDataBlock class called SimulateLongOperation and give it no parameters and a return type of void. Also, create another function named TimerProc. This function accepts a pointer to an Object as its parameter and also returns a void type. You can follow Listing 19.4 to help you make the changes to the IntDataBlock class.
19
278
Hour 19
LISTING 19.4 An Enhanced Data Object Class Using an Event and Timer to Simulate a Long Operation 1: public __gc class IntDataBlock 2: { 3: public: 4: IntDataBlock() 5: { 6: m_nLastData = 0; 7: m_bIsReading = false; 8: } 9: 10: void WriteData( int nData ) 11: { 12: Monitor::Enter( this ); 13: { 14: Console::WriteLine( “Entered Write Monitor” ); 15: if( m_bIsReading ) 16: { 17: Console::WriteLine( “Write Waiting” ); 18: Monitor::Wait( this ); 19: } 20: m_nLastData = nData; 21: 22: Console::Write(“Writing: {0}...”, __box(nData)); 23: SimulateLongOperation(); 24: Console::Write(“Done\n”); 25: 26: m_bIsReading = true; 27: Console::WriteLine( “Write Pulsing” ); 28: Monitor::Pulse(this); 29: } 30: Console::WriteLine( “Leaving Write Monitor” ); 31: Monitor::Exit( this ); 32: } 33: 34: int ReadData() 35: { 36: Monitor::Enter( this ); 37: { 38: Console::WriteLine( “Entered Read Monitor” ); 39: if( !m_bIsReading ) 40: { 41: Console::WriteLine( “Read Waiting” ); 42: Monitor::Wait( this ); 43: } 44: Console::WriteLine(“Reading: {0}\n”, __box(m_nLastData)); 45: 46: m_bIsReading = false; 47: Console::WriteLine( “Read Pulsing” ); 48: Monitor::Pulse( this );
Threading and Synchronization
LISTING 19.4
279
continued
49: } 50: Console::WriteLine( “Leaving Read Monitor” ); 51: Monitor::Exit( this ); 52: 53: return m_nLastData; 54: } 55: 56: private: 57: void TimerProc(Object* state) 58: { 59: m_pLongOpEvent->Set(); 60: } 61: 62: void SimulateLongOperation() 63: { 64: m_pLongOpEvent = new ManualResetEvent(false); 65: 66: TimerCallback* pTimerDelegate = new TimerCallback(this, TimerProc); 67: Timer* timer = new Timer 68: (pTimerDelegate, NULL, 1000, Timeout::Infinite ); 69: 70: m_pLongOpEvent->WaitOne(); 71: } 72: 73: int m_nLastData; 74: bool m_bIsReading; 75: ManualResetEvent* m_pLongOpEvent; 76: };
19 The long operation you will simulate will involve creating an event, setting a timer, and then waiting for the event to signal within the timer procedure. Within the SimulateLong Operation function, create a new ManualResetEvent instance and assign it to your private member variable you created earlier. Next, create a timer delegate function using your ThreadProc function as the function pointer to the delegate. This can be seen on line 77 of Listing 19.4. The next step involves creating the timer. The Timer class accepts four parameters in its constructor. The first parameter is the delegate you created in the previous line. The second parameter can be any object that will be passed to the delegate as long as it is a managed class. The third and fourth parameters control how the delegate is called while the timer is running. The third parameter is how long you want to wait before the timer first calls the delegate. This is known as the due time and is expressed in milliseconds. In this application, you should wait long enough for you to detect that a delay has occurred; 1,000 milliseconds should be good enough. The fourth parameter signifies how many milliseconds to wait before subsequent
280
Hour 19
calls are made to the delegate. Once the due time has passed, the delegate will be called every time that certain number of milliseconds has passed. For this application, you only want the delegate to be called once and no more. You can do this by simple passing Timeout::Infinite as the fourth parameter. Now that the timer has been created, SimulateLongOperation can wait for the event you created earlier to be signaled. This is done by calling the WaitOne method on the event object. As you may have guessed, the timer delegate will be responsible for signaling the event. After the 1,000 milliseconds has expired, the delegate signals the event by calling the Set member function. The last step before you can try your application out is to call the SimulateLong function. Place this function call in the WriteData function where the actual simulated write operation takes place. Operation
You are now done with your multiple-threaded and synchronized application. Once you compile and run your application, you will still get the same output as before. This time, however, you will notice a short delay each time the application is simulating a data write.
Summary Working with applications that create multiple threads is not a trivial matter. Being able to spot the places within your code where synchronization issues can occur can at times be quite difficult. Any time you create an application and create a new thread, you should immediately look for areas that might later lead into problems due to concurrent access. The .NET Framework has made creating, using, and synchronizing threads much easier than it was using the WIN32 API. However, it’s still up to you as the developer to make sure you use those classes in a way that permits seamless concurrent execution of multiple threads without running the risk of data corruption. This hour’s lesson took you beyond the basic Hello World program and dealt more with the common consumer/producer designed program. By using this lesson as a model in the future, you’ll be able to recognize potential hazards and have the knowledge and toolset available to eliminate them.
Q&A Q C# has a lock keyword. What is the equivalent keyword in C++? A There is no equivalent C++ keyword. However, the C# lock keyword behaves exactly the same as wrapping code with the Monitor::Enter and Monitor::Exit functions.
Threading and Synchronization
281
Q Do I have to add synchronization as was done in this hour every time I access data? A Only if the data type you are trying to synchronize was created by you. The .NET Framework collection classes contain built-in synchronization for this purpose. Many of them contain their own lock and unlock functions.
Workshop The Workshop provides quiz questions to help solidify your understanding of what was covered in this hour. Answers are provided in Appendix A, “Quiz Answers.”
Quiz 1. How are a managed event and a kernel object event different? 2. What happens to a thread if it is in the Stopped state and is restarted with a Start function call? 3. What is the difference between a thread delegate and a thread callback function used in the WIN32 API? What advantages does the delegate method give you?
19
HOUR
20
Database Access If you’ve been using computers for a while now, you may remember the early days of the World Wide Web. When it first arrived, the Web was just a collection of images and static text. Later, the emergence of animation and sound broke up the monotony, but static text still prevailed. Then, one of the biggest technological advancements that transformed the Web from a source of information to a viable application platform was the emergence of serverside data access. The use of data within databases has been around for a long time, but there hasn’t been a shortage or slowdown of advancements in the field. The redesign of ActiveX Data Objects (ADO) within Visual Studio .NET is a testament to this fact. This redesign was so drastic that ActiveX doesn’t really play a part in ADO.NET; however, the name, ActiveX Data Objects, was kept. As you will see in this hour, database access has changed substantially to keep up with the changing data-access models currently in use today.
284
Hour 20
In this hour you will learn: • How and why database access, specifically ADO.NET, is designed • How to connect to a DataStore and specify a command to fill a DataSet object • How to retrieve and navigate through data within a DataSet object • How to insert and delete data and then save the updated database
Connected vs. Disconnected Clients When Microsoft announced its plans for .NET, it was clear Microsoft was designing a framework that would allow developers to easily implement applications that work over the Internet. Sure, there are ways to do this with the previous version of Visual Studio and subsequent Software Development Kit (SDK) downloads, but it’s clear that the .NET Framework was designed with the Internet in mind. This is immediately apparent once you delve into ADO.NET. When Active Server Pages (ASP) and other related technologies were released, developers were finally able to push large amounts of data from a database residing on a server to an end user without having to copy the entire database and run it locally. However, this also ushered in a new era of database access—the disconnected client. Although saying that a user browsing the Web is a disconnected client may sound like an oxymoron, the disconnection in this case refers to the user and the data source. In a multitiered architecture, there are generally three key pieces to an application. First is the user interface, which is rendered for the client. The second tier is the piece that assembles the data or, more technically, contains the logic that connects the user interface with the third tier. This third tier is the data source. So, what exactly does it mean when I say that a client is disconnected? Suppose you had to construct the necessary logic to support a few simultaneous connections from clients. After you got past the code to synchronize the data, connect and disconnect clients, and transfer data, you’re essentially finished. With just a few client connections, your data connections are flowing smoothly. Now, add a hundred more simultaneous connections (or a thousand or a million) and watch as the performance of your application drops substantially. By keeping all these connections open, your application will sooner or later come crashing down. This is, in essence, the connected client scenario. In the early days of data access, creating an application of such magnitude would have been extremely difficult due to the complexity involved with the database-access API functions at that time.
Database Access
285
In the previous example, you need a data-access method that solves the performance problems associated with large numbers of simultaneous connections. One solution is to limit the number of simultaneous connections. However, as the number of clients grows, some of them may be waiting a long time. Another solution could entail transferring the entire database to the client along with the necessary logic to manipulate the data on the client machine. I think you can judge for yourself why this would be a bad idea. Another solution could be to connect with a client, give the code that houses the data-access logic a quick snapshot of the current data, and then disconnect the data connection and let the logic tier handle it from there, periodically reconnecting and disconnecting to make small changes. This is one of the methods currently implemented within ADO.NET, and it’s known as a disconnected client scenario.
Redesigning ADO for the .NET Framework With the ever-changing models for data access, ADO has undergone a complete redesign. However, in order for developers to transition, the older ADO methods are still available through the .NET COM interoperability services, which will be discussed in the next hour. The need for a complete redesign is inherently due to difficulty in supporting current technology using an older design model. When designing ADO.NET, Microsoft had several key goals in mind. The first and foremost design decision was to facilitate the use of the Extensible Markup Language (XML) throughout ADO.NET. XML is a text-based language that allows for easy data transfers through corporate networks. The design of XML allows for easier data-encoding mechanisms based on an industry standard rather than the binary object–transfer mechanisms used in the past. Supporting disconnected clients in a multitier architecture also contributed to the decision to redesign ADO. Although it was possible to create a disconnected client scenario with ADO, it was cumbersome. ADO was designed to more readily support connected clients. The key object within ADO.NET that contributes to the disconnected n-tier programming model is the DataSet object. The DataSet represents the root of an inmemory representation of the data you are working with. One of the advantages to this type of design is that you are allowed to have several tables of data from differing sources, all contained within that single DataSet (see Figure 20.1). Within the design of ADO, however, a RecordSet only contained one table. Although this table could be built from the results of a multiple table join, you were still limited to a single table. This could have been circumvented by using multiple RecordSets, but relationships could not have been established. Within ADO.NET, the DataSet object contains a collection containing relationships between parent and child tables.
20
286
Hour 20
FIGURE 20.1 Architecture of ADO.NET.
DataRelation Collection
Dataset
DataTable
DataTable
…
DataTable
DataTable
DataType
DataType
DataType
DataType
DataType
DataColumn Collection
DataColumn Collection
DataColumn Collection
DataColumn Collection
DataColumn Collection
DataType
DataType
DataType
DataType
DataType
DataColumn Collection
DataColumn Collection
DataColumn Collection
DataColumn Collection
DataColumn Collection
DataType
DataType
DataType
DataType
DataType
Constraints Collection
Constraints Collection
Constraints Collection
Constraints Collection
Constraints Collection
As you can see in Figure 20.1, a DataSet can contain several DataTables. These tables can contain several DataColumn collections and their associated DataRow components. While viewing a database though a database editor such as Access, you can see parallels between table design and the design of ADO.NET. This will become clearer as you work through a managed C++ lesson that uses an Access database and ADO.NET to access its data.
Creating the Authors ADO.NET Application In this lesson, you will be creating a managed C++ application that produces a Windows Form. The application will access data found within an Access database and display the results within the controls of the form. Furthermore, the application will allow you to add new records to the database. The first step is to create the main project solution. You will be creating a managed C++ application, so click New, Project from the File menu. Select Visual C++ Projects from the list of project types and select Managed C++ Application from the list of application templates. Give your solution the name AuthorDB and click OK to create the project. Now that the project has been created, you will need to create the main form’s code and the necessary event handlers for the various controls contained on the form. The user interface will consist of three edit controls, three label controls, and six buttons. The edit controls will be used to display the database contents, and the buttons will be used to navigate and change the data.
Database Access
287
For this project, create a separate file containing the Windows Form code. Click Project, Add Class from the main menu. Select the Generic C++ Class option from the list of class templates. Click Open and give your class the name AuthorDBForm. Click Finish to create the new class. Because this just creates a non-managed class, you will have to add the __gc keyword to the class declaration. Open the AuthorDBForm.h file and add the __gc keyword before the class keyword. Also, you will need to import and declare the necessary namespaces required for Windows Forms. Listing 20.1 shows the various namespaces you will need. LISTING 20.1 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37:
Class Declaration for the AuthorDB User Interface
#pragma once #include “stdafx.h” #include #using #using #using #using
using namespace System; using namespace System::Drawing; using namespace System::Windows::Forms; __gc class AuthorDBForm : public Form { public: AuthorDBForm(void); ~AuthorDBForm(void); protected: void InitForm(); private: Button* Button* Button* Button* Button* Button* Label* Label* Label* TextBox* TextBox* TextBox*
buttonPrevious; buttonNext; buttonFirst; buttonLast; buttonNew; buttonDelete; labelID; labelName; labelYear; textBoxID; textBoxName; textBoxYear;
void buttonNew_Click(Object* sender, EventArgs* e);
20
288
Hour 20
LISTING 20.1 38: 39: 40: 41: 42: 43: };
continued void void void void void
buttonDelete_Click(Object* sender, EventArgs* e); buttonFirst_Click(Object* sender, EventArgs* e); buttonPrevious_Click(Object* sender, EventArgs* e); buttonNext_Click(Object* sender, EventArgs* e); buttonLast_Click(Object* sender, EventArgs* e);
Because this is a Windows Form object, you will need to derive your class from the .NET Forms class. This can be seen on line 14 of Listing 20.1. The next step involves creating the controls for your form. As mentioned earlier, you will be creating six Button controls, three TextBox controls, and three Label controls. Create the necessary declarations for these controls in a private section of your class declaration. We will continue with the same design that has been used throughout this book for forms, so create a protected InitForm method that doesn’t accept any parameters and returns void. This form contains two groups of buttons. Four of these buttons will be used to navigate through the data. With these buttons, you will be able to move to the next or previous record and also be able to move to the first or last record within the DataTable. The other group of buttons will allow you to insert and delete records within the database. Therefore, create six event handler delegates for each of the buttons, as shown in Listing 20.2. LISTING 20.2 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21:
Setting Control Properties and Wiring Events Within InitForm
#include “StdAfx.h” #include “authordbform.h” AuthorDBForm::AuthorDBForm() { InitForm(); } AuthorDBForm::~AuthorDBForm() { } void AuthorDBForm::InitForm() { textBoxID = new TextBox(); textBoxName = new TextBox(); textBoxYear = new TextBox(); buttonPrevious = new Button(); buttonNext = new Button(); buttonFirst = new Button();
Database Access
LISTING 20.2 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66: 67: 68: 69:
289
continued buttonLast = new Button(); buttonNew = new Button(); buttonDelete = new Button(); labelID = new Label(); labelName = new Label(); labelYear = new Label(); SuspendLayout(); // textBoxID textBoxID->Location = Point(16, 24); textBoxID->Name = S”textBoxID”; textBoxID->Size = System::Drawing::Size(40, 20); textBoxID->TabIndex = 0; textBoxID->Text = S””; textBoxID->ReadOnly = true; // textBoxName textBoxName->Location = Point(64, 24); textBoxName->Name = S”textBoxName”; textBoxName->Size = System::Drawing::Size(192, 20); textBoxName->TabIndex = 1; textBoxName->Text = S””; // textBoxYear textBoxYear->Location = Point(264, 24); textBoxYear->Name = S”textBoxYear”; textBoxYear->Size = System::Drawing::Size(104, 20); textBoxYear->TabIndex = 2; textBoxYear->Text = S””; // buttonPrevious buttonPrevious->Location = Point(48, 56); buttonPrevious->Name = S”buttonPrevious”; buttonPrevious->Size = System::Drawing::Size(32, 23); buttonPrevious->TabIndex = 3; buttonPrevious->Text = S”add_Click(new EventHandler(this,buttonPrevious_Click)); // buttonNext buttonNext->Location = Point(80, 56); buttonNext->Name = S”buttonNext”; buttonNext->Size = System::Drawing::Size(32, 23); buttonNext->TabIndex = 4; buttonNext->Text = S”>”; buttonNext->add_Click( new EventHandler( this, buttonNext_Click )); // buttonFirst
20
290
Hour 20
LISTING 20.2 70: 71: 72: 73: 74: 75: 76: 77: 78: 79: 80: 81: 82: 83: 84: 85: 86: 87: 88: 89: 90: 91: 92: 93: 94: 95: 96: 97: 98: 99: 100: 101: 102: 103: 104: 105: 106: 107: 108: 109: 110: 111: 112: 113: 114: 115:
continued buttonFirst->Location = Point(16, 56); buttonFirst->Name = S”buttonFirst”; buttonFirst->Size = System::Drawing::Size(32, 23); buttonFirst->TabIndex = 5; buttonFirst->Text = S”Location = Point(112, 56); buttonLast->Name = S”buttonLast”; buttonLast->Size = System::Drawing::Size(32, 23); buttonLast->TabIndex = 6; buttonLast->Text = S”>>”; buttonLast->add_Click( new EventHandler( this, buttonLast_Click )); // buttonNew // buttonNew->Location = Point(232, 56); buttonNew->Name = S”buttonNew”; buttonNew->Size = System::Drawing::Size(64, 23); buttonNew->TabIndex = 7; buttonNew->Text = S”New”; buttonNew->add_Click( new EventHandler( this, buttonNew_Click )); // buttonDelete buttonDelete->Location = Point(304, 56); buttonDelete->Name = S”buttonDelete”; buttonDelete->Size = System::Drawing::Size(64, 23); buttonDelete->TabIndex = 8; buttonDelete->Text = S”Delete”; buttonDelete->add_Click( new EventHandler( this, buttonDelete_Click )); // // labelID // labelID->Location = Point(16, 8); labelID->Name = S”label1”; labelID->Size = System::Drawing::Size(32, 16); labelID->TabIndex = 9; labelID->Text = S”ID”; // labelName labelName->Location = Point(64, 8); labelName->Name = S”label2”; labelName->Size = System::Drawing::Size(100, 16); labelName->TabIndex = 10; labelName->Text = S”Name”;
Database Access
LISTING 20.2 116: 117: 118: 119: 120: 121: 122: 123: 124: 125: 126: 127: 128: 129: 130: 131: 132: 133: 134: 135: 136: 137: 138: 139: 140: 141: 142: 143: 144: 145: 146: 147: 148: 149: 150: 151: 152: 153: 154: 155: 156: 157: 158: 159: 160:
291
continued // labelYear labelYear->Location = Point(264, 8); labelYear->Name = S”label3”; labelYear->Size = System::Drawing::Size(100, 16); labelYear->TabIndex = 11; labelYear->Text = S”Year Born”; // Form1 AutoScaleBaseSize = System::Drawing::Size(5, 13); ClientSize = System::Drawing::Size(384, 102); Name = “AuthorDBForm”; Text = “Authors”; Controls->Add( Controls->Add( Controls->Add( Controls->Add( Controls->Add( Controls->Add( Controls->Add( Controls->Add( Controls->Add( Controls->Add( Controls->Add( Controls->Add(
labelYear); labelName ); labelID ); buttonDelete ); buttonNew ); buttonLast ); buttonFirst ); buttonNext ); buttonPrevious ); textBoxYear ); textBoxName ); textBoxID );
ResumeLayout(false); } void AuthorDBForm::buttonNew_Click(Object* sender, EventArgs* e) { } void AuthorDBForm::buttonDelete_Click(Object* sender, EventArgs* e) { } void AuthorDBForm::buttonFirst_Click(Object* sender, EventArgs* e) { } void AuthorDBForm::buttonPrevious_Click(Object* sender, EventArgs* e) { }
20
292
Hour 20
LISTING 20.2 161: 162: 163: 164: 165: 166: 167: 168:
continued
void AuthorDBForm::buttonNext_Click(Object* sender, EventArgs* e) { } void AuthorDBForm::buttonLast_Click(Object* sender, EventArgs* e) { }
Now that the user interface variables and member functions have been declared, you can set the control properties and wire the controls to their associated delegates. This is all done within the InitForm function, as shown in Listing 20.2. The last step is to make sure your applications entry-point function, _tmain, displays your new form. In your AuthorDB.cpp file, include the header file for your form class and add the code to display your form, as shown in Listing 20.3. LISTING 20.3 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15:
Displaying Your Main Windows Form
#include “stdafx.h” #using #include using namespace System; #include “authordbform.h” // This is the entry point for this application int _tmain(void) { Application::Run( new AuthorDBForm() ); return 0; }
At this point, your user interface is complete, albeit without any data to work with. Compile your application to make sure everything is working correctly. When you run your application, you should see a window similar to the one in Figure 20.2. FIGURE 20.2 The user interface for the AuthorDB application.
Database Access
293
Connecting to the Data Source For this lesson, you will be using a database that is installed by default when you install Internet Information Services (IIS) in Windows 2000 and Windows XP. If you do not have IIS installed, you should either install it now or use the database contained within the code for this hour, which can be downloaded from the Sams Web site. The database itself contains only a single table. Within this table are records corresponding to several author names and the years they were born. There is also an associated ID for each of the authors. If you have IIS installed, you can open the database in Microsoft Access, which is a part of Microsoft Office. By default, the database is located in the IIS folder in the iissamples\sdk\asp\database subdirectory. The first step in using a database within your application is obviously to connect to it. Create a member function within your form class called OpenDataSource. Because this is just a test application, you can forgo any error checking by just returning void and not specifying any parameters for the function. The first step in connecting to a data source is to create the main DataSet object. This object is your main access point to all the tables contained within as well as the specific records within those tables. Create a private DataSet member variable within your form class and create an instance of this object within the OpenDataSource function, as shown in Listing 20.4. There are two possible constructors for this object. The one used here accepts a string parameter. This is purely optional and allows you to specify a specific name for the DataSet. LISTING 20.4
Connecting to the Data Source and Filling the DataSet Object
1: void AuthorDBForm::OpenDataSource() 2: { 3: // create data set 4: m_pDataSet = new DataSet( “Author Database” ); 5: 6: // create data source connection 7: m_pAuthorsDB = new OleDbConnection( 8: S”Provider=Microsoft.Jet.OLEDB.4.0;\ 9: Data Source=\ 10: C:\\Inetpub\\iissamples\\sdk\\asp\\database\\Authors.mdb;”); 11: 12: // create selection command 13: OleDbCommand* pSelectCMD = new OleDbCommand( 14: S”SELECT * FROM Authors ORDER BY AU_ID”, m_pAuthorsDB ); 15: pSelectCMD->CommandTimeout = 30; 16: 17: // create the ole data adapter
20
294
Hour 20
LISTING 20.4 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: }
continued m_pDA = new OleDbDataAdapter(); m_pDA->SelectCommand = pSelectCMD; // fill the dataset with the data source m_pDA->Fill( m_pDataSet, “Authors” ); // save the author table pointer m_pAuthorTable = m_pDataSet->get_Tables()->get_Item(“Authors”); // set primary key to author id DataColumn *myColArray[] = {m_pAuthorTable->get_Columns()->get_Item(“AU_ID”)}; m_pAuthorTable->set_PrimaryKey( myColArray );
As mentioned earlier, DataSet is the main object within ADO.NET. In fact, with the DataSet object that you just created, you can manually add new tables and insert new data without having to use any type of database. This is useful because it can give you a way to internally organize data within an application without having to create your own data-persistence scheme. Once you are finished, you can save the data to an XML file and then read the persisted data back in when your application is restarted. For this lesson, however, you will be working with data that is already contained within a database. The next step is to connect to the data source and specify the command you want to run on the data source to retrieve the data. Connecting to a data source involves the use of the OleDBConnection object. This object is used regardless of the format of the database. In other words, if you plan on connecting to a Structured Query Language (SQL) database, you would use the same object (OleDBConnection) as you use for an Access database. In fact, you will soon realize that most of the data objects within the .NET Framework are used for differing types of databases. The difference lies within an object known as the data adapter. A data adapter is specialized to work with the format of a specific database. The .NET Framework currently ships with two data adapters: OleDbDataAdapter and SqlDataAdapter. For this lesson, you will be using OleDbDataAdapter. Each data adapter works with two objects to access a database: the OleDBConnection object, responsible for opening the database, and the OleDbCommand object. The OleDbCommand object is used to specify the type of command you want to execute on the data source to retrieve data. This lesson
Database Access
295
will use a SQL statement to retrieve the database contents. The database you are working with contains a single table named Authors. Because you are interested in all the data, you can use a SQL command that selects all rows and columns within the Authors table, as shown on line 13 of Listing 20.4. Also, in order to present the data in an ordered manner, you should order it by the ID assigned to each individual author. Even though the connection and command objects have been associated with the data adapter, a connection to the database still has not been made. The goal of a disconnected client scenario is to eliminate as much connection to the database as is possible. Even though you have specified which type of connection to use and which command to run, you haven’t written the code to fill the DataSet with that data. Therefore, there is no need to connect to the data source. To fill a DataSet with the data within the database, you can use the data adapter’s Fill method, as shown on line 22 of Listing 12.4. The Fill method accepts two parameters: a pointer to a DataSet, which is the DataSet you created earlier, and the name of the source table you wish to retrieve data from, which in this case is the Authors table. The last portion of the OpenDataSource function does two things. The first is a shortcut to the table contained within the DataSet. Create a pointer to a DataTable type within the private section of your class and retrieve the Authors table from the DataSet, as shown on line 25 of Listing 12.4. This isn’t a necessary step, but it will save you from having to access the Author table from the DataSet each time you need to retrieve data. The last portion of the OpenDataSource function is used to set a primary key. A primary key within a table is used to specify the column whose values are unique across each of the rows. If you were to add a row later that contained the same ID as another row, an exception would be thrown. The last step before you can check to see whether your application compiles correctly is to place the OpenDataSource function call within the constructor of your form. Place this call before your call to the InitForm function call. Once you finish that, compile your application. If you run your application, one of two things may happen: It may run the Windows Form just fine, or an exception may be thrown. If an exception is thrown, as shown in Figure 20.3, it may be the fact that the data source could not be found. If this happens, navigate to the directory you specified for the OleDbConnection object and ensure that the database exists at that location.
20
296
Hour 20
FIGURE 20.3 Specifying an invalid location for your data source will throw an exception.
Displaying and Navigating Through the Data The data that is contained within a DataTable is in the form of a collection. Because this is the case, you can index or enumerate through the data using familiar collection class methods. You can read Hour 16, “Collections and Arrays,” for a review of collections. Therefore, you should create a function that when given an index (more specifically a row index) will fill in the text boxes on the form. Create a private member function that returns a bool and accepts an integer. This integer specifies which row within the DataTable you want to display. Give your function the name FillFormData. Also, so that you can keep track of which row is currently displayed on the form, create a private member variable that is an integer and give it the name m_iCurRow. The first thing you should do within the FillFormData function is to check to make sure the row index that is passed in does not extend past the count of the number of rows within the DataTable. Also, you should make sure the row index is not less than zero, because negative row numbers are obviously undefined. This error checking can be seen on line 3 of Listing 20.5. If the conditional error checking passes, you can display the data on the form. Save the row index into your current row index member variable. Next, you will need to navigate to the row within the DataTable collection. Again, this is accomplished by getting a pointer to the Rows collection using the get_Rows member function contained within the DataTable object. This will give you the collection of all the rows within that table, but you are interested in only one of the rows, as specified by the row index passed in as a parameter. Call the get_Item member function contained within the Rows collection, as shown on line 8 of Listing 20.5. Now that you have navigated to the correct row, you can set the Text properties of the individual TextBox controls to their corresponding columns contained within the Row
Database Access
297
collection. Because calling get_Item on the collection of data returns a pointer to a .NET Object, convert that value to a string with the ToString function. Finally, call the FillFormData function within your form class’s constructor, passing it 0 as the row index you wish to view. Call this function after the call to initialize the Windows Form. In other words, the FillFormData function call should be the last function called from within the constructor. LISTING 20.5
Filling Form Controls with Data from the DataTable Object
1: bool AuthorDBForm::FillFormData( int iDBRow ) 2: { 3: if( m_pAuthorTable->get_Rows()->Count-1 < iDBRow || iDBRow < 0 ) 4: return false; 5: 6: m_iCurRow = iDBRow; 7: 8: DataRow* pRow = m_pAuthorTable->get_Rows()->get_Item(iDBRow); 9: 10: textBoxID->Text = pRow->get_Item(“AU_ID”)->ToString(); 11: textBoxName->Text = pRow->get_Item(“Author”)->ToString(); 12: textBoxYear->Text = pRow->get_Item(“YearBorn”)->ToString(); 13: 14: return true; 15: }
At this point, your application will display the first record within the database. Now you are going to implement the code that navigates through the database using the form’s navigation buttons. Navigating to the first record in the database is a trivial matter. You already do this in the form constructor, so call the FillFormData function with a row index of 0 in the buttonFirst_Click method. To navigate to the last row, you can use the Count property contained within the Rows collection. You will need to subtract 1 from the value received because row indexes start at 0. This can be seen on line 18 of Listing 20.6. Because you are keeping track of the row you are currently on, you can just call the FillFormData member function with one row index greater or one index smaller than the current row. This allows you to go forward or backward an index at a time. LISTING 20.6
Navigating Through the Data and Updating the Form Controls
1: void AuthorDBForm::buttonFirst_Click(Object* sender, EventArgs* e) 2: { 3: FillFormData(0); 4: } 5:
20
298
Hour 20
LISTING 20.6 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19:
continued
void AuthorDBForm::buttonPrevious_Click(Object* sender, EventArgs* e) { FillFormData( m_iCurRow - 1 ); } void AuthorDBForm::buttonNext_Click(Object* sender, EventArgs* e) { FillFormData( m_iCurRow + 1 ); } void AuthorDBForm::buttonLast_Click(Object* sender, EventArgs* e) { FillFormData( m_pAuthorTable->get_Rows()->Count -1); }
Compile and run your application. If everything is working correctly, you should be able to navigate through the records in the DataTable using the form’s navigation buttons. Figure 20.4 shows the application displaying the last record in the Authors database. FIGURE 20.4 The AuthorDB application displaying the last record in the Authors database.
Inserting and Deleting Table Records The form buttons each perform two different functions. When you click the Add button, your application will be placed into the insert mode. In this mode, the Add button will be changed to the Insert button, and the Delete button will be changed to the Cancel button. To accomplish this, you can create a private member variable of type bool, which signifies which mode you are in. Give this variable the name m_bInsertingRecord. You will first work on the New button. Because the buttonNew_Click delegate needs to handle two tasks based on the Boolean value you created earlier, you will need to have two blocks of code. Whenever you click the New button, you will need to place the form into insert mode. This is done by clearing all the TextBox controls by setting each Text property for the three TextBox controls to an empty string. Next, change the text of the two buttons by changing the Text properties of these Button controls. Change the New buttons’ text to “Add” and the Delete button to “Cancel,” as shown on line 12 of Listing 20.7. Finally, set the Boolean flag m_bInsertingRecord to true.
Database Access
299
If m_bInsertingRecord is true, you will add the contents of the TextBox controls to the database. The process of adding a new record involves creating a new row and adding that row to the DataTable. To begin, create a new DataRow object by calling the NewRow member function contained within the DataTable. Assign this to a local pointer to a DataRow variable, as shown on line 22 of Listing 20.7. Now that you have created a new row, you can start setting the individual columns within that row. Any time rows are being changed, the DataRow class fires events indicating the operations being performed, such as inserting, deleting, or changing individual columns. You can temporarily disable the firing of events by wrapping the code that changes a row with BeginEdit and EndEdit function calls. Because you aren’t handling events or adding large amounts of data at one time, disabling the event-firing mechanism probably doesn’t buy you any performance gains. However, for the sake of illustration, you will do it anyway. As mentioned earlier, call the BeginEdit function contained within the DataRow variable you created. Setting the individual columns within that row is similar to retrieving the data. For each column, call the set_Item function, passing the name of the database column and the value you want to set it to. Doing this for the Author column and the YearBorn column is trivial. Just set the data column to the value of the Text property of the corresponding TextBox control. The ID column you will need to set to a unique value. You can simply increment the index of the last row’s ID value, as shown on line 25 of Listing 20.7. Finish the row editing by calling the EndEdit function of the DataRow object to reenable event firing. LISTING 20.7
Implementing the Button Event Handler to Add Records
1: void AuthorDBForm::buttonNew_Click(Object* sender, EventArgs* e) 2: { 3: // user wants to add a new record 4: if( !m_bInsertingRecord ) 5: { 6: // clear text boxes 7: textBoxID->Text = S””; 8: textBoxName->Text = S””; 9: textBoxYear->Text = S””; 10: 11: // change button text 12: buttonNew->Text = S”Add”; 13: buttonDelete->Text = S”Cancel”; 14: buttonDelete->Enabled = true; 15: 16: // user is now in insert mode 17: m_bInsertingRecord = true;
20
300
Hour 20
LISTING 20.7 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: }
continued } else // user has finished filling in values, add to table { // create new row to insert DataRow* pNewRow = m_pAuthorTable->NewRow(); pNewRow->BeginEdit(); { pNewRow->set_Item( “AU_ID”, __box(Convert::ToInt32( m_pAuthorTable->get_Rows()->get_Item( m_pAuthorTable->get_Rows()->get_Count() -1)->get_Item(“AU_ID”)) + 1) ); pNewRow->set_Item( “Author”, textBoxName->Text ); pNewRow->set_Item( “YearBorn”, textBoxYear->Text ); } pNewRow->EndEdit(); // insert record into datatable m_pAuthorTable->get_Rows()->InsertAt( pNewRow, m_pAuthorTable->get_Rows()->Count ); m_pDA->Update( m_pDataSet, “Authors” ); // update current row index m_iCurRow = m_pAuthorTable->get_Rows()->Count-1; // update form // change button text back to original buttonNew->Text = S”New”; buttonDelete->Text = S”Delete”; FillFormData( m_iCurRow ); // done inserting record m_bInsertingRecord = false; }
Now that you have finished creating the row and setting the values of each of its columns, you are now ready to insert the new row into the DataTable. Because the Rows property in the DataTable is a collection, you can simply add your new row using a collection method that takes an index variable. In this case, use the InsertAt function of the DataTable, passing it an index value of one more than the current count of the number of rows. The first parameter to the InsertAt function is your new row. Because you are working in a disconnected client scenario, none of the operations you have performed so far have updated the actual database that exists on the file system. To
Database Access
301
update the database with the contents of the DataSet, you can call the data adapter’s member function Update, passing your DataSet member variable and the table within the data source to update, which in this case is the Authors table. This can be seen on line 37 of Listing 20.7. To finish the event handler for the New button, set the current row member variable to accommodate the new row. Finally, change the form’s buttons back to their original Text values and reset the inserting record Boolean member variable. Compile and run your application and then insert a new record. Don’t be surprised if an exception is thrown, as shown in Figure 20.5. FIGURE 20.5 Your application will throw an exception when trying to insert new records unless you create an OleDbCommandBuilder
object.
In this case, an exception was thrown because the SQL command associated with the data adapter does not contain an INSERT SQL statement. However, the .NET Framework contains a very useful class you can use in an instance like this—the OleDbCommandBuilder class. Any time you set the SelectCommand property of a data adapter, you will need to use this method. In the OpenDataSource member function you created earlier, place the following at the end of the function: // create the command builder for record insertion m_pCB = new OleDbCommandBuilder( m_pDA );
Believe it or not, that is all you need to do to remove the exception. This class will automatically generate the necessary SQL commands for any changes that occur to the DataSet. If you used other data-access technologies before, you’ve probably struggled with command syntax. By using the OleDbCommandBuilder object, you can manipulate the data while the appropriate database commands are created for you transparently. The last task for this hour’s lesson is to implement the code for the Delete button. When you are not in insert mode, as explained earlier, the Delete button will delete the currently displayed record. When you are in insert mode, this button will allow you to cancel out of inserting a new record. Therefore, you will need to create two code blocks based on the value of the m_bInsertingRecord member variable. If m_bInsertingRecord is true and the buttonDelete_Click event handler is called, the user is canceling out of the insert event. You must then set the form to the state it was in
20
302
Hour 20
prior to the user entering insert mode. Change the button text for each of the buttons to New and Delete, respectively. Also, if there are no records in the database, the current row member variable is equal to -1. In this case, there is no data to display, so set the Text properties of the TextBox controls to an empty string. If there is data (in other words, m_iCurRow is set to a positive integer), set the TextBox controls by calling the FillFormData member function, as shown on line 22 of Listing 20.8. If the form is not in insert mode and the Delete button is clicked, you will need to remove the current row from the data source. The first step is to make sure a row is currently being displayed. As was done previously, this can be determined by examining the value of the current row variable. If the variable is valid, you will remove the row. Removing a row from the DataTable is considerably easier than inserting one, because you do not have to directly access the individual columns of the row. To remove a row, you can use the collection method RemoveAt, which takes an integer as a parameter signifying which row index to remove. After the row has been removed, you will need to update the form to display a different row. Determining which row to display is purely a design decision. For this lesson, set the form data based on the current value of your current row variable. At first, you might think this is invalid because you deleted that row, but once a row is deleted, any rows following will have their row indexes decremented by 1. If FillFormData returns false, you are attempting to access a row that doesn’t exist, because it is greater than the number of rows currently in the DataTable. In this instance, fill the form data with the data from the last row in the DataTable, as shown on line 36 of Listing 20.8. If there are no more rows, set each of the TextBox controls to the empty string. LISTING 20.8
Implementing the Delete Event Handler
1: void AuthorDBForm::buttonDelete_Click(Object* sender, EventArgs* e) 2: { 3: if( m_bInsertingRecord ) 4: { 5: // user wants to cancel out of insert mode 6: m_bInsertingRecord = false; 7: buttonNew->Text = S”New”; 8: buttonDelete->Text = S”Delete”; 9: 10: // clear textboxes if not data available 11: if( m_iCurRow == -1 ) 12: { 13: // clear text boxes 14: textBoxID->Text = S””; 15: textBoxName->Text = S””; 16: textBoxYear->Text = S””;
Database Access
LISTING 20.8 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: }
303
continued return; } // set text boxes to previous record FillFormData( m_iCurRow ); } else { if( m_iCurRow == -1 ) return; // user wants to delete current record m_pAuthorTable->get_Rows()->RemoveAt(m_iCurRow); // fill with row after deleted row if( !FillFormData( m_iCurRow )) { // user is past last row, set to last row m_iCurRow = m_pAuthorTable->get_Rows()->Count - 1; // check to see if last row in table has been removed if( m_iCurRow == -1 ) { // clear text boxes textBoxID->Text = S””; textBoxName->Text = S””; textBoxYear->Text = S””; return; } FillFormData( m_iCurRow ); } }
Summary Although the application you created this hour was simple in appearance, it should have given you a good overview of how ADO.NET within the .NET Framework is designed. Microsoft realized the importance of data access, not just locally on a client’s system, but also in networking environments, including the Internet. In some instances, a previous design just doesn’t fit with the current technological demands in place. ADO is such an
20
304
Hour 20
example. It’s sometimes better to redesign a technology than to try and make it fit. So as not to leave out the developers who are familiar with an already established technology, Microsoft made the decision to support the older method. The disconnected client scenario is something that will show up again and again in the future. As data connections become faster and cheaper, it makes sense to store data at a central location rather than replicate it across several clients. As with most other technologies, design is important. A well-designed application can never hurt you. Not only will it generally lead to fewer defects, it will also lead to lower costs due to less maintenance. The design decision for data-access methods is as important a decision as any other technology out there.
Q&A Q Is ADO.NET the only data-access method within the .NET Framework? A No, it is not. As mentioned this hour, you can access regular ADO by using the COM Interop objects. Also, you may be familiar with the OLE DB Consumer and Producer templates present in ATL. These can still be used as well. In fact, they have been updated to work with attributes. The downloadable code, which can be found on the book’s Web site, contains an ATL Server project that uses the OLE DB Consumer templates with attributes. Q When starting a new project, should I immediately rule out ADO in favor of ADO.NET? A If competing technologies are available, you should never immediately rule out one over the other just because it is newer. Look at the pros and cons of each and then make your decision. As an example, if you are writing native code as opposed to managed code, you have to use ADO. Q Are OleDbDataAdapter and SqlDataAdapter the only data adapters in the .NET Framework? A They are the only data adapters currently shipping with the .NET Framework. You can, however, download an Open Database Connectivity (ODBC) .NET data adapter from Microsoft’s Web site. It’s also expected that third-party vendors will create other data adapters.
Workshop The Workshop provides quiz questions to help solidify your understanding of what was covered in this hour. Answers are provided in Appendix A, “Quiz Answers.”
Database Access
305
Quiz 1. While your application is running and a connection has been made to a data source, what happens if you delete the data source from the file system while you are navigating records? What about inserting or deleting records? 2. Do you need a database to create datasets, tables, and records within the .NET Framework? 3. What is a table relationship and how is it handled within a DataSet?
20
HOUR
21
COM with .NET The computer industry is probably one of the fastest moving industries in existence today. How many times have you heard people complain that the hardware they just bought will be obsolete in a short amount of time? Not only does this liquidity apply to hardware sales but also to software development. Software development is constantly changing to keep up with the latest breakthroughs in programming. The latest and greatest software you just purchased is soon replaced by a new and better version in less than a year. The component your company purchased from a third-party vendor is soon replaced by a component with greater flexibility and newer features. The Component Object Model (COM) was a breakthrough programming model. Finally there was a technology in which software could literally be built using a variety of reusable components. It also broke the barrier that existed between differing programming languages by allowing code written in one language to be used by another language. By applying a standard consisting of strict programming rules and certain design guidelines, any programming languages adhering to these rules could be used together in a seamless programming environment. However, with every advantage there is always a disadvantage. So, what was the disadvantage of using COM?
308
Hour 21
Simply put, COM was just too difficult to use for the average programmer. Sure, using components or ActiveX controls within Visual Basic was a somewhat trivial matter. However, take a group of programmers and ask how many can implement connection points within ATL or explain the differences between aggregation and containment, and you would see very few hands raised. One of the design tenants of the .NET Framework is to create a “componentized” programming environment without the complexities of COM. COM still exists within the .NET Framework, and many .NET objects are simply wrappers around COM objects, but the complexities of COM itself is almost transparent to the developer. The problem with any major shift in programming paradigms, though, is transitioning from one state to the other. This being the case, the .NET Framework was built to work with components written using COM, and vice versa. This ability to work between managed .NET objects and objects written using COM is known as COM Interop. In this hour you will learn: • How the .NET Framework is designed to handle .NET assembly and COM object communication • How to instantiate and use a COM object within a managed C++ application • How to use a .NET assembly and its associated classes within a native C++ application using COM
COM Interop Design One way the .NET Framework solves cross-language interoperability is through the use of common data types. A String object in Visual C++ .NET is the same as a String object in C# or VB .NET. With this feature, the developer doesn’t have to worry about type conversions using C++ casts or implementing some sort of data-conversion routines. However, the data types used within COM are not the same as the data types in the .NET Framework. To achieve communication between COM objects and .NET assemblies, parameters and return values have to use a process known as marshaling. Marshaling is the packaging and sending of parameters and return types to an intermediate object, sometimes called a proxy, where it is then converted and unpackaged for the receiving object. One of the nice things about the design of COM Interop within the .NET Framework is that this marshaling process can be done for the most part by the common language runtime (CLR). However, if there is a data type in which no suitable substitute exists, that data type will either be converted to a similar albeit slightly different data type or may not be marshaled at all. If the latter case occurs, you must implement a custom object that performs the necessary marshaling for you.
COM with .NET
309
When a .NET object calls a COM object, the runtime creates a runtime callable wrapper (RCW). The RCW is responsible for the marshaling behavior described earlier. In other words, whenever a .NET object calls an interface method of a COM object, the method call is routed to the RCW, marshaling is performed, and the RCW then calls the appropriate interface method in the COM objects. However, when a COM object wishes to call methods within a .NET object, a COM callable wrapper (CCW) is created (see Figure 21.1). FIGURE 21.1 Communication between .NET objects and COM objects using a CCW and an RCW.
Interface
Interface
CCW
COM Client
Interface
COM Object
.NET Object
Interface
RCW
.NET Client
In order for the wrappers that are created at runtime to effectively marshal data, they need to know the data types beforehand. As mentioned in earlier hours, every .NET assembly contains an assembly manifest. Within this manifest is all the necessary information about data types and the data members of that assembly. Likewise, a COM object contains a type library that performs a similar function. It would seem reasonable that to use a .NET object within a COM client, the manifest of the .NET object must be converted into a type library, and vice versa. This is, in fact, what you will do when creating the projects for this hour. After you create the .NET project, you will add a custom build step that takes the information contained within the assembly manifest and creates a type library that can then be used by a COM client. Furthermore, you will use the information within the type library of a COM object and create an assembly manifest that is used by the RCW.
Using COM Objects within .NET As you make the transition to .NET, you will probably find yourself using COM objects within .NET more than .NET objects within a COM project.
21
310
Hour 21
Creating the COM Object For the first lesson this hour, you will create a simple COM object using ATL and use the object within a managed C++ application. The first step is to create the ATL project and implement the COM object. Create a new project by selecting New, Project from the File menu. Select Visual C++ Projects from the list of project types and select the ATL Project template in the list of project templates. Give your project the name ATLCOMServer and click OK to create the project. Accept the default project settings by clicking Finish in the ATL Project Wizard. The COM object you will be creating is the beginning of a simple system information object. It contains one interface that, in turn, contains one method and one property. Click Project, Add Class on the main menu. In the Add Class dialog that’s displayed, select the ATL Simple Object template, as shown in Figure 21.2, and close the dialog by clicking Open. FIGURE 21.2 Creating an ATL Simple Object.
In the ATL Simple Object Wizard, enter the name SimpleSysInfo in the Short Name field, as shown in Figure 21.3. Accept all the default options for this object by clicking Finish. As mentioned earlier, your object will contain a single method. This method will be used to retrieve the current name of the machine the object is being run on by returning the results of the GetComputerName WIN32 API function. To add a method to an interface, expand the ATLCOMServer project in the Class View tree. Next, expand the Interfaces item. You should see the ISimpleSysInfo interface listed, as shown in Figure 21.4. Right-click this interface and select Add, Add Method in the context menu.
COM with .NET
311
FIGURE 21.3 ATL Simple Object options for the SimpleSysInfo object.
FIGURE 21.4 Adding an interface method using Class View.
When the Add Method Wizard is displayed, give your method the name GetComputerName. Because interface functions return an HRESULT and you also need to return the computer name back to the caller, you have to create an out parameter. In the Parameter Type combo box, select the BSTR* parameter type. This will enable the Out and Retval check box under the Parameter Attributes heading. Check the Out and Retval check box. By doing this, you are creating a parameter that is going to be used to return a result back to the caller. In other words, the caller will pass you a buffer that you, in turn, will fill with the name of the computer. Give your parameter the name sComputerName and click the Add button to add the parameter to the parameter list, as shown in Figure 21.5. Click Finish to create the new interface method.
21
312
Hour 21
FIGURE 21.5 Specifying the method attributes using the Add Method Wizard.
After you click Finish to create the method, the IDE will open the SimpleSysInfo.cpp file for you (if it isn’t already open) and automatically scroll down to your new method. This method, as already mentioned, will use the GetComputerName function contained within the WIN32 API. However, because we are working within COM, you cannot simply pass the BSTR variable to the GetComputerName function because it expects a pointer to a regular character string. However, because you are using ATL, you can take advantage of the string-conversion macros it contains. Before you can use any of the ATL stringconversion macros, you must first insert the USES_CONVERSION macro, as shown in Listing 21.1, at the beginning of your function block. This macro is responsible for the variables and function calls necessary to perform the appropriate conversions. LISTING 21.1
Implementing the GetComputerName Interface Method
1: STDMETHODIMP CSimpleSysInfo::GetComputerName(BSTR* sComputerName) 2: { 3: USES_CONVERSION; 4: 5: if( !sComputerName ) 6: return E_POINTER; 7: 8: DWORD dwSize = MAX_COMPUTERNAME_LENGTH + 1; 9: TCHAR sTemp[ MAX_COMPUTERNAME_LENGTH + 1 ]; 10: 11: if( !::GetComputerName( sTemp, &dwSize ) ) 12: return E_FAIL; 13: 14: *(sComputerName) = T2BSTR( sTemp ); 15: 16: return S_OK; 17: }
COM with .NET
313
Following the USES_CONVERSION macro, line 5 of Listing 21.1 checks to make sure the caller-supplied buffer is valid and, if not, returns an HRESULT failure code. Next, declare the necessary local variables and call the GetComputerName function. Notice that because your interface method has the same name as the GetComputerName API call, you will need to preface the function call with the scope resolution operator (::). Finally, using the ATL string-conversion macro, which converts a native character string into a BSTR, assign the result to the caller-supplied buffer and return, as shown on line 14 of Listing 21.1. Although you currently have no way of testing the functionality of your object yet, it would be a good idea to compile your project before continuing. Now you are going to add a property to the object. This property will actually perform the same functionality as the method you just added. Note that you would normally not have a property and function that perform the same tasks, but this is for illustration purposes only. Right-click the ISimpleSysInfo interface in Class View like you did earlier, but this time select Add, Add Property. In the Add Property Wizard, select the BSTR property type and give your property the name ComputerName, as shown in Figure 21.6. Click Finish to close the dialog. FIGURE 21.6 Creating the ComputerName
property for the ISimpleSysInfo
interface.
As it did when you added a new method, the IDE will open the SimpleSysInfo.cpp file and scroll to the functions you just created. A property can contain a get function and a put function. Also, because you accepted the defaults when you created the property, the wizard has generated both functions for you. All that is left to do is implement these functions. The get_ComputerName function has the exact same signature (other than the name of the function, of course) as the interface function you added earlier. Because they both serve the same purpose—to return the computer name—you can simply copy the implementation for the GetComputerName function and place it within the get_ComputerName
21
314
Hour 21
function. However, because the parameter name is different, you will have to change the code accordingly, as shown in Listing 21.2. The put_ComputerName function is used to change the computer name. Because this is just a lesson and you’re not creating a real shipping COM object, you might not want to implement this function. Listing 21.2 returns E_NOTIMPL, which is an HRESULT failure code signifying that the function is not implemented. LISTING 21.2 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24:
Implementation of the ComputerName Property
STDMETHODIMP CSimpleSysInfo::get_ComputerName(BSTR* pVal) { USES_CONVERSION; if( !pVal ) return E_POINTER; DWORD dwSize = MAX_COMPUTERNAME_LENGTH + 1; TCHAR sTemp[ MAX_COMPUTERNAME_LENGTH + 1 ]; if( !::GetComputerName( sTemp, &dwSize ) ) return E_FAIL; *(pVal) = T2BSTR( sTemp ); return S_OK; } STDMETHODIMP CSimpleSysInfo::put_ComputerName(BSTR newVal) { // not implemented return E_NOTIMPL; }
Creating the .NET Client Now that you have a working COM object, you can now learn how to use it within a managed application. Before you create the managed C++ application, however, you must change some of the build properties of the COM object to enable COM Interop. As mentioned at the beginning of this hour, COM Interop is accomplished by creating wrappers around the object you want to interoperate with. In this case, you will be creating a runtime callable wrapper (RCW) by running a utility provided by Visual Studio .NET on the type library that’s generated whenever your COM DLL is built. This utility is called TlbImp.exe. Don’t let the name confuse you, though. Although it sounds like it imports
COM with .NET
315
a type library, it doesn’t. Rather, it creates a separate DLL, which is an assembly that runs within the common language runtime (CLR). This assembly contains the RCW that your .NET client will access. The RCW then performs the necessary marshaling and calls the COM object acting on the .NET client’s behalf. You can set up the TlbImp.exe tool to run each time you build your project so that you don’t have to manually run it each time. In Class View, right-click the ATLCOMServer project and select Properties from the context menu. In the ATLCOMServer Property Pages dialog that’s displayed, select Build Events, Post-Build Events from the list on the left side of the dialog. Select All Configurations from the Configuration drop-down box, as shown in Figure 21.7, because you want to build the .NET assembly in both Debug and Release modes. By doing this, you are specifying a tool you want to run after your project has been built. FIGURE 21.7 Project properties to implement a custom build step.
In the Command Line field, you should already see a tool being run—the regsvr32 tool. This tool is responsible for registering your COM object in the Registry. Click in the Command Line field and then click the button with the ellipsis (. . .) displayed at the end of the field. This will bring up the Command Line dialog, which allows you to specify more than one command. Add a carriage return after the regsvr32 command to begin a new command. Enter TlbImp.exe and the parameters shown in Figure 21.8. The first command-line argument specifies the DLL from which you want to extract type information. The second command-line argument is optional but recommended. It tells the tool to create a new DLL rather than overwrite the existing one. The macros within the dollar signs can be found by clicking the Macros button, selecting the appropriate macro, and then clicking the Insert button. For this project, use the same DLL name with the letters ASM at the end to make a distinction between the two files. Click OK to close the Command Line dialog.
21
316
Hour 21
FIGURE 21.8 Using the Command Line dialog to specify custom build steps.
In order to avoid any path issues with your COM object DLL while you run your .NET application, click the Linker item on the left side of the Configuration dialog. Change the Output File field on the right to ../bin/ATLCOMServer.DLL. When you create your .NET application, the executable for the project will be placed in the same location. You can now close the Property Pages dialog and build your COM object project. You shouldn’t have any compilation errors, but because you have no way of testing the object, you cannot check for logical errors yet. Now you can create the managed .NET application that will use the COM object you just created. Select New, Project from the main menu. Select Visual C++ Project from the list of project types and select the Managed C++ Application project template. Make sure the Add to Solution radio button is selected. Then, give your new project the name ManagedClient and click OK to create the project. The TlbImp.exe tool created an assembly that can be used within managed applications. Because this is the case, you don’t need to do anything else to set up the communication between your .NET client and the COM object. It behaves just like all the other assemblies and classes contained within those assemblies. Therefore, in the ManagedClient.cpp file, import the ATLCOMServerASM.dll file with the #using keyword, as shown in Listing 12.3. The assembly that was created also creates a default namespace that contains the interfaces and their associated coclasses. By default, this namespace is the same name as the DLL without the file extension. Therefore, add the appropriate namespace declaration, as shown on line 9 of Listing 12.3
COM with .NET
LISTING 12.3 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43:
317
Using COM Objects within .NET
#include “stdafx.h” #using #using #include using namespace System; using namespace ATLCOMServerASM; void ComputerNameTest() { // create interop class CSimpleSysInfoClass *SysInfo = new CSimpleSysInfoClass; // get machine name String *sComputerName = SysInfo->GetComputerNameA();18: Console::WriteLine( “Computer Name as method: {0}”, sComputerName ); // access ComputerName property Console::WriteLine( “Computer Name as property: {0}”, SysInfo->ComputerName ); // attempt to set the computer name try { SysInfo->ComputerName = S”vcsmarkhsch3”; } catch( Exception* e) { Console::WriteLine( “Setting computer name returned: \ {0}\r\n”, e->Message ); } } // This is the entry point for this application int _tmain(void) { ComputerNameTest(); return 0; }
21 Before we get into the implementation details, it’s important to discuss what differences exist between the original COM object and the RCW assembly just created. When an assembly is created from a type library, the tool has to take into account the fundamental
318
Hour 21
design differences between the two technologies. First of all, objects within the .NET Framework do not automatically contain an interface for IUnknown or IDispatch as they do for COM objects. Because they do not exist and there are no equivalent interfaces, the TlbImp tool simply removes those interfaces. Because IUnknown is used for object lifetime, it is not needed because object lifetime is contained and controlled by the common language runtime. For each coclass contained within a COM server, an equivalent class is created within the .NET assembly that is generated, and Class is appended to the coclass name. In order to avoid the casting of interface pointers as you work with the new managed class and also to keep in line with the design of the .NET Framework, the RCW .NET assembly will flatten all interface members within a coclass. In other words, if a coclass implements several interfaces, the resulting .NET assembly will gather all the interface methods and properties for each implemented interface and place these within the managed class without using interfaces. Of course, one problem that is immediately apparent involves method and property name collisions. If one interface contains a method named A and another interface contains a method also named A, these two methods will collide when they are flattened within the managed class. To overcome this obstacle, the TlbImp tool will change the name of any colliding members by prefixing these members with the interface that implements them, followed by an underscore and the data member’s original name. Therefore, in the example just mentioned, the first method will remain the same, A, whereas the next, colliding function will be renamed InterfaceName_A. Because the COM object you created does not implement more than one custom interface, no name collisions will occur. However, it is important to know the differences between the types within the COM object and the resulting RCW .NET assembly that is created. Using the objects within the created RCW assembly is similar to using any other .NET Framework object. In the ManagedClient.cpp file, create a function named ComputerNameTest. It does not need to accept any parameters and can return void. To instantiate the class that represents the COM object, use the standard method of instantiating .NET objects with the C++ keyword new. This can be seen on line 14 of Listing 21.3, in which an object named CSimpleSysInfoClass is created. After creating the object, you can call its various methods and parameters, just as you would with any other .NET object. If you recall, when you created the COM object, you created an interface method named GetComputerName that accepts a BSTR as its parameter. However, because you are working within the CLR, there is no such data type as BSTR. Instead, use the equivalent data type, which is the System::String data type contained within the .NET Framework. The RCW will perform the necessary conversions from a String object to a BSTR, and vice versa.
COM with .NET
319
Following the call to GetComputerName, print out the ComputerName property. Because you are not assigning the property to a value, the get_ComputerName interface method will be invoked. Although you did not implement the setter function for the ComputerName property, call it anyway. You can see, starting on line 28 of Listing 21.3, that the code to set the ComputerName property is wrapped within a try-catch exception block. The .NET Framework does not use HRESULT as COM does. Instead, the RCW will convert any HRESULT errors it receives by throwing exceptions instead. If you are returning E_NOTIMPL within the setter function of your ComputerName property, the exception message you receive reads, “Not implemented.” If there is no equivalent exception within the .NET Framework’s COM Interop namespace that can be mapped from an HRESULT, as is the case with a custom HRESULT, then a generic exception is thrown instead. To finish your project, add the function call to ComputerNameTest within your _tmain function. Also, just as you did with the project properties of your COM object, change the Output File path to ../bin/ManagedClient.exe. Once you compile and run your application, your output should look similar to Figure 21.9. FIGURE 21.9 Output of the .NET client calling the COM object.
Using .NET Objects Within COM Projects Now you are going to reverse the communication flow by using managed objects within a COM project. Although there are some differences expected, the process of using .NET Framework objects in COM is similar to the process you just completed in the last project. This time, you will be creating a type library based on the information contained within the .NET assembly of your managed application. Rather than start from scratch, you are going to start with the project named ManagedInterfaces, which you created in Hour 17, “Managed Interfaces.” If you do not have that project, you can download the code for the book on the book’s Web site. Open the ManagedInterfaces project in Visual Studio .NET. Next, create a custom build step that runs a tool called RegAsm.exe. This tool performs two functions. It first creates a type library containing COM data types that were created from the data contained
21
320
Hour 21
within the assembly manifest. The next function RegAsm.exe performs is to register that type library, just as you would register any other COM object. However, there is one difference to this registration process, which will be explained later. Right-click the DogLib project in Class View and select Properties. Click Build Events to expand that item and then select Post-Build Event. Open the Command Line dialog by clicking the button labeled with an ellipsis found on the far right side of the Command Line field. There are three command-line arguments for RegAsm.exe that you will use for this project. The first is the input file, which is the binary executable created when you build your managed project. This is represented by using the “$(TargetPath)” macro. The second parameter, /codebase, is optional but valuable. Without this argument, the RegAsm tool will fail because the assembly from which you are trying to create a type library does not exist within the Global Assembly Cache (GAC). In order to run the RegAsm tool without specifying the /codebase parameter, you must first run the gacutil.exe tool to import your assembly into the GAC. In fact, this is the recommended solution, but the process explained here is more than adequate for testing purposes. By using the /codebase parameter, however, RegAsm will place an entry into the Registry with the path to your assembly so that the CCW created when using COM Interop will know where to find your assembly. Finally, the last parameter to the RegAsm tool is the path to the type library file that will be created. Using macros, specify this path using /tlb:”$(TargetDir)\$(TargetName).tlb” (see Figure 21.10). Also, just as you did with the Output File path for the previous projects, change the Output File path to place the project binaries within the ../bin folder. FIGURE 21.10 Creating the RegAsm.exe custom build step to create a type library from an assembly.
Now you can create the COM client that will use your .NET object. Select Add, Project from the main menu. Select the Visual C++ Project type from the list of project types. You will not be using ATL for this application, so select the Win32 Project template from the list of templates. Make sure the project you are creating is placed within the same solution as your .NET application by clicking the Add to Solution radio button. Give your project the name DogClientCom and click OK to close the dialog. This project will run
COM with .NET
321
in the console, so click Application Settings within the Win32 Application Wizard and then click the Console Application radio button under the Application Type heading, as shown in Figure 21.11. FIGURE 21.11 Creating the Win32 console application by changing the application settings.
For this project, you are going to take advantage of COM smart pointers. These are created for you by using the C++ preprocessor directive #import. After the inclusion of stdafx.h in the DogClientCom.cpp file, import the path of the type library that is created whenever your .NET project is built. You can see this on line 2 of Listing 21.4. Next, create a function that will be used to test the interaction between the COM client and the .NET object. Give this function the name CreateDogs as well as a return type of void and an empty parameter list. LISTING 21.4 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16:
Using .NET Objects Within an Unmanaged COM Application
#include “stdafx.h” #import “../bin/DogLib.tlb” void CreateDogs() { USES_CONVERSION; DogLib::IDogPtr pBulldog( __uuidof(DogLib::CBulldog)); DogLib::IDogPtr pPoodle( __uuidof(DogLib::CPoodle)); pPoodle->Name = L”Fifi”; pBulldog->Name = L”Butch”; printf( “The poodle named %s says %s and has %d fleas.\n”, OLE2A(pPoodle->Name), OLE2A(pPoodle->Bark()), pPoodle->NumberOfFleas );
21
322
Hour 21
LISTING 21.4 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31:
continued printf( “The bulldog named %s says %s and has %d fleas.\n”, OLE2A(pBulldog->Name), OLE2A(pBulldog->Bark()), pBulldog->NumberOfFleas );
} int _tmain(int argc, _TCHAR* argv[]) { CoInitialize(NULL); CreateDogs(); return 0; }
The CreateDogs function creates two COM smart pointers and instantiates two objects, as shown on lines 7 and 8. Following that, the function finishes by calling the methods and parameters of these objects. However, if you inspected Listing 21.4 closely, you may have noticed an error. On lines 7 and 8, the code refers to the CBulldog class and the CPoodle class within the DogLib namespace. However, the DogLib namespace doesn’t contain these classes. This was done to circumvent problems associated with COM Interop and the way the type library is created from the assembly manifest. Within the .NET object are three main interfaces: a base interface named IDog and two derived interfaces named IPoodle and IBulldog, respectively. However, these interfaces were all implemented by the same class. This method of interface inheritance doesn’t work within the architecture of COM. Interfaces within COM inherit from either IUnknown or IDispatch. Therefore, the three interfaces within the type library that was created from the assembly manifest lose the interface inheritance they employed. Because the IPoodle interface does not implement the Name property but rather inherits the IDog interfaces implementation, it does not contain a Name property when it is used within COM. To solve this problem, you must create three separate coclasses and move the inheritance logic from the interfaces to the classes themselves. This can be seen in Listing 21.5. LISTING 21.5 1: 2: 3: 4: 5: 6: 7:
Creating the Managed .NET Object
// DogLib.h #pragma once using namespace System; namespace DogLib
COM with .NET
LISTING 21.5 8: { 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: }
323
continued public __gc __interface IDog { String* Bark(); __property void set_Name( String* sName ); __property String* get_Name(); __property Int32 get_NumberOfFleas(); }; public __gc __interface IPoodle : public IDog { String* Bark(); __property Int32 get_NumberOfFleas(); }; public __gc __interface IBulldog : public IDog { String* Bark(); __property Int32 get_NumberOfFleas(); }; public __gc class CDog : public IDog { public: void set_Name( String* sName ) { m_sName = sName; } String* get_Name() { return m_sName; } String* Bark() { return “Woof Woof!”; } Int32 get_NumberOfFleas() { return 20; } private: String* m_sName; }; public __gc class CPoodle : public CDog, public IPoodle { public: // IPoodle interface String* Bark(){ return “Yap Yap!”; } Int32 get_NumberOfFleas() { return 100; } }; public __gc class CBulldog : public CDog, public IBulldog { public: // IBulldog interface String* Bark(){ return “Grrr Grrr!”; } Int32 get_NumberOfFleas() { return 2; } };
21
324
Hour 21
Before you compile the COM client, change the Output File path to place the binary in the ../bin directory. Running your application should produce output similar to Figure 21.12. FIGURE 21.12 Output of COM client using a managed .NET object.
Summary For those of you who have worked with COM and especially ATL, you might be saddened to hear people mention that they are being slowly phased out as the .NET platform becomes more mainstream. Although this may certainly be the case, I don’t believe it will happen anytime soon. Too many COM objects are currently in the marketplace, and to disable their functionality would have disastrous results. Furthermore, a new ATL feature, ATL Server, was introduced with Visual Studio .NET, which goes to show that Microsoft is still committed to COM and the powerful ATL library. During this hour, you saw how COM Interop plays a big role when you need to use existing objects within the .NET Framework. Also, you saw how some of the newer .NET objects can be used within older COM-based applications. Without this interoperability model in place, you would have no choice but to spend time and money converting code bases to support the new technologies.
Q&A Q Can ActiveX controls created with Visual C++ 6 be used within Visual Studio .NET? A Definitely. If you use VB .NET or C# .NET and Windows Forms, the process is rather trivial. However, because Visual C++ .NET does not contain some of the Rapid Application Design (RAD) tools the other .NET languages have, the process is a little more complicated.
COM with .NET
325
Q How can I inspect the globally unique identifiers (GUIDs), interface identifiers (IIDs), interfaces, and coclasses that are generated in the type library from an assembly manifest? A Use the OleView tool provided with Visual Studio .NET. Once you run the tool, select File, View Typelib from the main menu and navigate to your generated type library. Q The RegAsm tool creates a type library, but COM servers can only be DLLs or EXEs. What’s going on? A This is where you can see the CCW firsthand. If open the Registry and look for the CLSID you found with OleView, you’ll notice that the InprocServer32 Registry value is mscoree.dll. This file will create a CCW for your .NET object that can be used by COM clients.
Workshop The Workshop provides quiz questions to help solidify your understanding of what was covered in this hour. Answers are provided in Appendix A, “Quiz Answers.”
Quiz 1. If you use a .NET object within a COM-based application, do you need to release any interface pointers you obtain or does the common language runtime still perform garbage collection? 2. What happens when a parameter to a method within a .NET object is a custom data type that you created and you wish to build a type library using the assembly that contains this custom data type? 3. If TlbImp.exe creates an assembly, then what does the tool TlbExp.exe do and how is it different from the RegAsm.exe tool used during this hour?
21
HOUR
22
Mixing Managed and Unmanaged Code During the last hour you learned how to utilize COM Interop to communicate between .NET and COM. Although COM is certainly pervasive throughout the operating system, the WIN32 API is still contained in dynamic link libraries and exported as C-style functions with certain calling conventions. Because the WIN32 API was built before .NET came into existence, the API functions obviously all run within an unmanaged environment. Even though the .NET Framework can handle a wide variety of tasks, there may still be times when you need to invoke a function contained within a DLL. Include the fact that you may already have libraries used with some of your current projects, and you can see why this interoperability is important. There may also be times when you want to write unmanaged code within your managed application. Although this isn’t recommended, you may gain a slight performance gain because you are bypassing the common language runtime (CLR) and running native machine code. Visual C++ .NET has an advantage over the other .NET languages because it allows you to easily create a project with a combination of managed and unmanaged code.
328
Hour 22
In this hour you will learn: • How to create and utilize an unmanaged C++ class within a managed application • How Platform Invocation (P/Invoke) allows you to call functions within an unmanaged dynamic link library • How method signatures are created when custom data types are involved • How to change the way parameters are marshaled when using library functions
Unmanaged and Managed Code Together One thing that has plagued the C++ language and is often brought up in critique circles isn’t the fault of the language but rather is due to developer errors. Memory allocation and pointers contain several advantages, but when used incorrectly, they can lead to slow system degradation. If a program that is running continuously allocates but never frees that allocated memory, you’ll soon see the system on which the application is running come to a grinding halt. The .NET Framework and its garbage-collection mechanism were designed to prevent this from happening. Objects are allocated by the developer, but the act of releasing that memory once an object is destroyed is left up to the garbage collector. However, some C++ developers feel this is removing some of the power of the language by adding an extra layer that could easily be developed by them. Garbage-collection arguments will probably go on forever, but the fact remains that each method of memory allocation and deallocation has its advantages and disadvantages. Even though your application runs within the common language runtime, you still have the ability to run portions of your code in an unmanaged environment. You’ll recall that to declare a class as managed you use the __gc keyword. If that keyword is absent from the class declaration, the class by default is an unmanaged class. Furthermore, to remove any inconsistencies within your code, you can also use the __nogc keyword to designate a class as unmanaged. For this lesson, you will be creating an unmanaged C++ class and using it within a managed application. To begin, select New, Project from the File menu. Select the Visual C++ Projects project type and select the Managed C++ application project template. Give your project the name PInvoke and click OK to create the project. Open the PInvoke.cpp file and create an unmanaged class named CSystemTime by preceding the class keyword with the __nogc keyword. Believe it or not, that is all you need to do to create an unmanaged class. You are free to add member functions and member variables, with one exception: Even though you can use .NET objects within a member function, you cannot declare any .NET objects as member variables unless they are wrapped with the type-safe wrapper template gcroot. Because the String object is
Mixing Managed and Unmanaged Code
easy to use, you will create a String member variable within your unmanaged class. In order to use the gcroot template, you must include the vcclr.h header file. Place the preprocessor directive for vcclr.h to include it in your project, as shown on line 4 of Listing 22.1. To include a .NET object as a variable in an unmanaged class, you must use the gcroot object as just mentioned. Because this is a template, it expects a type as the template parameter. In this case, you are using a String object. Declare a String* member variable using the gcroot template, as shown on line 24 of Listing 22.1. Within the constructor of your class, create a new instance of the variable just as you would do with a regular String object. In order to test the class, create a public member function named SayHello that accepts no parameters and returns void. Within that function body, output the string variable you created earlier using Console::WriteLine. LISTING 22.1 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31:
Creating an Unmanaged Class Within a Managed Application
#include “stdafx.h” #using #include #include using namespace System; __nogc class CSystemTime { public: CSystemTime() { m_strHello = new String(“CSystemTime says...”); }; void SayHello() { Console::WriteLine(m_strHello); } private: gcroot m_strHello; }; // This is the entry point for this application int _tmain(void) { CSystemTime* pSysTime = new CSystemTime;
329
22
330
Hour 22
LISTING 22.1 32: 33: 34: 35: 36: 37: 38: }
continued pSysTime->SayHello(); // comment the following line to create a memory leak delete pSysTime; return 0;
Now that you have created your unmanaged class, you can use it within managed code. Using an unmanaged class is exactly the same as using a native C++ class. Within your _tmain function, instantiate an instance of the CSystemTime class and call the SayHello function. You aren’t finished, however. Because you are using an unmanaged class, you must make sure you delete the memory that was allocated, as shown on line 40 of Listing 22.1. You can now compile and run your application. You’ll see that the managed code works with your unmanaged class just fine. Your output should appear similar to what’s shown in Figure 22.1. FIGURE 22.1 Calling unmanaged code within a managed application.
Unmanaged Code Can Introduce Memory Leaks I’ll go ahead and admit it now: When I first started looking into mixing managed and unmanaged code, I was a little skeptical. I wasn’t certain that the unmanaged class I created was really unmanaged, and the CLR didn’t have anything to do with it. So, I created a little test, which you will now do. As mentioned at the beginning of this hour, memory leaks are a result of allocating memory and then not releasing that memory once your application exits. The C-Runtime library contains a mechanism that allows you to detect any memory leaks within your application and also shows you where these leaks occur. The method to do this is quite simple and should be employed as much as possible. To
Mixing Managed and Unmanaged Code
enable memory leak detection in an application, you must first define a preprocessor definition named _CRTDBG_MAP_ALLOC and include the header file crtdbg.h. Place the following code near the top of the PInvoke.cpp file: // enable memory leak detection #define _CRTDBG_MAP_ALLOC #include
The next step is to tell the C runtime to dump any memory leaks. This is done by placing a call to _CrtDumpMemoryLeaks at any exit points within your application. Because this is a simple application, there is only one exit point at the end of the _tmain function. Before you run your application, however, you need to create a memory leak. The only place this can happen is with the CSystemTime object you created within the _tmain function. All other variables that you use are managed and therefore subject to garbage collection. To create the memory leak, comment out the line that deletes the pSysTime object within _tmain. In order to see the output that contains memory leak information, you must run your application in debug mode. After compiling your application, click Debug, Start from the main menu. After your application has finished running, the memory leak information, as well as other debug statements issued by the WIN32 API, is contained within the Output window in the Visual Studio .NET IDE. Open the Output window if it isn’t visible and scroll up a few lines. If everything has worked correctly, you will see a line that reads “Memory Leaks Detected,” as shown in Figure 22.2. If you then uncomment the line and make sure the CSystemTime object is being deleted, you will see that once you run the application in debug mode, the memory leak is removed. This demonstrates that when you mix unmanaged code within a managed application, you are responsible for freeing any unmanaged objects before your application exits. FIGURE 22.2 Forgetting to delete unmanaged objects results in memory leaks, which can deteriorate a system quickly.
Platform Invocation If you were to immediately start using the WIN32 API or any of the native C++ data types within your managed application, you would lose the benefits the .NET Framework provides. Although you are certainly free to mix unmanaged and managed code together, doing so can lead to future problems, such as frequent data type conversions, which you
331
22
332
Hour 22
may need to perform as your code executes in and out of the common language runtime. If you still want to execute your code within a managed environment while still retaining the ability to call unmanaged dynamic link library functions, you can use a method known as Platform Invocation, otherwise known as P/Invoke. P/Invoke provides the ability to call unmanaged DLL functions using managed code. As you’ll recall from the last hour, the runtime callable wrapper (RCW) and the COM callable wrapper (CCW) enable communication between COM and .NET objects by marshaling data back and forth between the objects without the developer needing to worry about data type conversions, unless there’s no comparable data type in one of those objects. In much the same way, the P/Invoke service is responsible for locating the entry points within a dynamic link library, and once an entry point is found, P/Invoke marshals the parameters and return values between the managed client and the unmanaged DLL function. The first step in calling a DLL function from managed code involves the use of the DLLImport attribute. Thankfully, using this attribute means you don’t have to worry about loading the DLL dynamically and using the WIN32 API function GetProcAddress to find the appropriate function. This is all handled for you by using P/Invoke.
Using P/Invoke to Call the MessageBox Function With any new technology, it’s best to start out small and work your way up. You’re now going to expand on the project you’ve been working on this hour. As mentioned earlier, the first step is to use the DLLImport attribute. When applied to a function definition, the DLLImport attribute specifies which dynamic link library (DLL) the function you are referencing is contained within. In addition, there are several optional parameters for the attribute which control how that function is defined within your source code. The first function you are going to use is the WIN32 API function MessageBox. Before you can create its signature, however, you need to know which DLL that function is contained within. There are two ways you can do this. The first way is to use the Dependency Walker tool, depends.exe. Using this tool, however, is cumbersome and slow because it involves manually opening a system DLL such as user32.dll or kernel32.dll and looking through the large list of exports to see whether the function you need is there. If it isn’t, you must try a different DLL. Dependency Walker is a great tool, just not for this procedure. Figure 22.3 shows Dependency Walker with the MessageBox function highlighted.
Mixing Managed and Unmanaged Code
FIGURE 22.3 Finding DLL functions using Dependency Walker.
The easiest way to find which DLL contains the function you need is to use the MSDN documentation itself. Click Help, Index in the main menu. In the Look For field within the Index dialog, enter MessageBox function and click that phrase within the index result list. This will open the help documentation associated with the WIN32 API function MessageBox. Scroll down to the bottom of the document and you’ll see a line that says the function is implemented in user32.lib. Because there is a user32.dll file, you can infer that user32.lib is linked into that library and therefore the MessageBox function is contained in user32.dll. The first parameter for the DllImport attribute is the name of the DLL that contains the function you are interested in. Following that are a number of optional parameter attributes you can apply. These optional attribute parameters include the following: •
EntryPoint. Specifies which entry point function to call. This allows you to specify a function by ordinal rather than function name.
•
CharSet.
•
ExactSpelling.
This controls how strings are marshaled to the DLL function. Some functions are designed to accept either Unicode or ANSI strings and will mangle the name to show that distinction. By using the CharSet attribute, you can use the original function name. This is used to change the behavior of the CharSet parameter. indicates that the name of the entry point is the same name as the method definition that follows the DllImport attribute. If this parameter is true and the CharSet parameter is to Ansi, then a letter A will be appended to the function name. Likewise, if the CharSet parameter is set to Unicode, the letter W will be appended. The default value for this parameter is false. ExactSpelling
333
22
334
Hour 22
•
CallingConvention. This is used to coordinate P/Invoke and the DLL function that is being called by specifying the calling convention of the function. The default is WINAPI (__stdcall) and will rarely need to change.
•
PreserveSig.
This is used when invoking functions that return an HRESULT. If PreserveSig is true, a regular 32-bit integer will be returned. If it is false, however, any HRESULTs that are failure codes will throw an exception.
•
SetLastError.
Use the SetLastError parameter if you plan on using the function This is equivalent to calling the WIN32 API function GetLastError. The default for this parameter is false within Visual C++ .NET. Marshal::GetLastWin32Error.
After using the DLLImport attribute, you must then create a function signature for the function you wish to call. However, because you are working within managed code, the parameters to the function signature should be types that can be marshaled successfully from the managed data type to the unmanaged data type. Table 22.1 lists unmanaged data types and their equivalent managed data types. TABLE 22.1 Unmanaged Data Types and the Equivalent Managed Types Used for Marshaling Unmanaged Windows Type
Unmanaged C Language Type
Managed Class Name
HANDLE
void*
System.IntPtr
BYTE
unsigned char
System.Byte
SHORT
Short
System.Int16
WORD
unsigned short
System.UInt16
INT
Int
System.Int32
UINT
unsigned int
System.UInt32
LONG
Long
System.Int32
BOOL
Long
System.Int32
DWORD
unsigned long
System.UInt32
ULONG
unsigned long
System.UInt32
CHAR
Char
System.Char
LPSTR
char*
System.String or System.StringBuilder
LPCSTR
Const char*
System.String or System.StringBuilder
LPWSTR
wchar_t*
System.String or System.StringBuilder
LPCWSTR
Const wchar_t*
System.String or System.StringBuilder
FLOAT
Float
System.Single
DOUBLE
Double
System.Double
Mixing Managed and Unmanaged Code
The process of creating a function signature is similar to declaring any other function. However, because the function is not implemented in your assembly, the extern keyword must precede the actual definition. Before you add the code to define the MessageBox function contained within user32.dll, import the System::Runtime::InteropServices namespace. Next, define the MessageBox function by first creating the DllImport attribute and then creating the function definition with the equivalent managed data types. This is done as follows: [DllImport(“user32.dll”, ExactSpelling=true, CharSet=CharSet::Ansi)] extern int MessageBoxA(void* hWnd, String* pText, String* pCaption, unsigned int uType);
Within the SayHello function contained in the CSystemTime class, call the MessageBoxA function you just defined: MessageBoxA( 0, “Hello World”, “CSystemTime”, 0 );
Compile and run your application. If everything has worked correctly, you will see a message box similar to the one shown in Figure 22.4. FIGURE 22.4 Invoking the MessageBox function contained within user32.dll.
Using P/Invoke for Custom Data Types Most of the functions within the WIN32 API can easily be marshaled because their parameters have a similar managed data type. However, some functions may expect a struct as a parameter. To solve this, you must define a data structure within your source file whose members are equivalent to the data structure the DLL function expects to receive. To demonstrate this process, you will be using the WIN32 API function GetLocalTime, which expects a SYSTEM_TIME data structure as its only parameter. The process to do this is similar to creating the function definition. When you define a structure that will be passed via P/Invoke, you need to be aware of two things. First of all, your structure or class must be a managed type specified by using the __gc keyword. Second, you must specify how the structure is laid out by using the StructLayout attribute. This attribute can contain one of three different parameters. A
335
22
336
Hour 22
structure with a layout defined as LayoutKind::Automatic means that the runtime will choose an appropriate layout in memory for that object. Using this is not recommended because some functions expect the structure to be in a certain form when laid out in memory. A structure can also be described using LayoutKind::Sequential, which means the structure is laid out in memory the same way it is defined. In other words, the members of the structures follow each other sequentially in memory. This layout type is the most commonly used. The last layout type is LayoutKind::Explicit. By using this layout type, you must add additional parameters that describe the sizes of each of the data types. Although this gives you a greater level of control, it is error prone and would be hard to manage across different platforms. Create a managed structure named SystemTime that is arranged using a StructLayout of LayoutKind::Sequential, as shown on line 16 of Listing 22.2. Because each of the member variables within the SYSTEM_TIME structure are WORDs, define each of the parameters of your structure using the UInt16 managed data type. Next, create the GetLocalTime function definition, which is contained in kernel32.dll, similar to the way you did for the MessageBox function. This can be seen on line 39 of Listing 22.2 Create another public member function within the CSystemTime class named DisplayTime with no parameters and a void return type. Within this function, create an instance of the SystemTime structure, because the GetLocalTime function expects the memory to already be allocated. Call the GetLocalTime function using the structure variable you just created. Finally, using String objects and the String::Format function, output the results you received from the GetLocalTime function. LISTING 22.2 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17:
Using Custom Data Types and Marshaling Instructions for P/Invoke
#include “stdafx.h” #using #include #include #include // enable memory leak detection #define _CRTDBG_MAP_ALLOC #include #include using namespace System; using namespace System::Runtime::InteropServices; [StructLayout(LayoutKind::Sequential)] __gc struct SystemTime
Mixing Managed and Unmanaged Code
LISTING 22.2 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65:
337
continued
22
{ UInt16 UInt16 UInt16 UInt16 UInt16 UInt16 UInt16 UInt16
wYear; wMonth; wDayOfWeek; wDay; wHour; wMinute; wSecond; wMilliseconds;
}; [DllImport(“user32.dll”, ExactSpelling=true, CharSet=CharSet::Ansi)] extern int MessageBoxA(void* hWnd, String* pText, String* pCaption, unsigned int uType); [DllImport(“user32.dll”, ExactSpelling=true, CharSet=CharSet::Unicode)] extern int MessageBoxW(void* hWnd, String* pText, String* pCaption,unsigned int uType); [DllImport(“Kernel32.dll”, CharSet=CharSet::Auto)] extern void GetLocalTime(SystemTime* st); [DllImport(“kernel32.dll”)] extern int MultiByteToWideChar( unsigned int CodePage, unsigned long dwFlags, [MarshalAs(UnmanagedType::LPStr)] String* lpMultiByteStr, int cbMultiByte, [MarshalAs(UnmanagedType::LPWStr)] String* lpWideCharStr, int cchWideChar ); __nogc class CSystemTime { public: CSystemTime() { m_strHello = new String(“CSystemTime says...”); }; void SayHello() { Console::WriteLine(m_strHello); MessageBoxA( 0, “Hello from CSystemTime”, “CSystemTime”, 0 ); } void DisplayTime()
338
Hour 22
LISTING 22.2
continued
66: { 67: SystemTime* sysTime = new SystemTime; 68: GetLocalTime( sysTime ); 69: 70: String* sDate = new String(“”); 71: String* sTime = new String(“”); 72: String* sDateTime = new String(“”); 73: String* sWideDateTime = new String(“”); 74: 75: sDate = sDate->Format( S”{0}/{1}/{2}”, 76: sysTime->wMonth.ToString(), 77: sysTime->wDay.ToString(), 78: sysTime->wYear.ToString()); 79: 80: sTime = sTime->Format( S”{0}:{1}”, 81: sysTime->wHour.ToString(), 82: sysTime->wMinute.ToString()); 83: 84: sDateTime = sDateTime->Format( 85: “It is now {0} on {1}”, sTime, sDate ); 86: 87: MessageBoxA(0, sDateTime, “Today”, 0 ); 88: 89: int numChars = MultiByteToWideChar( 0, 0, sDateTime, 90: -1, sWideDateTime, sDateTime->Length*2 ); 91: 92: MessageBoxW(0, sWideDateTime, “Today”, 0 ); 93: } 94: 95: private: 96: gcroot m_strHello; 97: }; 98: 99: // This is the entry point for this application 100: int _tmain(void) 101: { 102: CSystemTime* pSysTime = new CSystemTime; 103: 104: pSysTime->SayHello(); 105: 106: // comment the following line to create a memory leak 107: delete pSysTime; 108: 109: // dump memory leaks 110: _CrtDumpMemoryLeaks(); 111: 112: return 0; 113: }
Mixing Managed and Unmanaged Code
Specifying Specific Data Types for Marshaling If you looked at the DisplayTime function shown in Listing 22.2, you ran across a few items that haven’t been discussed yet. As mentioned earlier, the common language runtime will use data types that are compatible with one another when marshaling data between managed and unmanaged code. However, the String object can be represented by several different native C data types. If you need to call a function contained within an unmanaged DLL that contains different data types the String object could represent, you must use the MarshalAs attribute when creating the function definition. An example of one of these functions is the MultiByteToWideChar function, which is used to convert a single- or multibyte string to a wide string. The third parameter to that function expects a LPTSTR (char*) data type, and the fifth parameter expects LPWSTR (wchar_t*). However, for the function definition, you want to use a String object for both of these parameters. To accomplish this, use the MarshalAs attribute, which takes a data type as a parameter specified by the UnManagedType enumeration, as shown on lines 46 and 48 of Listing 22.2. In order to test the MultiByteToWideChar function, you will also have to create a function definition for the wide string version of the MessageBox function. This is quite similar to the method you used for MessageBoxA, with the only difference being the entry point name, MessageBoxW. You are now ready to compile and run your application. If everything works correctly, the first message box will be displayed showing that the connection to your unmanaged class has been made. After that message box is dismissed, another message box will be displayed that shows the results of calling the GetLocalTime function using the custom data type. Finally, the last message box that is displayed is the wide version of the message box whose parameters were marshaled by using the MarshalAs attribute. This message box can be seen in Figure 22.5. FIGURE 22.5 Calling MessageBoxW by using the MarshalAs attribute.
339
22
340
Hour 22
Summary During the last two hours, you learned a variety of methods for integrating your legacy libraries and objects with the .NET Framework. If Microsoft hadn’t maintained this layer of interoperability, many developers would have spent lots of time and money converting their code to work in the managed environment. With this new technology comes yet another design decision you have to make when creating new applications. Do you create a managed application that runs within the common language runtime, or do you create an unmanaged application that can be used easily in both unmanaged and managed environments? Although some developers may be wary about creating unmanaged native C++ applications in fear of them later becoming obsolete due to not being supported, rest assured that unmanaged application support will still be around for a while. Only when you start seeing device drivers, computer games, and system DLLs written using the .NET Framework should you start worrying.
Q&A Q What does it mean when a data type is “blittable” or “non-blittable”? A A blittable type is a managed data type that has a common layout in memory in managed and unmanaged environments. In contrast, a non-blittable type has a different representation in memory when it’s managed than it does when it is unmanaged. Q Some WIN32 API functions expect a pointer to a callback function. Can I create a callback function within my managed application? A Yes, you can, although the process is slightly different from creating a regular callback. Create the callback definition with managed types and prefix that definition with the __delegate keyword. You can then pass an instance of that delegate function as the function pointer to the WIN32 API function.
Workshop The Workshop provides quiz questions to help solidify your understanding of what was covered in this hour. Answers are provided in Appendix A, “Quiz Answers.”
Mixing Managed and Unmanaged Code
341
Quiz 1. If you wanted to use MessageBox rather than the ANSI or Unicode functions, what changes need to be made to the DllImport attribute? 2. In an unmanaged class, are you responsible for freeing any memory that is allocated, regardless of the variable type? 3. If a DLL exports a C++ class that you wish to use within managed code, do you have to create definitions for each of the member functions within that class as well as the definition for the class?
22
HOUR
23
Control Class Libraries It’s been estimated that the market for custom controls and components will be in the billions of dollars in the coming years. Beginning with the emergence of COM and ActiveX, companies are realizing the full potential of code reuse through component-based architectures. Through the use of custom controls and components, you can save precious development time and money that is better spent on the more important aspects of your project. Just as Henry Ford revolutionized the manufacturing process with the application of assembly lines, component-based architectures have done the same for software development. Creating custom controls with the previous version of Visual Studio meant using COM and, more specifically, ActiveX controls. Visual Studio .NET has since changed this method to better align with the design goals of the .NET Framework. Although creating ActiveX controls is still possible and necessary in some instances, .NET controls free you from learning the intricacies of COM interfaces and automation compatibility as well as other difficult issues that arise from using ActiveX controls.
344
Hour 23
In this hour you will learn: • How to create a custom control with Visual C++ .NET • How to utilize that control within a managed application using Windows Forms • How to place your control onto the Visual Studio .NET Toolbox • How to create control properties and integrate them into the Property Browser window within the IDE
Controls Within the .NET Framework Even though the .NET Framework contains many controls you can use when creating a project that uses Windows Forms, there may be instances when you need a custom control that either expands on the functionality of an existing control or adds functionality that can’t be found with other controls. In this hour, you will be creating a custom control that can then be used with any of the .NET languages. Creating a custom control is similar to creating any other project that uses Windows Forms. However, rather than creating an executable that you can run to test your application, you’ll create a control that’s contained within a dynamic link library (DLL). Of course, by creating a DLL you are at a disadvantage because you must also create a separate project that uses that control. However, creating a test harness to test your control isn’t any different from creating the Windows Forms applications you’ve already created within this book. Adding your custom control to a Windows Form isn’t any different from adding any of the controls (buttons, labels, and text boxes, for example) that ship with Visual Studio .NET. When creating a new control, you have several options available to you based on what type of design you prefer. These options are all based on what base class your control inherits its base functionality from. You have three main options to choose from: • Inheriting from an existing Windows Form control • Inheriting from the UserControl class • Inheriting from the Control class
Inheriting from an Existing Windows Form Control The easiest option you have is to inherit from an already established control. You would want to do this if there is a control that provides similar functionality to the control you wish to create. Inherit from an existing control if you simply want to add new events or properties while still retaining a majority of the basic functionality of the base control.
Control Class Libraries
345
Inheriting from the UserControl Class A user control is a control that allows you to combine several controls within one single composite control. In other words, a user control contains one or more Windows Form controls combined together into a single entity. An example of a control of this type could be a database-navigation control. You could have several text boxes or labels contained within a single user control as well as buttons that change the values of those controls as the user navigates through the database.
Inheriting from the Control Class The Control class provides the basic functionality needed to create a custom control. By inheriting from this class, it is your responsibility to perform any custom painting by overriding or adding a handler for the Paint event. You would want to use this control if no other control contains similar functionality. For this hour’s lesson, you will be creating a control based on this design.
Creating the Custom Control Project Create a new project by selecting New, Project from the File menu. Select Visual C++ Projects from the list of project types and then select the Managed C++ Class Library from the list of templates. Selecting this template instructs the linker to create a DLL assembly rather than an executable. Give your project the name ShapeControl and click OK to create the project. One thing that is different in the Managed C++ Class Library Project Wizard compared to the Managed C++ Application Wizard is the fact that a namespace and a managed class is automatically created for you. However, the default name for the class is Class1, which obviously isn’t very descriptive. Open the ShapeControl.h file and change the name of the class from Class1 to ShapeControl. As mentioned earlier, your control will use the System::Windows::Form::Control class for its base functionality, so derive your class from the Control class, as shown in Listing 23.1. As you may have guessed, the control you are going to create will draw a certain shape. The control will be able to draw one of two different shapes: a rectangle or an ellipse. Create an enumerated data type in the public section of your class. Give this data type the name Shape. Also, because the enumerated data type is defined within your class, you must preface it with the __value keyword as is the rule with any .NET enumerated types, as can be seen on line 26 of Listing 23.1. Next, create a private member variable that will hold the current value of a Shape object that corresponds to the shape currently being drawn by the control.
23
346
Hour 23
Just as you have done with the Windows Forms projects throughout this book, create a function that will be used to initialize your class each time a new instance of that class is created. Create a function named InitControl that accepts no parameters and returns void. Within that function set your Shape member variable to Shape::Rectangle to set the default shape for your control, as shown on line 55 of Listing 23.1. Furthermore, call the InitializeControl function within your default constructor. LISTING 23.1 Creating a .NET Custom Control 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36:
// ShapeControl.h #pragma once #using #using #using using namespace System; using namespace System::Drawing; using namespace System::Windows::Forms; namespace ShapeControl { public __gc class ShapeControl : public System::Windows::Forms::UserControl { public: ShapeControl() { InitializeControl(); SetStyle(ControlStyles::ResizeRedraw, true); } __value enum Shape { Rectangle = 0, Ellipse = 1 }; protected: void PaintHandler( Object* sender, PaintEventArgs* e ) { System::Drawing::Rectangle rcRect = get_ClientRectangle();
Control Class Libraries
347
LISTING 23.1 continued 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: }
// deflate rect so circle isn’t cut off rcRect.Inflate( -5, -5 ); // Draw the rectangle or circle if( m_eDrawShape == Shape::Rectangle ) e->Graphics->DrawRectangle( new Pen(this->ForeColor, 1), rcRect ); else if( m_eDrawShape == Shape::Ellipse ) e->Graphics->DrawEllipse( new Pen(this->ForeColor, 1), rcRect ); } private: Shape m_eDrawShape; void InitializeControl() { m_eDrawShape = Shape::Rectangle; add_Paint( new PaintEventHandler(this, PaintHandler )); } };
At this point, your control has the necessary information it needs to function, but it is still missing one key piece: the OnPaint event handler. Because you are creating a custom control and not deriving from an already established control, it is your responsibility to provide the code to draw the user interface of the control. In the InitControl function, add an event handler for the OnPaint event by calling the add_Paint member function provided by the base class, as shown on line 57 of Listing 23.1. Next, create a protected member function for the PaintEventHandler delegate. This function will first get the current drawing rectangle by creating a local Rectangle variable. Following this, deflate the rectangle by a small amount. If you don’t do this, the border of the ellipse you draw will be clipped in some places. Finally, based on the current value of your Shape member variable, draw a rectangle or an ellipse, as shown on line 41 of Listing 23.1. You now have a working control. However, as mentioned earlier, you don’t have any way of testing it to see whether it works. For now, compile the project. If there are any errors, double-check your code against the preceding listing.
23
348
Hour 23
Using Custom Controls in a Managed C++ Application In order to test the control you just created, you will have to create an application that uses the control. Select New, Project from the File menu. Select Visual C++ Projects from the list of project types and select the Managed C++ Application template from the list of available templates. In order to prevent having to switch back and forth between the two projects, make sure the radio button labeled Add to Solution is selected. Give your project the name CPPTestHarness and click OK to create the project. The first thing you should do is set some of your project settings so that the two projects can work together. First, change the output path for the ShapeControl project so that the final binary file is placed into a bin directory regardless of whether it is built in debug or release mode. To do this, right-click the project name in Solution Explorer and select Properties. Select All Configurations from the Configurations drop-down box in the Property Pages dialog. Next, change the Output Directory property to point to the bin directory, as shown in Figure 23.1. Click OK to save the changes. The next step is to specify the build order of the two projects. Because the test harness project depends on the DLL of the control project to be present before it can be built, click Project, Project Dependencies on the main menu. In the Project Dependencies dialog, select the CPPTestHarness project in the Project drop-down list and then check the ShapeControl project in the Depends On list box, as shown in Figure 23.2. Click OK to make the changes. Finally, in order for the IDE to launch the test harness executable rather than trying to launch the control DLL when you debug your project, right-click the CPPTestHarness project and click on Set as StartUp Project. FIGURE 23.1 Changing the output directory to place binary files in a common location.
Control Class Libraries
349
FIGURE 23.2 Specifying build dependencies to ensure proper project build order.
23
The test harness itself will consist of a single Windows Form and will contain the control you created earlier. Create a new Windows Form class named CPPTestForm. Create a function named InitForm and call that function within the default constructor. The InitForm function is a private member function that accepts no parameters and returns void. You are now ready to create the custom control and place it on the form. In order to use the control, you must first import the assembly and reference the namespace you wish to use, which in this case is ShapeControl. As explained at the beginning of the hour, instantiating a custom control is similar to creating any other type of Windows Form control. Therefore, create a private member variable that is a pointer to a ShapeControl class, as shown on line 26 of Listing 23.2. The next step is to instantiate a new instance of the ShapeControl class within the InitForm function. The remaining steps simply involve setting the Location property of the control and setting the various other properties of the main form itself, as you have done in previous hours. Finally, ensure that you launch the form by calling Application::Run within your _tmain application entry point. LISTING 23.2 Testing ShapeControl with a Test Harness 1: 2: 3: 4: 5: 6: 7: 8: 9:
#include “stdafx.h” #using #using #using #using #using
#include
350
Hour 23
LISTING 23.2 continued 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53:
using using using using using
namespace namespace namespace namespace namespace
System; System; System::Drawing; System::Windows::Forms; ShapeControl;
__gc class CPPTestForm : public Form { public: CPPTestForm() { InitForm(); } private: ShapeControl* m_pShapeControl; void InitForm() { m_pShapeControl = new ShapeControl(); SuspendLayout(); m_pShapeControl->Location = System::Drawing::Point(10, 10); m_pShapeControl->Name = “ShapeControl”; // // Form1 // AutoScaleBaseSize = System::Drawing::Size(5, 13); ClientSize = System::Drawing::Size(292, 266); Control* pControls? = { m_pShapeControl }; Controls->AddRange( pControls ); Name = “CPPTestForm”; Text = “CPPTestForm”; ResumeLayout(false); } }; // This is the entry point for this application int _tmain(void) { Application::Run( new CPPTestForm ); return 0; }
Compile your project and launch the application by clicking Debug, Start Without Debugging from the main menu. At this point, you probably think I have been lying
Control Class Libraries
351
to you because I told you that using a custom control is the same as using any other Window Form control. However, when you launched the test harness you received a System.IO.FileNotFoundException, as shown in Figure 23.3. This is due to the fact that there are a few special procedures you must perform on your control assembly before it can be used. FIGURE 23.3 Custom controls cannot be used without first some assembly attributes being changed.
Strong-Named Assemblies and the Global Assembly Cache Two main requirements for a control must be satisfied before that control can be used within other applications. The first requirement is that the assembly must be a strongnamed assembly. A strong-named assembly is an assembly that has been associated with certain attributes that describe the assembly and has also been associated with a public key and digital signature. By you associating an assembly with a public key and digital signature, the assembly is guaranteed to be unique because no other assembly can generate the same key for an assembly. Although we won’t get into the details and various implementation specifics of strong-named assemblies, you will be shown how to create a strong-named assembly. The process of creating a strong-named assembly involves the use of a special tool that is shipped with Visual Studio .NET. This tool, named SN.exe, can be found in the common tools folder in the main Visual Studio .NET program folder. This tool will only need to be run once. Therefore, look for the SN.exe file in the Visual Studio .NET common tools folder. Open a console window at the location of that file. Next, generate a strong name key by entering the following command: Sn.exe –k shapecontrol.snk
23
352
Hour 23
This will create a file containing a unique key that you will assign to your assembly. Locate the shapecontrol.snk file you just created and copy it to the ShapeControl project directory. Assigning a strong name key to an assembly requires changing an attribute contained within the AssemblyInfo.cpp file that was created by the project wizard when you created your project. Open the AssemblyInfo.cpp file within the ShapeControl project and locate the attribute named AssemblyKeyFileAttribute. Change the parameter for that attribute, which is currently empty, by specifying the name of the strong name key you created earlier. Now, each time your assembly is built, it will be associated with that strong name key and consequently become a strong-named assembly. The second requirement necessary to use a control contained within an assembly is that it must be installed into the Global Assembly Cache (GAC). The GAC is a central location for assemblies that are meant to be shared by several .NET applications. Because controls are designed to be shared components, they are required to be installed into the GAC. You can view the contents of the GAC by navigating to the assembly directory contained within your Windows folder. You’ll notice as you navigate to that directory that the columns within Windows Explorer change to show information about the various assemblies installed on the system, as shown in Figure 23.4. This is due to a shell extension named shfusion.dll, which is known as the Assembly Cache Viewer. You can, in fact, install your assembly into the GAC by simply dragging and dropping your DLL into the assembly directory, but you would have to do that each time your assembly is built. FIGURE 23.4 The Assembly Cache Viewer shell extension allows you to view the contents of the Global Assembly Cache.
Control Class Libraries
353
Visual Studio .NET contains a utility that allows you to install and remove assemblies from the GAC. This utility is named Gacutil.exe and is contained within the common tools folder of Visual Studio .NET. In order to install your assembly into the GAC each time it is built, you will now create a custom build step that calls Gacutil.exe to perform the assembly installation. Right-click the ShapeControl project in Solution Explorer and select Properties. Make sure All Configurations is selected in the Configurations drop-down box. Select Build Events, Post-Build Event from the Configuration Properties list on the left side of the dialog. Click in the Command Line property and then click the button labeled with an ellipsis to open the Command Line Builder dialog. The first command you need to perform is to remove the old assembly that is in the GAC from the previous build. If you don’t do this step, you will end up with multiple versions of the assembly in the GAC. To uninstall the control assembly, enter the following command: gacutil.exe /u ShapeControl
Create a new line to start the next command. The process of installing an assembly is similar to uninstalling. However, rather than specifying the name of the assembly, as you did in the previous step, you must specify the path to the DLL. Enter the following command to install your assembly DLL into the GAC: gacutil.exe /i “$(TargetDir)\ShapeControl.dll”
Your assembly is now ready to be built, and as you do this, your assembly will be assigned a strong key and installed into the GAC. Because these steps have been performed, you can now run the test-harness application successfully. Your results should appear similar to what’s shown in Figure 23.5. FIGURE 23.5 Running the test harness after assigning the custom control a strong key and installing it into the GAC.
Using Custom Controls with C# .NET Even though using a custom control within Visual C++ .NET is fairly easy, using it within other .NET languages is even easier due to the support of the Windows Form Designer, which Visual C++ .NET currently lacks. You will now create a C# Windows application similar to the Visual C++ .NET test harness you created earlier.
23
354
Hour 23
For reasons that will become clearer in a moment, open a new instance of Visual Studio .NET rather than adding the C# project to your current solution. Click New, Project from the File menu and select Visual C# Projects from the list of project types and select Windows Application from the list of project templates. Give your project the name CSharpTestHarness and click OK to create the project. After the project files are created, Visual Studio .NET will open the Windows Form Designer so that you can create your user interface. Adding controls within C# involves the use of the Toolbox, which by default is displayed on the left side of the IDE (see Figure 23.6). If you scroll through the list of controls under the Windows Forms category in the Toolbox, you’ll notice that the control you created earlier is not shown. To add your custom control to the Toolbox, right-click anywhere on the Toolbox and select Customize Toolbox. On the Customize Toolbox dialog, select the .NET Framework Components tab. FIGURE 23.6 Adding controls with Visual C# .NET is accomplished with the Toolbox.
To add your control to the list of components, click the Browse button, navigate to the DLL that was created when you built your control, and click on Open. The control will then be added to the list of .NET Framework components, as shown in Figure 23.7. Once you click OK to close the Customize Toolbox dialog, scroll down the list of controls contained on the Toolbox, and you should see your ShapeControl, as shown in Figure 23.8.
Control Class Libraries
355
FIGURE 23.7 Add controls to the Toolbox using the Customize Toolbox dialog.
23
FIGURE 23.8 Adding controls onto a form with Visual C# .NET is accomplished with the Toolbox.
If you look at Figure 23.8 you’ll notice that the icon for the ShapeControl is different from what you have on your Toolbox. You can customize that icon by creating a 16×16 bitmap file and placing it within the same directory as the assembly. To associate the control with the bitmap file, add an attribute to your control class named ToolBoxBitmap and place the name of the bitmap file as the parameter to that attribute.
To add your custom control to the Windows Form for your C# application, drag the ShapeControl control from the Toolbox and drop it onto the Windows Form displayed in the Windows Form Designer. As you can see, adding custom controls within Visual C#
356
Hour 23
.NET is much easier due to the support of the Windows Form Designer. You can now compile your C# project and run the application. Your results should look similar to Figure 23.9. FIGURE 23.9 Using Visual C# .NET to test your custom control is simple with the help of the Windows Form Designer.
Stock Properties Your control inherited from the System::Windows::Form::Control class, and due to this, your control contains a lot of stock properties. In fact, not only do these properties exist, but you don’t have to provide any implementation for them because they have already been implemented for you by the base class. Within your Visual C# project you just created, click the ShapeControl you added to the Windows Form within the Windows Form Designer. At the bottom-right area of the IDE, you should see a window named Properties. Within that window, you can see several stock properties that have been created for you already. Scroll down the list of properties until you find the property named BackColor. Click the field of that property and then click the down-arrow button to display the available colors you can choose. Select a color other than the one currently displayed by clicking it. Once you do that, your ShapeControl will automatically change its color based on the color you chose. As you’ll recall when you created the control, your PaintHandler method draws the rectangle or ellipse based on the property ForeColor. Find the ForeColor property in the list of properties and choose another color. Your rectangle should now be drawn with a different color. Figure 23.10 shows a Windows Form with several ShapeControls, each with different values for their stock properties. In addition to different colors, you can also resize your control, add a background image, dock it to the side of the form, and even cause the cursor to change when the user passes the mouse cursor over the control during runtime. Currently, however, there is no way to change the control from a rectangle to an ellipse. You will now do this by adding a custom property for your control.
Control Class Libraries
357
FIGURE 23.10 Stock properties free you from having to implement properties common among all controls.
23
Creating Custom Control Properties Adding properties to a custom control is the same method used to add properties to any other managed class. In the ShapeControl project, open the ShapeControl.h file. Within the public section of the ShapeControl class, you will create a property using the __property keyword. As you’ll recall from other hours within this book, such as Hour 17, “Interfaces,” each property can have a getter and setter function. Because you want clients using the control to both retrieve the current shape and set the current shape of your control, you will be creating both. The property you will be creating, however, will be a String object. If you were to create a property to a Shape enumerated data type, then when the person using your control changes that property within the Property Browser, he will either see the number 0 or the number 1, rather than the string “Rectangle” or “Ellipse.” Create a property named DrawShape with both a get and set function using the __property keyword, as shown starting on line 51 of Listing 23.3. Within the get_DrawShape function, return either the string “Rectangle” or the string “Ellipse,” based on the current value of your private Shape enumerated data type. Within the set_DrawShape property function, do the reverse by changing the value of the private enumerated data variable based on the value received as a parameter to the property function. Also, because setting this property means that the shape of your control needs to change, call the Invalidate function so that PaintHandler is invoked, which in turn redraws your control using the proper shape. As mentioned earlier, simply adding a public property to your control ensures that a person using your control can change that property using the Property Browser within the IDE. Currently, however, the user must manually type in the strings to change the property. You will now learn how to change the field within the Property Browser for your property so that rather than having to manually type in a string for the property, you can select a shape from a drop-down list.
358
Hour 23
You can specify how the value of a property is displayed within the Property Browser using three different methods. The first, which has already been mentioned, is to just add a property requiring the user to manually enter in the value. The second method is to associate your control with a TypeConverter, which will create a drop-down list with the possible values available to choose from. The third way is to associate your control with a UITypeEditor object. This allows you to create any type of control that is shown when the user clicks in the property field. For example, you could create a drop-down list that graphically shows a rectangle and an ellipse rather than just the strings. (Note that you interacted with a property that used a UITypeEditor when you changed the various color properties earlier.) For this lesson, you will be using the second method, which uses a TypeConverter. Within the ShapeControl class, create another managed class before the ShapeControl class named ShapeConverter. Because the property uses String objects as its property, derive from the StringConverter base class, as shown in Listing 23.3. In order to avoid any forward declaration issues and class dependencies, you will declare the class at the top of the project file and implement the functions following the ShapeControl class. The StringConverter class contains three virtual functions that you will need to override to control the behavior of the drop-down box. The first function, named GetStandardValuesSupported, is called to determine whether your control contains a set of standard values that a user can choose from. Because your control supports two standard values, simply return True from this function, as shown on line 100 of Listing 23.3. The second function, named GetStandardValuesExclusive, is used to determine whether the standard values of your control are the only values possible. Because they are the only values supported (in other words, the user cannot manually enter any other value for the property), the function returns True, as shown on line 106 of Listing 23.3. Finally, the last function that the ShapeConverter class will override from the StringConverter base class is the method used to retrieve the collection of standard values displayed in the drop-down list. To do this, create an array of String objects and return a new instance of a StandardValuesCollection using the array of String objects you just created as a parameter to the constructor dynamically cast to an ICollection interface, as shown on line 118 of Listing 23.3. In order to use the functions you just overwrote, you will have to reference three additional namespaces. These three namespaces can be seen starting on line 12 of Listing 23.3.
Control Class Libraries
359
LISTING 23.3 Creating a Custom Control and Controlling Its Appearance in the Property Browser 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46:
// ShapeControl.h #pragma once #using #using #using using using using using using using
namespace namespace namespace namespace namespace namespace
System; System::Drawing; System::Windows::Forms; System::Drawing::Design; System::ComponentModel; System::Collections;
namespace ShapeControl { public __gc class ShapeConverter : public StringConverter { public: // StringConverter Overrides bool GetStandardValuesSupported( ITypeDescriptorContext* context); bool GetStandardValuesExclusive( ITypeDescriptorContext* context); TypeConverter::StandardValuesCollection* GetStandardValues( ITypeDescriptorContext* context); }; public __gc class ShapeControl : public System::Windows::Forms::UserControl { public: ShapeControl() { InitializeControl(); SetStyle(ControlStyles::ResizeRedraw, true); } __value enum Shape { Rectangle = 0, Ellipse = 1 }; [ TypeConverter(__typeof(ShapeConverter)),
23
360
Hour 23
LISTING 23.3 continued 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66: 67: 68: 69: 70: 71: 72: 73: 74: 75: 76: 77: 78: 79: 80: 81: 82: 83: 84: 85: 86: 87 : 88: 89: 90: 91: 92: 93: 94:
Category(“Appearance”), Description(“Sets the shape for the control”), DefaultValue(“Rectangle”), ] __property String* get_DrawShape() { if( m_eDrawShape == Shape::Rectangle ) return S”Rectangle”; else if( m_eDrawShape == Shape::Ellipse ) return S”Ellipse”; return 0; } __property void set_DrawShape( String* Value ) { if( Value == S”Rectangle” ) m_eDrawShape = Shape::Rectangle; else if( Value == S”Ellipse” ) m_eDrawShape = Shape::Ellipse; Invalidate(); } protected: void PaintHandler( Object* sender, PaintEventArgs* e ) { System::Drawing::Rectangle rcRect = ClientRectangle; // deflate rect so circle isn’t cut off rcRect.Inflate( -5, -5 ); // Draw the rectangle or circle if( m_eDrawShape == Shape::Rectangle ) e->Graphics->DrawRectangle( new Pen(this->ForeColor, 1), rcRect ); else if( m_eDrawShape == Shape::Ellipse ) e->Graphics->DrawEllipse( new Pen(this->ForeColor, 1), rcRect ); } private: Shape m_eDrawShape; void InitializeControl() { m_eDrawShape = Shape::Rectangle;
Control Class Libraries
361
LISTING 23.3 continued 95: 96: 97: 98: 99: 100: 101 102: 103: 104: 105: 106: 107: 108: 109: 110: 111: 112: 113: 114: 115: 116: 117: 118: 119: 120: 121: }
add_Paint( new PaintEventHandler(this, PaintHandler )); } }; bool ShapeConverter::GetStandardValuesSupported( ITypeDescriptorContext* context) { return true; } bool ShapeConverter::GetStandardValuesExclusive( ITypeDescriptorContext* context) { return true; } TypeConverter::StandardValuesCollection* ShapeConverter::GetStandardValues( ITypeDescriptorContext* context) { String* pShapes? = { S”Rectangle”, S”Ellipse” }; return new StandardValuesCollection( dynamic_cast(pShapes) ); }
The last step before you can view the drop-down list in the Property Browser is to associate your ShapeConverter class with the DrawShape property contained within the ShapeControl. Changing how the property is displayed within the Property Browser is accomplished by applying attributes to the property. To associate the ShapeConverter with the property, use the TypeConverter attribute with the parameter __typeof(ShapeConverter), as shown on line 46 of Listing 23.3. The final three attributes you see within the listing control various property attributes that the Property Browser uses to display the property. The Category attribute is used to place your property in the associated category within the Property Browser. Because the DrawShape property affects the appearance of the control, place that property within the Appearance category. The next attribute, Description, simple displays a description of what the property is used for. Finally, the DefaultValue attribute specifies what the initial value for the property should be. You can now compile your control. If you receive an error when linking your project that states that the linker cannot open the file, this is due to something I mentioned earlier when you created your Visual C# project. If you receive this error, it means your project
23
362
Hour 23
is open and you are currently using the Windows Form Designer. Because the ShapeControl is in use, the linker cannot update your DLL. However, simply closing the Windows Form Designer does not fix this problem. You must close the Visual Studio .NET instance containing the Visual C# .NET project before updating your control. The fact that Visual C++ .NET does not contain a Windows Form Designer is actually a blessing in this case, because you can still keep that project open as you work on your custom control. Close the Visual Studio .NET windows containing the C# project and rebuild your control. After your control is built, start a new instance of Visual Studio .NET and open the C# project you created to test your control. Open the main Windows Form and select the ShapeControl contained on the form by clicking it. If you look in the Property Browser window now, you will see the DrawShape property with the word Rectangle as its value. Clicking that value will display a down-arrow button. When you click that button, you will see the drop-down list containing the two possible values to select from, as shown in Figure 23.11. FIGURE 23.11 Creating a drop-down list in the Property Browser for your custom control.
Summary Visual Studio .NET has greatly simplified the process of creating custom controls. Because this is the case, the soon-to-be billion-dollar control market will increase even more due to the increased number of controls that will likely result. The question you still have to ask yourself, however, is what language to use for your creating controls. As of this writing, Visual C++ .NET does not have a Windows Form Designer. Based on this fact, do you immediately rule out Visual C++ .NET in favor of one of the other .NET languages? As I’ve said before, you have to weigh the advantages and disadvantages of each language and decide for yourself based on your project objectives. This hour you learned how to create a custom control using Visual C++ .NET. Although you didn’t know it at the time, you created a managed C++ application in order to facilitate
Control Class Libraries
363
easier testing without having to close Visual Studio .NET and reopen it each time to build your control, as you have to do with a Visual C# test harness. You learned how to create a custom property for your control and also how to change the behavior of that property, as it’s shown in the Property Browser. Now that you have the basics of control creation down, you’re on your way to tapping into the control market with your own custom controls.
Q&A Q Can I use a control created in Visual C# .NET or VB .NET within a managed C++ application? A Yes, you can. Using the control within a managed application is no different than it is using any other .NET control. Q Can I create custom events for a control? A Even though it wasn’t covered this hour, the process for creating an event is quite easy. You can use the Unified Event Model to create events, just as it is explained in Hour 18, “Events and Delegates.” Q Can I create a control derived from the Control class and then use that control in another control derived from the UserControl class? A Once again, the answer is yes. The UserControl class is meant to hold a collection of different .NET Framework controls, and because a control derived from the Control class means that it is a .NET Framework control, you can use it within a composite control.
Workshop The Workshop provides quiz questions to help solidify your understanding of what was covered in this hour. Answers are provided in Appendix A, “Quiz Answers.”
Quiz 1. Can you insert an ActiveX control into your control project, and if so, what method is used to perform this layer of communication between the two controls? 2. You used a PaintHandler delegate to paint your control. What is another method you could have used to draw your control? 3. How would you allow a user to both select a property value from a list and also have the ability to manually type in a value within the Property Browser?
23
HOUR
24
Serialization Have you ever worked with an application and changed some of the user interface elements in a manner that more suited your working style? Have you ever worked with an application that allowed this but failed to save the state the application was in when you last used it so that you had to change these elements each time you launched the application? If so, this is a type of application that could take advantage of serialization but is not doing so. Serialization is the process of packaging the current state of an object into a form that can easily be saved to a storage device or transported across process or machine boundaries. Rather than you manually entering entries into a file or the Registry, serialization allows you to send a stream of data from one object to another object or device, such as a hard disk, where it can either be constructed into an exact copy of that object or later retrieved to reconstruct the object. In this hour you will learn: • The two types of serialization supported by the .NET Framework and the differences between them
366
Hour 24
• How to construct a .NET object that supports serialization • How to create a managed C++ application with a Windows Form that uses a serialized object to maintain state information
Binary and XML Serialization The first time I heard the word serialization, I walked in the other direction. Why they give some software technologies seemingly complicated names is beyond me, but as you work through this hour, you’ll notice that the basic mechanisms behind serialization in the .NET Framework are not too complicated at all. The first question you have to ask yourself is why you would need or want to use serialization in the first place. The first reason, as just mentioned, is to save the state of an object. If you look through parts of your Registry, especially through the HKEY_LOCAL_MACHINE\ Software section, you might run into a few applications that save information such as recently used files, the window size of the application when it was closed last, and other bits of state information. If that information is not contained within the Registry, it may be written to an INI file or other custom-designed file format. Rather than having to go through the somewhat manual and complicated process of saving Registry values or performing file input and output (I/O), the developer could instead have chosen to use serialization. A serialized class within the .NET Framework can have all its member variables, whether public or private, converted into a binary format and streamed onto a storage medium. The process of then taking the file that results from the serialization process and reconstructing the original managed class is known as deserialization. Another way to use serialization—which you may have used without knowing it—is to transfer an object across process or machine boundaries. In other words, the object state is saved and converted to a form that can then easily be transferred to another application domain to be reconstructed at that destination point. The .NET Framework supports two types of serialization: binary serialization and XML serialization. Binary serialization is best used to preserve the state of an object onto a storage medium. When an object undergoes binary serialization, all its public and private members are streamed in binary form into a file on the specified storage medium. The object can then be deserialized and subsequently reconstructed to be an exact copy of the original object later. The other type of serialization is XML serialization. Unlike binary serialization, which preserves type information, XML serialization does not preserve type fidelity. For instance, if a member variable is an Int16 data type, the type of that member variable will be lost. Also, XML serialization will only serialize the public members of an object, as
Serialization
367
compared to binary serialization, which serializes both public and private data members. With these differences, you can see that XML serialization is best suited for transferring objects across platform boundaries using well-known protocols, such as the Hypertext Transfer Protocol (HTTP). For the lesson this hour, you will be creating a managed C++ application that preserves state information for a Windows Form. Serialization will be handled using binary serialization, which saves the state information for a managed object onto the hard disk.
Creating the ObjectSerialization Class and Windows Form As was just mentioned, you will be creating a managed C++ application. Select New, Project from Visual Studio .NET’s File menu. Select Visual C++ projects from the list of project types and select the Managed C++ application project template. Give your project the name ObjectSerialization and click OK to create the project. The project you are going to create consists of a main Windows Form. Contained on that form is a text box that will be used to dump the values of various data members so that you don’t need to view them in the debugger each time you run your application. Serialization and deserialization will occur when you click the corresponding form’s button. Also, in order to demonstrate serialization customization, two text boxes will be used to set the values of the internal member variables. Listing 24.1 shows the necessary code for the main user interface of the form. LISTING 24.1 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16:
Creating the ObjectSerialization Form
#include “stdafx.h” #using #using #using #using
#include using namespace System; using namespace System::Drawing; using namespace System::Windows::Forms; __gc class ObjectSerializationForm : public System::Windows::Forms::Form { private:
24
368
Hour 24
LISTING 24.1
continued
17: // UI Controls 18: Label* label1; 19: Label* label2; 20: Label* label3; 21: TextBox* m_tbObjectDump; 22: TextBox* m_tbSerializedString; 23: TextBox* m_tbNonSerializedString; 24: Button* m_btnSetSerialized; 25: Button* m_btnSetNonSerialized; 26: Button* m_btnSerialize; 27: Button* m_btnLoad; 28: Button* m_btnDump; 30: 31: public: 32: ObjectSerializationForm() 33: { 34: InitializeComponent(); 35: } 36: virtual ~ObjectSerializationForm(){}; 37: 38: private: 39: void InitializeComponent() 40: { 41: m_tbObjectDump = new TextBox(); 42: m_tbSerializedString = new TextBox(); 43: m_tbNonSerializedString = new TextBox(); 44: 45: label1 = new Label(); 46: label2 = new Label(); 47: label3 = new Label(); 48: 49: m_btnSetSerialized = new Button(); 50: m_btnSetNonSerialized = new Button(); 51: m_btnSerialize = new Button(); 52: m_btnLoad = new Button(); 53: m_btnDump = new Button(); 54: SuspendLayout(); 55: 56: // m_tbObjectDump 57: m_tbObjectDump->Anchor = AnchorStyles( 58: AnchorStyles::Top | 59: AnchorStyles::Bottom | 60: AnchorStyles::Left | 61: AnchorStyles::Right); 62: m_tbObjectDump->Location = System::Drawing::Point(8, 120); 63: m_tbObjectDump->Multiline = true; 64: m_tbObjectDump->Name = “m_tbObjectDump”; 65: m_tbObjectDump->ReadOnly = true;
Serialization
LISTING 24.1 66: 67: 68: 69: 70: 71: 72: 73: 74: 75: 76: 77: 78: 79: 80: 81: 82: 83: 84: 85: 86: 87: 88: 89: 90: 91: 92: 93: 94: 95: 96: 97: 98: 99: 100: 101: 102: 103: 104: 105: 106: 107: 108: 109: 110: 111: 112: 113:
369
continued m_tbObjectDump->Size = System::Drawing::Size(426, 160); m_tbObjectDump->TabIndex = 0; m_tbObjectDump->Text = “”; // m_tbSerializedString m_tbSerializedString->Anchor = AnchorStyles( AnchorStyles::Top | AnchorStyles::Left | AnchorStyles::Right); m_tbSerializedString->Location = System::Drawing::Point(8, 24); m_tbSerializedString->Name = “m_tbSerializedString”; m_tbSerializedString->Size = System::Drawing::Size(234, 20); m_tbSerializedString->TabIndex = 1; m_tbSerializedString->Text = “”; // label1 label1->Anchor = AnchorStyles(AnchorStyles::Top | AnchorStyles::Left | AnchorStyles::Right); label1->Location = System::Drawing::Point(8, 8); label1->Name = “label1”; label1->Size = System::Drawing::Size(118, 16); label1->TabIndex = 2; label1->Text = “Serialized String”; // label2 label2->Anchor = AnchorStyles(AnchorStyles::Top | AnchorStyles::Left | AnchorStyles::Right); label2->Location = System::Drawing::Point(8, 56); label2->Name = “label2”; label2->Size = System::Drawing::Size(118, 16); label2->TabIndex = 3; label2->Text = “Non-Serialized String”; // m_tbNonSerializedString m_tbNonSerializedString->Anchor = AnchorStyles( AnchorStyles::Top | AnchorStyles::Left | AnchorStyles::Right); m_tbNonSerializedString->Location = System::Drawing::Point(8, 72); m_tbNonSerializedString->Name = “m_tbNonSerializedString”; m_tbNonSerializedString->Size = System::Drawing::Size(234, 20); m_tbNonSerializedString->TabIndex = 4; m_tbNonSerializedString->Text = “”; // m_btnSetSerialized m_btnSetSerialized->Anchor = AnchorStyles(AnchorStyles::Top |
24
370
Hour 24
LISTING 24.1 114: 115: 116: 117: 118: 119: 120: 121: 122: 123: 123: 124: 125: 126: 127: 128: 129: 130: 131: 132: 133: 134: 135: 136: 137: 138: 139: 140: 141: 142: 143: 144: 145: 146: 147: 148: 149: 150: 151: 152: 153: 154: 155: 156: 157: 158: 159: 160:
continued AnchorStyles::Right); m_btnSetSerialized->Location = System::Drawing::Point(250, 24); m_btnSetSerialized->Name = “m_btnSetSerialized”; m_btnSetSerialized->Size = System::Drawing::Size(40, 23); m_btnSetSerialized->TabIndex = 6; m_btnSetSerialized->Text = “Set”; m_btnSetSerialized->Click += new System::EventHandler( this, SetSerialized_Click); // m_btnSetNonSerialized m_btnSetNonSerialized->Anchor = AnchorStyles(AnchorStyles::Top | AnchorStyles::Right ); m_btnSetNonSerialized->Location = System::Drawing::Point(250, 72); m_btnSetNonSerialized->Name = “m_btnSetNonSerialized”; m_btnSetNonSerialized->Size = System::Drawing::Size(40, 23); m_btnSetNonSerialized->TabIndex = 8; m_btnSetNonSerialized->Text = “Set”; m_btnSetNonSerialized->Click += new System::EventHandler( this, SetNonSerialized_Click); // label3 label3->Location = System::Drawing::Point(8, 104); label3->Name = “label3”; label3->Size = System::Drawing::Size(100, 16); label3->TabIndex = 9; label3->Text = “Object Dump”; // m_btnSerialize m_btnSerialize->Anchor = AnchorStyles(AnchorStyles::Top | AnchorStyles::Right); m_btnSerialize->Location = System::Drawing::Point(354, 16); m_btnSerialize->Name = “m_btnSerialize”; m_btnSerialize->TabIndex = 10; m_btnSerialize->Text = “Serialize”; m_btnSerialize->Click += new System::EventHandler (this, Serialize_Click); // m_btnLoad m_btnLoad->Anchor = AnchorStyles(AnchorStyles::Top | AnchorStyles::Right); m_btnLoad->Location = System::Drawing::Point(354, 48); m_btnLoad->Name = “m_btnLoad”; m_btnLoad->TabIndex = 11; m_btnLoad->Text = “Load”; m_btnLoad->Click += new System::EventHandler (this, Load_Click); // m_btnDump
Serialization
LISTING 24.1 161: 162: 163: 164: 165: 166: 167: 168: 169: 170: 171: 172: 173: 174: 175: 176: 177: 178: 179: 180: 181: 182: 183: 184: 185: 186: 187: 188: 189: 190: 191: 192: 193: 194: 195: 196: 197: 198: 199: 200: 201: 202: 203: 204: 205: 206: 207: 208:
371
continued m_btnDump->Anchor = AnchorStyles(AnchorStyles::Top | AnchorStyles::Right); m_btnDump->Location = System::Drawing::Point(354, 80); m_btnDump->Name = “m_btnDump”; m_btnDump->TabIndex = 12; m_btnDump->Text = “Dump”; m_btnDump->Click += new System::EventHandler (this, Dump_Click); // Form1 AutoScaleBaseSize = System::Drawing::Size(5, 13); ClientSize = System::Drawing::Size(442, 294); Control* pControls? = { m_btnDump, m_btnLoad, m_btnSerialize, label3, m_btnSetNonSerialized, m_btnSetSerialized, m_tbNonSerializedString, label2, label1, m_tbSerializedString, m_tbObjectDump}; Controls->AddRange( pControls ); MinimumSize = System::Drawing::Size(450, 328); Name = “Form1”; Text = “Object Serialization”; ResumeLayout(false); } void Serialize_Click(Object* sender, EventArgs* e) { } void Load_Click(Object* sender, EventArgs* e) { } void Dump_Click(Object* sender, EventArgs* e) { } void SetSerialized_Click(Object* sender, EventArgs* e) { }
24
372
Hour 24
LISTING 24.1 209: 210: 211: 212: 213: 214: 215: 216: 217: 218: 219: 220:
continued void SetNonSerialized_Click(Object* sender, EventArgs* e) { }
}; int _tmain(void) { Application::Run(new ObjectSerializationForm ); return 0; }
If you decided to write in all the code yourself rather than download the project from the Sams Web site, the probability that you entered at least one error is pretty high. Compile and run your application to ensure everything is working. You should see a form similar to the one in Figure 24.1 when your application is running. FIGURE 24.1 The ObjectSerialization Windows Form.
Serializing with Attributes The first step to perform in serialization is to mark an object as serializable. To do this, place the attribute [Serializable] preceding the class declaration for your Windows Form. Believe it or not, that is all you need to do to enable object serialization. You still need to invoke the serialization process, but as of right now your object is ready to be serialized. Well, sort of. If you were to serialize your Windows Form object right now, you would get a SerializationException, as shown in Figure 24.2. This occurs because serialization is attempted on the base Form class, which has not been marked with the Serializable attribute.
Serialization
373
FIGURE 24.2 Serialization exception notification dialog.
To circumvent this problem, you will create a separate managed class that will serve as the serializable object. Add a new managed class named StateInfo before the class declaration of your main Windows Form. Apply the Serializable attribute to the class and remove the Serializable attribute from the ObjectSerializationForm class. The StateInfo class will keep state information for four properties within the form class. These properties correspond to the two values of the form’s text boxes you created as well as the Size and Location properties of the form. Within the private section of the StateInfo class, create four variables corresponding to the four properties you will be serializing. This can be seen on line 50 of Listing 24.2. Next, add the four properties to the public section of the StateInfo class, creating a get and set function for each property. These properties will simply return or set the values of the private data members you created earlier. LISTING 24.2 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16:
Creating a Serializable Object
[Serializable] __gc class StateInfo { public: StateInfo() { } __property String* get_SerializedString() { return m_pstrSerialized; } __property void set_SerializedString( String* pString ) { m_pstrSerialized = pString;
24
374
Hour 24
LISTING 24.2
continued
17: } 18: 19: __property String* get_NonSerializedString() 20: { 21: return m_pstrNonSerialized; 22: } 23: 24: __property void set_NonSerializedString( String* pString ) 25: { 26: m_pstrNonSerialized = pString; 27: } 28: 29: __property Size get_Form_Size() 30: { 31: return m_pFormSize; 32: } 33: 34: __property void set_Form_Size( Size pSize ) 35: { 36: m_pFormSize = pSize; 37: } 38: 39: __property Point get_Form_Location() 40: { 41: return m_pFormLocation; 42: } 43: 44: __property void set_Form_Location( Point pPoint ) 45: { 46: m_pFormLocation = pPoint; 47: } 48: 49: private: 50: String* m_pstrSerialized; 51: [NonSerialized] String* m_pstrNonSerialized; 52: Size m_pFormSize; 53: Point m_pFormLocation; 54: };
Customizing the Serialization Process At this point, every data member contained within the StateInfo class will be serialized and written to disk. However, there may be times when a certain variable does not need to be serialized. To prevent a variable from being serialized, apply the NonSerialized attribute to each data member that doesn’t need to be serialized. As you may have guessed,
Serialization
375
the private member variable m_pstrNonSerialized will be marked as nonserialized, so apply the NonSerialized attribute to that variable, as shown on line 51 of Listing 24.2. The process of marking individual members as nonserialized is known as selective serialization. You can further customize the serialization process by implementing the ISerializable interface defined within the .NET Framework and providing an implementation of the GetObjectData function declared in that interface. This allows you to specify the data that will be serialized rather than relying on the current value of that variable. An obvious question is why you would ever want to do that. If you have a member variable whose value would be considered invalid after the deserialization process but still needs a special value to successfully reconstruct that object, you would perform this process, which is known as custom serialization. For this hour, however, we are just going to use selective serialization.
Serializing and Deserializing Objects Now that you have created a serializable object, you are ready to add the code that will perform the actual serialization and deserialization. Create two private member functions within the ObjectSerializationForm class named SaveState and RestoreState. Both functions do not take any parameters, and they both return void. As you may have guessed, serialization will occur within the SaveState function, and deserialization will take place in the RestoreState. We will concentrate on serialization to begin with. The first step is to create a StateInfo private member variable for the form class. Create a new instance of this class within the constructor of your form class. As mentioned at the beginning of this hour, the form contains two text boxes used to change the value of two variables. These variables correspond to the serializable and nonserializable string properties contained within the StateInfo class. In the event handler named SetSerialized_Click, set the SerializedString property of the StateInfo class to the Text property of the m_tbSerializedString variable. Likewise, set the NonSerializedString property of the StateInfo class to the Text property of the m_tbNonSerializedString member variable, as shown in Listing 24.3. Within the SaveState member function, save the Size and Location properties of your Windows Form to the corresponding properties within the StateInfo class. Now that the appropriate state information is saved within the StateInfo class, you can serialize that object and save it in binary form on the hard disk. The first step is to create a new BinaryFormatter object. This is the .NET Framework class responsible for performing binary serialization. Next, create a new FileStream object, which will be used for writing the binary stream to a file, as shown on line 35 of Listing 24.3. To perform the actual
24
376
Hour 24
serialization, call the BinaryFormatter member function Serialize, passing the FileStream object and the StateInfo object as the two parameters. Finally, close the FileStream object by calling the Close member function contained within the FileStream class. LISTING 24.3 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42:
Serializing and Deserializing Objects
void Serialize_Click(Object* sender, EventArgs* e) { SaveState(); } void Load_Click(Object* sender, EventArgs* e) { RestoreState(); } void Dump_Click(Object* sender, EventArgs* e) { m_tbObjectDump->Text = String::Format( “Serialized: {0}\r\n \ Non-Serialized: {1}”, m_pStateInfo-ializedString, m_pStateInfo->NonSerializedString ); } void SetSerialized_Click(Object* sender, EventArgs* e) { m_pStateInfo->SerializedString = m_tbSerializedString->Text; } void SetNonSerialized_Click(Object* sender, EventArgs* e) { m_pStateInfo->NonSerializedString = m_tbNonSerializedString->Text; } void SaveState() { // save form properties first m_pStateInfo->Form_Size = this->Size; m_pStateInfo->Form_Location = this->Location; IFormatter* formatter = new BinaryFormatter(); Stream* stream = new FileStream(“state.info”, FileMode::Create, FileAccess::Write, FileShare::None); if( !formatter || !stream ) return; formatter->Serialize(stream, m_pStateInfo ); stream->Close();
Serialization
LISTING 24.3 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62:
377
continued return;
} void RestoreState() { IFormatter* formatter = new BinaryFormatter(); Stream* stream = new FileStream(“state.info”, FileMode::Open, FileAccess::Read, FileShare::Read); m_pStateInfo=dynamic_cast(formatter->Deserialize(stream)); stream->Close(); // restore form properties this->Size = m_pStateInfo->Form_Size; this->Location = m_pStateInfo->Form_Location; this->m_tbNonSerializedString->Text=m_pStateInfo->NonSerializedString; this->m_tbSerializedString->Text = m_pStateInfo->SerializedString; return; }
Because the SaveState function is not called anywhere (and therefore serialization will not occur), call the SaveState function within the Save button’s event handler named Serialize_Click. Deserializing is very similar to serializing an object. Create another member function within the ObjectSerializationForm class named RestoreState. Hook this up to the Load button by calling it within the Load_Click event handler for that button. Just as you did within the SaveState function, create a BinaryFormatter object and a FileStream object. The FileStream object, however, will open the file in read rather than write mode. To deserialize an object, call the Deserialize member function contained within the BinaryFormatter object, passing the FileStream object as the only parameter. This function returns a generic Object, so you must cast the return value to your StateInfo member variable. The last step in the deserialization process is to transfer the properties of the StateInfo object into the corresponding properties or member variables in your Windows Form class, as shown on line 56 of Listing 24.3. The last step to complete your project is to add some additional namespace declarations and to output the values of the two strings contained within the StateInfo object. The code that was added to support serialization requires the use of six additional namespaces
24
378
Hour 24
contained in two assemblies. Import the appropriate assemblies and declare the namespaces you will use by adding the following code at the top of the ObjectSerialization.cpp file: #using #using using using using using using using
namespace namespace namespace namespace namespace namespace
System::Collections; System::ComponentModel; System::Data; System::Runtime::Serialization::Formatters::Binary; System::IO; System::Runtime::Serialization;
Finally, write the values of the two string objects contained within the StateInfo private member variable by setting the Text property of the m_tbObjectDump within the Dump_Click event handler. You are now ready to compile and launch your application. First, set two values for each of the text boxes by entering a string and clicking the Set button. You can verify the values of the two strings by clicking the Dump button. Next, move and resize your form so that it is in a different state from what it was when initially launched. Save the current state of the form by clicking the Serialize button. In order to ensure that the serialization was successful, once again change the values of the two text boxes and then move and resize your form to a different location. Click the Dump button to make sure the values are in fact different. Now you can deserialize the StateInfo object by clicking the Load button. Once you do that, your form will automatically resize itself and move to the location at the point where you clicked the Serialize button. Finally, click the Dump button. Notice that the serialized string did in fact revert back to its serialized state, but the nonserialized string is now empty because its value was not serialized.
Summary Serialization can be a valuable tool as you create projects in the future. You saw how easy it is to create a serializable object. You also saw the simple process needed to serialize that object and then subsequently deserialize it. Compare this to the process it would take to save individual Registry entries or, worse yet, come up with your own file format and object-saving scheme. The .NET Framework makes serialization so attractive that you can probably find a use for it in any project you work on.
Serialization
379
Q&A Q Can I serialize and deserialize a class within that class itself? A Theoretically you could by creating a pointer to the class and assigning the value of the this pointer. However, there is no way to change the actual class itself because that requires changing the this pointer, which is a const. Q Can you serialize a binary object such as bitmap data using XML serialization? A Yes, you can. XML supports binary encoding, which converts binary data into an encoded ASCII format.
Workshop The Workshop provides quiz questions to help solidify your understanding of what was covered in this hour. Answers are provided in Appendix A, “Quiz Answers.”
Quiz 1. How can you determine at runtime whether an object is serializable? (Hint: Look at the Type class.) 2. If you don’t want to apply the NonSerializable attribute to several data members, how would you ensure that those values will not be serialized? 3. Explain what happens when you serialize an object, add or remove a member variable to that object, and then attempt to deserialize it?
24
APPENDIX
A
Quiz Answers This appendix provides you with answers to the quiz questions at the end of each hour.
Hour 1 1. A solution is a collection of one or more projects and related files in Visual Studio .NET. 2. Yes. 3. One step. You can build a solution with one menu command or keyboard sequence.
Hour 2 1.
__gc
2. Defined macros 3.
/clr
382
Appendix A
Hour 3 1.
System::Windows::Forms::Form.
2. A delegate is the function type that is called to handle a specific event. 3. Events are handled through the message map in MFC, whereas they are handled by assignment of delegates with the .NET Framework.
Hour 4 1. No, it’s up to you as the developer as to which one you want to use. In the end, native C++ types resolve to the .NET Framework types. 2. The Global Assembly Cache. 3. An assembly can be a single file, such as an EXE or DLL, or it can be a combination of several files. An assembly doesn’t have a limit to the number of files it may have. 4. The common language runtime or CLR.
Hour 5 1. There is no way to know for sure. The only thing you know as a developer is that the object is destroyed sometime after your application is done using it. 2. Only Visual C++ .NET allows for mixing managed and unmanaged code. 3. The garbage collector.
Hour 6 1. They are packaged within assemblies. The assembly contains a manifest that details the contents of that package, such as classes, resources, and so on. 2. No, it is a matter of preference. Any user interface work will likely be more difficult using C++ due to the lack of a forms designer. For this reason, using VB .NET or C# .NET for the interface and C++ for the underlying logic is a good choice. 3. They are loaded when they are first used.
Hour 7 1. The one used as an MDI container has its IsMDIContainer property set to True. 2. No, they are all defined through the .NET Framework API.
Quiz Answers
3.
383
System::Windows::Forms::MainMenu.
4. They are assigned as an index into the bound image list.
Hour 8 1. In case you need to make changes to the resources contained within the .resx file. You can’t make changes to a compiled .resources file. 2. Managed resources are handled by the common language runtime and thus gain all the advantages that it entails. However, there is currently more work that needs to be done to Visual C++ .NET to make working with managed resources easier. 3. Yes, you can. The GetString function is just a modified GetObject call that automatically performs the cast from the Object type to the String object. You can call GetObject and cast to a String object to retrieve a string resource.
Hour 9 1.
Graphics.
2. No. 3. It provides transparent colors for creating brushes and pens. The AlphaBlend() function only paints a bitmap to where it appears transparent. 4. Override the OnPaintBackground() method and do nothing in the new implementation.
Hour 10 1.
PageSettings
2.
PrintPage
3.
PrintDocument
Hour 11 1. Universal Description, Discovery, and Integration. 2.
System::Web::Services::WebService.
3. It uses a proxy class that simulates the actual Web Service class. 4. By using the Add Web Reference option from the Solution Explorer or the Project menu. The Web Service is then located and added to the application with the Add Web Reference dialog.
A
384
Appendix A
Hour 12 1. It declares an interface that is then implemented by a class. 2. You declare a method with the [soap_method] attribute to indicate that the method uses SOAP for communication. 3.
HRESULT
4. By declaring the last parameter with the [out,
retval]
attribute.
5. Each return value must be declared as a pointer to a type. You cannot directly return a value.
Hour 13 1.
System::Exception.
2.
System::Diagnostics.
3. It is executed after the code in the try block or after the code in the catch block that handles an exception. If no catches match the thrown exception, the finally block is called before the function exits. 4. It points to the previous exception in a chain of exceptions.
Hour 14 1. An if statement and a while statement. 2. The special name given to a replacement tag. 3. It allows you to perform initialization of your request handler while still giving you access to the response and request objects. 4. Yes, you can. Just check the Create as Web Service option in the ATL Server Project Wizard.
Hour 15 1. The coclass attribute injects the code that provides that derivation. If you look at the merged source, you will see your class and all its base classes as a result of applying that attribute. 2. Although you might think it has something to do with letter case, context sensitivity within attributes refers to the notion that an attribute is aware of what context it is being applied in. In other words, an attribute knows whether it is being applied to a class or an interface, for instance.
Quiz Answers
385
Hour 16 1. An Array object has a set size, whereas an ArrayList object can grow as items are added to it. 2. No, they don’t. Stacks and queues are implemented more like linked lists than arrays. You cannot index a Stack or Queue object in the same way you can index an Array object. 3. You will receive an ArgumentException.
Hour 17 1. One of the biggest distinctions that arises is that of inheritance. If you have a group of unrelated classes that need to share a common convention, such as a certain protocol, you should use interfaces. However, in order to maintain an “is-a” relationship among objects as you progress up and down an inheritance hierarchy, you should use classes. 2. Yes, interfaces always have a public access specifier. 3. No, an interface can only contain public methods and properties. 4. No, only properties are allowed. 5. Yes, as long as the second object implements that interface.
Hour 18 1. Native C++ event handling uses a linked list that stores function pointers for each event handler. It also creates a special function that, when called, walks through the list and calls each of those functions. 2. A delegate is another name for the event handler for that event. In other words, when a delegate is associated with an event, that delegate will be called when the event is fired. 3. A delegate can contain more than one pointer to an event handler when the CombineImpl function is declared in the delegate class. To find out how many functions the delegate is bound to, call the function GetInvocationList and access the Count property of that result. 4. Call the GetInvocationList member function and then walk through the array of delegates that was returned, calling each function to which each delegate is bound.
A
386
Appendix A
Hour 19 1. Managed events do not use operating system–specific objects. Any object can create and fire managed events, but kernel object events require the use of a certain object that must be signaled in order for that event propagate to appropriate handlers. 2. Your application will throw an exception. Once a thread is in the Stopped state, it cannot be restarted. 3. Thread delegates are contained within the class scope they are created in. In other words, the delegate has full access to the public and private variables within the class. WIN32 callback functions must either be global functions or static member functions within a class. In both cases, you do not have access to class member variables.
Hour 20 1. Nothing happens. The database file is not locked, so you can delete it. You can add and delete records because you are just interfacing with a DataSet. However, once you attempt to update the data source with the new DataSet, an exception will be thrown because the file cannot be found. 2. No, you can create an entire database during runtime using the .NET Framework classes. 3. A table relationship is an association of two tables in a parent-child relationship. Table relationships are contained within the DataRelationCollection object in the DataSet.
Hour 21 1. Because your COM object is not running within the confines of the common language runtime, you must release your interface pointers. 2. You will have to implement a custom marshaler. 3.
creates a type library from an assembly. RegAsm.exe does the same, but it also registers that type library, whereas TlbExp.exe does not. TlbExp.exe
Quiz Answers
387
Hour 22 1. Change the CharSet attribute for DllImport to either Auto or remove it completely. 2. No, managed types within the unmanaged class will still be garbage collected. You will, however, need to free any memory allocated for unmanaged member variables within that class. 3. Yes.
Hour 23 1. Yes, you can. The method involved is COM Interop. 2. You can override the OnPaint method provided by the base class. 3. Return False from the GetStandardValuesExclusive function.
Hour 24 1. You can call the IsSerializable function provided by the Type class. 2. Use custom serialization instead of selective serialization. 3. An exception will be thrown because the signature of that object has changed and cannot be deserialized properly.
A
APPENDIX
B
Visual Studio .NET IDE Reference One thing you’ll notice if you’re used to working with Visual C++ 6.0 is the drastic changes made to the Visual Studio .NET IDE. Many of these changes were the result of merging several Microsoft programming languages into a single, unified environment. The most visible changes, however, are designed to solve a specific problem with an application of this size: window clutter.
Source Window As the centerpiece of Visual Studio .NET, the source code window is where you do most of the work. Even though source code is in an ASCII text-only format, programming languages contain syntax rules that allow Visual Studio .NET to take advantage of them to organize and present the source code in various ways.
390
Appendix B
Outlining If you’ve ever used the previous version of Visual Basic, you’ll recall that functions were separated from each other by horizontal lines. Although this might have been annoying to some, it at least organized the code into discernable blocks. These horizontal lines are gone now, but in their place is a very nice addition to source code layout: outlining. Visual Studio .NET determines where certain blocks of code exist within in a source file and separates them into collapsible segments. This occurs regardless of the programming language being used. At the very top of the block of code, you can see a box that is used to collapse or expand a block of code simply by clicking it. If a block of code is collapsed, you will see the first line of that source code block followed by a box containing an ellipsis, indicating that more code will be revealed once that block is expanded. Specifically within Visual C++ .NET, code blocks occur frequently throughout a source code file. You can collapse entire class definitions, preprocessor directives, structures, functions, and even conditional statement blocks. Figure B.1 shows an open source code file with two collapsed classes and an expanded function. FIGURE B.1 Source code outlining helps with code readability.
Task List Shortcuts One thing I have noticed that many developers do, including myself, is to add comments with TODO statements. In fact, by using #pragma statements, you can constantly be reminded that you have something “to do.” Visual Studio .NET has expanded on this practice by
Visual Studio .NET IDE Reference
391
creating the Task List window. The Task List window serves several different purposes. The one feature of the Task List you will no doubt become familiar with involves the build warnings and error tasks that show up after each build. After each build, all the warnings and errors that occurred are transferred to the Task List window, where you can check off the warnings and errors as you fix them. Instead of using #pragma statements to display TODO items, you can add a comment followed by the word TODO and the entire comment line will be added to the Task List. If you need to jump to that comment, you can double-click the Task List item, and the file will be opened at that location. Figure B.2 shows the Task List with a TODO Task List item. FIGURE B.2 Creating TODO comments automatically adds Task List items.
B
Reducing Window Clutter While you’re working on a project, it’s inevitable that you’ll have to use several windows within Visual Studio .NET. As windows are opened, you may find yourself running out of room to work on your source code. Luckily, Visual Studio .NET contains several window-handling features to help reduce this clutter.
Docking Although present in the previous version of Visual Studio, docking windows are still worth mentioning here. With the exception of the main source code windows, all the supporting child windows within Visual Studio .NET can be docked into various positions
392
Appendix B
within the IDE. Docking means that a window can be anchored to a fixed position within the IDE, as opposed to a window that is floating and covering up windows underneath it. The default layout of Visual Studio contains several windows docked to the right side and bottom of the IDE, as you’ve seen in several figures throughout this book.
Auto-Hiding Although docking windows helps to organize your workspace, it doesn’t do much to help with the clutter that could still result. The Visual Studio .NET IDE now supports autohiding windows. If you look at a docked window, you will see a small thumbtack icon in the upper-right corner. Once you click that icon, the window becomes an auto-hiding window. When the mouse cursor is in another part of the IDE, the auto-hiding window will collapse onto the edge of the IDE and display a small tab with the name of the window. When the mouse cursor hovers over that tab, the window will be shown. Figure B.3 shows a layout of Visual Studio .NET with the child windows automatically hidden. FIGURE B.3 Maximize your workspace by auto-hiding all the IDE child windows.
Preset Layouts and Developer Profiles Visual Studio .NET can save preset window layouts. If you find that a certain window layout is better suited for a particular programming language or if you don’t like the default window layout, you can use one of the preset window layouts. To change the default window layout, click View, Web Browser, Home from the main menu. This will open the Visual Studio .NET Start page. In the list of links on the left of the Start page,
Visual Studio .NET IDE Reference
393
click the My Profile link. Within the profile page, you can change either the developer profile (which changes various settings based on the profile you choose) or individual settings, such as keyboard and (as just mentioned) window layouts. Figure B.4 shows the My Profile page. FIGURE B.4 Change the Visual Studio .NET IDE to support your individual programming style.
New Developer Studio Windows In addition to all the new features added to Visual Studio .NET, a few new windows have been added to the IDE: • Solution Explorer. The Solution Explorer is an enhancement to the File View present in the previous version of Visual Studio. The enhancement is primarily in the way icons are displayed based on the language used for the project. Also, the Solution Explorer displays all the projects contained within a single solution. • Command window. The Command window is designed for manually entering in automation commands. The IDE, like its predecessor, contains several automation commands that can be used by COM-enabled applications. With the Command window, you can enter the available commands to invoke these methods. Open the Command window by clicking View, Other Windows, Command Window. Enter the command File.AddNewProject. The Add New Project dialog will be displayed, as shown in Figure B.5.
B
394
Appendix B
FIGURE B.5 Use the command window to perform automation tasks.
• Server Explorer. The Server Explorer acts as a server-management application within Visual Studio .NET. It allows you to open data connections and browse databases located on the local machine or on the network. The Server Explorer also lets you drag and drop nodes onto various designers within Visual Studio .NET.
Help System The previous version of Visual Studio interacted with the MSDN library to provide documentation. Visual Studio .NET still interacts with MSDN but in a visually different manner.
Integrated Help MSDN consisted of an external program launched whenever help was requested in Visual C++ 6.0. Although communication between Visual Studio and the MSDN library still allowed for context-sensitive help, it was cumbersome having to switch back and forth between the two applications. Visual Studio .NET contains an internal Web browser that can be viewed in the same area in which source code windows and designers are viewed. In fact, when you launch Visual Studio .NET, the first thing you see is the Start page, which is an HTML page itself. Anytime help is requested via the help index or contents, the results are displayed within the Visual Studio .NET IDE.
Visual Studio .NET IDE Reference
395
Dynamic Help The help system within Visual Studio .NET is now “smart.” The Dynamic Help window, located at the bottom-right corner of the IDE by default, changes as you work within the IDE. For instance, you can be designing a Windows Form with C# and the Dynamic Help window will display links to topics relevant to Windows Form design. Switch over to a Visual C++ class declaration and Dynamic Help will automatically change its links to display information on the class keyword.
Comment Web Pages Not only is help available through MSDN and Dynamic Help, but you now have an option to create help documentation for your own projects. Visual Studio .NET has added a new feature called Comment Web Pages that parses through your source code, regardless of the programming language, and creates documentation for the various objects, functions, and other components within your project. To create Comment Web Pages for your project, click Tools, Build Comment Web Pages from the main menu. Figure B.6 shows a project with Comment Web Pages built from its source files. FIGURE B.6 Document your source code with Comment Web Pages.
B
INDEX A Abort method, 267 About dialogs, adding to child windows, 91-92 AboutDialog class, 91-92 abstract keyword, 18 Active Server Pages. See ASP Active Template Library. See ATL ActiveX controls, 10 ActiveX Data Objects. See ADO; ADO.NET Add Class command (Add menu), 202 Add Class command (Project menu), 287 Add function, 223 Add Member Variable Wizard, 28 Add menu commands, Add Class, 202 Add Method Wizard, 311 Add New Project dialog, 61 Add Reference dialog, 66 Add Web Reference dialog, 150 add_ButtonClick() method, 90 add_Click() method, 86
ADO (ActiveX Data Objects), 283-286 ADO.NET, 283 AuthorDB project, 286-287 displaying main Windows Form, 292 InitForm control properties and events, 288, 292 interface class declaration, 287-292 connecting to data sources, 293-295 DataSet object, 285-286 redesign of ADO, 285-286 XML, 285 alpha blending (GDI+), 108 antialiasing (GDI+), 109 APIs, Win32, 10 Application class, 44-45 Application Options (ATL), 183 Application Settings hyperlink, 200 Application Wizard, 26-27, 72 ApplicationException class, 174 applications, 10-11 Active Template Library. See ATL
ActiveX. See ActiveX C++, 11 clients, creating, 238-241 custom wizards. See wizards DelegateTest, 246-249 bind buttons, 249-253 bound functions, 254 delegate class declarations, 250-252 Invoke button, 253-254 extended stored procedures. See extended stored procedures GDI+, 110 building with .NET Framework, 110-114 MFC, 114-117 ManagedEvents, 255 creating event source and receiver classes, 255-257 event receiver implementation, 257-259 MFC creating, 26-29, 35-36 HelloMFC, 29 MFC. See MFC MIDI compiling, 93 running, 93
398
applications
.NET Framework creating, 29-36 deploying, 48-49 integrated languages in, 60-66 synchronization. See synchronization threading. See threading Web Services within, 149-151 Win32 API. See Win32 API architecture multitiered, 284-285 Unified Event Model, 244-245 Array class, 214-216 Array object, 218-221 ArrayList class, 218-221 arrays, 211 ASP (Active Server Pages), 284 ASP.NET, 140 assemblies external, 22 integration, 60 marshaling, 308 strong-names, Global Assembly Cache (GAC), 351-353 AssemblyInfo.cpp, 142 /ASSEMBLYMODULE:filename linker option, 23 Assert() method Debug class, 174 Trace class, 174 ATL (Active Template Library), 10, 155, 179-180, 244 attributes, 196-198 build process, 198-199 creating attributed objects, 202-203 injected code inspection, 208-209 programming, 199-208 cached data types, 182 classes, 244 creating COM objects, 310-311 adding methods, 310-313 adding properties, 313-314 creating Web Services, 155-157 defining interface, 157-158
implementing interface, 158-160 test applications, 160-162 events sequence, 184-185 NumberGuess project creating, 181-184 replacement functions, 188-191 SRF, 187-188 replacement tags, 186 server options, 182 SRF, 184-188, 198 Web services, 11 ATL Project Wizard dialog, 200 ATL Server Project Wizard, 156, 181 ATL Simple Object Wizard, 202, 310 ATLAttributes.cpp file, 200 ATLCOMServer project creating COM object, 310-311 adding methods, 310-313 adding properties, 313-314 attribute keyword, 20 attributes, 195 ATL, 196-198 build process, 198-199 creating attributed objects, 202-203 injected code inspection, 208-209 programming, 199-208 compared to macros, 199 DllImport parameters, 333-334 event_receiver, 256 CSink class, 256 event_source, parameter values, 255 module parameters, 201 Serializable, 373 serialization, 372-374 user-defined, creating, 20 AttributeTest.cpp file, 205208 AttributeTest.h file, 204 AuthorDB project, creating, 286-287 displaying main Windows Form, 292
InitForm control properties and events, 288, 292 interface class declaration, 287-292 AuthorDB.cpp file, 292 auto-hiding windows, 392 Autohide feature Visual Studio .NET IDE, 8-9 AutoResetEvent event, 277
B BackColor property, 356 BeginEdi function, 299 BeginInvoke function, 251, 258 Binary Large Object. See BLOBs binary serialization, 366-367 BinaryFormatter object, 375-377 Bind1 button, 249 Bind2 button, 249 BindBoth button, 249-253 bending alpha, GDI+, 108 BLOBs (Binary Large Object), 182 blocks finally, 170 try..catch, 170 BoundFunction class, 251 box keyword, 18 Browser Capabilities Support option (ATL), 182 build process ATL attributes, 198-199 errors, viewing, 230 Button event handler, 299-300 buttonFirst_Click method, 297 buttons Bind1, 249 Bind2, 249 BindBoth, 249-253 Invoke, 253-254 Push, 78
C C#, 64, 66-67 C# .NET, custom controls, 353-356
CNumberGuessHandler class
C++ applications, 11 classes, 66-67 defining, 61-64 transforming into managed code, 31-32 code, accessing managed .NET code from, 57 managed class libraries, 11 managed Web services, 11 types, .NET Framework representations, 42-43 callback functions, events, 244 CallingConvention parameter (DllImport attribute), 334 Capacity property (ArrayList class), 220 cardinal splines, GDI+, 107 CattributeTest data type, 207 CCWs (COM callable wrapper), 309, 332 CDialog class, 78 CharSet parameter (DllImport attribute), 333 CHelloMFCDlg::OnBnClic kedOK() method, 29 child windows, MDI adding About dialogs, 91-92 adding menus, 83-88 adding toolbars, 88-90 creating, 80-82 class keyword, 287 classes AboutDialog, 91-92 Application, 44-45 ApplicationException, 174 Array properties, 216 ATL, 244 BoundFunction, 251 C#, 66-67 C++, 66-67 CDialog, 78 CNumberGuessHandler, 189 collections, 212-214 Array, 214-216 ArrayList, 218-221 creating, 228-233 Hashtable, 223-224 queues, 221-223 stacks, 221-223
Consumer, 268 delegates, 275-276 ThreadRun method, 268 Control, 345, 356 CSink, 255-258 CSource, 255-257 CSystemTime, 328-330 Data, 46 Data Object, simulating long operations, 278-280 DataSet, 46 declarations, AuthorDB project, 287-292 defining C#, 64 C++, 61-64 Delegate, 253 delegates, declarations, 250-254 error handling, 166 Debug, 166, 171-174 Exception, 166-171 Trace, 166, 171-174 event sources, 255 Exception, 174 File, 45 FileInfo, 45 Form, 45 Graphics, 46 IntDataBlock, 271 SimulateLongOperatio n function, 277-280 TimerProc function, 277 integration, 60 interfaces, 213, 228 libraries, managed C++, 11 ManualResetEvent, 277 MDIChildForm, InitForm() method, 81-82 MDIWindowFrame, 83-86, 122-123 Monitor, 271 Enter method, 271 Exit method, 271 Pulse function, 273 ReadData function, 271-273 TryEnter method, 271 Wait method, 271 WriteData method, 271 MulticastDelegate, 251-258
399
MyException, 175-176 ObjectSerialization, creating, 367, 372 ObjectSerializationForm, 373-375 OleDbCommandBuilder, 301 Page, 47 PrintDialog, 132 Producer, 268 delegates, 275-276 ThreadRun method, 268 ReadData, 271-273 receiver creating, 255-257 implementation, 257-259 ResourceManager, 100 SimpleWindowsForm, 74-77 StateInfo, 374-375 StringStack, 229-230 System namespace, 43 Thread, 45, 265, 270 exceptions, 270 Start method, 265-266 ThreadStart, 268 Timer, 279 unmanaged, creating, 328-330 UserControl, 345 ValueType, 47-48 WebService, 46 Windows Forms, 74 XML, 46-47 XMLReader, 47 CLI (cross-language interoperability), 18 Click event, MenuItem object, 87 client applications, creating, 238-241 clients connected, 284-285 creating, 314-319 disconnected, 284-285 event receivers, 245 MFC, ATL objects, 206-208 CLR (common language runtime), 49-50, 195, 308 marshaling, 308 option, 22 CLS (common language specification), 50 CNumberGuessHandler class, 189
400
code
code attributes, 208-209 Comment Web Pages feature, 395 managed code, 53 accessing from unmanaged C++ code, 57 C++ transformations, 31-32 calling DLL functions from, 332-335 custom controls, 348-351 memory management, 55-56 relationship to unmanaged code, 54-55 running in unmanaged environments, 328330 Web Service files, 142 source code window, 389 outlining feature, 390 task list shortcuts, 390-391 synchronization, 263-265, 270-271 creating Consumer object, 274-276 creating multi-threaded objects, 272-274 creating Producer object, 274-276 events, 277-280 timers, 277-280 unmanaged code, 53 accessing from .NET, 56 data types, 334 memory leaks, 330-331 migrating legacy code, 56 P/Invoke, 331-332 relationship to managed code, 54-55 running managed code with, 328-330 codepage tag, 186 collections, 211 classes, 212-214 Array, 214-216 ArrayList, 218-221 creating, 228-233 Hashtable, 223-224 queues, 221-223 stacks, 221-223
DataColumn, 286 DataTables, 296 enumerations, 216-218 interfaces ICollection interface, 212 IEnumerable interface, 213 Rows, 297 StringStack implementation, 231-233 CollectionTest.cpp file, 214-219, 229 color correction, GDI+, 108 COM (Component Object Model), 195, 244, 307 objects creating, 310-314 creating .NET clients, 314-319 in .NET, 317-319 marshaling, 308 regsvr32, 315 projects .NET objects, 319-322 COM callable wrapper. See CCWs COM Interop, 308-309 COM value, event source attribute parameter value, 255 Combine function, 253, 258 Command Line dialog, 315 Command window, 393-394 commands Add menu, Add Class, 202 File menu New, 72 Save As, 97 New menu, Project, 72, 181 Project menu Add Class, 287 New, 141 Views menu, Other Windows, 230 comment tag, 186 Comment Web Pages feature, 395 common language runtime. See CLR common language specification (CLS), 50 compilers, new options, 22-23
compiling, 12-14 default Web Service, 142-145 MDI applications, 93 resx file setup, 98-99 Component Object Model. See COM component projects, creating, 60-61 components, adding references to, 65-66 ComputerName property, 313-314 Configuration Manager (Visual Studio .NET), 13 connected clients, 284-285 connection points, events, 244 Consumer class, 268 Consumer object, creating, 274-276 containers, GDI+, 109-110 Control class, 345 control properties, InitForm (AuthorDB project), 288, 292 controls, 344 custom C#, 353-356 creating, 344-347 creating properties, 357-362 managed code, 348-351 stock properties, 356 DataTable object, 297 inheritance, 344 Control class, 345 UserControl class, 345 TextBox, Text property, 298-299 updating, 297-298 CopyTo function, 212 Count property, 212, 254 CPPTestHarness project, 348-351 CreateInstance method, 214 CreateMenu() method, 84-86, 123-125 CreateToolbar() method, 89, 126-127 cross-language interoperability. See CLI CRWs (runtime callable wrapper), 309 CSharp, 44 CsharpTestHarness project, 354
directories
CSink class, 255-258 CSLibrary namespace, 64 CSource class, 255-257 CsystemTime class, 328-330 CurrentDateTime() method, 145-147 custom controls C#, 353-356 creating, 344-347 creating properties, 357-362 managed code, 348-351 stock properties, 356 custom data types, P/Invoke, 335-338 custom exceptions, 176 creating, 174-175 MyException class, 175 custom serialization, 374-375 custom wizards, 10 Customize Toolbox dialog, 354
D data adapters, 294 Data class, 46 Data Object class, simulating long operations, 278-280 data sources, connecting to, 293-295 data types, 308 CAttributeTest, 207 IAttributeTest, 207 P/Invoke, 335-338 specifying for marshaling, 339 unmanaged, 334 databases ADO (ActiveX Data Objects), 283, 285-286 ADO.NET, 283 AuthorDB project, 286-292 connecting to data sources, 293-295 DataSet object, 285-286 redesign of ADO, 285-286 XML, 285 connected clients, 284-285
DataTables, 286, 295 data collections, 296 displaying data, 296-298 navigating data, 296-298 disconnected clients, 284-285 displaying data, 296-298 navigating data, 296-298 tables deleting records, 298-303 inserting records, 298-301 DataColumn collections, 286 DataRow object, 286, 299 DataSet class, 46 DataSet object, 285-286 connecting to data sources, 293-295 Fill method, 295 DataTable collection, 296 DataTable object, filling form controls, 297 DataTables, 286, 295 data collections, 296 displaying data, 296-298 navigating data, 296-298 Debug, WriteLine() method, 171 Debug class, 171-173 Assert() method, 174 asserting on invalid values, 174 Debug statements, 171-173 debugging, 12-14, 142-145 #define keyword, 199 Delegate class, 253 delegate keyword, 18 delegates, 246-249 bound implementation, 254 classes, declarations, 250-254 Consumer class, 275-276 event handling, 255 creating event source and receiver classes, 255-257 event receiver implementation, 257-259 Invoke button, 253-254 multicasting, 249
401
object-oriented programming, 274 Producer class, 275-276 DelegateTest application, 246-249 bind buttons, 249-253 bound functions, 254 delegate class declarations, 250-252 Invoke button, 253-254 Delete event handler, 302-303 deleting table records, 298-303 deploying .NET Framework applications, 48-49 Deployment Support option (ATL), 185 deserialization, 375-378 design, COM Interop, 308309 destructors, 55-56 Developer Studio, windows, 393 Command, 393-394 Server Explorer. See Server Explorer Solution Explorer. See Solution Explorer Developer Support Options dialog, 184 device context, 129 dialogs About, adding to child windows, 91-92 Add New Project, 61 Add Reference, 66 Add Web Reference, 150 ATL Project Wizard, 200 ATL Server Project Wizard, 181 Command Line, 315 Configuration Manager, 13 Customize Toolbox, 354 Developer Support Options, 184 MFC Application Wizard, 206 New Project, 26, 61, 141, 156, 199 Page Setup, 133-134 Project Settings, 182 DictionaryEntry object, 224 directories, Web Services, 151-152
402
disconnected client scenario
disconnected client scenario, 285 disconnected clients, 284-285 discovering Web Services, 152 displaying database data, 296-298 DisplayTime function, 339 DivByZeroException.cpp, 168-176 DllImport attribute, optional parameters, 333-334 DLLs (dynamic link library), 10 creating, 344 functions, calling from managed code, 332-335 MFC, 10 docking windows, 391-392 documentation creating, 395 interfaces, 231 DogClientCom project, 320 DrawCurve() method, 107 DrawShape property, 361-362 due time, 279 dynamic link library. See DLLs
E editing resx file, 97-99 EndEdit function, 299 EndInvoke function, MulticastDelegate class, 251, 258 Enqueue function, 222 Enter method, Monitor class, 271 EntryPoint parameter (DllImport attribute), 333 enumerations collections, 216-218 System namespace, 43 environment (Visual Studio .NET). See IDE error handling classes, 166 Debug, 171-174 Exception, 167-171 Debug, 166 Trace, 166 Exception, 166 Trace, 171-174 event keyword, 18, 256
EventHandler function, 257 events, 243 AutoResetEvent, 277 callback functions, 244 connection points, 244 delegates, 246-249 bound implementation, 254 declarations, 250-254 event handling, 255259 Invoke button, 253-254 multicasting, 249 handlers adding, 34-35 Button, 299-300 Delete, 302-303 InitForm (AuthorDB project), 288, 292 ManualResetEvent, 277 names, 252 PrintDocument object, 129 receivers, 245 sinks, 245 sources, 245, 255-257 synchronization, 277-280 System namespace, 43 Unified Event Model, 244-245 viewing, 230 EventTest function, 256 event_receiver attribute, 256 event_source attributes, 255 ExactSpelling parameter (DllImport attribute), 333 Exception class, 167-168, 174 finally processing, 170-171 InnerException property, 169-170 StackTrace property, 168-169 exceptions custom, 174-176 SerializationException, 372 Thread class, 270 Exit method, Monitor class, 271 ExitInstance() method, 114-115 Expand Attributed Source property, 251 extended stored procedures, 10
Extensible Markup Language. See XML extern keyword, 335 external assemblies, 22
F FIFO (First-In First-Out), 221 File class, 45 File menu commands New, 72 Save As, 97 FileInfo class, 45 filename.mrg.cpp file, 208 FilePrint_Click() method, 127-128 files ATLAttributes.cpp, 200 AttributeTest.cpp, 208 AttributeTest.cpp file, 205 AttributeTest.h, 204 AuthorDB.cpp, 292 CollectionTest.cpp, 214-215, 229 filename.mrg.cpp, 208 IDL, attributed programming, 204-206 ManagedClient.cpp, 316 ObjectSerialization.cpp, 378 PInvoke.cpp, 328-330 rc, 96 resource, 96 creating, 96-97 integration, 97-99 reading at runtime, 100-103 resx, 96 compile setup, 98-99 creating, 96-97 displaying image information, 101 editing, 97-99 SimpleSysInfo.cpp, 312 SimpleWindowsForm.cpp, 72-74 StringStack.h, 229 Web Service, managed code, 142 FileStream object, 376-377 Fill method, DataSet object, 295 FillFormData function, 296-297, 302
get function
finally blocks, 170 finally keyword, 18, 170 finally processing, 170-171 FireEvent function, 256, 258 First-In First-Out. See FIFO flicker, drawing, 117-119 ForeColor property, 356 Form class, 45 forms controls DataTable object, 297 updating, 297-298 creating, 32-34 InitForm (AuthorDB project) control properties, 288, 292 events, 288, 292 ObjectSerialization, 367, 372 UserGuess, 188 Windows Forms, 71, 77 building MDI interfaces, 79-80 creating, 72-74, 77 GraphicsWindowsForm , 111-112 initialization, 75-77 Push button, 78 Visual Basic, 64-65 FU option (move / to beginning of entry), 23 functions. See also methods Add, 223 BeginEdi, 299 BeginInvoke, 251, 258 callback events, 244 Combine, 253, 258 CopyTo, 212 DisplayTime, 339 DLL, calling from managed code, 332-335 EndEdit, 299 EndInvoke, MulticastDelegate class, 251, 258 Enqueue, 222 EventHandler, 257 EventTest, 256 FillFormData, 296-297, 302 FireEvent, 256 get interface properties, 235 GetComputerName, 312-313 GetEnumerator, 217
GetField, 189 GetFlags, 185 GetHashCode, 223 GetInvocationList, 254 GetLocalTime, 339 GetObject (ResourceManager class), 100 GetString (ResourceManager class), 100 get_IsSynchronized, 232 get_Rows, 296 InitForm (ResourceManager class), 100 InitializeHandler, 185 InsertAt, 300 Interrupt, 267 Invoke, MulticastDelegate class, 251, 258 Join, 266 main(), 36, 77, 151 MessageBox, 128, 332-335 MoveNext, 217 MultiByteToWideChar, 339 NewRow, 299 OnBnClickedHello, 206-207 OnGetLastError, 191 OnGetNumGuesses, 191 OnInitDialog, 208 OpenDataSource, 293-295, 301 outlining feature (source code window), 390 Pop, 222 PrintCollection, 217, 220-224 PrintDictionaryCollection, 224 Pulse, Monitor class, 273 Push, 222 ReadData, 271-273 replacement, NumberGuess project (ATL), 188-191 Reset, 217 RestoreState, 375 Resume, 267 SaveState, 375-377 SelectObject(), 110 set interface properties, 235 set_IsSynchronized, 232 set_Item, 299
403
SimulateLongOperation, IntDataBlock class, 277-280 Sleep, 266 Suspend, 267 TestArray, 217 TestArray(), 215 TestQueue, 222 TestStack, 222 TestStringStack, 229 TimerProc, IntDataBlock class, 277 tmain, 229, 268, 274 ToString, 297 UserGuessedCorrectly, 188-190 ValidateAndExchange, 189 WaitForSingleObject, 270 WinMain(), 36 WriteData, Monitor class, 271
G GAC (Global Assembly Cache), 351-353 Gacutil.ex, 353 garbage collection, 56 gc keyword, 18-19, 287 GDI+, 106 applications, 110 building with .NET Framework, 110-114 MFC, 114-117 drawing flicker, 117-119 new features, 106 alpha blending, 108 antialising, 109 cardinal splines, 107 color correction, 108 gradiet brushes, 106-107 graphics containers, 109-110 metadata, 109 persistent paths, 107 recoloring, 108 scalable regions, 108 transformations, 108 programming methodology, 110 General Protection Fault. See GPF get function, interface properties, 235
404
GetComputerName function
GetComputerName function, 312-313 GetEnumerator function, 217 GetEnumerator method, 216 GetField function, 189 GetFlags function, 185 GetHashCode function, 223 GetInvocationList function, MulticastDelegate class, 254 GetLocalTime function, 339 GetObject function (ResourceManager class), 100 GetString function (ResourceManager class), 100 GetType method (Object class), 216 get_IsSynchronized function, 232 get_Rows function, 296 Global Assembly Cache. See GAC Global.asax, 142 Global.asax.h, 142 GPF (General Protection Fault), 13, 166 gradient brushes, GDI+, 106-107 graphics containers, 109-110 displaying information in resx files, 101 drawing flicker, 117-119 GDI+. See GDI+ Graphics class, 46 Graphics object, 110
H handler tag, 186 Hashtable class, 223-224 HelloMFC application, 29 help, Visual Studio .NET IDE, 7-8 help system (Visual Studio .NET), 394 documention creation, 395 integration, 394-395 Help window, 8 helpstring parameter (module attribute), 201
hiding windows, 392 hook keyword, 257 hook method, 245 HTTP (Hypertext Transfer Protocol), 191, 367 hyperlinks, Application Settings, 200 Hypertext Transfer Protocol. See HTTP
I IattributeTest data type, 207 IAttributeTest interface, 203-208 ICM (Image Color Matching), 108 ICollection interface, 212, 229 methods, declaring, 232-233 properties, 212, 231-232 IDE (Integrated Development Environment), 6 Autohide feature, 8-9 help, 7-8 MDI interface, 8 startup page, 6 tabbed interface, 7 identifier keyword, 18 IDL files, attributed programming, 204-206 IEnumerable interface, 213, 229, 232-233 IEnumerator object, 216-218 IfileCache object, ATL, 182 IIS (Internet Information Services), 293 Image Color Matching. See ICM ImageList object, 90 Images. See graphics include tag, 186 infrastructure, Web Services, 140 Inheritance controls, 344-345 Control class, 345 UserControl class, 345 InitForm (AuthorDB project) control properties, 288, 292 events, 288, 292
InitForm function, ResourceManager class, 100 InitForm() method, 81-82, 88, 91-92 InitializeHandler function, 185 initializing Windows Forms, 75-77 InitInstance() method, 35, 114-115 InnerException property (Exception class), 169-170 INSERT statement, 301 InsertAt function, 300 IntDataBlock class, 271 SimulateLongOperation function, 277-280 TimerProc function, 277 IntDataBlock object, 274-277 IntegrateApp components, C#, 66-67 Integrated Development Environment. See IDE IntegratedApp components, C++, 66-67 IntelliSense interface properties, 235 interface keyword, 18 interfaces, 212, 227 ATL Web Services defining, 157-158 implementing, 158-160 classes, 228 collections ICollection interface, 212 IEnumerable interface, 213 creating, 233 declarations, 234-236 implementation, 236-238 creating collection classes, 228-233 documentation, 231 IAttributeTest, 203-208 ICollection, 212, 229 methods, 232-233 properties, 212, 231 IEnumerable, 213, 229, 232-233 IRequestHandler, 185 IsimpleSysInfo, adding properties to COM objects, 313
listings
MDI, building with Windows Forms, 79-80 objects, 228 properties get function, 235 IntelliSense, 235 pseudo data member, 235 set, 235 transitive, 239-241 Visual Studio .NET MDI, 8 tabbed, 7 Internet Information Services. See IIS Internet Services API. See ISAPI Interrupt function, 267 Interrupting state (threads), 267 IntPtr pointer, 251 Invoke button, 253-254 Invoke function, 251-252, 258 IrequestHandler interface, 185 ISAPI (Internet Services API), 10, 185 IsimpleSysInfo interface, adding properties to COM objects, 313 IsSynchronized property, ICollection interface, 212, 232
J-K Join function, 266 Join method, 270 JScript, 44 Key property, DictionaryEntry object, 224 keywords, 18-19 #define, 199 #using, 316 abstract, 18 attribute, 20 box, 18 class, 287 delegate, 18 event, 18, 256 extern, 335 finally, 18, 170 gc, 18-19, 287
hook, 257 identifier, 18 interface, 18 nogc, 19 pin, 19 property, 19, 235 raise, 256 sealed, 19 try cast, 19 typeof, 19 unhook, 257 value, 19
L Last-In First-Out (LIFO), 221 layouts, windows, 392-393 Leaving Calculate() method, 173 legacy code, migrating, 56 Length property, Array class, 216 libraries Active Template Library. See ATL MFC, 10 LIFO (Last-In First-Out), 221 linker options, 23 listings AboutDialog class, 91-92 adding and viewing Array class properties, 215-216 ArrayList class, 220-221 ATL 3.0 Header file, 196-197 ATL Server Project Wizard, 186 ATL simple object using attributes, 197-198 ATLWebService.h, 158-160 ATLWebServiceApp.cpp, 161-162 AuthorDB interface class declaration, 287-292 Button Handler for MFC dialog, 207-208 ButtonClick event handler, 90 C++ classes, transforming into managed code, 31-32 COM objects within .NET, 317
405
ComputerName property implementation, 314 connecting to data sources, 293-295 CPPClass definition in CPPLibrary, 62 CPPUnmanagedClass Definition in CPPLibrary, 62-63 CreateMenu() method, 84-86, 124-127 CreateToolbar() method, 89, 126-127 creating and adding items to Hash table, 224 creating custom controls, 346-347 creating custom controls and controlling appearance of in property browser, 359-362 creating event sources and receiver classes, 255-257 creating Label and PictureBox controls, 101-102 creating managed .NET objects, 322-324 creating multi-threaded applications, 268-270 creating multi-threaded objects, 272-274 creating NumberGuess.srf file, 187-188 creating ObjectSerialization form, 367, 372 creating serializable objects, 373-374 creating stacks and queues, 222-223 creating StringStack managed class, 229 creating unmanaged classes within managed applications, 329-330 data object classes using events and timers to simulate long operations, 278-280 declaring ICollection and IEnumerable interface methods, 232-233 defining CurrentDateTime() method, 146-147
406
listings
defining RectangleArea() method, 146-148 DelegateTest Windows Form code, 246-249 delegte class declarations, 250-254 displaying main Windows Form, 292 DivByZeroException.cpp, 168-176 enumerating and outputting collection values, 218 enumerating printing values within dictionary collections, 225 event handlers, 34-35, 66-67 event receiver class implementation, 257-259 filling form controls with data from DataTable object, 297 flickering, 118 forms, creating, 32-34 GetComputerName interface method implementation, 312-313 GetUnmanagedMsg() Declaration in CPPClass, 63-64 GraphicsWindowsForm Windows Form class declaration, 111-112 Header file for WinForm using resources, 100 implementation class for interfaces, 237-238 implementing button even handlers to add records, 299-301 implementing constructors and delegates for Producer and Consumer classes, 275-276 implementing Delete event handlers, 302-303 implementing HelloWorld interface method, 206 InitForm() method, 88 InitForm() method (MDIChildForm class), 81-82 InitInstance() method and ExitInstance() method, 114-115
launching WinForm, 103 main() function modifications, SimpleWindowsForm object, 77 MDI Windows Forms, 79-80 MDIWindowFrame class, 83-84, 122-123 MenuItem object, Click event handlers, 87 module attribute, 200 module functions for nonattributed ATL projects, 201-202 navigating through data and updating form controls, 297-298 .NET objects within unmanaged COM applications, 321-322 onDraw() method, 116-117 OnFilePageSetup() method, 134 OnFilePrintPreview() method, 135 OnPaint() method (GraphicsWindowsForm class), 112-114 OnPrintPage() method, 130-131 pragmas, 21-22 PrintForm() method, 130-133 rendering content using HTTP response stream, 191 serializing and deserializing objects, 376-377 setting control properties and wiring events within InitForm, 288, 292 SimpleWindowsForm, 75-76 InitForm(), 76-77 SimpleWindowsForm class, 74 SimpleWindowsForm.cpp, 72-73 StringStack object, 230 testing ShapeControl with test harness, 349-350 transitive properties, 240-241
UserGuessCorrectly tag replacement function, 190-191 using custom data types and marshaling instructions, 336-338 viewing attributes of ATL simple object header files, 204-205 WebService.cpp, 143-145 WebService.h, 145-146 WebServiceApp.cpp, 151 locale tag, 186
M macros, compared to attributes, 199 main() function, 36, 77, 151 managed applications (C++), 11 C++ class libraries, 11 C++ Web services, 11 .NET Framework, 29-36 managed code, 53 accessing from unmanaged C++ code, 57 C++ class transformations, 31-32 calling DLL functions, 332 MessageBox, 332-335 custom controls, 348-351 memory management, 55 destructors, 55-56 garbage collection, 56 small memory allocations, 55 relationship to unmanaged code, 54-55 running in unmanaged environments, 328-330 Web Service files, 142 managed interfaces. See interfaces managed pragma, 20-22 managed resources, 95 managed value, event source attribute parameter value, 255 ManagedClient project, 316 ManagedClient.cpp file, 316
multitiered architectures
ManagedEvents application, 255 creating event source and receiver classes, 255-257 event receiver implementation, 257-259 ManualResetEvent class, 277 ManualResetEvent event, 277 marshaling, 308 specifying data types, 339 unmanaged data types, 334 MDI applications compiling, 93 running, 93 child windows adding About dialogs, 91-92 adding menus, 83-88 adding toolbars, 88-90 creating, 80-82 interfaces, building with Windows Forms, 79-80 Multiple Document Interface), 7 Visual Studio .NET interface, 8 MDIChildForm class, InitForm() method, 81-82 MDIWindowFrame class, 83-86, 122-123 memory managed code, 55-56 unmanaged code, 330-331 MenuItem object, 86-87 menus adding to child windows, 83-88 printing, 122-128 MessageBox function, 128, 332-335 metadata, GDI+, 109 methods. See also functions Abort, 267 adding to COM objects, 310-313 add_ButtonClick(), 90 add_Click(), 86 Assert(), 174 buttonFirst_Click, 297 CHelloMFCDlg::OnBnCli ckedOK(), 29 CreateInstance, 214 CreateMenu(), 84-86, 123-125
CreateToolbar(), 89, 126-127 CurrentDateTime(), 145-147 DrawCurve(), 107 Enter, Monitor class, 271 Exit, Monitor class, 271 ExitInstance(), 114-115 FilePrint_Click(), 127-128 Fill, DataSet object, 295 FireEvent, 258 GetEnumerator, 216 GetType (Object class), 216 hook, 245 ICollection, declaring, 232-233 IEnumerable, declaring, 232-233 InitForm(), 81-82, 88, 91-92 InitInstance(), 35, 114-115 Invoke, 252 Join, 270 Leaving Calculate(), 173 OnDraw(), 116-117 OnFilePageSetup(), 134 OnFilePrintPreview(), 135 OnOK(), 78, 91-92 OnPaint(), 118 OnPaint(), GraphicsWindowsForm class, 112-114 OnPaintBackground(), 118 OnPrintPage(), 130-131 OnToolbarButtonClick(), 127-128 PrintForm(), 129-133 RectangleArea(), 145-148 Run(), 77 ShowDialog(), 92 Start, 265-266, 270 ThreadRun, 268 TryEnter, Monitor class, 271 unhook, 245 Wait, Monitor class, 271 WaitOne, 280 WriteData, ReadData class, 271-273 WriteLine(), 171 MFC (Microsoft Foundation Classes), 252 ActiveX controls, 10 applications creating, 26-29, 35-36 HelloMFC, 29
407
clients, ATL objects, 206-208 GDI+ applications, 114 creating projects, 114-115 painting client areas, 115-117 ISAPI (Internet Services API), 10 library, 10 MFC Application Wizard dialog, 206 Microsoft Foundation Classes. See MFC Microsoft Interface Definition Language, 204, 234 Microsoft namespace, 44 MIDL (Microsoft Interface Definition Language), 204, 234 migration, legacy code, 56 module attribute parameters, 201 Monitor class, 271 Enter method, 271 Exit method, 271 Pulse function, 273 ReadData function, 271-273 TryEnter method, 271 Wait method, 271 WriteData function, 271 mouse, interface properties, 235 MoveNext function, 217 MSDN, Visual Studio .NET help system, 394 documention creation, 395 integration, 394-395 MSIL (Microsoft Intermediate Language), 50, 60 MultiByteToWideChar function, 339 MulticastDelegate class, 251, 254, 258 multicasting, 249 Multiple Document Interface. See MDI multithreading, 264-265 creating multi-threaded applications, 268-270 creating multi-threaded objects, 272-274 multitiered architectures, 284-285
408
MyException class
MyException class, 175-176 m_bInsertingRecord flag value, 298
N n-tier programming model, 285 name parameter (module attribute), 201 names, events, 252 namespaces, 42 Collections, 214 CSLibrary, 64 Diagnostics, 166 Microsoft, 44 System, 42 Threading, 265 C++ types, 42-43 classes, 43 enumerations, 43 events, 43 Win32, 44 native value, event source attribute parameter value, 255 navigating database data, 296-298 .NET Framework applications creating, 29-36 deploying, 48-49 integrated languages in, 60-66 assemblies, marshaling, 308 classes Application, 44-45 Data, 46 File, 45 FileInfo, 45 Form, 45 Graphics, 46 Page, 47 Thread, 45 ValueType, 47-48 WebService, 46 XML, 46-47 clients, creating, 314-319 GDI+ applications, 110114 namespaces, 42 Microsoft, 44 System, 42-43
objects COM objects, 317-322 creating, 322-324 Win32, 44 New command (File menu), 72 New command (Project menu), 141 new features compiler options, 22-23 external assemblies, 22 GDI+, 106 alpha blending, 108 antialiasing, 109 cardinal splines, 107 color correction, 108 gradient brushes, 106-107 graphics containers, 109-110 metadata, 109 persistent paths, 107 recoloring, 108 scalable regions, 108 transformations, 108 keywords, 18-19 linker options, 23 pragmas, 20-22 New menu commands, Project, 72, 181 New Project dialog, 61, 141, 156, 199 New Project dialog box, 26 NewRow function, 299 nogc keyword, 19 NumberGuess project (ATL), 181 creating, 181-184 replacement functions, 188-189 renderging content using HTTP response stream, 191 UserGuessCorrectly, 190-191 SRF, 187-188
O object-oriented programming, delegates, 274 objects Array, 218-221 ATL attributed, 202-203 BinaryFormatter, 375-377
collections. See collections COM creating, 310-314 creating .Net clients, 314-319 in .NET, 317-319 marshaling, 308 regsvr32, 315 Consumer, creating, 274-276 DataRow, 286, 299 DataSet, 285-286 connecting to data sources, 293-295 Fill method, 295 DataTable, filling form controls, 297 deserialization, 375-378 DictionaryEntry, 224 FileStream, 376-377 Graphics, 110 IEnumerator, 216-218 IfileCache, ATL, 182 ImageList, 90 IntDataBlock, 274, 277 interfaces. See interfaces MenuItem, 86-87 .NET creating, 322-324 in COM projects, 319-322 OleDbCommand, 294 OleDBConnection, 294 PaintEventArgs, 118 PrintDialog, 132-133 PrintDocument, 128-131 changing page setup, 133-134 events, 129 print preview, 134-135 selecting printers, 132-133 Producer, creating, 274-276 proxy, 308 serialization, 375-378 SimpleSysInfo, 311 SimpleWindowsForm, 77 StateInfo, 377-378 StringStack, 230 ObjectSerialization class, creating, 367, 372 ObjectSerialization.cpp file, 378 ObjectSerializationForm class, 373-375
properties
OLE DB data sources, ATL, 182 OleDbCommand object, 294 OleDbCommandBuilder class, 301 OleDBConnection object, 294 OleDbDataAdapter, 294 OnBnClickedHello function, 206-207 onDraw() method, 116-117 OnFilePageSetup() method, 134 OnFilePrintPrevbiew method, 135 OnGetLastError function, 191 OnGetNumGuesses function, 191 OnInitDialog function, 208 OnOK() method, 78, 91-92 OnPaint() method, 112-114, 118 OnPaintBackground() method, 118 OnPrintPage() method, 130-131 OnToolbarButtonClick() method, 127-128 OpenDataSource function, 293-295, 301 Other Windows command (View menu), 230 outlining feature (source code window), 390
P P/Invoke, 331-332 calling MessageBox function, 332-335 custom data types, 335-338 Page class, 47 Page Setup dialog, 133-134 PaintEventArgs object, 118 painting client areas, GDI+ in MFC, 115-117 parameters DllImport attribute, 333-334 event_source attribute, 255 module attribute, 201 paths, GDI+, 107 persistent paths, GDI+, 107
pin keyword, 19 PInvoke.cpp file, 328-330 Platform Invocation. See P/Invoke pointers IntPtr, 251 Object, 251 Pop function, 222 pragmas, 20-22 managed, 20-22 unmanaged, 20-22 PreserveSig parameter (DllImport attribute), 334 previews, print, 134-135 print preview, 134-135 PrintCollection function, 217-224 PrintDialog class, 132 PrintDialog object, 132-133 PrintDictionaryCollection function, 224 PrintDocument object, 128-131 changing page setup, 133-134 events, 129 print preview, 134-135 selecting printers, 132-133 printers, selecting, 132-133 PrintForm() method, 129133 printing, 121 menus, 122-128 PrintDocument object, 128-131 changing page setup, 133-134 events, 129 print preview, 134-135 selecting printers, 132-133 toolbar, 122-128 PrintPage event (PrintDocument object), 129 procedures, stored. See stored procedures Producer class, 268 Producer object, creating, 274-276 profiles, windows, 392-393 programming attribute, ATL, 199-209 GDI+, 110 Project command (New menu), 72, 181
409
Project menu commands Add Class, 287 New, 141 Project Settings dialog, 182 projects (Visual Studio .NET), 11 Active Template Library. See ATL ATL, NumberGuess, 181-191 ATLCOMServer, creating COM objects, 310-314 AuthorDB, creating, 286-292 CPPTestHarness, 348-351 creating, 141-142 adding references to components, 65-66 C# component class, 64 component projects, 60-61 defining C++ classes, 61-64 Visual Basic Windows Form, 64-65 CSharpTestHarness, 354 DogClientCom, 320 GDI+ in MFC, 114-115 ManagedClient, 316 .NET objects, 319-322 ShapeControl CPPTestHarness, 348-351 creating custom controls, 345 testing, 349-351 ThreadSynch, creating, 267-270 Web Setup Windows Installer, 11 properties adding to COM objects, 313-314 ArrayClass, 216, 220 BackColor, 356 ComputerName, 313-314 Count, 254 custom controls properties, 357-362 stock properties, 356 DictionaryEntry object, 224 DrawShape, 361-362
410
properties
Exception class InnerException, 169-170 StackTrace, 168-169 Expanded Attributed Source, 251 ForeColor, 356 ICollection interface, 212, 231-232 InitForm (AuthorDB project), 288, 292 interfaces get function, 235 IntelliSense, 235 pseudo data member, 235 set function, 235 transitive, 239-241 Rows, 300 SelectCommand, 301 TextBox controls, 298-299 ThreadState, 265 property keyword, 19, 235 proxy objects, 308 pseudo data members, 235 Pulse function, Monitor class, 273 Push button, Windows Forms, 78 Push function, 222
Q-R queues, 221-223 rc files, 96 RCWs, 332 ReadData class, 271-273 ReadData function, 271-273 receiver classes creating, 255-257 implementation, 257-259 recoloring (GDI+), 108 records (tables) deleting, 298-303 inserting, 298-301 RectangleArea() method, 145-148 references, adding to components, 65-66 RegAsm.exe, 320-322 regions, GDI+, 108 registration, COM objects, 315 regsvr32, 315
replacement functions, NumberGuess project (ATL), 188-189 rendering content using HTTP response, 191 UserGuessCorrectly, 190-191 replacement tags, ATL, 186 ResEditor, creating resource files, 96-97 Reset function, 217 resgen.exe, 98 resource files, 95-96 creating, 96-97 integration, 97-99 reading at runtime, 100-103 ResourceManager class, 100 resource_name parameter (module attribute), 201 RestoreState function, 375 Resume function, 267 Resuming state (threads), 267 resx files, 96 compile setup, 98-99 creating, 96-97 displaying image information, 101 editing, 97-99 Rows collection, 297 Rows property, 300 running MDI applications, 93 Running state (threads), 265-266 runtime, reading resources, 100-103 runtime callable wrapper. See CRWs
S Save As command (File menu), 97 SaveState function, 375-377 scalable regions, GDI+, 108 SDK (Software Development Kit), 10 SelectCommand property, 301 selecting printers, 132-133 selective serialization, 375 SelectObject() function, 110 Serializable attribute, 373
serialization, 365, 375-378 attributes, 372-374 binary, 366-367 customizing, 374-375 ObjectSerialization class, 367, 372 selective, 375 XML, 366-367 SerializationException, 372 Server Explorer, 394 server response file. See SRFs servers, ATL, 10, 179-180 Active Template Library), 179 attributes, 196-209 cached data types, 182 creating COM objects, 310-314 events sequence, 184-185 NumberGuess project, 181-191 replacement tags, 186 server options, 182 SRFs, 184-188 services, Web ASP.NET, 140 changing, 145-148 compiling, 142-145 creating, 139-142 creating with ATL, 155-162 debugging, 142-145 directories, 151-152 discovering, 152 infrastructure, 140 Web references, 149-151 within applications, 149 set function, interface properties, 235 SetLastError parameter (DllImport attribute), 334 set_IsSynchronized function, 232 set_Item function, 299 ShapeControl project CPPTestHarness project, 348-351 creating custom controls, 345 testing, 349-351 ShowDialog() method, 92 Simple Object Access Protocol. See SOAP SimpleSysInfo object, 311 SimpleSysInfo.cpp file, 312
TODO statements
SimpleWindowsForm, 76-77 SimpleWindowsForm class, 74-77 SimpleWindowsForm object, 77 SimpleWindowsForm.cpp file, 72-74 SimulateLongOperation function, 277-280 sinks, 245 Sleep function, 266 small memory allocations, 55 SOAP (Simple Object Access Protocol), 140 Software Development Kit. See SDK Solution Explorer, 12, 184-185, 393 solutions (Visual Studio .NET), 11-12 source code Comment Web Pages feature, 395 window, 389 outlining feature, 390 task list shortcuts, 390-391 sources, 245 splines, GDI+, 107 SqlDataAdapter data adapter, 294 SRFs (server response file), 183-188, 198 stacks, 221-223 StackTrace property (Exception class), 168-169 Start method, 265-266, 270 startup page, Visual Studio .NET IDE, 6 StateInfo class, 374-375 StateInfo object, 377-378 statements Debug, 171-173 INSERT, 301 TODO, 390-391 trace, adding to applications, 171-173 states (threads) Interrupting, 267 Resuming, 267 Running, 265-266 Suspending, 267 WaitSleepJoin, 266 Stdafx.cpp, 142 Stdafx.h, 142
Stencil Processing Support option (ATL), 183 stock properties, custom controls, 356 stored procedures, 10 StringStack class, 229-230 StringStack collection, 231-233 StringStack object, 230 StringStack.h file, 229 strong-named assemblies, 351-353 subhandler tag, 186 Suspend function, 267 Suspending state (threads), 267 synchronization, 263-265, 270-271 creating Consumer object, 274-276 creating multi-threaded objects, 272-274 creating Producer object, 274-276 events, 277-280 timers, 277-280 SyncRoot property, ICollection interface, 212 System namespace, 42 C++ types, 42-43 classes, 43 Collections namespace, 214 Control class, 356 Debug class, 166 Diagnostics namespace, 166 enumerations, 43 events, 43 Exception class, 166 Object pointer, 251 Threading namespace, 265 Trace class, 166
T tabbed interface (Visual Studio .NET IDE), 7 tables DataTables, 286, 295 data collections, 296 displaying data, 296-298 navigating data, 296-298
411
deleting records, 298-303 inserting records, 298-301 tags, replacement (ATL), 186 Task List (Visual Studio .NET), 14 Task list TODO items, 390-391 Task List window, viewing build errors, 230 test applications, ATL Web Services, 160-162 TestArray() function, 215, 217 TestQueue function, 222 TestStack function, 222 TestStringStack function, 229 Text property, 298-299 TextBox controls, 298-299 Thread class, 45, 265, 270 exceptions, 270 Start method, 265-266 threading, 263-264 creating threads, 265 Interrupting state, 267 multithreading, 264-265 creating multi-threaded applications, 268-270 creating multithreaded objects, 272-274 Resuming state, 267 Running state, 265-266 Suspending state, 267 synchronization. See synchronization ThreadSynch project, 267-270 WaitSleepJoin state, 266 ThreadRun method, 268 ThreadStart class, 268 ThreadState property, 265 ThreadStateException, 270 ThreadSynch project, creating, 267-270 Timer class, 279 TimerProc function, IntDataBlock class, 277 timers, synchronization, 277-280 TlbImp.exe, 314-319 tmain function, 229, 268, 274 TODO statements, 390-391
412
toolbars
toolbars adding to child windows, 88-90 printing, 122-128 ToString function, 297 Trace class, 171 adding trace statements, 171-173 Assert() method, 174 asserting on invalid values, 174 WriteLine() method, 171 trace statements, adding to applications, 171-173 transformations, GDI+, 108 trasitive properties, interfaces, 239-241 try cast keyword, 19 try..catch block, 170 TryEnter method, Monitor class, 271 type libraries, creating RegAsm.exe, 320-321 TypeConverter attribute, 361 typeof keyword, 19 typeof(ShapeConverter) parameter (TypeConverter attribute), 361
U UDDI (Universal Description, Discovery, and Integration), 151 unhook keyword, 257 unhook method, 245 Unified Event Model, 244-245 Universal Description, Discovery, and Integration, 151 unmanaged classes, creating, 328-330 unmanaged code, 53 accessing from .NET, 56 data types, 334 memory leaks, 330-331 migrating legacy code, 56 P/Invoke, 331-332 relationship to managed code, 54-55 running managed code with, 328-330
unmanaged pragma, 20-22 user controls, 345 user-defined attributes, creating, 20 UserControl class, 345 UserGuess form, 188 UserGuessCorrectly function, 190-191 UserGuessedCorrectly function, 188-190 #using keyword, 316 uuid parameter (module attribute), 201
V ValidateAndExchange function, 189 Validation Support option (ATL), 183 value keyword, 19 Value property, DictionaryEntry object, 224 ValueType class, 47-48 Views menu commands, Other Windows, 230 Visual Basic, 44, 64-65 Visual C++ .NET, 5 applications. See applications Visual Studio .NET Command window, 393-394 compiling. See compiling Configuration Manager. See Configuration Manager debugging. See debugging help system, 394 documention creation, 395 integration, 394-395 IDE, 6 Autohide feature, 8-9 help, 7-8 MDI interface, 8 startup page, 6 tabbed interface, 7 projects, 11 Server Explorer. See Server Explorer Solution Explorer. See Solution Explorer docking, 391-392 layouts, 392-393 profiles, 392-393
solutions, 11 source code window, 389 outlining feature, 390 task list shortcuts, 390-391 Task List. See Task List viewing build errors, 230 windows, 391 auto-hiding, 392 Developer Studio, 393 Visual Studio for Applications (VSA), 44 VSA (Visual Studio for Applications), 44
W Wait method, Monitor class, 271 WaitForSingleObject function, 270 WaitOne method, 280 WaitSleepJoin state (threads), 266 Web references, 149-151 Web services, ASP.NET, 140 ATL, 11 changing, 145-148 compiling default Service, 142-145 creating, 139-142 creating with ATL, 155-157 defining interface, 157-158 implementing interface, 158-160 test applications, 160-162 debugging default Service, 142-145 directories, 151-152 discovering, 152 files, managed code, 142 infrastructure, 140 managed C++, 11 within applications, 149-151 Web Setup Windows Installer project, 11 Web.config, 142 WebService class, 46 WebService.asmx, 142 WebService.cpp, 142-145
XML
WebService.h, 142-146 WebService.vsdisco, 142 WebServiceApp.cpp, 151 Win32 API, 10 Win32 namespace, 44 Windows, 391 auto-hiding, 392 child, MDI, 80-92 Developer Studio, 393 Command window, 393-394 Server Explorer. See Server Explorer Solution Explorer. See Solution Explorer docking, 391-392 Help, 8 layouts, 392-393 profiles, 392-393 Solution Explorer. See Solution Explorer source code, 389 outlining feature, 390 task list shortcuts, 390-391 Task List, 230 Windows Forms, 71 creating, 72-74, 77 classes, 74 initialization, 75-77 main() function modifications, 77 Push button, 78 GraphicsWindowsForm, 111-112 initializing, 75-77 MDI interfaces, 79-80 ObjectSerialization, 367, 372 Visual Basic, 64-65 WinMain() function, 36 wizards Add Member Variable, 28 Add Method, 311 Application, 26-27, 72 ATL Server Project, 156, 186 ATL Simple Object, 202, 310 custom, 10 WriteData function, Monitor class, 271 WriteData method (ReadData class), 271-273
X-Z XML (Extensible Markup Language), 285 ADO.NET design, 285 class, 46-47 serialization, 366-367
413