BLUEVISION PUBLISHING
James Henry
.NET Custom Controls & Designers using C# Developing
JAMES HENRY
Developing
.NET Custom Controls & Designers using
C#
Copyright 2002 James Henry All rights reserved. No part of this book may be reproduced, stored in a retrieval system or transmitted in any form or by any means, without the sole prior written permission of the publisher, except in the case of brief quotations embodied in critical reviews and articles. The author and publisher have made every effort to make the information in this book as accurate as possible. However, the information in this book is sold without warranty of any sort, either express or implied. Neither the author, BlueVision, LLC, nor its dealers, resellers nor distributors will be held liable for any damages caused or alleged to be caused either directly or indirectly by this book. The example people, products, organizations, and companies mentioned and depicted herein are fictitious, and no association with any real person, product, organization, or company is intended or should be inferred.
Published by BlueVision, LLC, 3395 English Oaks Drive, Kennesaw, GA 30144 www.bluevisionsoftware.com Printed in USA ISBN 0-9723179-0-2
Dedications This book is dedicated to my mother, Joyce, who helped me to become interested in computer programming at the age of 11.
Acknowledgements Microsoft, MSDN, ActiveX, COM, C#, Visual Basic, VB.NET, Visual C++, Visual J++, Visual Studio, and Visual Studio .NET are either trademarks or registered trademarks of Microsoft Corporation. Other Microsoft products and subsidiaries may also be trademarks or registered trademarks of Microsoft Corporation. Java is a registered trademark of Sun Microsystems. Other products are either trademarks or registered trademarks of their respective owners. BlueVision has made every effort to provide trademark information about the companies and products mentioned in this book. BlueVision, however, cannot and will not guarantee the accuracy of this information.
Credits Author James Henry
Project Administrator James Henry
Technical Reviewers James Henry Tamala Matthews
Illustrations James Henry
Technical Editors James Henry Kerri Betts
Cover Alfred Griffin James Henry
Proof Readers Kerri Betts James Henry Tamala Matthews
iii
iv
About the Author James E. Henry is the owner and Development Manager of BlueVision, LLC (http://www.bluevisionsoftware.com ), a company that specializes in software consulting and development. His responsibilities include evaluating and making decisions regarding new technologies. He is also the author of the BlueVision.NET Framework, which is written in C# and incorporates .NET’s designer technology. He is currently getting MCAD certified in .NET development. His experience with personal computers dates back to 1986, when he was only 11 years old. He became interested in programming when introduced to the Tandy Color Computer, which was then equipped with only a BASIC interpreter. Hoping to learn to write games, he began reading his first computer programming book, which accompanied the Tandy computer. Having no idea that programming would one day become his career, he coded simple print statements and calculators to amuse family members, friends and neighbors. James has worked and consulted with numerous companies providing his skills as a programmer. He initially began working with Waterways Experiment Station. His job responsibilities there included barge-dam hit analysis and a massive data-lookup program written in FORTRAN on a UNIX system. This was a real challenge. He has also worked with a company later bought by Harland Financial Solutions. There he was primarily responsible for traveling to various banking locations throughout the US to perform Y2K upgrades. The programming languages involved included Visual C++ and Visual Basic, as well as a proprietary scripting language. James also helped Microsoft in their beta test for MapPoint.NET, which is a web services framework designed to provide geo-lookups and location rendering via XML and SOAP. He has also investigated Microsoft’s recent beta, Speech .NET. His most recent primary job responsibility includes converting an MFC-based desktop application to C#, along with its COM based object model, for Siemens. Technologies used include SQL Server 2000 with its XML capabilities, XSLT, COM and .NET. James is also the developer of the BlueVision.NET Framework, a set of reusable controls, editors and designers to aid developers in accelerating their .NET development. He is also a natural graphic designer and web developer. He is the primary designer of www.bluevisionsoftware.com .
v
His skills include C++, COM, XML, XSLT, HTML, ASP, Java, Jscript, Microsoft Speech Recognition, DirectX, C#, Visual Basic, MFC, SQL, and Graphics Design. His published works include coding standards and guidelines papers, corporate-internal articles concerning the .NET Framework, design documents and functional specifications, and a public Tips and Tricks collection found at www.bluevisionsoftware.com/WebSite/TipsAndTricks.aspx . He has also written several articles for www.gotdotnet.com, a Microsoft .NET Online Community.
vi
Contents Summary Chapter 1: Introduction 1 Chapter 2: Events and Event Handlers 17 Chapter 3: Type Converters 29 Chapter 4: UITypeEditors 63 Chapter 5: Introduction to Windows Forms 93 Chapter 6: Windows Forms Data Binding 111 Chapter 7: GDI+ 143 Chapter 8: Introduction to Web Forms 177 Chapter 9: Rendering Server Controls 203 Chapter 10: ASP.NET State Management 229 Chapter 11: Templated and Composite Server Controls 261 Chapter 12: Introduction to Designers 281 Chapter 13: Design-Time Support 319 Chapter 14: Licensing 355 Chapter 15: Developing the Windows-based Wizard Control 375 Chapter 16: Developing the Web-based Tab Control 419 Index 455
vii
Table of Contents Chapter 1:Introduction
1
Overview............................................................................................................ 1 Audience ........................................................................................................... 1 Requirements.................................................................................................... 2 What This Book Will Cover .............................................................................. 2 History of Control Reusability......................................................................... 4 Evolution of .NET.............................................................................................. 4 Web Forms versus Windows Forms............................................................... 5 Inside Visual Studio .NET ................................................................................ 5 Start Page...................................................................................................................... 6 Server Explorer ............................................................................................................. 9 Toolbox........................................................................................................................10 Document Outline........................................................................................................ 10 Solution Explorer ......................................................................................................... 11 Class View................................................................................................................... 11 Object Browser............................................................................................................ 12 Internal Online Help..................................................................................................... 12 Task List ......................................................................................................................13 Intellisense...................................................................................................................13
Summary ......................................................................................................... 14
Chapter 2:Events and Event Handlers
17
Overview.......................................................................................................... 17 Delegates......................................................................................................... 18 Multi-cast Delegates....................................................................................... 20 Events.............................................................................................................. 21 Naming Conventions and Guidelines ........................................................... 26 Summary ......................................................................................................... 26
Chapter 3:Type Converters
29
Overview.......................................................................................................... 29 Introducing ITypeDescriptorContext ............................................................ 29 Introduction to Type Converters ................................................................... 30 Standard Values Support............................................................................... 34
Common .NET Type Converters.................................................................... 36 System.ComponentModel.StringConverter................................................................. 36 System.ComponentModel.BooleanConverter............................................................. 36 System.ComponentModel.CharConverter .................................................................. 37 System.ComponentModel.CollectionConverter .......................................................... 37 System.ComponentModel.CultureInfoConverter ........................................................ 37 System.ComponentModel.DateTimeConverter .......................................................... 37 System.ComponentModel.EnumConverter................................................................. 37 System.ComponentModel.ReferenceConverter ......................................................... 38 System.ComponentModel.ComponentConverter ....................................................... 38 System.ComponentModel.ExpandableObjectConverter ............................................ 38 System.ComponentModel.GuidConverter .................................................................. 38 System.ComponentModel.TimeSpanConverter.......................................................... 38 System.ComponentModel.TypeListConverter ............................................................ 39 System.Drawing.ColorConverter................................................................................. 39 System.Drawing.FontConverter .................................................................................. 39 System.Drawing.ImageConverter ............................................................................... 39 System.Drawing.ImageFormatConverter.................................................................... 39 System.Drawing.PointConverter ................................................................................. 40 System.Drawing.RectangleConverter ......................................................................... 40 System.Drawing.SizeConverter .................................................................................. 40 System.Web.UI.WebControls.FontNamesConverter.................................................. 40 System.Web.UI.WebControls.FontUnitConverter ....................................................... 40 System.Web.UI.WebControls.UnitConverter .............................................................. 40 System.Windows.Forms.OpacityConverter ................................................................ 41
Implementing a TypeConverter ..................................................................... 41 Summary ......................................................................................................... 60
Chapter 4:UITypeEditors
63
Overview.......................................................................................................... 63 Introducing IWindowsFormsEditorService .................................................. 66 DropDownControl........................................................................................................ 67 CloseDropDown .......................................................................................................... 68 ShowDialog ................................................................................................................. 68
Revisiting ITypeDescriptorContext............................................................... 72 Container property....................................................................................................... 72 Instance property......................................................................................................... 72 PropertyDescriptor property ........................................................................................ 72 OnComponentChanging method ................................................................................ 72 OnComponentChanged method ................................................................................. 73
Overriding UITypeEditor Methods ................................................................ 73 GetEditStyle method ................................................................................................... 73 EditValue method ........................................................................................................ 73
x
GetPaintValueSupported method ............................................................................... 74 PaintValue method ...................................................................................................... 74
Activity Diagram (Editing).............................................................................. 76 Activity Diagram (Painting)............................................................................ 80 Implementing a Simple UITypeEditor ........................................................... 81 Example: ColorEditor .................................................................................................. 82
Implementing a CollectionEditor .................................................................. 85 Example: ToolbarItemCollectionEditor........................................................................ 86
Summary ......................................................................................................... 90
Chapter 5:Introduction to Windows Forms
93
Overview.......................................................................................................... 93 Windows Forms Architecture........................................................................ 93 Main() Method ............................................................................................................. 94 InitializeComponent..................................................................................................... 95 Resources ................................................................................................................... 97 Localization................................................................................................................102
Control Layout .............................................................................................. 104 The Human Factor (Fitts’s Law) ................................................................................104 Controls Collection ....................................................................................................104 Docking......................................................................................................................106
Summary ....................................................................................................... 109
Chapter 6:Windows Forms Data Binding
111
Overview........................................................................................................ 111 Data Binding Concepts ................................................................................ 111 Data Providers...........................................................................................................111 Data Consumers .......................................................................................................113
Binding and BindingContext ....................................................................... 113 Binding.......................................................................................................................114 BindingContext ..........................................................................................................114
CurrencyManager ......................................................................................... 116 PropertyManager .......................................................................................... 119 Simple Binding Example.............................................................................. 119 Data Binding Interfaces ............................................................................... 127 IList ............................................................................................................................127 Typed IList .................................................................................................................127 IList and IComponent ................................................................................................127 IListSource.................................................................................................................127 ITypedList ..................................................................................................................128
xi
IBindingList ................................................................................................................128 IEditableObject ..........................................................................................................129 IDataErrorInfo ............................................................................................................131
Complex Binding Example .......................................................................... 132 Advanced Data Binding ............................................................................... 136 Dynamic Properties...................................................................................... 138 Summary ....................................................................................................... 140
Chapter 7:GDI+
143
Overview........................................................................................................ 143 Drawing Basics............................................................................................. 143 Pens and Brushes ........................................................................................ 146 Brushes .....................................................................................................................146 Pens ..........................................................................................................................148
The ControlPaint class................................................................................. 148 Manipulating Images and Icons .................................................................. 157 Creating an Oval Button .............................................................................. 158 Irregularly Shaped Forms ............................................................................ 165 Summary ....................................................................................................... 174
Chapter 8:Introduction to Web Forms
177
Overview........................................................................................................ 177 Server-Based Control Architecture............................................................. 177 CGI ............................................................................................................................178 MFC ISAPI Extensions..............................................................................................179 ASP ...........................................................................................................................180 ASP.NET ...................................................................................................................180
Web Server Controls .................................................................................... 183 Validation Controls....................................................................................... 186 Custom and User Controls .......................................................................... 187 Summary ....................................................................................................... 201
Chapter 9:Rendering Server Controls
203
Overview........................................................................................................ 203 Runtime Rendering ...................................................................................... 203 Resource-Based Scripts and Style sheets................................................. 212 Design Time Rendering ............................................................................... 224
xii
Summary ....................................................................................................... 226
Chapter 10:ASP.NET State Management
229
Overview........................................................................................................ 229 ASP.NET Intrinsic Objects ........................................................................... 229 HttpApplicationState..................................................................................................230 HttpSessionState.......................................................................................................231
Hidden Fields ................................................................................................ 232 Advantages................................................................................................................233 Disadvantages...........................................................................................................233
Query Strings................................................................................................ 233 Advantages................................................................................................................233 Disadvantages...........................................................................................................234
Cookies.......................................................................................................... 234 Advantages................................................................................................................234 Disadvantages...........................................................................................................234 Usage in ASP.NET....................................................................................................235
Using the View State .................................................................................... 239 Advantages................................................................................................................240 Disadvantages...........................................................................................................240
Handling Post-back Scenarios .................................................................... 240 Interfaces, Properties and Methods Related to Post Back .......................................241 Life Cycle of a Web Forms Control ...........................................................................245
Completing the ColorPicker Control........................................................... 248 Summary ....................................................................................................... 258
Chapter 11:Templated and Composite Server Controls
261
Overview........................................................................................................ 261 Managing Child Controls ............................................................................. 261 The Naming Container ..............................................................................................261 Parsing Behavior .......................................................................................................262 IParserAccessor ........................................................................................................264 Control Builders.........................................................................................................265
Data Binding ................................................................................................. 268 Implementing a Templated Control ............................................................ 269 Example: AddressControl..........................................................................................270
Summary ....................................................................................................... 279
Chapter 12:Introduction to Designers
281 xiii
Overview........................................................................................................ 281 The Designer Hierarchy ............................................................................... 281 Windows Forms Designer Hierarchy.........................................................................283 Web Forms Designer Hierarchy ................................................................................284
Designer Architecture .................................................................................. 307 Sites...........................................................................................................................308 Windows Forms Designer Architecture.....................................................................308 Web Forms Designer Architecture ............................................................................309
The Root Designer........................................................................................ 310 Service Providers ......................................................................................... 311 Service Container......................................................................................................311 Common Designer Services......................................................................................314
Introducing the DesignerHost ..................................................................... 316 Summary ....................................................................................................... 317
Chapter 13:Design-Time Support
319
Overview........................................................................................................ 319 The Toolbox .................................................................................................. 319 ToolboxItemAttribute .................................................................................................320 ToolboxItem...............................................................................................................321 IToolboxService.........................................................................................................322 ToolboxItemFilterAttribute .........................................................................................324 ToolboxBitmapAttribute .............................................................................................325
Designer Verbs ............................................................................................. 327 Extender Provider......................................................................................... 333 Implementing an Extender Provider..........................................................................334
Persistence ................................................................................................... 336 Persistence in Windows Forms .................................................................................336 Persistence in Web Forms ........................................................................................336
Serialization .................................................................................................. 338 DesignerSerializationVisibility ...................................................................................338 Designer Serializers ..................................................................................................341
Transaction Support .................................................................................... 347 Revisiting the Designer Host .....................................................................................348 Working with Transactions ........................................................................................349
Summary ....................................................................................................... 352
Chapter 14:Licensing
355
Overview........................................................................................................ 355 LicenseProvider............................................................................................ 355
xiv
LicFileLicenseProvider ..............................................................................................356
LicenseException ......................................................................................... 356 License .......................................................................................................... 357 LicenseContext............................................................................................. 357 LicenseManager ........................................................................................... 358 Using the LicFileLicenseProvider ............................................................... 361 Step 1: .......................................................................................................................362 Step 2: .......................................................................................................................363 Step 3: .......................................................................................................................363 Step 4: .......................................................................................................................364 Step 5: .......................................................................................................................364
Implementing A Custom LicenseProvider ................................................. 367 Summary ....................................................................................................... 372
Chapter 15:Developing the Windows-based Wizard Control
375
Overview........................................................................................................ 375 Step 1: The Architecture .............................................................................. 375 Step 2: The User Interface ........................................................................... 376 Wizard .......................................................................................................................377 BaseWizardPage.......................................................................................................377 ExteriorWizardPage ..................................................................................................378 InteriorWizardPage....................................................................................................380
Step 3: Runtime Implementation................................................................. 381 Step 4: Design-Time Support ...................................................................... 399 Summary ....................................................................................................... 416
Chapter 16:Developing the Web-based Tab Control
419
Overview........................................................................................................ 419 Step 1: The Architecture .............................................................................. 419 Step 2: The User Interface ........................................................................... 420 Step 3: Runtime Implementation................................................................. 426 Step 4: Design-Time Support ...................................................................... 444 Summary ....................................................................................................... 453
Index
455
xv
1
Chapter
Introduction Overview Welcome to .NET and the C# language. Or should I say welcome back, for seasoned .NET developers whose aims are to go deep into the core of creating custom controls and designers? We have worked very hard to develop what we consider an informative and challenging learning experience for you. The purpose of this book is to provide intermediate to senior-level developers the information they need to successfully implement custom controls targeting both windows and the web. Why do we also teach you other related architectures, such as designers? Unlike previous technologies relating to custom control development, the designer architecture is now exposed directly to the developer. And many books published thus far do not delve deep enough into the .NET framework to discuss topics such as type converter implementations and editors, even though they represent a vast portion of the .NET architecture. After reading this book, you as a developer will be able to write custom controls with ease, as well as gain a good understanding of the .NET design-time architecture. To assist in helping the readers to understand, we offer a suite of reusable .NET controls and editors, along with templates and sample code for creating custom ones. This will take your confidence level and expertise of the .NET framework to new heights.
Audience The audience consists of developers who already have some knowledge of C#, with the intent of working on the leading edge of .NET. They range from junior-level programmers to business professionals who are experienced with object-oriented design concepts and programming. They should also be familiar with Windows and web development. Any
C H A P T E R
1
experience in any of the following languages will also be a big plus: C++, Java, ASP, and HTML.
Requirements In order to successfully use this book and the sample code, you must have Visual Studio .NET RC1. In actuality, you only need the .NET framework SDK to successfully build and run C# applications. But to get the most of custom control and designer development, which is what this book covers, it would be easier to follow along with VS.NET. In order to successfully install and use VS.NET RC1, you must have at least a Pentium II based PC running at least Windows NT 4.0 with the Option Pack (although Windows 2000 is strongly preferred) and 64 MB RAM, 500 MB disk space on the system drive and 3 GB on the installation drive, a CDROM or DVD-ROM drive, a video card supporting at least an 800x600 display with 256 colors, a Microsoft mouse or compatible pointing device, and a legal copy of VS.NET RC1.
What This Book Will Cover In Chapter 1, “Introduction,” we present some historical information on reusability and we introduce the reader to .NET. We differentiate between the two main architectures exposed by .NET, Windows Forms and Web Forms. We also introduce Visual Studio .NET, describing some of the best features of the new IDE. In Chapter 2, “Events and Event Handlers,” we first introduce delegates. We then go on to discuss events by describing the event pattern and walking through a sample illustrating how to define and raise events. In Chapter 3, “Type Converters,” we define a type converter. We also list some of the most common type converters that are provided with the .NET framework. We end the chapter by implementing a custom type converter. Chapter 4, “UITypeEditors,” first introduces the role of a UITypeEditor in a .NET application. In this chapter, questions similar to the following will be answered: What is an editor? Why use an editor? We thoroughly examine the activities that take place during an editing session, and end the chapter by providing two samples. Chapter 5, “Introduction to Windows Forms,” talks about the .NET architecture as it relates to standard windows development. Here you will find material related to control layout, localization, and window docking.
2
I N T R O D U C T I O N
In Chapter 6, “Windows Forms DataBinding,” we examine the binding architecture of Windows Forms. We define data providers and data consumers, and provide examples on both simple and complex data binding. Chapter 7, “GDI+,” begins by going over the drawing basics. It ends by taking the reader through the details of creating an irregularly shaped form. Chapter 8, “Introduction to Web Forms,” introduces the reader to the new successor of ASP, coined ASP.NET. This chapter begins by introducing several of the server based architectures that were predecessors of ASP.NET. It then moves on to discuss the different types of controls that can be developed and run in an ASP.NET application. Chapter 9, “Rendering Server Controls,” describes custom control rendering in detail. It takes you through examples of both runtime rendering and design-time rendering. This is undoubtedly the most important chapter on Web Forms. In Chapter 10, “ASP.NET State Management,” we begin by describing the various ways to maintain state between post backs in an ASP.NET application. Pros and cons are given for each. We then talk about the interfaces provided by the .NET framework that relate to post back, and then discuss the life cycle of a web forms control. Finally, we provide the complete implementation of a custom server control that includes post back handling. Chapter 11, “Templated and Composite Server Controls,” discusses in more detail the types of server controls that can be created for use in an ASP.NET application. In this chapter, we implement a full-fledged templated server control. Chapter 12, “Introduction to Designers,” approaches custom control development from a new perspective. It introduces the reader to the .NET designer architecture by first detailing a list of common designers that are freebies in the .NET framework. Chapter 13, “Design-Time Support,” deals with some of the tools and services that relate directly to writing designers and enhancing design time functionality for controls and components. Topics such as persistence and serialization are discussed here. In Chapter 14, “Licensing,” we introduce the new licensing model and architecture that is available with .NET. We describe the classes and steps involved in implementing a custom licensing scheme. In Chapter 15, “Developing the Windows-based Wizard Control,” we used the knowledge provided in this book to implement a customizable windows-based Wizard control. This control conforms to the Wizard 97 standard, giving the user a reusable mechanism for developing wizards.
3
C H A P T E R
1
In Chapter 16, “Developing the Web-based Tab Control,” we use the web-based technologies as well as other chapters from this book to create a reusable ASP.NET tab control. The control supports both vertical and horizontal styles, allowing it to be customized to a user’s specific needs.
History of Control Reusability There have been many cases, with Windows and Web development, where powerful development tools don’t stand up to project-specific requirements. Perhaps a control or component doesn’t work, it’s too complex to learn, or it just doesn’t solve the problem. This leads to the developer having to build custom controls to match specific needs. Control reusability really began in 1996, with the advent of ActiveX. Some ancient developers may argue that theory, but it is acceptably true. Many large-scale MFC windows applications are embedded with dozens of ActiveX controls developed both internally and by third parties. With ActiveX, controls could be developed in one language, and then reused by any application that could invoke the methods of the ActiveX interfaces. In short, any application that was COM aware could take advantage of this technology. This attempt of control reusability, however, failed in web scenarios. In order for web applications to effectively appreciate the benefits of using ActiveX controls, target browsers must support them. And because ActiveX was developed by Microsoft, of course, only Internet Explorer would support them in the beginning. But that is no reason to criticize Microsoft, because Java applets followed the same proprietary path. They are even less efficient and slower than ActiveX controls.
Evolution of .NET Adding to the line of reusability techniques, Microsoft introduced .NET. .NET has actually revolutionized the software development industry, despite this section’s heading. ASP.NET, specifically, solved the problem introduced with ActiveX, by allowing the server to render pure HTML and send that to the client, ensuring that all target browsers will be supportive. .NET is a new framework, a new API, and a new runtime, which targets both windows and web development, with heavy focuses on the latter. As a framework, .NET incorporates the logic of a specific design. With a framework, a design is reused. And as an API, .NET provides a set of classes that will accomplish common tasks. You must note that an API and a framework are different in regards to what is actually reused. With frameworks, the application code can be reworked to a user’s ability. Because frameworks are hard to design and reimplement, they typically have an end result of high quality. On the other hand, an API is
4
I N T R O D U C T I O N
useful if the same piece of code will most likely be written by almost every developer, regardless of a specific design. Hence, code is reused. The .NET runtime provides the environment for your applications to run. Often referred to as the Common Language Runtime, or CLR, it allows your applications to run in a protected environment. When we say protected, we mean prohibiting your application from writing to any location of memory to which it has not been granted the rights.
Web Forms versus Windows Forms The Windows Forms programming model can be thought of as the .NET equivalent of the old Windows API. In all honestly, it is a lot more than that. It is also a framework, providing a model that can be reused by most application developers. Windows forms applications can be created such that all processing occurs on the client’s machine. However, the Windows Forms architecture allows the ability to easily connect to remote components via .NET web services. This concept can greatly simplify module reuse, introducing us to the rich client. The Web Forms architecture is just ASP revamped. Coined ASP.NET, web forms differ from ASP in that the architecture is fully object oriented. And unlike ActiveX, most browsers will support them because they render HTML, XML and other markup on the server. The rendered output is then sent to the client browser. Even though Windows Forms offer “zero deployment,” which means that applications can be downloaded over the internet and installed automatically, they still will be installed. Conversely, with web forms, the only installation requirement is that of the browser. Any code changes do not have to be reshipped to the client with service updates. But the downside of this is that web forms tend to be less responsiveness than windows forms. Control development with web forms is also more difficult than with windows forms, because everything is based on HTML at the core. You depend on the browser to correctly draw your controls, whereas in Windows development you control the drawing. With today’s advanced applications, you will no doubt have to tap into a little of both. Internet explorer with its ability to host ASP.NET controls, and Windows forms calling ASP.NET web services, both prove this theory. Now that we have covered the basics of the .NET architecture, we will move on to discuss the IDE that will help you cover the grounds, Visual Studio .NET.
Inside Visual Studio .NET Visual Studio .NET is the main development environment that you will use to build your applications and components. It is fully integrated with a debugger, code editor, online help
5
C H A P T E R
1
and a designer, among others, some of which will be discussed in more detail shortly. VS.NET combines both the environment of Visual Basic 6 and Visual C++. With VS.NET, you can develop windows applications, web applications, installation programs, and web services, all within on IDE. Code can be written as rapidly as code was written in VB6, with its RAD designer architecture. We will now discuss the individual elements of the IDE separately.
Start Page The VS.NET start page contains a list of tabs, referred to as applications, on the left, with each displaying a different UI on the right. The first time you start VS.NET, you will be introduced to an HTML start page, similar to the following:
My Profile The start page displays the “My Profile” tab, which allows you to customize the IDE so that it meets your needs. Because programmers will be coming from different backgrounds, this is a very helpful tool, allowing you to work in a way that you are accustomed to. In the illustration,
6
I N T R O D U C T I O N
you will notice that the “Visual C++ 6” profile has been selected, which automatically hides the toolbox in the IDE. Get Started The Get Started page displays a list of your most recently saved projects. This page gives you a quick way to load a commonly used project, as well as a way to quickly create a new project. It also contains a Find Samples tab that allows you to search for samples on your machine, shown here:
What’s New This tab searches the internet for the most recent technical news and events. Service packs can normally be downloaded from this tab. In actuality, at the time of this writing, I am about to use it to download Service Pack 2 of the .NET framework. Online Community You may find this tab very useful. It contains links to code sharers, news groups, and component vendors. You now have the ability to search for reusable code and samples right where you need it the most, the IDE. Headlines News links may also be found here. Similar links include service pack updates, and links to .NET articles.
7
C H A P T E R
1
Search Online This tab simply allows you to search the MSDN online directory. Downloads Downloads contain almost everything you would find at MSDN downloads. Subscribers can also login right through the environment to make changes to their subscriptions and download updates. XML Web Services The tab connects directly to the UDDI registry. It allows you to register a web service right through the IDE. You also have the ability to search the UDDI for available services that match a certain category, as shown:
Web Hosting The Web Hosting tab lists some of the .NET hosting companies. Hosting for all type of solutions can be found here. Visual Studio even lets you connect to the hosting company and deploy your web project in a very trivial way.
8
I N T R O D U C T I O N
BlueVision You will not see this tab unless you have customized your VS.NET start page to look like mine. Yes, the start page can be customized to meet your specific needs. This section will not discuss the details of this, but a very cool article can be found on the tips and tricks section of www.bluevisionsoftware.com, as well as the General .NET Links and Resource Center at www.gotdotnet.com. Just search for Start Page.
Server Explorer The server explorer is a great feature of the IDE indeed. You can configure database connections, create tables and execute stored procedures, as well as read the event log and configure services. It’s like working on what you love without leaving home. See below:
9
C H A P T E R
1
Toolbox Next to server explorer, by default, is the toolbox. This context sensitive toolbox automatically refreshes its items, depending on the type of document currently open. Later, in Chapter 13, you will see how you can write a designer to programmatically add toolbox items whenever the designer document is opened, and remove them when they are closed. This allows your designer to expose your tools without forcing the user to select “Customize Toolbox” on the menu to manually add them. Clipboard Ring One of the tabs on the toolbox is the Clipboard Ring. This is another one of the IDE’s best improvements. When you copy or cut items and paste them in documents, the clipboard ring keeps track of up to 20 items. Therefore, when its time to paste something that you copied a few iterations ago, you can simply select that item from the list and paste it to your document.
Document Outline This view is very useful for web developers. When viewing an ASP.NET web form in the designer, you will sometimes need to select a particular element, for example, a TD instead of a TR, to change its properties. By using the document outline, you can correctly select the appropriate element. It is also an efficient way to help a developer see the layout of a page, without having to switch to HTML view and be overwhelmed with unnecessary markup. Here is the document outline in action:
10
I N T R O D U C T I O N
Solution Explorer With VS.NET, all projects are now managed as solutions. Visual J++ developers will be used to this, but to Visual C++ developers, this may be a little Greek. But the concept is generally the same as that of the old VC++ workspace. A solution is a collection of projects. The solution file, with the SLN extension, does not define any workspace elements. These are defined in a solution options file, with the SUO extension. Therefore, it made more since to use the name solution instead of workspace.
Class View The Class View remains the same with its ancestors. Classes will be displayed with methods and properties, showing parent classes deeper in the hierarchical tree. It allows you to add methods, properties, fields and indexers. Visual C++ users will be familiar with this.
11
C H A P T E R
1
Object Browser The Object Browser, which originated in VB6, is similar to Class View, except that it allows you to add many assemblies to the view. Each assembly is added as a root node to a tree. The child nodes of the root will display all types within that assembly. A summary is also shown in the bottom pane of the view. This is illustrated below:
Internal Online Help Now, what you will do most has been made as easy as ever. Help has now been integrated into the IDE. Not just the Help menu, but the full application. Online Help is also Dynamic Help, because it allows an automatic search of what is selected in source code. In the following snapshot, you can see that the same text highlighted in source code has been automatically searched for in the Help collection:
12
I N T R O D U C T I O N
Task List This pane lists the common tasks that are related to your project. Whenever there is a line that begins with the comment // TODO, VS.NET automatically adds that line to the task list. You can also right click a line of code to add a shortcut to that line to the task list. Therefore, whenever you double-click that item in the task list, you will be directed to the related line of code.
Intellisense Intellisense is a powerful weapon when it comes to software development. And VS.NET does not remove this weapon from history. Intellisense shows the members of an object as well as a brief description of each method. Even with ASP.NET, intellisense remains. Through the use of schemas, VS.NET displays any available attributes for the element that is currently being typed. And for those interested, you can also create your own schema so that your custom web
13
C H A P T E R
1
controls benefit from intellisense. See the asp.xsd file, located in the sub path, Common7\Packages\schemas\xml, of your VS.NET installation directory.
Summary In this chapter, we provided a brief history of reusability and its impacts on control development. We also surveyed the elements of the new IDE involved in building new generation applications, Visual Studio .NET. Now that we have this background, we will move on to the next few chapters to introduce some prerequisites to developing custom controls.
14
2
Chapter
Events and Event Handlers Overview Many programming languages have had to implement their own mechanisms for handling events. For example, in C++, we use function pointers. Many developers may refer to these as callbacks. This is useful in sort routines. You would simply invoke the Sort method of a class, passing it a pointer to a function that serves as the callback. The sort method would then invoke the callback using the pointer passed to it. In ATL, we use connection points. Connection points serve as classes that implement callback interfaces. The COM server invokes methods on this interface, passing any relevant data to a subscribed client. Both of these implementations had serious drawbacks. In C++, only one client at a time was able to receive events through function pointers. To support multi-client updates and events, developers would have to cook up their own implementations to somehow store a list of all function pointers. With ATL, the code involved in setting up a connection point could be overwhelming, and some developers would stray away. Even client-side C++ application used a nasty way of subscribing to COM events. The client would have to implement an event interface called a sink object. This interface is referred to as an outgoing interface. The COM object would then invoke methods on the sink object. A delegate in C# is similar to a function pointer and a connection point. A delegate is simply an object that encapsulates a reference to a method. This encapsulated method can either be an instance method or a static method. Delegates and events are closely tied. We will now explore them in more detail.
C H A P T E R
2
Delegates A delegate in C# is similar to a C++ function pointer and an ATL connection point. A delegate is a C# object that encapsulates a reference to a method. A delegate is object oriented and type safe. Also, it can reference a static or instance method, whereas a function pointer can only reference a static method. The Delegate class is an abstract class, which means that you cannot instantiate it directly. Only compilers and the CLR can instantiate the Delegate class directly. A Delegate instance is instantiated by defining and declaring a delegate with a reference to a method handler. Here is the syntax for declaring a delegate: delegate int Calculate(int x, int y);
The keyword “delegate” is a C# specific keyword. The “Calculate” method in the above example is now an instance of the System.Delegate class. You can now use “Calculate” as if it were a class, as shown below: delegate int Calculate(int x, int y); Calculate additionHandler = new Calculate(MyAdditionHandler);
In the code above, we have instantiated a delegate instance called “additionHandler.” The constructor of any delegate instantiation takes a method as a parameter. The method passed to it must match the delegate exactly; that is, it must use the same parameter list and the same return type: delegate int Calculate(int x, int y); Calculate additionHandler = new Calculate(this.MyAdditionHandler); private int MyAdditionHandler(x, y) { // Your addition implementation goes here. }
18
E V E N T S
A N D
E V E N T
H A N D L E R S
The method passed to your delegate’s constructor must adhere to the accessibility rules. For instance, a private method of another object will produce a compiler error. Even though the example illustrated above is not a likely callback scenario, it illustrates the syntax in declaring and defining a delegate. The Delegate class has two public properties, Method and Target. Method: Returns the method represented by the delegate. This is an instance of MethodInfo, which is a class that describes a method’s metadata. This property will throw a MemberAccessException if the caller does not have access to it. The MethodInfo returned will always be the last method in the invocation list. Target: Returns the class instance which contains the method handler. This property may return null if the delegate represents a static method. In addition to these properties, the Delegate class also has a few public methods that are worth mentioning: GetInvocationList: Returns an array of delegates representing the invocation list. If the delegate is single-cast, the array will only have one element. Each delegate in the array is a non-combinable delegate. Therefore, the invocation list of each delegate in the array will reference a single method. Otherwise, each delegate in the array would contain the same invocation list. DynamicInvoke: Dynamically invokes the method associated with the current delegate. This is referred to as late-bound invocation. This method takes a single parameter, which is an array of object instances representing the method’s parameter list. It returns a single object representing the result of the method call. Combine: A static method that concatenates the invocation lists of combinable delegates. This returns a single delegate with the concatenated invocation list. CreateDelegate: A static method that creates a delegate for static methods only. Remove: A static method that removes the invocation list of one delegate from the invocation list of another delegate. This method is the opposite of Combine. When discussing delegates, it may be confusing unless you know the context in which you are using the term. In some documentation, delegates will often be used to represent the delegate instance as well as the delegate class. In other words, you define a class and instantiate an object. With delegates, you define a delegate and instantiate a delegate.
19
C H A P T E R
2
A delegate can be either single-cast or multi-cast. A single-cast delegate is non-combinable whereas a multi-cast delegate is combinable. Combinable means that the invocation lists may be concatenated to create a new delegate with the combined invocation lists.
Multi-cast Delegates A multi-cast delegate is derived from System.MulticastDelegate. Every delegate that is defined with a return type of void is automatically derived from System.MulticastDelegate. Multi-cast delegates can wrap more than one method. Invoking these delegates result in all encapsulated methods being invoked. For this reason, the return type should be void. Otherwise, only the return value of the last method will be used. The internal implementation uses an ordered list to store references to all methods attached to the delegate. Delegates can be multi-cast by simply using the overloaded + and += operators. For example: Calculate additionHandler = new Calculate(this.MyAdditionHandler); Calculate substractionHandler = new Calculate(this.MySubstractionHandler); Calculate calculateHandler = additionHandler + substractionHandler; // NOT GOOD int result = calculateHandler(10, 5); private int MyAdditionHandler(int x, int y) { return x + y; } private int MySubstractionHandler(int x, int y) { return x – y; }
The code above is still not good code. Because as just stated, multi-cast delegates should not be defined to return a type other than void. There is no determination of knowing which return value should be used anyway. The return value of the last method will always be returned.
20
E V E N T S
A N D
E V E N T
H A N D L E R S
Events By definition, an event is a message sent or raised by an object to signal or notify a subscriber about the occurrence of an action. This action is normally in response to user input. An event is a special type of delegate. The idea behind events is that you want certain code to be informed when some action or event takes place. For example, a Word processor needs to be informed when a character key is pressed on a keyboard, in order to display that character on the screen. A button needs to listen for mouse clicks in order to invoke the action represented by the button. Whenever action is taken by the listener of the event, we say that the listener handles the event. The process of associating an event handler with an event is called event wiring. During the communication of the event, the sender of the event does not know which object will handle it. It uses subscribed delegates to communicate the occurrence of an event. Here is the basic event pattern:
21
C H A P T E R
2
Client Object
Event Handler Method
Subscribes
Client Object
Client Object
Event Handler Method
Event Handler Method
Subscribes Subscribes
Invokes Invokes Event User Input, Timer, etc.
Event Generator Object
From the architecture, you will notice that multiple clients may subscribe to the same event, which is contained in the Event Generator object. The event generator object may be any object that raises events, such as a control. Whenever some environmental change happens, such as user input or a timer elapse, the event generator will invoke the event’s delegates, which is known as raising the event. All subscribed callers will then have their handler methods called. The syntax for declaring and defining an event is similar to declaring and
22
E V E N T S
A N D
E V E N T
H A N D L E R S
defining a delegate. But with an event, you don’t instantiate the delegate. It is up to the subscribers, or clients, to do so. Here is the syntax for defining, declaring, and invoking an event: public MyControl : Component { public event MouseEventHandler Click; private bool _mouseDown = false; protected virtual void OnLeftButtonDown { _mouseDown = true; } protected virtual void OnLeftButtonUp { if (_mouseDown) { int x = Cursor.Position.X; int y = Cursor.Position.Y ; MouseEventArgs e = new MouseEventArgs(MouseButtons.Left, 1, x, y, 0); OnClick(e); } }
}
protected virtual void OnClick(MouseEventArgs e) { if (Click != null) { Click(this, e); } }
From the snippet, you can see that since the Click event has not been instantiated, we must perform a check to see if it is non null. If it is valid, we then invoke all delegates that subscribed to it. Even though Click is initially null, clients can still reference using the + and += operators. This is due to the event keyword.
23
C H A P T E R
2
When developing a custom control which will have events, special care should be taken on how you implement your events. In particular, a control that will raise many events should have its event implementation coded differently than a control with only a few events. The compiler will generate a single field per delegate instance. Therefore, it’s better to use static objects and the control’s Events property for storing the event delegates. In this case, here is the syntax for implementing an event: public class MyControl : Component { private static readonly object _clickEvent = new object(); private static readonly object _mouseMoveEvent = new object(); private static readonly object _mouseDownEvent = new object(); private static readonly object _mouseUpEvent = new object(); public event MouseEventHandler Click { add { Events.AddHandler(_clickEvent, value); } remove { Events.RemoveHandler(_clickEvent, value); } } public event MouseEventHandler MouseMove { add { Events.AddHandler(_mouseMoveEvent, value); } remove { Events.RemoveHandler(_mouseMoveEvent, value); } } public event MouseEventHandler MouseUp { add { Events.AddHandler(_mouseMoveEvent, value); } remove {
24
E V E N T S
}
}
A N D
E V E N T
H A N D L E R S
Events.RemoveHandler(_mouseMoveEvent, value);
public event MouseEventHandler MouseDown { add { Events.AddHandler(_mouseDownEvent, value); } remove { Events.RemoveHandler(_mouseDownEvent, value); } }
}
public event MouseEventHandler MouseMove { add { Events.AddHandler(_mouseMoveEvent, value); } remove { Events.RemoveHandler(_mouseMoveEvent, value); } }
In the code above, we simply add delegates to the Events property of the control, which is an instance of EventHandlerList. The EventHandlerList class acts as a linked list, and uses an internal class called ListEntry, which acts as a linked list element. The ListEntry class contains three members: a key of type object; a key representing the next object in the linked list; and the delegate to be invoked, combined or removed. The AddHandler method first checks to see whether the key has already been stored. If so, it retrieves the delegate associated with that key. It then combines the delegate passed in with this delegate. If the key has not been stored, it uses the delegate passed to it, and stores it in the linked list.
25
C H A P T E R
2
Naming Conventions and Guidelines When defining delegates, events and event handlers, we must adhere to some coding standards. These standards are necessary since multiple programming languages may use events and delegates from C#. When defining a delegate for an event, you should append the delegate with “EventHandler.” Some possible names are CommandEventHandler, MouseEventHandler, and KeyEventHandler. When defining an event, you should not append the event with “Event.” Some possible event names are Click, MouseMove, MouseDown, KeyPress, and RowUpdated. When defining a delegate for an event, you should provide two parameters. The first should be of type Object, which will represent the object that raised the event. The second should be a type derived from System.EventArgs. This object will contain the data specific to the event. Use the correct verbiage when naming events. For example, events that denote something that has happened should be named with the past-tense of that event, such as Clicked. Events that denote something that is happening or is about to happen should be named with a gerund, like “ing.” Do not name events such as OnClick and OnClose. Names like these should be used as virtual methods of the event generator class. Instead, names like Click, Clicking, Close and Closing would suffice. When defining an EventArgs class, you should append the class name with “EventArgs.” Some possible names are MouseEventArgs, KeyEventArgs, and CommandEventArgs.
Summary In this chapter, we learned the similarities and differences between delegates, C++ function pointers, and ATL connection points. We discussed several coding standards and guidelines regarding event implementation. You should now feel comfortable implementing events in your custom components and controls.
26
3
Chapter
Type Converters Overview As we all know, there are times when we need to convert from one data type to another one. For example, a date type will need to be converted to a string representation in order to be displayed on the screen. Conversely, the string value of a date must be converted to a date type to be stored appropriately in a database. Casting is enough for simple types. But as objects become more and more complex, a better technique is needed. .NET solves this problem with type converters. Type converters are classes that describe how a particular object converts to and from other data types. We will go into complete detail of type converters. But first, we must introduce an important prerequisite interface, ITypeDescriptorContext.
Introducing ITypeDescriptorContext The ITypeDescriptorContext interface provides contextual information about a component. Such information includes the container the component is hosted on and the component’s PropertyDescriptor. The properties of this interface are described next: Container: When associated with type converters, this property typically returns the container used to display or edit the value that is being converted. It basically gets the container that represents the TypeDescriptor request. Instance: Returns an instance of the object that is connected to the TypeDescriptor request. During type conversions, this may be an instance of the object that is being converted. It also may represent an instance of a control used to host the object. It is entirely up to the developer to control how this property is used.
C H A P T E R
3
PropertyDescriptor: Gets the PropertyDescriptor for the object representing the TypeDescriptor request. The following methods are also members of the ITypeDescriptorContext interface: OnComponentChanged: This method should raise the ComponentChanged event. In order to do this, you should retrieve the IComponentChangeService and invoke its OnComponentChanged method. You may also decide to implement IComponentChangeService when implementing ITypeDescriptorContext. Or you may provide a public event named ComponentChanged, and invoke the event inside this method. The choice is yours. OnComponentChanging: This method should return a Boolean value indicating whether the component can be changed. Callers should call this method before making any changes to the object directly or through the PropertyDescriptor. The PropertyGrid has its own internal implementation of ITypeDescriptorContext, called PropertyDescriptorGridEntry. In this implementation, the Container is normally a null value. The Instance property is set to the object currently selected into the PropertyGrid. The PropertyDescriptor will change throughout, representing each property being displayed.
Introduction to Type Converters Type converters are classes that define how an object converts to and from other data types. They are typically used during design time for string conversions. They are also used during runtime for validation and conversions. Though, these are not their only uses. For example, the PropertyGrid allows a property to be represented as a string when it displays the property. Any changes made to that string value in the PropertyGrid will then need to be converted back to an equivalent value of the original object’s data type. .NET enables this conversion through its TypeConverter class. Before we dig into the details of the TypeConverter class, we will talk about another class that is primarily related to type conversions, the InstanceDescriptor. An InstanceDescriptor provides the information necessary to create or recreate instances of an object. TypeConverter objects sometimes use an InstanceDescriptor to create an instance of an object during conversions. For example, let’s say we have a Size object that has been serialized to a string and needs to be recreated. First, we must get the Size object’s constructor, as shown in the following code:
30
T Y P E
C O N V E R T E R S
ConstructorInfo ctor = typeof (Size).GetConstructor (new Type [] {typeof (int), typeof (int)});
We use the GetConstructor method of the Type object, passing it an array of parameter types for the constructor. This returns a ConstructorInfo object, which can be passed to the InstanceDescriptor. Next, we used the serialized values, the ConstructorInfo object, and an InstanceDescriptor to reconstruct the Size object. int x = GetX(); int y = GetY(); InstanceDescriptor instance = new InstanceDescriptor(ctor, new object[] {x, y}); instance.Invoke();
We can then use the Invoke method, if necessary, of the InstanceDescriptor. This method returns an instance of the object that the InstanceDescriptor describes. The value returned is really an object array. Now that we have a basic understanding of the InstanceDescriptor class, let’s move into the details of the TypeConverter. TypeConverters are applied using the TypeConverterAttribute, found in the System.ComponentModel namespace. This attribute simply provides a type derived from TypeConverter that will be used to perform conversions on the given object. This attribute may be applied to properties and fields of an object, as well as a type, such as a class. If the TypeConverterAttribute is applied to a type, it does not need to be reapplied to individual properties of that type, unless you wish to override the TypeConverter for those properties. Here is the syntax for applying a TypeConverter to a class: [TypeConverter(typeof(ShapeConverter))] public class Shape { … }
31
C H A P T E R
3
Here is the syntax for applying a TypeConverter to a property, this time, using a fully qualified type name. (We are using the fully qualified type name just for demonstration purposes. We could have just as well used the Type): namespace Office { public class Fixture { private Shape _fixtureShape;
}
}
[TypeConverter(“Office.ShapeConverter, OfficeAssembly”)] public Shape FixtureShape { get { return _fixtureShape; } set { _fixtureShape = value; } }
public class ShapeConverter : TypeConverter { … }
In the snippet above, note that the fully qualified type name includes the namespace, type and assembly name. If the assembly was placed in the Global Assembly Cache, we would also have to specify the public key token and version number. To override a TypeConverter with no converter, simply apply the TypeConverterAttribute with the default constructor, as shown below: public class InvisibleFixture { [TypeConverter()] public new Shape FixtureShape { …
32
T Y P E
}
C O N V E R T E R S
}
When accessing a TypeConverter, you should never instantiate a TypeConverter directly. You should use the TypeDescriptor.GetConverter method to ensure that the correct converter is returned. As a quick note, a TypeDescriptor describes most of the metadata on a property or type. For instance, the GetConverter method returns the TypeConverter represented by the TypeConverterAttribute; the GetEditor method returns the an object represented by the EditorAttribute; the GetDefaultProperty and GetDefaultEvent methods return PropertyDescriptor object and EventDescriptor object respectively, that represent the DefaultPropertyAttribute and DefaultEventAttribute. Now, let’s examine some of the useful methods of the TypeConverter class: CanConvertFrom: Returns a Boolean value indicating whether the converter can convert an object of the specified type to the type that this converter represents. CanConvertTo: Returns a Boolean value indicating whether the converter can convert an object to the specified type. ConvertFrom: Converts the specified value to the type represented by this converter. ConvertFromInvariantString: Converts the string representation of a value to a type that this converter represents, using the invariant culture, which is English. ConvertFromString: Converts the string representation of a value to a type that this converter represents, using the given culture. ConvertTo: Converts the given object to the specified type. ConvertToInvariantString: Converts the given object to a string, using the invariant culture. ConvertToString: Converts the given object to a string, using the specified culture. CreateInstance: Creates or recreates an object given a dictionary of property values. The dictionary contains property name-value pairs.
33
C H A P T E R
3
GetCreateInstanceSupported: Returns a Boolean value indicating whether CreateInstance has been implemented. GetProperties: Returns a collection of PropertyDescriptor objects for the given object. GetPropertiesSupported: Returns a Boolean value indicating whether the given object supports properties. GetStandardValues: Returns a collection of standard values for the type that this converter represents. GetStandardValuesExclusive: Returns a Boolean value indicating whether the standard values are mutually exclusive. GetStandardValuesSupported: Returns a Boolean value indicating whether GetStandardValues is implemented. IsValid: Returns a Boolean value indicating whether the specified value is valid for the type that this converter represents.
Standard Values Support Standard values support is enabled in the TypeConverter class through the GetStandardValuesSupported and GetStandardValues methods. These methods enable a converter to return a collection of supported values for the type that the converter represents. This is useful when the UI needs to fill a list box, for example, with a list of supported data values for a user to select. The .NET framework includes several of these converters, including the EnumConverter and ColorConverter. Some of the common .NET type converters, such as these two, will be discussed in the next section. But first, let’s look at a couple of pictures of the PropertyGrid displaying standard values:
34
T Y P E
C O N V E R T E R S
The picture above shows the standard values for the FormBorderStyle enumeration type. For enumerations, the set of standard values will typically be a set of all the enumeration members. But be aware, because a custom type converter could easily override this set of standard values.
35
C H A P T E R
3
In the picture above, the set of standard values include all available cultures supported by the operating system. An additional value, “(Default)”, is added to the set to represent the invariant culture. Each of the values in the set of standard values represents a CultureInfo object that can be converted to and from a string.
Common .NET Type Converters The .NET framework is already equipped with several useful and reusable type converters. We will examine each of these type converters briefly to help provide knowledge of what is already out there, so that the wheel won’t be reinvented.
System.ComponentModel.StringConverter This class provides the capability to convert strings to and from other representations.
System.ComponentModel.BooleanConverter This class provides the capability to convert Boolean values to and from other representations. Actually, this converter can only convert to and from string representations. A value of true will be converted to and from “True.” A value of false will be converted to and from “False.”
36
T Y P E
C O N V E R T E R S
System.ComponentModel.CharConverter This class provides the capability to convert a Char value to and from other representations. The default implementation can only convert to and from one-length strings. You may never actually have to use this converter, since the String class inherently supports casting and converting to and from Char values.
System.ComponentModel.CollectionConverter This class is designed to convert a collection to a string representation. The string representation is typically “(Collection)”. Also, GetProperties and GetPropertiesSupported are overridden to return null and false respectively.
System.ComponentModel.CultureInfoConverter This converter can only convert cultures to and from string representations. It uses the static method CultureInfo.GetCultures to override the GetStandardValues method. It converts the string “(Default)” to the invariant culture, and vice-versa.
System.ComponentModel.DateTimeConverter This type converter can convert DateTime objects to and from string representations only. It uses the DateTimeFormatInfo and the Parse method of the DateTime class to perform conversions.
System.ComponentModel.EnumConverter This converter can only convert enumerations to and from a string. It uses the Parse method of the Enum class to aid in this conversion. The PropertyDescriptor uses this converter to provide a drop down list for selection of enum values. Each enum value is added to the StandardValuesCollection returned by GetStandardValues.
37
C H A P T E R
3
System.ComponentModel.ReferenceConverter This type converter converts object references to and from other representations. It is typically used with sited components and design environments. It converts references of objects that implement IComponent. It makes use of the System.ComponentModel.Design.IReferenceService, which can return all references to a specified object within the designer. These references are added to the StandardValuesCollection along with the null value. Implementers should override the IsValueAllowed method if a specific value should not be added to the StandardValuesCollection.
System.ComponentModel.ComponentConverter This class derives from ReferenceConverter. It converts components to and from other representations by overriding GetProperties and GetPropertiesSupported to return the properties through the GetProperties method of the TypeDescriptor.
System.ComponentModel.ExpandableObjectConverter This converter converts objects to expandable representations. It overrides GetProperties and GetPropertiesSupported to return the properties through the GetProperties method of the TypeDescriptor.
System.ComponentModel.GuidConverter This converter converts Guid objects to and from string representations. It does not support standard values; therefore, GetStandardValuesSupported returns false. It uses the Guid constructor when converting from a string and the Guid’s ToString method when converting to a string.
System.ComponentModel.TimeSpanConverter This converter can only convert TimeSpan objects to and from a string. It uses the TimeSpan.Parse method when converting from a string and the ToString method when converting to a string. The string representation is normally in the form, “hh:mm:ss”.
38
T Y P E
C O N V E R T E R S
System.ComponentModel.TypeListConverter This converter is used to provide a list of possible types for the object being converted. One such situation is when a list box needs to be populated to allow selection of a type for dynamic invocation. This class is abstract; it is up to the developer to provide the list of available types in the constructor of the derived class.
System.Drawing.ColorConverter This class converts Color objects to and from string representations. All standard colors and system colors that will be returned by GetStandardValues are stored in a Hashtable. The string representation of a color is typically the value of the Color.Name property. However, sometimes, it may be the RGB value if the color doesn’t have a name.
System.Drawing.FontConverter This converter converts Font objects to and from other representations. The string representation of a Font object is usually a combination of the Font’s Name, Size and Unit. A lot of string manipulation is used when converting fonts.
System.Drawing.ImageConverter The ImageConverter converts images to and from other representations, typically to and from MemoryStream objects.
System.Drawing.ImageFormatConverter This converter converts ImageFormat objects to and from strings. It uses the static properties of ImageFormat to return a StandardValuesCollection through GetStandardValues. Such values include Bmp, Gif, Icon, Jpeg, Tiff, Wmf, Png, and Icon, among others.
39
C H A P T E R
3
System.Drawing.PointConverter This converter converts Point structures to and from string representations. It implements GetCreateInstanceSupported and CreateInstance so that a change to the X or Y property results in a new Point object.
System.Drawing.RectangleConverter This converter converts Rectangle structures to and from string representations. Similar to the PointConverter class, it implements GetCreateInstanceSupported and CreateInstance so that a change to the X, Y, Width, or Height properties results in a new Rectangle object.
System.Drawing.SizeConverter This converter converts Size structures to and from string representations. Similar to the RectangleConverter class, it implements GetCreateInstanceSupported and CreateInstance so that a change to the Width or Height properties results in a new Size object.
System.Web.UI.WebControls.FontNamesConverter This converter converts a string containing font names to an array of strings of individual font names. Each font name in the string must be separated by a comma in order to be converted correctly.
System.Web.UI.WebControls.FontUnitConverter This class converts FontUnit objects to and from string representations. It can also convert FontUnit objects to FontSize objects.
System.Web.UI.WebControls.UnitConverter This class converts Unit objects to and from strings. It uses the Parse method of the Unit class to aid in the conversion process.
40
T Y P E
C O N V E R T E R S
System.Windows.Forms.OpacityConverter This class provides the capability to convert Opacity values to and from string representations. It simply converts double values to percentages for display purposes, and vice-versa.
Implementing a TypeConverter First worth noting is that the same type converter can be used in both web forms and windows forms. In order to implement a full-fledged type converter, we must follow these steps: 1. Derive a class from System.ComponentModel.TypeConverter. 2. Override the CanConvertFrom method to specify the types that the converter can convert from. 3. Override the ConvertFrom method to perform the type conversion from the given type. 4. Override the CanConvertTo method to specify the types that the converter can convert to. 5. Override the ConvertTo method to perform the type conversion to the given type. 6. Override the IsValid method to perform a validity check. 7. Override GetStandardValuesSupported to indicate whether a set of standard values is supported. 8. Override GetStandardValues to return a collection of the type’s standard values. 9. Override GetCreateInstanceSupported to indicate whether instances of an object of the type can only be created with CreateInstance. 10. Override CreateInstance to recreate instances of an object. Now, that we have the ten commandments for creating a type converter, let’s do just that. In this example, we will define a Vehicle class that represents a vehicle, of course. The vehicle will have a Body style that can be either of a Coupe, Sedan, or Wagon; and a property indicating a cylinder value, such as 4, 6, 8, or 12. Each body style will also have a custom exterior color. Here are the classes, properties and enums that represent the vehicle:
41
C H A P T E R
3
namespace TypeConverterSample { public enum Cylinder { Four, Six, Eight, Twelve } public class Vehicle { private Cylinder _cylinder; private Body _bodyStyle; public Cylinder Cylinder { get { return _cylinder; } set { _cylinder = value; } }
}
public Body BodyStyle { get { return _bodyStyle; } set { _bodyStyle = null; } }
public abstract class Body { private Color _exteriorColor; public Color ExteriorColor { get
42
T Y P E
C O N V E R T E R S
{
}
}
return _exteriorColor; } set { _exteriorColor = value; }
public class Coupe : Body { private bool _sunroof;
}
public bool Sunroof { get { return _sunroof; } set { _sunroof = value; } }
public class Sedan : Body { private bool _air;
}
public bool AirConditioning { get { return _air; } set { _air = value; } }
public class Wagon : Body { private Size _trunkSize;
43
C H A P T E R
}
}
3
public Size TrunkSize { get { return _trunkSize; } set { _trunkSize = value; } }
From the code above, we can see that the Coupe may or may not have a sunroof. The Wagon’s trunk size can be adjusted. And the Sedan may or may not have air conditioning. Now, create a new Windows Forms project named TypeConverterSample, and drag the PropertyGrid to the form. If the PropertyGrid can not be found in the tool box, right click the toolbox, choose “Customize Toolbox…” and select the PropertyGrid from the .NET Framework Components tab. Set the PropertyGrid’s Dock property to Fill, and set the Form’s size to something around 608 pixels wide and 448 pixels high, so that the properties in the PropertyGrid will be nicely visible. Add the code above to the project in another file. Now, inside the constructor of the form, after the call to the InitializeComponent, add the following snippet: Vehicle honda = new Vehicle(); honda.Cylinder = Cylinder.Six; honda.BodyStyle = null; propertyGrid1.SelectedObject = honda;
First we create an instance of a Vehicle, called honda. We set the vehicle’s cylinder to Cylinder.Six, representing a 6-cylinder vehicle. We then set the vehicle’s BodyStyle to null, because we want this property to be set explicitly by the user in the PropertyGrid.
44
T Y P E
C O N V E R T E R S
If you compile and run the code, here is what you will see:
From the picture above, you will notice that the Cylinder property is set to Six, but the BodyStyle property is disabled, and contains no visual value. The reason is because the BodyStyle value is null and no TypeConverter is associated with the BodyStyle property. Let’s now set the BodyStyle to an instance of a Sedan, as shown in the following code: Vehicle honda = new Vehicle(); honda.Cylinder = Cylinder.Six; honda.BodyStyle = new Sedan(); propertyGrid1.SelectedObject = honda;
If we now compile and run the program, this is what we will see:
45
C H A P T E R
3
Note that the BodyStyle property is still disabled because no TypeConverter is associated with the property. But there is now a value displayed, “TypeConverterSample.Sedan.” This value is returned from the ToString method of the BodyStyle class. Since the ToString method is not overridden, the base implementation simply returns the full type name. Let’s now define two TypeConverter classes. The first one will derive from EnumConverter. Its purpose is to provide a better string representation of a Cylinder value. For example, the enumeration Cylinder.Six should be converted to “6-cylinder (V-6).” The second TypeConverter class will derive from TypeConverter. Its purpose is to enable creation of all available body styles for a vehicle. But first, let’s reset the BodyStyle in the code back to the original null value: honda.BodyStyle = null;
Add a new C# file to the TypeConverterSample project, and derive a class named CylinderConverter from EnumConverter in the TypeConverterSample namespace, as shown: using System;
46
T Y P E
C O N V E R T E R S
using System.Collections; using System.ComponentModel; namespace TypeConverterSample { public class CylinderConverter : EnumConverter { private readonly Type _enumType = null;
}
}
public CylinderConverter(Type enumType) : base(enumType) { _enumType = enumType ; }
Since we are deriving from EnumConverter, we must implement the one parameter constructor that it requires. Also, there is no need to override certain methods. These methods include CanConvertFrom, CanConvertTo, IsValid, GetStandardValuesSupported, GetStandardValues, GetCreateInstanceSupported, and CreateInstance. The default implementations of EnumConverter for these methods are appropriate. But because we are changing the string representation, we must override ConvertFrom and ConvertTo. We will first override ConvertTo. We know that we want each enumeration member to be converted to a string similar to “6-cylinder (V-6).” Here is the list of the enumeration members and the string representations we want for each of them: Cylinder.Four: 4-cylinder (V-4) Cylinder.Six: 6-cylinder (V-6) Cylinder.Eight: 8-cylinder (V-8) Cylinder.Twelve 12-cylinder (V-12) Now, let’s override ConvertTo and do just that. The four parameter types passed to the virtual ConvertTo method are: ITypeDescriptorContext, used to provide contextual information about the enumeration member being converted, CultureInfo used to provide the culture for conversion, an object instance to be converted, and a Type representing the type the object is to be converted to. Let’s first look at the entire code snippet. Then we will walk through each line:
47
C H A P T E R
3
using System; using System.Collections; using System.ComponentModel; namespace TypeConverterSample { public class CylinderConverter : EnumConverter { private readonly Type _enumType = null; private static readonly Hashtable _enumStringMap = new Hashtable(); public CylinderConverter(Type enumType) : base(enumType) { _enumType = enumType; if (enumType == typeof(Cylinder) && _enumStringMap.Count==0) { _enumStringMap.Add(Cylinder.Four, “4-cylinder (V-4)”); _enumStringMap.Add(Cylinder.Six, “6-cylinder (V-6)”); _enumStringMap.Add(Cylinder.Eight, “8-cylinder (V-8)”); _enumStringMap.Add(Cylinder.Twelve, “12-cylinder (V12)”); } } public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) { if (destinationType == typeof(string) && value is Cylinder) { return _enumStringMap[value]; } return base.ConvertTo(context, culture, value, destinationType); } } }
Let’s now walk through the code above. We know that we want each enumeration member to be associated with a particular string value, so it only makes sense to put this information into a
48
T Y P E
C O N V E R T E R S
dictionary, or Hashtable. We can declare this Hashtable as static and readonly, since we only want one instance to ever exist, and that instance will never be modified: private static readonly Hashtable _enumStringMap = new Hashtable(); public CylinderConverter(Type enumType) : base(enumType) { ...
}
if (enumType == typeof(Cylinder) && _enumStringMap.Count==0) { _enumStringMap.Add(Cylinder.Four, “4-cylinder (V-4)”); _enumStringMap.Add(Cylinder.Six, “6-cylinder (V-6)”); _enumStringMap.Add(Cylinder.Eight, “8-cylinder (V-8)”); _enumStringMap.Add(Cylinder.Twelve, “12-cylinder (V-12)”); }
All four enumeration members are now mapped to a specific string representation in a readonly, static Hashtable. Now, we override ConvertTo to convert each enumeration member to its associated string representation, when the destination type is a string: public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) { if (destinationType == typeof(string) && value is Cylinder) { return _enumStringMap[value]; } return base.ConvertTo(context, culture, value, destinationType); }
We first check to see if the destination type is a string and if the value passed in is an instance of the Cylinder enumeration. If not, we simply let the base EnumConverter class handle the conversion. If so, we retrieve the string representation for the specified value from the Hashtable, and return it.
49
C H A P T E R
3
Let’s now go ahead and apply the TypeConverterAttribute to the Cylinder type, like this: [TypeConverter(typeof(CylinderConverter))] public enum Cylinder { Four, Six, Eight, Twelve }
We could now compile and run the program, but it may throw runtime exceptions. Because we have overridden ConvertTo, it’s almost entirely necessary to override ConvertFrom. The PropertyGrid needs to know how to convert each string back to an enumeration value. Here is the code to do that: public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { if (value is string) { object convertedValue = null; foreach (object key in _enumStringMap.Keys) { if (_enumStringMap[key] == value) { convertedValue = key; break; } } } }
return convertedValue;
return base.ConvertFrom(context, culture, value);
50
T Y P E
C O N V E R T E R S
In this method, we simply do the opposite of what we did in ConvertTo. We first determine whether the value passed in is a string. If not, we call the base method. If so, we enumerate through each key in the Hashtable, testing whether the value mapped to that key is equal to the string passed in. Once we find that key, we return it, since it represents the enumeration equivalence to the string passed in. If you compile and run the program now, here are some screen shots of what you should see:
Clicking the drop down button next to the Cylinder property, will yield a screen-shot similar to the following:
51
C H A P T E R
3
Now that that’s out of the way, let’s get to the fun stuff. We must fully implement the TypeConverter for the BodyStyle property. First, add a new C# file to the TypeConverterSample project. In this file, define a class derived from ExpandableObjectConverter, named BodyConverter. Here is the definition: namespace TypeConverterSample { public class BodyConverter : ExpandableObjectConverter { } }
Remember, ExpandableObjectConverter allows properties to be expanded as a tree in the PropertyGrid. We do not have to implement a parameterized constructor, the default constructor of the base is enough. First, we will override the CanConvertFrom and CanConvertTo methods as shown: public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) { if (sourceType == typeof(string))
52
T Y P E
{ } }
C O N V E R T E R S
return true;
return base.CanConvertFrom(context, sourceType);
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) { if (destinationType == typeof(string)) { return true; } }
return base.CanConvertTo(context, destinationType);
Notice that we will only perform conversions if the type is a string. If the source type or destination type is not a string, we simply call the base version of both methods. Next, we override ConvertFrom and ConvertTo. Here is the code for these two methods: public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { if (value is string) { Body bodyStyle = null; switch ((string)value) { case “Coupe”: { bodyStyle = new Coupe(); } case “Sedan”: { bodyStyle = new Sedan(); } case “Wagon”: { bodyStyle = new Wagon(); } } return bodyStyle;
53
C H A P T E R
3
} }
return base.ConvertFrom(context, culture, value);
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) { if (destinationType == typeof(string)) { string convertedValue = “(None)”; if (value is Coupe) { convertedValue = “Coupe”; } else if (value is Sedan) { convertedValue = “Sedan”; } else if (value is Wagon) { convertedValue = “Wagon”; } }
return convertedValue;
return base.ConvertTo(context, culture, value, destinationType); }
You really wouldn’t hardcode string values inside both methods like this. But the idea is that a Coupe object, Sedan object, and Wagon object, are convertible to and from their string representations of “Coupe”, “Sedan”, and “Wagon”, respectively. In both methods, we first check to see if the type is a string. If not, we call the base versions of these methods. Notice that in the ConvertTo method, we initialize the convertedValue variable to “(None)”. This is so that null values can be converted. We then check the instance of the object to determine the exact type. In the ConvertFrom method, we do exactly the opposite. We do a switch on the value passed in to figure out which Body object should be returned. We return a null value if the string passed in is not associated with any Body object.
54
T Y P E
C O N V E R T E R S
Now the question is, how do we get the PropertyGrid to display a list of the available Body objects? The answer lies in the implementation of GetStandardValues and GetStandardValuesSupported. We first override GetStandardValuesSupported to return a value of true: public override bool GetStandardValuesSupported(ITypeDescriptorContext context) { return true; }
We then override GetStandardValues to return a collection of initialized Body objects. But first, we must initialize a private member variable with the available Body objects: private readonly object[] _standardValues = new object[]{null, new Coupe(), new Sedan(), new Wagon()};
Now, overriding GetStandardValues is as simple as this: public override TypeConverter.StandardValuesCollection GetStandardValues(ITypeDescriptorContext context) { return new TypeConverter.StandardValuesCollection(_standardValues); }
We can now compile and execute the program, which will reveal screen-shots similar to these:
55
C H A P T E R
3
Notice that the BodyStyle property now displays “Sedan.” Expand the BodyStyle property on the left and you get this:
56
T Y P E
C O N V E R T E R S
Clicking the BodyStyle drop down button on the right yields this:
Now, let’s select “(None)” and see what it does:
57
C H A P T E R
3
It worked as we expected, since “(None)” is equivalent to null. Do you notice that black bar right below the Cylinder property? That is where a previous entry was being displayed. The problem is that the PropertyGrid has not refreshed its entire surface. Let’s now change the BodyStyle once more:
58
T Y P E
C O N V E R T E R S
We have now changed the BodyStyle to “Wagon” but the PropertyGrid has not updated. The solution to the problem is the RefreshPropertiesAttribute. Any property that will cause a drastic change to the PropertyGrid layout, or that will cause other properties to change, should have this attribute applied. The attribute’s constructor takes one parameter, which is a RefreshProperties enumeration member. Since we want the entire PropertyGrid to be updated when the BodyStyle is changed, we will apply the attribute with RefreshProperties.Repaint, which forces the PropertyGrid to repaint itself after the property has been changed. The other available values are RefreshProperties.All, which means that the properties should be requeried and the view should be repainted, and RefreshProperties.None, which means that no refresh is necessary. As a final step, add this attribute to the BodyStyle property of the vehicle: [RefreshProperties(RefreshProperties.Repaint)] public Body BodyStyle { … }
59
C H A P T E R
3
Compile and run the code once more. Now everything should work as expected. Apply the System.ComponentModel.NotifyParentPropertyAttribute if it should notify its parent property of changes to its value.
Summary In this chapter, we introduced type converters by first giving an introduction to an important interface, the ITypeDescriptorContext. This interface provides contextual information about an object that is being converted. We also introduced the InstanceDescriptor, which is used to create instances of objects that have been serialized or converted. We discussed how to apply a type converter using the TypeConverterAttribute, and examined the public methods exposed by a TypeConverter class. We then briefly discussed some of the common type converters that are available with the .NET framework. Finally, we walked through an implementation of a custom type converter by deriving from the TypeConverter class, describing each override in the process. By now, you should have a better understanding of how type converters work and how the PropertyGrid displays, converts, and recreates property values. In the next chapter, we will talk about a cousin of the TypeConverter class, the UITypeEditor class. Both the type converter and the editor are accessed through the TypeDescriptor, and both utilize the ITypeDescriptorContext.
60
4
Chapter
UITypeEditors Overview ActiveX controls were first introduced by Microsoft in 1996. This marked a turning point in the reusability of UI controls. Controls could be developed in one language, and then reused by any application that supported the ActiveX interfaces. Even web browsers, such as Internet Explorer, were able and still are able to host ActiveX controls. Common controls include calendars, date pickers, grids, and a slew of others. Many of these controls were developed to work in response to user input, in which most times a single object or property was being edited. The problem with this concept is that the application hosting these controls must know how to instantiate, or activate, each control when user input is necessary. For example, consider an object model mirroring the architecture of a vehicle. This object model will be used by other client applications to allow users to build their dream cars. Suppose somewhere deep into the model there is an object called BodyStyle, which has a Color property. In the past, the developers of each client application would have two choices: build a custom color picker control, or obtain one from a third party. With .NET, there is a third option. Why not let the Color property know how to edit itself visually? Strange? Well that’s where the UITypeEditor class comes into play. Some may argue that this concept is marrying, or coupling the UI to the object model. But this is definitely not the case, as the architecture in the next section will prove. This chapter will walk through the architecture and some implementations of UITypeEditors. By the end of this chapter, you should be able to understand the UITypeEditor architecture, learn the roles of the IWindowsFormsEditorService and ITypeDescriptorContext classes, and implement simple and complex UITypeEditors. You will also learn that the use of a UITypeEditor is very loosely coupled to your object model. The basic UITypeEditor architecture looks like this:
C H A P T E R
4
Hosts UITypeEditor
CONTROL How to edit and paint a value. What edit styles?
Or FORM
References Displays
Object Property
Info about the object being edited. Who the container is.
Invokes Edits ITypeDescriptorContext CONTAINER
How to display an “editor”. What location? How to Drop Down a Control. How to Show a Form.
IWindowsFormsEditorService
64
U I T Y P E E D I T O R S
From the architecture, we can see that the actors involved include the Container, such as the DataGrid or PropertyGrid, the property being displayed or edited, the UITypeEditor referenced by that property, the control or form being hosted by that editor, an IWindowsFormsEditorService and an ITypeDescriptorContext. Please note that the referenced UITypeEditor does not have to be a strong reference. Therefore, a property may or may not have an editor. The developer should handle this appropriately. We assign an editor to a component or property with System.ComponentModel.EditorAttribute. Here is the syntax for applying an editor using a strong reference: [Editor ( typeof (BodyStyleEditor), typeof (UITypeEditor ) ) ] public class BodyStyle { [ Editor ( typeof ( ColorEditor ), typeof ( UITypeEditor ) ) ] public Color BodyColor { get { … } set { … } } }
The first parameter is a UITypeEditor derived class, the one responsible for overriding the EditValue and PaintValue methods that we will go over shortly. The second parameter is the base type of the first parameter, which in this case, is UITypeEditor. The base type is provided so that multiple editors may be applied to a single component. But currently, the only supported base type is UITypeEditor. Note that the EditorAttribute may be applied to either a class definition or a property. Here is the syntax for applying an editor using a weak reference: [ Editor ( “MyNamespace.ColorEditor, MyAssembly”, typeof ( UITypeEditor ) ) ] or [ Editor ( “MyNamespace.ColorEditor, MyAssembly”, “System.Drawing.Design.UITypeEditor, System.Drawing“ ) ]
65
C H A P T E R
4
The first parameter is a UITypeEditor derived class. But this time, the type is loosely referenced. The attribute uses the assembly qualified name of the type, which includes the namespace, the assembly name, and optionally, the version and public key token. In the example above, we excluded the version and public key token for simplicity. In this snippet, we are asking the PropertyDescriptor for its UITypeEditor, if any. If we find one, we call its EditValue method. IServiceProvider sp = ... PropertyDescriptor pd = ... Object someValue = ... UITypeEditor editor = pd.GetEditor ( typeof ( UITypeEditor ) ); if ( editor != null ) { editor.EditValue( sp, someValue ); }
This basic theory is simple: If you don’t know how to edit the value visually, then ask the value to edit itself. All of the actors are self explanatory except the IWindowsFormsEditorService and ITypeDescriptorContext, which are discussed next.
Introducing IWindowsFormsEditorService This service provides an interface to display forms or drop down controls. It is typically exposed through a service provider. This interface is part of the System.Windows.Forms.Design namespace and defined in the System.Windows.Forms assembly. Here is the syntax for obtaining the IWindowsFormsEditorService: IWindowsFormsEditorService myService = GetService(typeof(IWindowsFormsEditorService)
66
U I T Y P E E D I T O R S
This interface is typically queried when implementing a UITypeEditor. public class ColorEditor : UITypeEditor { private ColorPicker _colorPicker = new ColorPicker; private IWindowsFormsEditorService _edsv = null; public override object EditValue ( ITypeDescriptorContext context, IServiceProvider provider, object value) { _edsv = provider.GetService ( typeof ( IWindowsFormsEditorService ) ); _edsv.DropDownControl ( _colorPicker ); return value; } }
A container control, such as the DataGrid, should either implement this interface, or implement some way of obtaining this interface when invoking a UITypeEditor. The service’s primary use is to provide real estate for the UITypeEditor. The Windows Forms PropertyGrid interacts with a class that implements this interface: PropertyGridView. PropertyGridView is internal, so it won’t be discussed in too much detail. But briefly speaking, it represents the cell being edited in the PropertyGrid. The members of this interface are listed below:
DropDownControl Definition: void DropDownControl ( Control control );
Implement this method to drop down the specified control. This can be any control: a list box, color picker, or any other. The PropertyGridView implements this method to drop down a list box for collections of standard values. As we have learned from Chapter 3, any type may provide a collection of standard values. This is proven by the fact that the PropertyGrid displays a drop down list for Boolean properties. It also displays the color picker for Color
67
C H A P T E R
4
properties. The PropertyGrid’s editing session using the DropDownControl method looks like this.
The control being dropped is actually a list box, even though visually, it appears to be a drop down list.
CloseDropDown Definition: void CloseDropDown ( );
Implement this method to close any previously dropped control. You should normally call this method when the user has completed editing the value shown by the control. In some cases, you may also wish to call this method when the user presses [ESC], for instance, on the dropped control.
ShowDialog Definition: void ShowDialog Form dialog );
68
(
U I T Y P E E D I T O R S
Implement this method to display a modal or modeless form. By implementing this method, you are in control and may change any of the Form’s property as you wish. For instance, suppose that the requirements of a Time Sheet application that your company is developing state that the DataGrid, written by you and used to display the week’s time data and projects, must always be accessible. Now suppose that another developer has been assigned to create a custom UITypeEditor for editing a particular project. The developer of the UITypeEditor, unaware of the requirements of the DataGrid, decides to use a modal dialog for editing projects. By implementing the ShowDialog method, the DataGrid is allowed to keep consistency with the requirements. This way, the container is not obligated to display a Form that violates the container’s UI restrictions. Here is the definition of a DataGridCollectionEditor. It displays a Form containing a DataGrid as a modal dialog: public class DataGridCollectionEditor : UITypeEditor { private Form _form = new Form(); DataGrid _grid = new DataGrid(); public DataGridCollectionEditor() { _form.ShowInTaskBar = false; _form.StartPosition = FormStartPosition.CenterParent; _form.Width = 500; _form.Controls.Add(_grid); _grid.CaptionVisible = false; _grid.Dock = DockStyle.Fill; } public override object EditValue ( ITypeDescriptorContext context, IServiceProvider provider, object value ) { _form.Text = context.PropertyDescriptor.DisplayName; _grid.SetDataBinding(value, “”); IWindowsFormsEditorService edsv = provider.GetService ( typeof ( IWindowsFormsEditorService ) ); edsv.ShowDialog ( _form ); return value; } }
The entire implementation of the DataGridEntryView class, the class responsible for displaying windows over the DataGrid, is shown below:
69
C H A P T E R
4
protected internal sealed class DataGridEntryView : IWindowsFormsEditorService { private Rectangle _rectEditingBounds; private int _nOriginalDroppedControlWidth; private Control _droppedControl = null; private DataGrid _dataGrid = null; static internal Form _containerForm = null; public DataGridEntryView ( DataGrid dataGrid, Rectangle rectEditingBounds ) { System.Diagnostics.Debug.Assert ( dataGrid != null, "The DataGrid must not be null!" ); _dataGrid = dataGrid; _rectEditingBounds = rectEditingBounds; if (_containerForm == null) { _containerForm = new Form(); _containerForm.FormBorderStyle = FormBorderStyle.None; _containerForm.StartPosition = FormStartPosition.Manual; _containerForm.ShowInTaskbar = false; } _containerForm.Deactivate += new System.EventHandler(this.For_Deactivate); } ~DataGridEntryView() { _containerForm.Deactivate -= new System.EventHandler(this.For_Deactivate); } private void Form_Deactivate(object sender, EventArgs e) { CloseDropDown(); } public void CloseDropDown() { if (_containerForm.Visible && _droppedControl != null) { _droppedControl.Width = _nOriginalDroppedControlWidth; _containerForm.Hide();
70
U I T Y P E E D I T O R S
}
}
public void DropDownControl(Control control) { _droppedControl = control; _nOriginalDroppedControlWidth = control.Width; control.Width = _rectEditingBounds.Width < _nOriginalDroppedControlWidth ? _nOriginalDroppedControlWidth : _rectEditingBounds.Width; _containerForm.Width = control.Width+control.Left*2+1; _containerForm.Height = control.Height+control.Top*2+1; _containerForm.BackColor = Color.Black; Point desktopLocation = new Point( _rectEditingBounds.Right - control.Width, _rectEditingBounds.Bottom); desktopLocation = _dataGrid.PointToScreen(desktopLocation); if (desktopLocation.Y + _containerForm.Height > Screen.PrimaryScreen.WorkingArea.Bottom) { desktopLocation.Y = desktopLocation.Y _rectEditingBounds.Height – _containerForm.Height; } if (desktopLocation.X < Screen.PrimaryScreen.WorkingArea.Left) { desktopLocation.X = Screen.PrimaryScreen.WorkingArea.Left; } _containerForm.DesktopLocation = desktopLocation; _containerForm.Controls.Clear(); _containerForm.Controls.Add(control); _containerForm.Show(); } form)
public DialogResult ShowDialog(System.Windows.Forms.Form {
if (form.Text.Length > 0) {
71
C H A P T E R
}
}
4
form.Text += " - "; } form.Text += "To be implemented!"; return form.ShowDialog();
Revisiting ITypeDescriptorContext The ITypeDescriptorContext interface provides contextual information about a component. Such information includes the container that the component is hosted on and the component’s PropertyDescriptor. The members of this interface are discussed again below, as they relate directly to UITypeEditors:
Container property When an ITypeDescriptorContext is involved with UITypeEditors, this property typically returns the container that queried and invoked the editor.
Instance property During an editing session, this is normally an instance of the object being edited.
PropertyDescriptor property This property should always return the PropertyDescriptor of the object being edited.
OnComponentChanging method This method returns a Boolean value indicating whether this object can be edited. This is useful where functional overhead is a concern.
72
U I T Y P E E D I T O R S
OnComponentChanged method This method should raise the ComponentChanged event. As stated in Chapter 3, this public event should either be added to the implementation of ITypeDescriptorContext, or you should implement or query IComponentChangeService and invoke its OnComponentChanged method.
Overriding UITypeEditor Methods UITypeEditor provides a base class for editing values of objects through value editors. You must override several methods to ensure the correct behavior of your editor.
GetEditStyle method Definition: public virtual UITypeEditorEditStyle GetEditStyle ( ITypeDescriptorContext context );
Override this method to return the edit style associated with this editor. An IWindowsFormsEditorService or container may call this method to determine if an edit operation should be allowed, or to determine how to paint a portion of the container. For example, the PropertyGrid calls this method to determine whether it should paint an ellipses or a drop down arrow button. The ITypeDescriptorContext provides information about the container, so that the editor may provide different styles for different containers. GetEditStyle announces its intended editing behavior or style.
EditValue method Definition: public virtual object EditValue (
73
C H A P T E R
);
4
ITypeDescriptorContext context, IServiceProvider provider, object value
Override this method to edit the specified value passed in as a parameter. The first parameter, ITypeDescriptorContext, provides information about the container and the property being edited. This information may be used to edit a value in different ways, depending on the container. The second parameter, IServiceProvider, is used to obtain additional services; typically, it is used to query the IWindowsFormsEditorService. The third parameter is the value to be edited. The method should return the new value of the object, or the value passed in if the object has not been changed. In cases where a Form is displayed or a control is dropped, this method should normally return the original value, because the original value will have not changed yet. The container would then use the ITypeDescriptorContext that it passed, and listen for the ComponentChanging and ComponentChanged events. But it is the UITypeEditor’s responsibility, in this scenario, to invoke the ITypeDescriptorContext.OnComponentChanging and ITypeDescriptorContext.OnComponentChanged methods.
GetPaintValueSupported method Definition: public virtual bool GetPaintValueSupported ( ITypeDescriptorContext context );
This method takes one parameter, an ITypeDescriptorContext, which is used to provide information about the container. It returns a Boolean value, which simply indicates whether PaintValue is implemented. This method is called by containers to determine if the editor can paint values. If not, this gives the container a chance to paint an alternate representation of an object.
PaintValue method Definition:
74
U I T Y P E E D I T O R S
public virtual void PaintValue ( PaintValueEventArgs e );
Override this method to paint a representation of an object. This method takes one parameter, a PaintValueEventArgs that contains the bounds in which painting should occur, an ITypeDescriptorContext that contains information about the property and container, a Graphics object for painting, and the value to be painted. The PaintValue method should normally be called inside a Paint method of the container. Here is the PaintValue method in action: public override void PaintValue ( PaintValueEventArgs e) { if (e.Context != null && e.Context.PropertyDescriptor != null) { ...
}
}
Bitmap image = GetSomeBitmapFromResources(); e.Graphics.DrawImage(image, e.Bounds);
We will now look at a pictorial activity diagram illustrating what is happening in the editing process:
75
C H A P T E R
4
Activity Diagram (Editing) CONTAINER
IWindowsFormsEditorService
ITypeDescriptorContext
UITypeEditor
Requests Editor
[No]
Has Editor ?
[Yes]
Edits Value
[Yes]
Open Control
: Object [Initial Value]
Value Changed
Close Control
Update Display
: Object [New Value]
76
[No]
: Control [Focus]
Control Action
: Control [No Focus]
Has Control ?
Notify Component Changed
U I T Y P E E D I T O R S
From the activity diagram, we can see that the actors involved are the container, the IWindowsFormsEditorService, the ITypeDescriptorContext, and the UITypeEditor. The control, if any, and object are also involved and their states may be changed throughout the flow. During an editing session, the container first requests the editor. As mentioned before, in code, this looks like: UITypeEditor editor = propertyDescriptor.GetEditor ( typeof ( UITypeEditor ) );
If the requested editor is returned, the container may wish to query its edit style to determine if an edit operation should occur. This is not shown in the diagram, because it is not strictly required by the container. However, developers of UITypeEditors should still override GetEditStyle to be safe. In code, this looks like: bool shouldEdit = false; switch (editor.GetEditStyle()) { case UITypeEditorEditStyle.DropDown: { // Do Something break; } case UITypeEditorEditStyle.Modal: { // Do Something break; } case UITypeEditorEditStyle.None: { // Do Something break; } }
If the edit style is appropriate, the container then invokes the EditValue method, passing it an ITypeDescriptorContext, an IServiceProvider, and the initial value of the object being edited. Once invoked, the container should immediately check to see whether the returned value and
77
C H A P T E R
4
the initial value are different. If they are, the container may choose to immediately update its display with the new value. If they are not, the container should wait for the ComponentChanged event that should either be exposed by the implementer of ITypeDescriptorContext, or preferably, IComponentChangeService. Here is a snippet that demonstrates this: object newValue = editor.EditValue(serviceProvider, initialValue); if (newValue == oldValue) { context.ComponentChanged += new ComponentChangedEventHandler(this.ValueChanged); } else if (newValue != initialValue) { ComponentChangedEventArgs e = new ComponentChangedEventArgs(context.Instance, context.PropertyDescriptor,
}
ValueChanged (context, e);
oldValue, newValue);
Meanwhile, while in an editing session, the editor determines its desired edit operation, based on the input parameters. Its desired operation should be consistent with its desired edit style. Typically, the editor will then query for an IWindowsFormsEditorService from the IServiceProvider. If an IWindowsFormsEditorService is returned, the editor should redirect its edit operations through it. For example: public override object EditValue ( ITypeDescriptorContext context, IServiceProvider provider, object value ) { _context = context; if (_context != null && _context.Instance != null && provider != null) { _editorService = provider.GetService ( typeof (IWindowsFormsEditorService) ); if (_editorService != null) { _colorPicker.Color = (Color) value; _editorService.DropDownControl ( _colorPicker ); }
78
U I T Y P E E D I T O R S
} }
return value;
As stated before, if the return value and the initial value are the same, the container will wait on the ComponentChanged event. Therefore, the control being displayed should notify the ITypeDescriptorContext of any necessary events, such as selection changed or form closed. An example follows: public ColorEditor() { _colorPicker.SelectionChanged += new EventHandler(this.SelectedColorChanged); } private void SelectedColorChanged(object sender, EventArgs e) { if (_editorService != null ) { if (_context != null) { _context.PropertyDescriptor.SetValue(_context.Instance, _colorPicker.Color); _context.OnComponentChanged(); } _editorService.CloseDropDown(); } }
The container will then receive the event and update any display with the new value. The following diagram illustrates the activities that occur in a painting scenario:
79
C H A P T E R
4
Activity Diagram (Painting) CONTAINER
UITypeEditor
Requests Editor
[No] Has Editor ?
[No] Can Paint?
Paint Value
80
U I T Y P E E D I T O R S
In this activity diagram, the actors involved are only the container and the UITypeEditor. The object being painted should never change its state in response to painting; otherwise, you may experience a huge amount of flickering. The container initiates the process by requesting the editor. This is normally done inside a Paint method, or in response to a Paint event, on the container. If no editor is returned, or if one is returned but it doesn’t support painting, the container may choose to paint its own representation of the object. If any editor is returned, and it does support painting, the container should normally let the editor paint the value. The container should provide the editor with bounds for painting, as well as a Graphics object and the value being painted. The container’s Graphics object is used so that it determines exactly what is painted on its surface. In some cases, the container paints a portion, and the editor paints another portion. This is demonstrated as follows: if (editor.GetPaintValueSupported()) { Rectangle bounds = new Rectangle(cellBounds.X + 3, cellBounds.Y + 3, 20, 15); Color color = (Color) GetColumnValueAtRow(dataSource, rowNum); graphics.FillRectangle(Brushes.White); PaintValueEventArgs e = new PaintValueEventArgs(null, color, graphics, bounds); editor.PaintValue(e); } base.Paint(graphics, cellBounds, dataSource);
As the snippet shows, the editor paints a color on a portion of the cell. Regardless, the cell still paints itself using some default base method. Therefore, the container is in full control of what is painted on its surface.
Implementing a Simple UITypeEditor To create a simple UITypeEditor, you must follow these steps: 1. 2. 3. 4. 5.
Define a class that derives from System.Drawing.Design.UITypeEditor. Override GetEditStyle to return a supported UITypeEditorEditStyle. Override EditValue and pass any controls necessary to the IWindowsFormsEditorService. Override GetPaintValueSupported. Override PaintValue if the editor supports painting.
81
C H A P T E R
4
Example: ColorEditor To demonstrate these steps, we are going to develop the ColorEditor, which uses the ColorPicker found in the downloadable code. In each step, new code appears shaded, while code already discussed has a white background. Step 1: Define the ColorEditor class by deriving it from System.Drawing.Design.UITypeEditor. In your project, you must add a reference to the System.Design and System.Drawing assemblies, as well as to the assembly containing the sample ColorPicker control. Not all of these assemblies are required in this step, but they will be needed later. Then go ahead and define a skeleton default constructor:
using System; using System.ComponentModel; using System.Drawing.Design; namespace CustomWindowsControls.Design { public class ColorEditor : UITypeEditor { public ColorEditor() { } } }
Step 2: Next, we override GetEditStyle to return the UITypeEditorEditStyle.DropDown value: using System;
82
U I T Y P E E D I T O R S
using System.ComponentModel; using System.Drawing.Design; namespace CustomWindowsControls.Design { public class ColorEditor : UITypeEditor { public ColorEditor() { } public override UITypeEditorEditStyle GetEditStyle ( ITypeDescriptorContext context ) { if (context != null && context.Instance != null) { return UITypeEditorEditStyle.DropDown; }
}
}
return base.GetEditStyle(context);
}
This code returns a UITypeEditorEditStyle.DropDown value only if a valid ITypeDescriptorContext was passed in. In this scenario, we do not care who the container is; we will always return the same value as long as the ITypeDescriptorContext.Instance property is non-null. Calling the base GetEditStyle method ensures that the default behavior is preserved if no valid context was passed in. Step 3: This editor will use a custom control, the ColorPicker, to edit color values. The ColorPicker has a Color property, of type Color, and a SelectedColorChanged event of type EventHandler. The editor will call the DropDownControl method of the IWindowsFormsEditorService: using using using using
System; System.ComponentModel; System.Drawing.Design; CustomWindowsControls;
namespace CustomWindowsControls.Design {
83
C H A P T E R
4
public class ColorEditor : UITypeEditor { private ColorPicker _colorPicker = new ColorPicker(); private IWindowsFormsEditorService _editorService = null; private ITypeDescriptorContext _context = null; public ColorEditor() { _colorPicker.Location = new Point(1, 1); _colorPicker.SelectedColorChanged += new EventHandler(this.SelectedColorChanged); } ~ColorEditor() { _colorPicker.SelectedColorChanged -= new EventHandler(this.SelectedColorChanged); } public override UITypeEditorEditStyle GetEditStyle ( ITypeDescriptorContext context ) { if (context != null && context.Instance != null) { return UITypeEditorEditStyle.DropDown; } }
return base.GetEditStyle(context);
public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value) { _context = context; if (context != null && context.Instance != null && provider != null) { _editorService = provider.GetService(typeof(IWindowsFormsEditorService)); if (_editorService != null && value is Color) { _colorPicker.Color = (Color) value; _editorService.DropDownControl(_colorPicker); } } return value;
84
U I T Y P E E D I T O R S
}
We create an event handler, SelectedColorChanged, to handle the SelectedColorChanged event of the ColorPicker. This allows us to inform the container that the value has now been changed. private void SelectedColorChanged(object sender, EventArgs e) { if (_editorService != null) { if (_context != null && _context.OnComponentChanging()) { _context.PropertyDescriptor.SetValue(_context.Instance, _colorPicker.Color); _context.OnComponentChanged(); }
}
}
}
}
_editorService.CloseDropDown();
Note that we must set the new value of the property using the PropertyDescriptor. Even though the SetValue method of the PropertyDescriptor automatically calls the component change methods of the IComponentChangeService, we can’t be sure that the container subscribed to the service. Therefore, it is required to call the OnComponentChanged method of the ITypeDescriptorContext. Finally, we hide the control by calling the CloseDropDown method of IWindowsFormsEditorService.
Implementing a CollectionEditor A collection UITypeEditor, simply referred to as a CollectionEditor, is used to edit collections. This normally includes adding new items to the collection, removing items, rearranging items if the collection is derived from IList, and modifying individual properties of collection items. The standard collection editor will typically satisfy all collection editing needs. However, there are times when defining your own implementation is equally important. This includes situations where a collection may have items of different types. To implement a collection editor, follow these steps:
85
C H A P T E R
4
1. Derive a class from System.ComponentModel.Design.CollectionEditor. 2. Override GetPaintValueSupported, if the collection editor should support painting. The default implementation returns false. 3. Override PaintValue if painting is supported. 4. If you need to provide a custom CollectionForm, derive a class from CollectionEditor.CollectionForm. 5. Override CollectionForm.CanSelectMultipleInstances if multiple collection items can be selected at once. 6. Override CollectionForm.DisplayError to display exceptions to the user. 7. Override CollectionForm.GetService to return any specialized service interfaces. 8. Override CollectionForm.OnEditValueChanged to perform any special processing when the collection has changed. 9. Override CollectionForm.ShowEditorDialog to show the collection form. 10. Override CreateCollectionForm to return a custom implementation derived from CollectionEditor.CollectionForm. The collection form is only accessible within the CollectionEditor. 11. Override CreateCollectionItemType to return the data type of the collection items. The default implementation returns the type returned by the Item property of the collection. Note that this only works for homogenous collections. 12. Override CreateInstance to create instances of item types. 13. Override CreateNewItemTypes to return an array of types that this editor can contain. This is typical for heterogeneous collections, collections of different item types. The default implementation returns an array of one type; this type is the one returned by the Item property of the collection. 14. Override DestroyInstance to destroy instances of item types. This is useful when certain collection items should be disposed, or when resources should be freed. 15. Override GetItems to return an array of objects representing the collection items. 16. Override SetItems to set the collection items of a collection.
Example: ToolbarItemCollectionEditor The ToolbarItemCollectionEditor provides a user interface for editing toolbar items. The Toolbar class can be found in the downloadable code. The toolbar items may be of different types; for example, these include ToolbarButton, ToolbarGripper, and ToolbarSeparator. In each step, new code appears shaded while code already discussed has a white background.
86
U I T Y P E E D I T O R S
Step 1: Define the ToolbarItemCollectionEditor by deriving a class from System.ComponentModel.Design.CollectionEditor. You must add a reference to the System.Design and System.Drawing assemblies to your project. Also, implement the required one-parameter constructor. using using using using using
System; System.ComponentModel; System.ComponentModel.Design; System.Drawing; System.Drawing.Design;
namespace CustomWebControls.Design { public class ToolbarItemCollectionEditor { public ToolbarItemCollectionEditor(Type type) : base(type) { } } }
Step 2: Since we do not need to support painting, we will simply not implement GetPaintValueSupported. The default implementation returns false, which is the value that we want returned. Step 3: Again, since we are not supporting painting, we will not implement PaintValue. Step 4 – Step 10: With the ToolbarItemCollectionEditor, we will use the default collection form. It provides all the UI functionality that we need: adding items, removing items, rearranging items, and updating items.
87
C H A P T E R
4
Step 11: Since the ToolbarItemCollectionEditor will support items of multiple types, there is no need to override CreateCollectionItemType. Step 12: We override CreateInstance to set properties of the ToolbarButton. using using using using using
System; System.ComponentModel; System.ComponentModel.Design; System.Drawing; System.Drawing.Design;
namespace CustomWebControls.Design { public class ToolbarItemCollectionEditor { public ToolbarItemCollectionEditor(Type type) : base(type) { }
}
}
88
protected override object CreateInstance(Type itemType) { object obj = base.CreateInstance(itemType); if (obj is ToolbarButton) { ToolbarButton button = (ToolbarButton) obj; button.ID = button.ToolTip = button.Text; } return obj; }
U I T Y P E E D I T O R S
Step 13: We override CreateNewItemTypes to return an array of types that this editor supports. The available types include the ToolbarButton, ToolbarSeparator, and ToolbarGripper. For efficiency, these types are stored in a read-only array. using using using using using
System; System.ComponentModel; System.ComponentModel.Design; System.Drawing; System.Drawing.Design;
namespace CustomWebControls.Design { public class ToolbarItemCollectionEditor { private Type _collectionType = null; private static readonly _newItemTypes = new Type[] { typeof(ToolbarButton), typeof(ToolbarSeparator), typeof(ToolbarGripper) }; public ToolbarItemCollectionEditor(Type type) : base(type) { _collectionType = type; } protected override Type[] CreateNewItemTypes() { if (_collectionType.Equals(typeof(ToolbarItemCollection)) { return _newItemTypes; } return base.CreateNewItemTypes(); } protected override object CreateInstance(Type itemType) { object obj = base.CreateInstance(itemType); if (obj is ToolbarButton) { ToolbarButton button = (ToolbarButton) obj; button.ID = button.ToolTip = button.Text; } return obj;
89
C H A P T E R
}
4
}
} Steps 14 – Step 16: We will skip these steps since we do not need to perform any custom cleanup during DestroyInstance. Also, we are satisfied with the GetItems and SetItems default implementation.
Summary In this chapter, we began by describing the UITypeEditor architecture. We discussed the actors involved and went into the details of overriding a UITypeEditor. We dissected the IWindowsFormsEditorService and showed how it works in conjunction with the container and editor. Then we described the activities by providing two activity diagrams, demonstrating both painting and editing. Finally, we used all of the knowledge learned to implement a couple of UITypeEditors. You should now have a good understanding of the .NET UITypeEditor architecture. With this knowledge, you should be able to implement simple and complex editors with no problems. This chapter completes the first part of this book. Next, we will move on and look into developing Windows Forms controls.
90
5
Chapter
Introduction to Windows Forms Overview The origin of Windows applications began with C programmers which eventually led to the Win32 API. This API contained useful functions for building graphical applications as well as making low-level calls to the operating system. Programmers had to deal with pointers and Windows handles on the norm. Then along came Visual C++ which used the new MFC library, a hierarchy of classes, most of which thinly wrapped the Win32 API functions. So MFC may have not been fully object-oriented. Because MFC only wrapped the library functions, it did not make life too much easier. Visual Basic also entered the scene to make things a little bit easier, with its RAD-like development environment. Its language was entirely different than Visual C++, but as the name “Basic” suggests, it was really for beginners. To make matters worse for those beginners but better for the world, it is not object-oriented. Now with .NET, multiple languages have joined together, including revamped Visual Basic and C++, and the new Java-like C#, to aid in programming a model called Windows Forms. They combine to give the RAD-ness of Visual Basic and the strengths of C++ and C#, to make developing Windows applications even easier. In this chapter, we will thoroughly examine the Windows Forms architecture, and examine how resources and localization play a big part in .NET Windows Forms development.
Windows Forms Architecture Every Windows Form will need some way to be presented and initialize the controls it contains. Furthermore, they also need to know how to localize properties of controls and store
C H A P T E R
5
customizable properties in resource files. In this section, we will walk through the primary methods involved when instantiating a Windows Form, as well as details of resource management and the localization process. First, we will introduce the Main() method.
Main() Method In C and C++, as you may recall, every application required a single method to be an entry point, called main( ). Windows applications had an entry point called WinMain( ). Both of these methods were global methods that were called by the operating system at runtime. With Windows Forms, as well as any other C# application, including console apps, we have a single public static method called Main( ). Here is the code that is required to run a Windows application: public class Form1 : Form { public class Form1() { }
}
public static void Main(String[] args) { Form1 form = new Form1(); Application.Run(form); }
In this code, we have defined a Form class named Form1. It contains a constructor and the required Main method. The Main method can either return a type of int or a type of void. If it has a return type of int, application or form results can be returned to the caller. The significance of the return values are decided by the developer. But for consistency, a return value of zero should always indicate a success. Also, the string array parameter is optional. It is used to provide arguments to be processed by the application or form. Please note that not all Form classes need a Main method, only the startup form. You may set the start form in the project’s properties dialog, as shown:
94
I N T R O D U C T I O N
T O
W I N D O W S
F O R M S
Change the Start Object to select a Form in the project. Also note in the code above the following: Application.Run(form);
This code instructs the operating system to run the famous message loop for that particular form. The Run method of the Application class is static. The Application class is part of the System.Windows.Forms namespace. Along with the Run method, it provides other useful static methods and properties for message processing and application management.
InitializeComponent Inside the constructor of every Windows Form, should be a call to InitializeComponent. This method is used to initialize any property values of the form and any controls that are contained. In every form’s constructor generated by Visual Studio .NET, you will notice the following code and comments:
95
C H A P T E R
5
// // Required for Windows Form Designer support // InitializeComponent(); // // TODO: Add any constructor code after InitializeComponent call //
This first comment means that this method is required if the from should have design-time support. The designers use this method to serialize property values that have been set in the designer. Designers will be discussed in more detail in chapters 12 and 13. The second comment instructs the developer to call InitializeComponent before adding any other code in the constructor. This ensures that all controls are initialized first. The InitializeComponent method for the TypeConverterSample of Chapter 3 is shown below: #region Windows Form Designer generated code /// <summary> /// Required method for Designer support - do not modify /// the contents of this method with the code editor. /// private void InitializeComponent() { this.propertyGrid1 = new System.Windows.Forms.PropertyGrid(); this.SuspendLayout(); // // propertyGrid1 // this.propertyGrid1.CommandsVisibleIfAvailable = true; this.propertyGrid1.Dock = System.Windows.Forms.DockStyle.Fill; this.propertyGrid1.LargeButtons = false; this.propertyGrid1.LineColor = System.Drawing.SystemColors.ScrollBar; this.propertyGrid1.Name = "propertyGrid1"; this.propertyGrid1.Size = new System.Drawing.Size(600, 421); this.propertyGrid1.TabIndex = 0; this.propertyGrid1.Text = "propertyGrid1";
96
I N T R O D U C T I O N
T O
W I N D O W S
F O R M S
this.propertyGrid1.ViewBackColor = System.Drawing.SystemColors.Window; this.propertyGrid1.ViewForeColor = System.Drawing.SystemColors.WindowText; // // Form1 // this.AutoScaleBaseSize = new System.Drawing.Size(5, 13); this.ClientSize = new System.Drawing.Size(600, 421); this.Controls.AddRange(new System.Windows.Forms.Control[] { this.propertyGrid1}); this.Name = "Form1"; this.Text = "Form1"; this.ResumeLayout(false);
} #endregion
In the code above, all properties that were set in the designer, either explicitly or implicitly, are serialized into code via InitializeComponent. It is not recommended to modify the code inside InitializeComponent with the code editor, because designer serialization may wipe out your code. Once again, though, this method is only required by the designer. We could have, just as easily, set all properties that are set inside InitializeComponent, in the constructor. As a programmer, we know that a method should not be made a method unless it is called more than once. But because the designer needs somewhere to generate code for the properties that were set through it, we must live with this method. In the code above, all property values are set in code. This practice may violate the fact that magic numbers and hard-coded strings should not be present in methods. With Windows Forms and .NET, we also get resource management. Resource files can be used to persist property values that can be modified outside of the code. We will discuss resources in more detail next.
Resources Resources can range from user interface objects, such as bitmaps, to user specific data. Resource files can be binary files, plain text files, or XML files. With Visual Studio .NET, you can simply add items to your project and they can automatically be added to resource files. However, text files must be converted before they can be used as resources with Visual Studio .NET.
97
C H A P T E R
5
We will first show how to create resources using a simple text file. Text file resources will usually serve as string tables. However, they can also be used to retrieve property values that can be deserialized from a string. Using the TypeConverterSample example from Chapter 3, we will add the available BodyStyle names to a text file called BodyStyles.txt, and the Cylinder string representations to CylinderTypes.txt, as shown below: BodyStyles.txt Coupe = Coupe Sedan = Sedan Wagon = Wagon
CylinderTypes.txt Four = 4-cylinder (V-4) Six = 6-cylinder (V-6) Eight = 8-cylinder (V-8) Twelve = 12-cylinder (V-12)
The text file must contain a name=value format. The value before the = sign is the key, while the value after the = sign is the value. The spacing between the key and value is irrelevant. Also, note that you do not have to embed the key or value within quotes. Now, in order to embed the resource into an assembly, we must use the ResGen.exe utility to generate a binary resource file. Only binary resources and XML resources can be embedded into an assembly. Here is the syntax to generate binary resource files from the text files. resgen BodyStyles.txt resgen CylinderTypes.txt
You will now find the following files in the same directory as BodyStyles.txt and CylinderTypes.txt: BodyStyles.resources CylinderTypes.resources
98
I N T R O D U C T I O N
T O
W I N D O W S
F O R M S
If you add these two files to the project, Visual Studio .NET will automatically set the Build Action to Embedded Resource, and upon compilation, the resources will be added to the assembly. If you would rather not perform all these steps each time you need to make a change to one of the text files, you have the option to create XML resource files. XML resource files are automatically compiled by Visual Studio .NET and added to an assembly. You can either add a new XML resource file (.resx) to the project, or you can use the ResGen.exe utility to generate an XML resource file from the binary resource file. Here is the syntax to do that: resgen BodyStyles.resources BodyStyles.resx resgen CylinderTypes.resources CylinderTypes.resx
Now, you simply add the .resx file to the project, and make changes to that file whenever there is a need to do so. Visual Studio .NET will automatically reparse the changed file and embed the resources into the assembly. We will soon look at how to access the resources from the assembly. But first, it is worth noting that there is one more way to generate resources, using the System.Resources.ResourceWriter class. Here is a small code snippet to generate the resources above in code: ResourceWriter rw1= new ResourceWriter(“BodyStyles.resources”); ResourceWriter rw2 = new ResourceWriter(“CylinderTypes.resources”); rw1.AddResource(“Coupe”, “Coupe (DX)”); rw1.AddResource(“Sedan”, “Coupe (LX)”); rw1.AddResource(“Wagon”, “Coupe (EX)”); rw2.AddResource(“Four”, “4-cylinder (V-4)”); rw2.AddResource(“Six”, “6-cylinder (V-6)”); rw2.AddResource(“Eight”, “8-cylinder (V-6)”); rw2.AddResource(“Twelve”, “12-cylinder (V-12)”); rw1.Close(); rw2.Close();
The AddResource method is overloaded to take a string, a byte array, or an object. Calling the Close method automatically calls the Dispose method, which in turn calls the Generate method, which is responsible for writing the data to the file.
99
C H A P T E R
5
We will now access the resources embedded in the assembly compiled by the TypeConverterSample project. In order to do this, we must understand the concept of resource namespaces. In short, a resource namespace is equal to the DefaultNamespace, as defined in the project’s properties, plus any subfolders that contain the resource. For the example above, the resource namespace is simply TypeConverterSample, because that is the DefaultNamespace defined in the project’s properties, and the resource files are not located within any subfolders in the project. But, for the sake of argument, let’s say we added the BodyStyles.resx file to a subfolder named “Resources \ Classes” and the CylinderTypes.resx file to a subfolder named “Resources \ Enumerations.” Then the full resource names would be respectively defined as follows: TypeConverterSample.Resources.Classes.BodyStyles TypeConverterSample.Resources.Enumerations.CylinderTypes
Therefore, these are the names to use when accessing the resources. In order to read the resources, we will use the System.Resources.ResourceManager class. This class is used the get values from a single resource. You can then use the methods of the ResourceManager class to get values from that resource. Here is the syntax for instantiating resource managers for the resources generated above: Assembly assembly = Assembly.GetExecutingAssembly(); ResourceManager rm1 = new ResourceManager(“TypeConverterSample.Resources.Classes.BodyStyles ”, assembly); ResourceManager rm2 = new ResourceManager(“TypeConverterSample.Resources.Enumerations.Cylin derTypes”, assembly);
The ResourceManager class takes two parameters. The first parameter is a string representing the full resource name. The second parameter is the Assembly from which to look for and load the resource. Here is the code to access the individual values in the resources: String coupe = rm1.GetString(“Coupe”); String sedan = rm1.GetString(“Sedan”); String wagon = rm1.GetString(“Wagon”); String fourCylinder = rm1.GetString(Cylinder.Four.ToString()); String sixCylinder = rm1.GetString(Cylinder.Six.ToString()); String eightCylinder = rm1.GetString(Cylinder.Eight.ToString());
100
I N T R O D U C T I O N
T O
W I N D O W S
F O R M S
String twelveCylinder = rm1.GetString(Cylinder.Twelve.ToString());
If an image was stored in the resource file, we would use the GetObject method, and cast the value returned to an Image instance. In order to use the above snippet, the resource file must have been embedded in the assembly as public. You can check this by using the ildasm.exe utility:
You can see that all resources embedded in the assembly are public, as defined with the .mresource public declaration. All resources embedded by using Visual Studio are public. We can also use the assembly generation tool, al.exe, to embed a resource as either public or private, as seen below: al /embed:CylinderTypes.resources,CylinderTypes,public al /embed:BodyStyles.resources,BodyStyles,private
The ResourceManager class also has another method, GetResourceSet, which returns a ResourceSet instance responsible for enumerating over resources for a particular culture. Before we delve into the ResourceSet class, let’s look at localization.
101
C H A P T E R
5
Localization The Form class has two properties, Localization and Language, which allow an application to target and support multiple languages. These two properties work together when localizing property values of controls on a Windows Form. In order to enable localization, simply set the Localization property of the Form to true. This causes the designer to generate culture-specific resources. These resources are compiled into culture-specific assemblies, also called satellite assemblies. Note that satellite assemblies can also be created by using the assembly generation tool, specifying the /culture option. Once we set the Localization property, we can set the Language property to specify the Culture for the resource to be generated. We can then change property values of controls which will be persisted in the resources for that Culture property. In the picture below, we have set the Localizable property to true, and changed the Language to French:
Now, in Solution Explorer underneath Form1, you will notice files similar to the following:
102
I N T R O D U C T I O N
T O
W I N D O W S
F O R M S
Form1.fr.resx was created to hold resources for the French culture. Any property values changed will be persisted in the resource file specified by the Language property, provided that the Localization property is set to true. Building the project now yields a file named TypeConverterSample.resources.dll in an “fr” subdirectory beneath the output directory. At runtime, the CLR uses the Thread.CurrentCulture property to determine which resource file to use for reading resources. But if we decided to read from the resources ourselves, we need to have some way of doing the same thing. As we state earlier, the ResourceManager class has a method called GetResourceSet, which returns a ResourceSet instance responsible for enumerating over resources for a particular culture. When using the ResourceManager class to read values, it uses the Thread.CurrentCulture property for determining which culture-specific resource assembly to read. When using the ResourceSet class, it represents the resource for a particular culture. Here is the syntax for reading the resources created above from the French resource: Assembly assembly = Assembly.GetExecutingAssembly(); ResourceManager rm1 = new ResourceManager(“TypeConverterSample.Resources.Enumerations.Cylin derTypes”, assembly); ResourceSet rs = rm1.GetResourceSet(new CultureInfo(), true, true); String fourCylinder = rs.GetString(Cylinder.Four.ToString());
With this code, we can retrieve a culture-specific resource value regardless of the current culture.
103
C H A P T E R
5
Control Layout With any Windows architecture, including Windows Forms, it is necessary to understand the UI architecture or layout. We will first discuss the human factor involved with creating good UI designs.
The Human Factor (Fitts’s Law) Before we dig into a couple of .NET Windows Forms specific topics, let’s first understand the human factor involved with any Windows application. One set of rules that we should not forget about as UI designers and control developers, is Fitts’s Law. All GUIs and custom controls should be designed with these set of rules in mind. The idea behind Fitts’s Law is that as the mouse pointer is moved around the screen, certain characteristics of controls and objects on the screen make them either easier, or harder, to click. For example, the smaller a control or object is, the harder it will be to click on. The larger it is, the easier it is to click, but the increase in size will decrease real estate for other elements. Also, the farther away an object is from the mouse pointer, the more effort it will take to reach that object with the mouse pointer. So in other words, control size and position play an important role in determining whether a UI design is good or bad. We tend to place toolbars and menus either on the top or side of the screen. This is because of the natural acceptance of the human eye to z-order. Z-order arrangement means arranging elements left-to-right, top-to-bottom. Also, by placing these elements here, it makes them infinitely targetable. The mouse can almost never go past these areas. So if you had to drag the mouse pointer from the bottom of the screen to click the “File” menu, you will mostly likely reach that target without error. The worst possible scenario for a bad UI design is an object that is farthest away from the mouse pointer and is very small. This makes the target more unreachable. With the knowledge of Fitts’s Law, we will now discuss some important aspects regarding control layout in Windows Forms.
Controls Collection Controls will sometimes act as a container for other controls. A form is a good example of this. A form may contain buttons, text boxes, group boxes, etc. Every control added to the form must somehow be associated with that form programmatically. The Control class enables this
104
I N T R O D U C T I O N
T O
W I N D O W S
F O R M S
through its Controls property, which is an instance of Control.ControlCollection. It is part of the System.Windows.Forms namespace. Note that the collection name is Control.ControlCollection, which means that ControlCollection is defined within the Control class. This collection is an ordered collection. Therefore, individual controls can be accessed by a numeric index. Here are a few methods that are part of the Control.ControlCollection class: Add: Adds the specified control to the collection at the end. Note that a control can only be a part of one collection at a time. If the same control is added to a second collection, it is removed from the first one. AddRange: Adds a range of controls to the collection at the end. The range is specified as an array of Control instances. Note that a control can only be part of one collection at a time. If the same control is added to a second collection, it is removed from the first one. Remove: Removes the specified control from the collection. RemoveAt: Removes the control from the collection found at the specified index. IndexOf: Returns the numeric index of the specified control instance in the collection. Contains: Returns a Boolean value indicating whether the specified control instance is contained in the collection. Clear: Removes all controls from the collection. SetChildIndex: Sets the specified index for the specified control in the collection. This re-indexes all other controls in the collection. GetChildIndex: Gets the index of the specified control in the collection. This method does the same as IndexOf, except that it is overloaded to throw an exception if the control specified is not found. Every modification to the ControlCollection affects the z-order of controls relative to their parent. Z-order simply means left-to-right, top-to-bottom. So arranging controls in z-order means arranging them from left-to-right, then top-to-bottom. Therefore, control arrangement will affect docking, and vice-versa. Docking is described next.
105
C H A P T E R
5
Docking Docking is the process of allowing controls to be automatically resized or repositioned to be locked to the edges of parent controls. This is useful if forms will be resized, and you want a particular control to always appear at a certain place on the form. An example of a docked control is the Styles and Formatting window of the Word document, on which I am using to write this book. Every control can participate in docking by setting the DockStyle property of the control instance. The DockStyle enumeration specifies how a control is docked to its parent. The DockStyle enumeration allows the following values: None: The control is not docked to its parent. Top: The top edge of the control is docked to its parent’s top edge. Bottom: The bottom edge of the control is docked to its parent’s bottom edge. Left: The left edge of the control is docked to its parent’s left edge. Right: The right edge of the control is docked to its parent’s right edge. Fill: The control is resized to be docked to the left, right, top and bottom edges of its parent. Here is a view of the PropertyGrid and the DockStyle editor, which is used to dock a control at design time:
106
I N T R O D U C T I O N
T O
W I N D O W S
F O R M S
A total of 6 buttons are displayed in the editor, each corresponding to a DockStyle enumeration member. When using Visual Inheritance, inherited controls must be declared as Protected in order to change the DockStyle property. When docking controls to their parent, the last control added to the parent’s Controls collection has the highest priority of being docked. For example, if myControl1, myControl2, and myControl3 are all added to a Panel control respectively, and all three controls have a docking style of DockStyle.Top, then myControl3 will be the topmost control. Consider the following code fragment, and the sample output that follows: // The following code fragment creates an outlook style tab control // snapshot.
107
C H A P T E R
5
// ( Note that only minimal properties are shown and no events are // subscribed to for clarity. ) System.Windows.Forms.Panel parentPanel = new System.Windows.Forms.Panel(); parentPanel.Width = 500; // First TabButton System.Windows.Forms.Button button1 = new System.Windows.Forms.Button(); button1.Width = parentPanel.Width; button1.DockStyle = DockStyle.Top; // Second TabButton System.Windows.Forms.Button button2 = new System.Windows.Forms.Button(); button2.Width = parentPanel.Width; button2.DockStyle = DockStyle.Top; // Last TabButton System.Windows.Forms.Button button3 = new System.Windows.Forms.Button(); button3.Width = parentPanel.Width; button3.DockStyle = DockStyle.Top; parentPanel.Controls.AddRange(new Control[] { button1, button2, button3 });
Notice that button3 is the topmost control, because it was the last control added. Knowing this, you should take care when setting the DockStyle property of a control at runtime. At design time, Visual Studio .NET automatically takes this into account, and adjusts the parent’s Controls collection accordingly.
108
I N T R O D U C T I O N
T O
W I N D O W S
F O R M S
Summary The Windows Forms architecture offers any programming language that targets the .NET framework the benefit of rapid application development. Visual Studio .NET works closely with this architecture to enable visual design time support for building Windows Forms applications. Unlike legacy architectures, Windows Forms inherently supports resource management and localization. Resources are an integral part of the .NET Framework and the Windows Forms architecture. With resources, property values can be persisted outside of the development environment to enable modification by non-developers. Resource management enables localization to use resources as its basis for multi-lingual support. Localization is the process of providing culture-specific formatting and data. Setting a couple of properties on the form enables localization through resource-only assemblies, which are also known as, satellite assemblies.
109
6
Chapter
Windows Forms Data Binding Overview One of the most powerful aspects of .NET and Windows Forms is data binding. Historically, data binding was used to bind views to data stored in databases. Some database management systems, such as Microsoft Access, have provided GUI APIs to help developers quickly bind to data. Each DBMS normally had its on associated API for data binding purposes. Some even had no associated API, which forced developers to provide the implementation from scratch. Binding to other types of data structures, such as arrays, were out of the question. .NET, however, solves all these problems and more. With .NET, a client can bind to almost any data structure, including arrays, collections, data rows, and data views.
Data Binding Concepts For .NET data binding to be possible, there must be providers of data and consumers of data. A provider of data is an object or component that exposes its data to the outside. A consumer is an object or component that uses the data exposes by a provider with the intention to display or modify that data. With .NET data binding, the minimum requirement to support list-based data binding is for the provider to implement the IList interface. The IList interface represents an index-based collection.
Data Providers The following objects implement the IList interface; so they are, inherently, data providers.
C H A P T E R
6
Arrays An array is simply a collection of objects that can be accessed by a numeric index. Arrays can be either single-dimensional or multi-dimensional. DataSet The DataSet is a .NET representation of a database. It does not, however, need to actually be connected to a real database. As a matter of fact, it acts as a “disconnected” data source, with the ability to track changes and merge new data. When binding to a DataSet, the consumer is responsible for asking for the particular DataTable to bind to. In some cases, the consumer would really be binding to the DataSet’s default DataViewManager. DataTable A DataTable typically represents a table in a database. Though, it may also be used to represent the structure of an XML element, or the parent of a collection. A DataTable contains a DataColumn collection and a DataRow collection. Complex controls, such as the .NET DataGrid, can be bound to a DataTable with ease. Note that when you bind to a DataTable, you really bind to the table’s default DataView. DataView A DataView is simply a customized view of the data in a DataTable. For instance, it may contain all rows sorted by a particular column, or all rows that match a certain expression. When data binding to a DataView, all controls involved in the binding process will receive a snapshot of the data at that particular moment. Whenever the underlying data changes, the bound controls must have some way of knowing how to refresh themselves. This process will be discussed shortly. DataViewManager The DataViewManager represents the entire DataSet. Like the DataView, it is a customized snapshot view of the DataSet. The only difference is that it also includes relations. DataColumn A DataColumn typically represents, and is analogous to, a column in a database table. Although, it may also represent an XML attribute or an attributeless XML element. You can only simple-bind to a DataColumn. This means that only simple controls, such as a text box, can be bound to a DataColumn. Other .NET Objects
112
W I N D O W S
F O R M S
D A T A
B I N D I N G
In actuality, any .NET object may support data binding, but you may not automatically reap all of the benefits provided by the .NET architecture. Also, when binding to these objects, only the public properties (not public fields) can be bound to. Therefore, you must be careful when data binding to data sources exposed by web services. The public properties of any types returned by a web service will be converted to public fields in the web service’s client proxy code. Be careful. You can only bind to the public properties, not the public fields, of data source objects.
Data Consumers A consumer is an object or component that uses the data exposed by a provider whose intent is to display or modify that data. In Windows Forms, a consumer is typically a data-bound control. Simple data-bound controls include, but are not limited to, text boxes, labels, check boxes, and radio buttons. These controls can only display one data value provided by a data source. On the other hand, controls such as data grids, list boxes and combo boxes can display a list of values. These controls are therefore referred to as complex data-bound controls.
Binding and BindingContext .NET Controls either support simple binding or complex binding. Controls that support simple binding include the text box. A text box can only support one data value at a time. The following example shows how to bind a text box control with a name field of a Customers DataTable: TextBox nameTextBox = new TextBox(); DataSet dataSet = CreateMyDataSet(); nameTextBox.DataBindings.Add(“Text”, dataSet, “Customers.FirstName”);
113
C H A P T E R
6
Binding Every Windows Form control has a DataBindings property, which is an instance of ControlBindingsCollection. The ControlBindingsCollection is a collection of Binding objects, which bind the property of a control to a data source member. Whenever the data source member changes, the control’s property is automatically updated to reflect the change, and vice versa. Different properties of the same control may also be bound to different data sources.
BindingContext Every container control on a Windows Form, including a form itself, contains at least one BindingContext. Actually, all controls derived from System.Windows.Forms.Control have the BindingContext property, but only container controls really make use of it. Non-container controls will simply return the BindingContext of their immediate container. A BindingContext is just an object that provides binding support to multiple data sources. Since more than one data source can be viewed on a form, the BindingContext enables retrieval of any particular data source. Specifically, a BindingContext manages a collection of BindingManagerBase objects. BindingManagerBase is an abstract class that enables synchronization of data-bound controls that are bound to the same data source. A BindingContext can be visualized as follows (The dashed lines represent the BindingContext): Form MyForm
Panel
GroupBox
114
W I N D O W S
F O R M S
D A T A
B I N D I N G
GroupBox
In the pictures above, the BindingContext simply says, “I will manage and keep track of all controls and their associated data sources and data-bound members. If the current record in the one of the managed data sources changes, I will refresh all controls that I track with the new values.” By default, only one BindingContext is created for a Form, regardless of the number of controls contained on the form. Here is the syntax for retrieving a data source from the BindingContext: BindingManagerBase customers = this.BindingContext[dataSet, “Customers”];
Here is the syntax for creating a new BindingContext. groupBox1.BindingContext = new BindingContext(); groupBox2.BindingContext = new BindingContext();
In the snippet above, two BindingContext objects are created and are assigned to two group box controls. This allows the contained controls in both group boxes to be bound to the same data source, but using two different binding managers. The two classes derived from BindingManagerBase are described next.
115
C H A P T E R
6
CurrencyManager Any data source that is bound to a .NET Windows Forms control will be associated with a CurrencyManager. Actually, the true name for CurrencyManager should be “concurrency manager” or “current manager.” During the days of ADO, the collection itself kept track of the current record. The problem with this approach was that multiple consumers could not reuse the same collection concurrently in an efficient manner. For example, if there were two grid controls on a dialog that used ADO to display their data, and if both grids used the current record for highlighting purposes, there would be no way for each grid to highlight a different item at the same time. With .NET, the current record is not maintained in the data source itself, which makes the data source truly disconnected. The current record is, instead, maintained by the CurrencyManager. A CurrencyManager has a one-to-one relationship with a data source. A CurrencyManager is automatically created when a Binding object is created, if it is the first time that the data source has been bound. ( Remember that there is only one CurrencyManager per data source per BindingContext.) The following diagram shows the relationship between a Form, Panel, CurrencyManager objects, and data sources:
116
W I N D O W S
F O R M S
D A T A
B I N D I N G
MyForm
BindingContext
CurrencyManager
Panel
DataTable
BindingContext
CurrencyManager
CurrencyManager
Collection
Array
In the diagram above, the Form contains the automatically created BindingContext, which contains two CurrencyManager objects, one managing an array, and the other managing a collection. The Panel contains a newly created BindingContext (remember that only the Form’s BindingContext is created by default), which also contains two CurrencyManager objects, one managing the same collection that is bound to the Form, and the other managing a DataTable. Normally, only one CurrencyManager would be created for the Collection; but since there are two BindingContext objects, each must contain its own collection of CurrencyManager objects. The following diagram shows control binding in action:
117
C H A P T E R
6
Control ControlBindingsCollection Binding
Binding
Binding
Property
Property
Property
DataSource
DataSource
DataSource
DataMember
DataMember
DataMember
BindingContext CurrencyManager
CurrencyManager
DataSource
DataSource
In the diagram above, a particular control has three properties that are participating in data binding, as we can note from the three Binding objects. These bindings are stored in the control’s ControlBindings property, which is an instance of ControlBindingsCollection. The ControlBindingsCollection class is a collection of Binding objects. A Binding associates the property of a control with a data source member. Whenever the data source member value changes, the control’s property is updated, and vice-versa. Two of the bindings are associated with the same data source, while the third one is associated with a different data source. The CurrencyManager ensures that the properties that are associated with the same data source are synchronized.
118
W I N D O W S
F O R M S
D A T A
B I N D I N G
PropertyManager The PropertyManager is used to identify and maintain the current property of an object. PropertyManager derives from BindingManagerBase; but oddly, most of all of the base properties and methods are overridden to do nothing. For instance, setting the Position property of the object has no effect. Also, the AddNew and RemoveAt methods throw a NotSupportedException. Your guess is as good as mine as to why this object was derived from BindingManagerBase. As of this writing, PropertyManager is only used by the PropertyGrid. The PropertyGrid uses the current property to raise events, display property descriptions, and invoke appropriate editors. The following code shows how to return a PropertyManager from the BindingContext: Customer singleCustomer = new Customer(); PropertyManager pm = this.BindingContext[singleCustomer] as PropertyManager;
Simple Binding Example With simple binding, the property on a control is bound to a single data source member. The data source will typically be a collection, array, or DataTable. If the data source is a collection or array, the binding will occur on the property of a collection item. If the data source is a DataTable, the binding will occur on a DataColumn of the DataTable. In an example, we will walk through the implementation of binding customer data to controls on a form. First, create a new Windows Form in Visual Studio .NET. Drag a group box to the form. Then drag three labels and three text boxes to the form. Finally, drag four buttons to the form. Your form should look similar to the following:
119
C H A P T E R
6
Name your controls as follows: Group Box: _groupBox1 First Name Label : _firstNameLabel Last Name Label: _lastNameLabel Phone Number Label: _phoneNumberLabel First Name Textbox: _firstNameTextBox Last Name Textbox: _lastNameTextBox Phone Number Textbox: _phoneNumberTextBox; First Button: _firstButton Previous Button: _previousButton Next Button: _nextButton Last Button: _lastButton
120
W I N D O W S
F O R M S
D A T A
B I N D I N G
For each of the four buttons, add an event handler for the Click event called Button_Navigate. To do this manually, the syntax would be: _firstButton.Click += new EventHandler(this.Button_Validate);
Now, we must define a Customer. Create a new C# class file, and add the following: public class Customer { private String _firstName; private String _lastName; public String _phoneNumber; public Customer(String firstName, String lastName, String phoneNumber) { _firstName = firstName; _lastName = lastName; _phoneNumber = phoneNumber; } public String FirstName { get { return _firstName; } } public String LastName { get { return _lastName; } } public String PhoneNumber { get
121
C H A P T E R
{
}
}
6
return _phoneNumber;
}
For simplicity, we are only storing a customer’s name and phone number. Inside the constructor of the Form, we will initialize an array of customers with some arbitrary values. We will declare the array as readonly since we do not intend to reassign the collection value. private readonly Customer[] _customers = null; public Form1() { InitializeComponent( );
}
_customers = new Customer[] { new Customer(“James”, “Henry”, “123-123-1234”), new Customer(“Bill”, “Gates”, “234-234-2345”), new Customer(“Tupac”, “Shakur”, “345-345-3456”), }
We will now data bind all text box controls to the Customer array. This is illustrated as follows: private readonly Customer[] _customers = null; public Form1() { InitializeComponent(); _customers = new Customer[] { new Customer(“James”, “Henry”, “123-123-1234”), new Customer(“Bill”, “Gates”, “234-234-2345”), new Customer(“Tupac”, “Shakur”, “345-345-3456”),
122
W I N D O W S
F O R M S
D A T A
B I N D I N G
} _firstNameTextBox.DataBindings.Add(“Text”, _customers, “FirstName”); _lastNameTextBox.DataBindings.Add(“Text”, _customers, “LastName”); _phoneNumberTextBox.DataBindings.Add(“Text”, _customers, “PhoneNumber”); }
Finally, we must handle the Click event of the buttons in order to provide navigation: private void Button_Navigate(object sender, System.EventArgs e) { BindingManagerBase manager = _groupBox1.BindingContext [_customers];
}
if (sender == _firstButton) { manager.Position = 0; } else if (sender == _previousButton) { manager.Position--; } else if (sender == _nextButton) { manager.Position++; } else if (sender == _lastButton) { manager.Position = manager.Count – 1; }
The Button_Navigate handler handles the Click event for all four navigation buttons. In this code, we first retrieve the BindingManagerBase object from the BindingContext of _groupBox1. The instance is actually a CurrencyManager. We simply ask the BindingContext to “give me the CurrencyManager for the _customers data source.” The CurrencyManger changes its Position property, depending on which button was clicked. As the Position is changed, all bound controls are updated automatically.
123
C H A P T E R
6
Now, in order to demonstrate the purpose of the BindingContext, let’s add another group box to the form, and three more text boxes to this group box. Name these controls as follows: Group Box: _groupBox2 First Name Label: _firstNameLabel2 Last Name Label: _lastNameLabel2 Phone Number Label: _phoneNumberLabel2 First Name Textbox: _firstNameTextBox2 Last Name Textbox: _lastNameTextBox2 Phone Number Textbox: _phoneNumberTextBox2 We will now data bind this second set of text box controls to the Customer array. This is illustrated as follows: private readonly Customer[] _customers = null; public Form1() { InitializeComponent(); _customers = new Customer[] { new Customer(“James”, “Henry”, “123-123-1234”), new Customer(“Bill”, “Gates”, “234-234-2345”), new Customer(“Tupac”, “Shakur”, “345-345-3456”), } _firstNameTextBox.DataBindings.Add(“Text”, _customers, “FirstName”); _lastNameTextBox.DataBindings.Add(“Text”, _customers, “LastName”); _phoneNumberTextBox.DataBindings.Add(“Text”, _customers, “PhoneNumber”); _firstNameTextBox2.DataBindings.Add(“Text”, _customers, “FirstName”); _lastNameTextBox2.DataBindings.Add(“Text”, _customers, “LastName”);
124
W I N D O W S
F O R M S
D A T A
B I N D I N G
_phoneNumberTextBox2.DataBindings.Add(“Text”, _customers, “PhoneNumber”); }
Before we can actually see the advantage of the BindingContext, we must ensure that each set of text boxes “lives” in its own binding context. To do this, we must create a BindingContext for each group box. Remember that by default, the Form automatically creates a single BindingContext for it and all child controls. Here is the constructor after creating two new BindingContext objects: private readonly Customer[] _customers = null; public Form1() { InitializeComponent(); _customers = new Customer[] { new Customer(“James”, “Henry”, “123-123-1234”), new Customer(“Bill”, “Gates”, “234-234-2345”), new Customer(“Tupac”, “Shakur”, “345-345-3456”), } _groupBox1.BindingContext = new BindingContext(); _groupBox2.BindingContext = new BindingContext(); _firstNameTextBox.DataBindings.Add(“Text”, _customers, “FirstName”); _lastNameTextBox.DataBindings.Add(“Text”, _customers, “LastName”); _phoneNumberTextBox.DataBindings.Add(“Text”, _customers, “PhoneNumber”); _firstNameTextBox2.DataBindings.Add(“Text”, _customers, “FirstName”); _lastNameTextBox2.DataBindings.Add(“Text”, _customers, “LastName”); _phoneNumberTextBox2.DataBindings.Add(“Text”, _customers, “PhoneNumber”); }
125
C H A P T E R
6
Now, each group box and any child controls have their own context for data binding. Even through the controls in both group boxes may bind to the same data source, they will be bound using different CurrencyManager objects. We can visualize how the text box controls are data bound and synchronized in the following diagram:
BindingContext for _groupBox1 First Name: Last Name: Phone Number:
BindingContext for _groupBox2 First Name: Last Name: Phone Number:
CurrencyManager CurrencyManager
Customers Data Source
From the diagram above, we can see that each group box has its own CurrencyManager for the Customers data source. Therefore, changing the Position property on the first CurrencyManager will have no effect on the text boxes contained in the second group box.
126
W I N D O W S
F O R M S
D A T A
B I N D I N G
And changing the Position property on the second CurrencyManager will have no effect on the text boxes contained in the first group box.
Data Binding Interfaces .NET provides a standard set of interfaces related to data binding. Each of these interfaces is described below.
IList Any class that implements IList must support a list of homogenous types. That is, all list items must be of the same type. The first item in the list always determines the type. Some of the base classes that implement IList include Array, ArrayList, CollectionBase, DataView, and DataViewManager.
Typed IList Similar to IList, the list must be of homogenous types. However, classes of this type can only be data bound at runtime.
IList and IComponent When a class implements both IList and IComponent, the class can be data bound at design time. When dragging the component from the toolbox to a form, you will notice that the component appears in the component tray, like a DataSet and DataAdapter.
IListSource This interface allows an object to “act” like a list for data binding purposes. The implemented object is not an instance of IList, but it should be able to provide one. The DataSet and DataTable objects both implement this interface. IListSource provides a single property and a single method which are described below:
127
C H A P T E R
6
ContainsListCollection: Indicates whether the collection is a collection of IList objects. For the DataSet implementation, this property returns true, because the DataSet contains a collection of collections. For the DataTable implementation, this property returns false, because the DataTable contains a collection of objects. In simple terms, implement this property to indicate how deep to go for returning a bindable list. GetList: Returns the IList that will be data-bound. The DataSet uses this property to return a DataViewManager. The DataTable uses this property to return a DataView.
ITypedList This interface allows an object to expose its items’ properties. This interface is useful in situations where the public properties of the object should be different than the properties available for data binding. This interface is also necessary during complex binding when a list is empty but you still need to know the properties of the list item. (Remember, IList alone uses the data type of the first item in the list.) This is useful when columns headers should be created for empty lists.
IBindingList This interface offers change notification when the list and list items have changed. There is one property, SupportsChangeNotification, which determines whether this interface’s ListChanged event will be raised. The ListChangedEventArgs contains a ListChangedType property for describing the type of change that occurred. The available ListChangedType values are as follows: ItemAdded: An item has been added to the list. The index of the new item is the value of ListChangedEventArgs.NewIndex. ItemChanged: An item in the list has been changed. The index of the changed item is the value of ListChangedEventArgs.NewIndex. ItemDeleted: An item has been removed from the list. The index of the deleted item is the value of ListChangedEventArgs.NewIndex.
128
W I N D O W S
F O R M S
D A T A
B I N D I N G
ItemMoved: An item has been moved to another location within the list. The previous index is the value of ListChangedEventArgs.OldIndex. The new index is the value of ListChangedEventArgs.NewIndex. PropertyDescriptorAdded: A PropertyDescriptor has been added. PropertyDescriptorChanged: A PropertyDescriptor has been changed. PropertyDescriptorDeleted: A PropertyDescriptor has been deleted. Reset: The list has a lot of changes and controls should refresh themselves.
IEditableObject This interface supports transaction-like operations. It allows objects to specify when changes should be made permanent. Hence, it allows changes to be rolled back. The DataGrid is one control that opts to call methods of this interface. The following methods are defined in this interface: BeginEdit: Signals that an edit operation has started. Any changes to the object should be temporarily stored after this method has been called. When implementing this method, be sure that back-to-back calls are non-destructive. That is, the method itself should not create any changes to any temporary objects. CancelEdit: Cancels any changes made after the BeginEdit call. In other words, all temporary objects can be destroyed when this method is called. EndEdit: Commits any changes made after the BeginEdit call. Once this method is called, changes cannot and should not be rolled back. The following example illustrates IEditableObject with an implantation of a Customer object.
public class Customer : IEditableObject { private bool _transactionStarted = false; private String _firstName, _originalFirstName; private String _lastName, _originalLastName; private String _phoneNumber, _originalPhoneNumber; public String FirstName
129
C H A P T E R
{
}
{
} {
}
6
get { return _firstName; } set { _firstName = value; }
public String LastName get { return _lastName; } set { _lastName = value; } public String PhoneNumber get { return _phoneNumber; } set { _phoneNumber = value; }
void IEditableObject.BeginEdit() { if (!_transactionStarted) { _transactionStarted = true;
}
130
}
_originalFirstName = _firstName; _originalLastName = _lastName; _originalPhoneNumber = _phoneNumber;
W I N D O W S
F O R M S
D A T A
B I N D I N G
void IEditableObject.CancelEdit() { if (_transactionStarted) { _transactionStarted = false; _firstName = _originalFirstName; _lastName = _originalLastName; _phoneNumber = _originalPhoneNumber; } }
}
void IEditableObject.EndEdit() { if (_transactionStarted) { _transactionStarted = false; _originalFirstName = “”; _originalLastName = “”; _originalPhoneNumber = “”; } }
IDataErrorInfo This interface offers custom error information that controls can bind to. During data binding, this allows controls to retrieve specific error information from the data source itself. For instance, if a particular column in a DataTable is an Integer type, setting a field to a string for this column will cause the data source to return an appropriate error. This interface provides to properties: Error: Returns an error message indicating what is wrong. Item: An indexer that gets the error message for the specified column name or property name.
131
C H A P T E R
6
Complex Binding Example In the last section, we saw how to implement simple binding. We discussed how to bind public properties of controls to properties of objects and columns of DataTable objects, and synchronize the data, one item at a time. But there are also situations where an entire collection of data needs to be bound, such as viewing a list of software bugs. Typical controls that support such complex data binding include the DataGrid, ListBox, ComboBox, and ErrorProvider controls. All complex data bound controls will expose two important properties: DataSource and DataMember. The DataSource property can be any type derived from the interfaces discussed earlier. The DataMember is a string containing either the table name or a public property to bind to. For example, if the DataSource is a DataSet, the DataMember should specify which table to bind to; if the DataSource is a collection, the DataMember should be null; and if the DataSource is an object that has the binding collection as one of its public properties, the DataMember will be the name of that property. In this example, we will utilize the DataGrid to bind to the array of customers used in the previous simple binding example. First, drag the DataGrid control from the toolbox to the form that you created in the simple binding example. Also, drag another set of navigation buttons to the form. Each set of navigation buttons should correspond to a group box, and hence, a BindingContext. The DataGrid will display the entire list of customers (which is only three items for this example). We also want to use the row navigation events of the grid to change the current item in the first group box. You will have to rearrange the controls and resize the form as shown:
132
W I N D O W S
F O R M S
D A T A
B I N D I N G
Also, in order to tackle another bird with this stone, add a PropertyGrid control to the form. The PropertyGrid is not added to the toolbox by default, so right-click the toolbox, click “Customize Toolbox…,” navigate to the .NET Framework Components tab, and select the PropertyGrid control. The PropertyGrid will be synchronized with the current item in the list, displaying that item’s properties. The PropertyGrid control should be placed on the form as shown:
133
C H A P T E R
6
Name your controls as follows: DataGrid: _dataGrid PropertyGrid: _propertyGrid Now, using the code from the simple binding example, add two these lines to the constructor: _dataGrid1.DataSource = _customers; _propertyGrid1.DataBindings.Add(“SelectedObject”, _groupBox1.BindingContext[_customers], “Current”);
Here is a breakdown of what is happening with this code. First, we set the DataSource property of the data grid to the Customers collection. Since this is the collection we want to bind to, there is no need to set the DataMember property. Next, we synchronize the PropertyGrid with the current customer of the first group box. The PropertyGrid exposes a property, SelectedObject, which is used to display all public browsable properties of an object.
134
W I N D O W S
F O R M S
D A T A
B I N D I N G
Now compile and run the sample. Notice that by clicking the navigation buttons of the first group box, the PropertyGrid automatically updates its display for the new current object. It does this with only one line of code. But there is one small problem: Selecting different rows of the DataGrid does not cause navigation in the first group box that we expected. By now, you should already know what the problem is. It’s the BindingContext. Since we did not explicitly assign a BindingContext to the DataGrid, it will use the Form’s default BindingContext. And in this example, the Form’s default BindingContext isn’t managing any data bindings. To get around this problem, we simply assign the BindingContext of _groupBox1 to the BindingContext of the DataGrid, as shown: _groupBox1.BindingContext = new BindingContext(); _groupBox2.BindingContext = new BindingContext(); … _dataGrid1.DataSource = _customers; _dataGrid1.BindingContext = _groupBox1.BindingContext;
Now if we run the code, navigation will work as expected, with only one extra line of code. The full constructor is shown below: private readonly Customer[] _customers = null; public Form1() { InitializeComponent(); _customers = new Customer[] { new Customer(“James”, “Henry”, “123-123-1234”), new Customer(“Bill”, “Gates”, “234-234-2345”), new Customer(“Tupac”, “Shakur”, “777-777-7777”), } _groupBox1.BindingContext = new BindingContext(); _groupBox2.BindingContext = new BindingContext(); _dataGrid1.DataSource = _customers; _dataGrid1.BindingContext = _groupBox1.BindingContext; _propertyGrid1.DataBindings.Add(“SelectedObject”, _groupBox1.BindingContext[_customers], “Current”); _firstNameTextBox.DataBindings.Add(“Text”, _customers, “FirstName”); _lastNameTextBox.DataBindings.Add(“Text”, _customers, “LastName”);
135
C H A P T E R
6
_phoneNumberTextBox.DataBindings.Add(“Text”, _customers, “PhoneNumber”); _firstNameTextBox2.DataBindings.Add(“Text”, _customers, “FirstName”); _lastNameTextBox2.DataBindings.Add(“Text”, _customers, “LastName”); _phoneNumberTextBox2.DataBindings.Add(“Text”, _customers, “PhoneNumber”); }
Advanced Data Binding As we know from the previous sections, all controls on a form will contain a DataBindings property. Here is a view of the PropertyGrid for a TextBox, showing this property:
By default, the Text and Tag property are shown when expanding the DataBindings property. The Tag property of a control is used to provide custom data associated with the control. You may add additional properties to this expanded list by choosing them from the Advanced Data
136
W I N D O W S
F O R M S
D A T A
B I N D I N G
Bindings dialog box, which is accessed by clicking the ellipsis next to “Advanced.” This dialog is shown here:
In order for Advanced Data Binding to work, your form must contain a design-time data source component. You may provide a design-time data source by dragging a DataSet from the toolbox. Once you have a data source component, you simply associate each control you want bound in the “Advanced Data Binding” dialog with the data source component. As you associate each property with the data source, the property will be added along with the Text and Tag property beneath (DataBindings) in the PropertyGrid. When using Advanced Data Binding, you must be sure that properties are not bound twice. If you use the Advanced Data Binding dialog to bind a control’s property, and then use the control’s DataBindings property programmatically to bind the same property, a runtime error will occur.
137
C H A P T E R
6
Dynamic Properties By default, any properties set on a control in the designer are persisted either in code or in a resource file. If the Localization property of the parent form is set to true, a control’s properties are persisted in a resource file. Otherwise, they are persisted in code. There may be situations, however, where certain properties should be customized by the user or some other customization application. These scenarios include customizing the BackColor of a form, or the FlatStyle property of a button. In situations like these, it is common to implement configuration files. And with .NET, this capability is built in. Every Windows Forms application will expect to read from configuration files using a predetermined naming convention. The format is MyApp.exe.config. For example, if your application is named MyApp.exe, then your configuration file should be named MyApp.exe.config, and it should be placed in the same directory as the application itself. Note that you should not add the MyApp.exe.config file to the project, because it may be regenerated. If you need a configuration file to use during development, then the file should be app.config. Set the Build Action for this file to None. Compilation uses this file to regenerate MyApp.exe.config in the appropriate runtime directory. Every Windows Forms control has a design-time property named “DynamicProperties.” This property is a collection of name-value pairs which map key names to property names. These key names are stored in the application’s configuration file. Property values can then be persisted in this configuration file and retrieved during a form’s initialization. In code, each property will be associated with a key name. This key name is then used to read the property’s value. Here are snapshots of the DynamicProperties section in the PropertyGrid and the DynamicProperties dialog for the TextBox.Text property:
138
W I N D O W S
F O R M S
D A T A
B I N D I N G
As more properties are selected, which is indicated by a check box, these properties will be expanded beneath DynamicProperties in the PropertyGrid. The Key mapping on the right contains a list of all keys that have been added, and which can be cast to that property’s type. In other words, you can specify more than one property to use the same key, but the value of that key must be able to be cast to the types of both properties; otherwise it won’t appear in the list. This is determined by the first property that is configured. For example, the ReadOnly property will have an available key mapping of all Boolean properties that have been configured; the Text property will contain a key mapping of all Boolean properties and all String properties that have been configured.
139
C H A P T E R
6
Summary In this chapter we looked at the concepts and the architecture behind .NET data binding. We showed the relationship between controls and the BindingContext, and covered the interfaces related to data binding. We also walked through examples of the two types of data binding: simple binding and complex binding. Simple binding involves binding a single property of a control to a single column or property. Complex binding involves viewing a collection of objects and properties or rows and columns. Most importantly, we learned that the synchronization of a data source with its controls is loosely coupled through the CurrencyManager. Data no longer remembers its current position. This allows for multiple bindings and synchronization on the same data. Lastly, we discussed dynamic properties, their use, and how they are persisted in application configuration files. This concept allows the user to be in more control over the user interface, if desired. You should now have a better understanding of data binding in .NET Windows Forms. And remember, data no longer remembers its current position.
140
7
Chapter
GDI+ Overview In this chapter, we will discuss one of the most important topics relating to control development with .NET. This topic is GDI+ (Graphical Device Interface Plus). GDI+’s predecessor, GDI, has been around since the beginning of Windows. It was simply a set of Windows API functions that handled drawing to a particular device. The details were abstracted from the caller. These devices included screens, printers, video cards, and any other device capable of displaying visual content. They interfaced with the Windows operating system via device drivers, in which each contained different instruction sets or capabilities. GDI hides the differences between these devices. Therefore, a caller would simply call the same methods of the GDI API, and the device itself would determine how to render the output. Because GDI is an API, GDI+ aimed to provide an object-oriented way to make programming more intuitive. The GDI+ library is pretty huge, so we won’t go into the full details and implementation of GDI+. Besides, GDI+ is really a book in itself. We will, however, discuss enough about GDI+ so that you will be able to write custom controls and designers with confidence.
Drawing Basics In any drawing application, you will usually be presented with a device context (DC). The device context represents a device for calling methods of the GDI (or GDI+) API. The device context knows about the capabilities of the device that it represents. Note that when we say device, we don’t necessarily mean physical hardware devices. Devices, as stated earlier, can be a video card, printer, or screen. A device may even be just an area in memory. The device context is the bridge between the device and the caller.
C H A P T E R
7
With GDI+, the device context is now abstracted from the caller. We, instead, receive a reference to a Graphics object. The Graphics class is part of the System.Drawing namespace. Calling methods on a Graphics object calls the equivalent GDI API functions behind the scenes. We will soon discuss some of the methods and properties in the Graphics class. But first, we should mention that every .NET Windows Forms control has a method called CreateGraphics, which is responsible for returning a Graphics object that represents the device context for drawing. In order to use this method, the caller must have been given the UIPermission attribute. Security is beyond the scope of this book, but this permission prevents unwanted code from obtaining a reference to your Graphics objects, with the possible intention to destroy your application. Here is the simple syntax for obtaining a Graphics object. Control myControl = new Control(); Graphics g = myControl.CreateGraphics();
Every windows control has a protected virtual method named OnPaint, which is responsible for raising the Paint event for any subscribers. When inheriting from a control with the intention to do custom painting, it is recommended to override the OnPaint method. The Paint event should be used by callers of the control. . Here is the structure of the OnPaint method. protected virtual void OnPaint( PaintEventArgs e );
PaintEventArgs is a class that contains the event data for painting. The properties of this class are described below: ClipRectangle: This is the Rectangle in which painting is to be done. The property is readonly, and can only be set through the PaintEventArgs constructor. Graphics: Represents the Graphics object in which painting will occur. The property is readonly, and can only be set through the PaintEventArgs constructor. We will now examine some of the methods and properties of the Graphics class. First, let’s go over some of the properties:
144
G D I +
Clip: This property is used to limit the drawing region of a Graphics object. The property is an instance of the System.Drawing.Region class. ClipBounds: Returns the rectangle that represents the clipping region for drawing. The rectangle returned is an instance of System.Drawing.RectangleF. DpiX: Returns the horizontal resolution of the Graphics object. The value returned is dots per inch. DpiY: Returns the vertical resolution of the Graphics object. The value returned is dots per inch. PageScale: Gets or sets the scaling for the Graphics object. This property scales between world units and page units. PageUnit: A System.Drawing.GraphicUnits enumeration value that measures page coordinates. Possible values include Display (1/75 inch), Document (1/300), Inch, Millimeter, Pixel, Point (1/72 inch) and World. VisibleClipBounds: Represents the visible clipping region of the Graphics object. Some of the useful methods include: DrawArc: Draws an arc, which is a portion of ellipse, given the width and height, starting point, ending point, and angle. DrawBezier: Draws a Bezier curve given four points. A Bezier is a curve defined by four points. The four points simply serve as a way to pull the curve in each point’s direction. DrawEllipse: Draws an ellipse defined by a rectangle, width and height. DrawCurve: Draws a curved line through an array of points. DrawClosedCurve: Draws a curved line through an array of points, and connecting the end point to the start point using a straight line. DrawIcon: Draws an image represented by the specified Icon object. DrawImage: Draws an image represented by the specified Image object. DrawLine: Draws a line through two end points.
145
C H A P T E R
7
DrawPath: Draws a GraphicsPath object, which is a series of curves and lines connected by points. DrawPolygon: Draws a closed polygon through an array of points. DrawRectangle: Draws a rectangle given a starting point, width and height. FillEllipse: Draws an ellipse defined by a rectangle, width and height, and fills the ellipse with the specified brush. FillPath: Draws a GraphicsPath object, which is a series of curves and lines connected by points, and fills the object with the specified brush. FillPolygon: Draws a closed polygon through an array of points, and fills the polygon with the specified brush. FillRectangle: Draws a rectangle given a starting point, width and height, and fills the rectangle with the specified brush. FillClosedCurve: Draws a closed curve, filling it with the specified brush.
Pens and Brushes With GDI, and any successor, we will almost always encounter pens and brushes. A pen is normally used for drawing lines and borders, while a brush is used for painting colors and patterns. A pen is represented by the System.Drawing.Pen class. Similarly, a brush is represented by the System.Drawing.Brush class. The difference with using pens and brushes between GDI and GDI+ is that neither a pen nor a brush is now saved between method calls. For example, with GDI, a caller was required to call the SelectObject method to associate either a pen or a brush with a device context. Afterwards, any method calls to the GDI API would use the currently selected pen or brush. Now, with almost every operation, the caller is required to supply a pen and brush.
Brushes Each brush is derived from System.Drawing.Brush. This class is abstract, so it can’t be instantiated directly. The System.Drawing namespace also contains a Brushes class, which
146
G D I +
contains a number of already defined brushes. The properties of this class are all static. Here is the syntax for obtaining a brush from this class: Brush brush = Brushes.AliceBlue
You could also instantiate the brush using a derived brush’s constructor, such as: Brush brush = new SolidBrush(Color.AliceBlue);
The code above creates a solid brush, which means that the specified color will be used for painting an object, using a solid pattern. Another type of brush is the hatch brush, which paints a region using a specified pattern. The class that represents this brush is System.Drawing.Drawing2D.HatchBrush. Notice that the class does not belong to the normal System.Drawing namespace, because of its advanced features. The constructor takes a System.Drawing.Drawing2D.HatchStyle enumeration value. Here is the syntax: HatchBrush brush = new HatchBrush(HatchStyle.DottedGrid , Color.Green);
Another type of brush is the System.Drawing.Drawing2D.LinearGradientBrush, which represents a fill that changes linearly across an object. Here is an example of a linear brush in action: e.Graphics.FillRectangle(new System.Drawing.Drawing2D.LinearGradientBrush(e.ClipRectangle , Color.White, Color.Black, 45, true), e.ClipRectangle);
As you can see, the control’s back color is white, changing linearly at an angle of 45 degrees, ending in black. Another brush similar to the linear gradient brush is the System.Drawing.Drawing2D.PathGradientBrush. This brush allows a color to vary along a path.
147
C H A P T E R
7
Pens All pens in GDI+ are represented by the single class System.Drawing.Pen. With pens, you typically specify a color and width. However, other properties such as Alignment, DashStyle, and PenType can also be set. With pens, you also have the ability to draw hatches. Similar to brushes, a set of default pens can be found in the System.Drawing.Pens class. This class contains a list of static properties that represent pre-defined pens. Note that the Pen class, however, is not an abstract class. Therefore, you can instantiate pens directly, as shown below: Pen greenPen = new Pen(Color.Green); Pen thickGreenPen = new Pen(Color.FromArgb(0, 255, 0), 20);
You can also use a brush to create pens: Brush yellowHatchBrush = new HatchBrush(HatchStyle.Cross, Color.Yellow); Pen hatchPen = new Pen(yellowHatchBrush);
Even though you can uses pens and brushes to do just about any type of drawing, .NET provides an easy way for some of the common drawing tasks. It does this through a sealed class with static methods, called ControlPaint.
The ControlPaint class The ControlPaint class is part of the System.Drawing namespace. It helps to reduce reinvention of the wheel by providing a number of static methods and a single property to aid in control rendering. We will go over each of these, so that you will have a better understanding of what .NET already gives us. Using a sample application, we will also demonstrate each of these methods in action: ContrastControlDark: The one and only property of the ControlPaint class, that returns the Color to use as the ControlDark color. This property will either return SystemColors.WindowFrame or SystemColors.ControlDark. The former will be returned if the control’s HighContrast property is set to true. The latter will be returned if the control’s HighContrast property is set to false. Dark: Creates a new dark color based on the specified color for an object.
148
G D I +
DarkDark: Creates a new darker color based on the specified color for an object.
DrawBorder: Draws a border on a button-like control.
DrawBorder3D: Draws a 3-D border on a control.
149
C H A P T E R
7
DrawButton: Draws a button.
DrawCaptionButton: Draws a caption button, which is of one of the following styles: Help, Close, Maximize, Minimize, and Restore
DrawCheckBox: Draws a check box.
150
G D I +
DrawComboButton: Draws a combo box button.
DrawContainerGrabHandle: Draws a handle used to grab a container control.
DrawFocusRectangle: Draws a focus rectangle around a control.
151
C H A P T E R
7
DrawGrabHandle: Draws a grab handle for a control.
DrawGrid: Draws a grid with dots.
DrawImageDisabled: Draws a disabled image, using the image specified.
152
G D I +
DrawLockedFrame: Draws a locked frame around the control.
DrawMenuGlyph: Used to draw a menu glyph on a menu item of a control.
DrawMixedCheckBox: Draws a three-state check box.
153
C H A P T E R
7
DrawRadioButton: Draws a radio button.
DrawReversibleFrame: Draws a reversible selection frame.
DrawReversibleLine: Draws a reversible line.
154
G D I +
DrawScrollButton:
DrawSelectionFrame: Draws a selection frame around the control.
DrawSizeGrip: Draws a size grip, typically used for resizing a control.
155
C H A P T E R
7
DrawStringDisabled: Draws a grayed out string.
FillReversibleRectangle: Fills a rectangle with a reversed color.
Light: Creates a new light color based on the specified color for an object.
156
G D I +
LightLight: Creates a new lighter color based on the specified color for an object.
Manipulating Images and Icons Another common task as a developer is to display images that reside in files or memory. The Graphics class has a few methods to draw images and icons. DrawImage and DrawImageUnscaled draws images, while DrawIcon and DrawIconUnstretcted draws icons. An image is represented by the System.Drawing.Image class. An icon is represented by the System.Drawing.Icon class. We instantiate an Image object similar to the following: Image myImage = Image.FromFile(“myLogo.jpg”);
This instantiates an image that is one of the following formats: bmp, jpeg, gif, and png. You can then display that image using one of the methods described above.
157
C H A P T E R
7
One important thing about the Icon class and the Image class, is that their instances should be disposed of when they are no longer needed. Images use a lot of memory while they are in use. Simply use the following syntax for disposal: myImage.Dispose();
Creating an Oval Button In this example, we will demonstrate how we can use the Graphics class to develop an Oval Button control. This control will mimic controls used in applications such as the new Media Player. First, create a new Windows Forms project in Visual Studio .NET. Then create a new UserControl named OvalControl, and add it to the form. When you first create the control, you will see an empty container control, waiting for other controls to be dragged to it. However, we will do all rendering directly with GDI+. Here is a view of what you should see right now in the designer:
For this control, we will respond to the mouse events so that the appearance of the button will change. We will also give it a 3-D effect. Let’s now switch to the Events tab of the PropertyGrid for the OvalControl, and add handlers for the following events: MouseDown, MouseMove, MouseLeave, and MouseUp. To add an event handler through the PropertyGrid, simply double click the event entry. If you now switch to code view, you should see something similar to the following: private void OvalButton_MouseLeave(object sender, System.EventArgs e) { } private void OvalButton_MouseMove(object sender, MouseEventArgs e) {
158
G D I +
} private void OvalButton_MouseUp(object sender, MouseEventArgs e) { } private void OvalButton_MouseDown(object sender, MouseEventArgs e) { }
Before we implement the handlers, we must override the OnPaint method and define some public properties. We know we need a Text property for the button’s text. Since we have already talked about the linear gradient brush, and we have seen what we can do with it, we will use that brush for painting our button. Because we know the LinearGradientBrush uses two colors, a starting color and an ending color, let’s go ahead and define those three properties: private Color _startGradient = Color.White; private Color _endGradient = Color.Gray; private String _text = “Button1”; public Color StartGradient { get { return _startGradient; } set { _startGradient = value; } } public Color EndGradient { get { return _endGradient; } set { _endGradient = value; } } public new String Text { get { return _text; } set { _text = value; } }
Furthermore, we want the button’s color to change in response to the mouse hovering over the control. So let’s add two more properties for this:
159
C H A P T E R
7
private Color _startGradient = Color.White; private Color _endGradient = Color.Gray; private String _text = “Button1”; private Color _mouseOverStartGradient = Color.LightBlue; private Color _mouseOverEndGradient = Color.DarkBlue; public Color StartGradient { get { return _startGradient; } set { _startGradient = value; } } public Color MouseOverStartGradient { get { return _mouseOverStartGradient; } set { _mouseOverStartGradient = value; } } public Color EndGradient { get { return _endGradient; } set { _endGradient = value; } } public Color MouseOverEndGradient { get { return _mouseOverEndGradient; } set { _mouseOverEndGradient = value; } } public new String Text { get { return _text; } set { _text = value; } }
Now, let’s override OnPaint to render the control: protected override void OnPaint(PaintEventArgs e) { Color startColor = _startGradient; Color endColor = _endGradient; Brush gradient = new LinearGradientBrush(this.ClientRectangle, startColor, endColor, 45);
160
G D I +
}
e.Graphics.FillEllipse(gradient, this.ClientRectangle);
In the code above, the control will have a two-toned fading appearance. We must implement the mouse handlers so that this appearance changes as a result of the mouse events: private void _mouseOver = false; ... private void OvalButton_MouseMove(object sender, MouseEventArgs e) { bool oldMouseOver = _mouseOver; Point cursorPos = Cursor.Position; Point centerPoint = new Point(this.Width/2, this.Height/2); double radius = this.Width/2; // Get the distance from the center point to the mouse cursor double x = Math.Abs(cursorPos.X this.PointToScreen(centerPoint).X); double y = Math.Abs(cursorPos.Y this.PointToScreen(centerPoint).Y); double lengthToCursor = Math.Sqrt(x*x + y*y); // If the distance is less than the radius, we will hover. // (We only want a hover if the cursor is inside the ellipse). _mouseOver = lengthToCursor < radius;
}
// Only refresh the control if the hover has changed. if (oldMouseOver != _mouseOver) { this.Refresh(); }
In the code above, we first declare a Boolean member variable named _mouseOver. Its purpose is to store the state of the mouse hover. We don’t use the MouseHover event, since it tracks hovering over the full control. It will also be slower in performance, since the MouseMove event gets raised first. We then capture the current cursor’s position, determining if it is within the bounds of the oval, or ellipse. We then set the _mouseOver member accordingly. Finally, we force the control to repaint itself, but only if the hover state has changed from its last state, to prevent redundant repaints.
161
C H A P T E R
7
We also have to handle the MouseLeave event. Even though the MouseMove handler sets both the hover and non-hover state, we will never receive the event once the cursor moves out of the control. The MouseLeave handler is simple: private void OvalButton_MouseLeave(object sender, EventArgs e) { _mouseOver = false; }
this.Refresh();
Now, we want a different appearance when the button has been clicked. To do this, we handle the MouseUp and MouseDown events, as shown here: private bool _mouseDown = false; ... private void OvalButton_MouseUp(object sender, MouseEventArgs e) { _mouseDown = false; }
this.Refresh();
private void OvalButton_MouseDown(object sender, MouseEventArgs e) { // This code is copied from the MouseMove handler. // Typically, we would use a method to prevent duplicate code. Point cursorPos = Cursor.Position; Point centerPoint = new Point(this.Width/2, this.Height/2); double radius = this.Width/2; double x = Math.Abs(cursorPos.X this.PointToScreen(centerPoint).X); double y = Math.Abs(cursorPos.Y this.PointToScreen(centerPoint).Y); double lengthToCursor = Math.Sqrt(x*x + y*y); if (lengthToCursor < radius) { _mouseDown = true;
162
G D I +
}
}
this.Refresh();
We pretty much use the same logic from the MouseMove handler. The only difference is that we now update a new member variable, _mouseDown, to represent the state of the mouse button. We must now modify the OnPaint method to make use of the new state variables we have just added. Here is the updated code: protected override void OnPaint(PaintEventArgs e) { Color startColor = _mouseOver ? _mouseOverStartGradient : _startGradient; Color endColor = _mouseOver ? _mouseOverEndGradient : _endGradient; if (_mouseDown) { startColor = ControlPaint.Light(startColor); endColor = ControlPaint.Light(endColor); } Brush gradient = new LinearGradientBrush(this.ClientRectangle, startColor, endColor, 45); e.Graphics.FillEllipse(gradient, this.ClientRectangle); SizeF textSize = e.Graphics.MeasureString(_text, new Font("Verdana", 10)); e.Graphics.DrawString(_text, new Font("Verdana", 10), Brushes.Black, this.Width / 2 textSize.Width/2, this.Height / 2 - textSize.Height/2); }
If you now compile and run the code, you will notice the following results:
163
C H A P T E R
7
Moving the cursor within bounds of the oval yields this:
Clicking the left mouse button yields this:
164
G D I +
Irregularly Shaped Forms As you may have noticed, some of the newer applications targeting the Windows platform are bringing a 3-D look and feel to the table. These looks include borderless forms and rounded edges. Applications such as WinAmp and Windows Media Player are examples. They provide “skins” for extra support. A skin is simply a graphical plug-in that renders according to a specific interface. This can all be done through GDI, but with .NET and GDI+, it is a piece of cake. We will illustrate how to develop irregularly shaped forms by walking through an example. In this example, we will develop a simple 3-D cellular phone that supports both compact and full mode. In full mode, the entire form will be shown with the phone. In compact mode, only the cellular phone will be shown, creating a borderless look. First, add a new button along with the button’s Click event handler to the sample form created above. Then add a new Windows Form named CellularPhoneForm to the project. In the button’s event handler, add the following code to invoke the new form: private void button1_Click(object sender, System.EventArgs e) { CellularPhoneForm form = new CellularPhoneForm(); form.Show(); }
Now, go to the newly created form and switch to design view. Set the BackgroundImage of the form and adjust the form’s width and height accordingly. If you use the image from the samples, your designer should look similar to the following:
165
C H A P T E R
7
Now, set the FormBorderStyle property to None and the TransparencyKey property to the background color of the image. This will make the form borderless and cause any areas of the form that are the same color as the TransparencyKey to become transparent. A form does not receive any events from the transparent areas. The events will instead be transferred to any windows below the form. So for the image above, set the TransparencyKey to White. We now need to provide a way for switching between compact and full mode. Because in compact mode only the phone will be displayed, it only makes sense to implement the functionality through a context menu. We will implement three menu items: Compact Mode, Full Mode, and Exit. Add these three constants to the form as shown: private const string FullMode = "Full Mode"; private const string CompactMode = "Skin Mode"; private const string Exit = "Exit";
166
G D I +
Now add the code to load the context menu to the constructor. We must assign each menu item with an event handler. Here is the code: public CellularPhoneForm() { // // Required for Windows Form Designer support // InitializeComponent(); // // TODO: Add any constructor code after InitializeComponent call // EventHandler handler = new EventHandler(OnContextMenuItemClick); MenuItem[] menuItems = { new MenuItem(FullMode, handler), new MenuItem(CompactMode, handler), new MenuItem(Exit, handler), }; this.ContextMenu = new ContextMenu(menuItems); }
We created a single event handler named OnContextMenuItemClick, as shown: EventHandler handler = new EventHandler(OnContextMenuItemClick);
We need to implement the handler as follows: e)
private void OnContextMenuItemClick(object sender, EventArgs {
MenuItem menuItemClicked = (MenuItem)sender; switch (menuItemClicked.Text) { case FullMode: { this.FormBorderStyle = FormBorderStyle.Fixed3D; this.TransparencyKey = Color.FromArgb(0, 0, 1); break; } case CompactMode:
167
C H A P T E R
7
{
}
}
this.FormBorderStyle = FormBorderStyle.None; this.TransparencyKey = Color.White; break;
} case Exit: { this.Close(); break; }
In the code above, we first check to text of the menu item clicked. From the text, we determine which action to take. If Full Mode is selected, we add a border to the form by setting its FormBorderStyle property, as shown: this.FormBorderStyle = FormBorderStyle.Fixed3D;
We then change the TransparencyKey to a value that is not rendered anywhere on the form. Through simple guessing, we added the following line: this.TransparencyKey = Color.FromArgb(0, 0, 1);
When Compact Mode is selected, we do exactly what we did to the initial properties in design mode. We set the TransparencyKey to White, and the FormBorderStyle property to None. When Exit is selected, we just Close the form. Because forms can generally only be moved via the title bar, we must handle the MouseDown and MouseMove events to implement our own form moving logic. However, we only want to use our custom logic when the form is in Compact mode. To accomplish this, we must store the mode whenever it is switched. This can be done easily using an enumeration type and a member variable. Add the following code before the constructor: private SkinMode _skinMode = SkinMode.Compact; private enum SkinMode { Compact, Full }
168
G D I +
Now add the code to the switch statement to save the mode when the mode is changed, as shown: case FullMode: { _skinMode = SkinMode.Full; ... } case CompactMode: { _skinMode = SkinMode.Compact; ... }
Finally, we need to handle the mouse events. Whenever the mouse is pressed down on the form, we need to capture its position in order to move the control accordingly. So add the following private member to the form: private Point _mouseOffset;
Now handle the MouseDown event, as shown: e)
private void CompactMouseDown(object sender, MouseEventArgs { }
_mouseOffset = new Point(-e.X, -e.Y);
The code above assumes that you attached the MouseDown event to the CompactMouseDown handler. If you used Visual Studio .NET’s property grid for attaching the event, the name of your handler may be different. Now handle the MouseEvent to change the location of the form with respect to the private member variable we just added. Here is the code: e)
private void CompactMouseMove(object sender, MouseEventArgs {
if(e.Button == MouseButtons.Left && _skinMode == SkinMode.Compact) { Point mousePos = Control.MousePosition; mousePos.Offset(_mouseOffset.X, _mouseOffset.Y);
169
C H A P T E R
}
}
7
this.Location = mousePos;
Now compile and run the program. Clicking on the button should initially yield the cellular phone form in compact mode. You should be able to switch between modes by using the context menu. Multiple cellular phone forms may be opened since we did not create them as modal dialogs. But note that there will be no way to differentiate between active and inactive cellular phone forms in compact mode. Forms with borders hint when they are inactive by painting the title bar a different color. Here are the outputs of several runs:
170
G D I +
171
C H A P T E R
172
7
G D I +
173
C H A P T E R
7
Summary GDI+ is somewhat different than the old GDI API. GDI+ is a collection of managed .NET classes that wraps calls to the older API. Calls between drawing methods are now stateless. The .NET Framework comes equipped with a handy class, ControlPaint, used to perform some common drawing tasks. These tasks include drawing buttons, check boxes, menu glyphs and the like. We did not go into too much detail in this chapter, because as you should have probably learned by playing around with the Graphics class itself, that GDI+ is a book within itself. But you should have learned enough to enable you to start rendering your own custom controls, as well as creating a better UI using some of the standard controls. This is the last chapter on Windows Forms. We will now move into the next generation of things from .NET, Web Forms.
174
8
Chapter
Introduction to Web Forms Overview ASP.NET is a technology that is embedded into the .NET framework, used for delivering dynamic content via HTTP. Fully named Active Server Pages .NET, it is the successor to ASP. Some similar technologies include PHP and ColdFusion. The languages that worked with ASP, such as Jscript and VBScript, can still be used with ASP.NET; however, VB has been completely revamped. Also, since ASP.NET is part of the .NET framework, it inherently supports C#, which means that it is now object oriented. This object oriented architecture allows controls to be developed using normal OO practices. Creating an ASP.NET server control now should be no harder than creating a full-fledged Windows control. We no longer have to deal with what some called, “spaghetti code.” All ASP.NET pages that generate HTML are called Web Forms; hence, we named the chapter after this. In this chapter, we’ll first look at the architecture of server-side technology. Then we’ll look at some of the predecessors to ASP.NET, and finally the architecture of ASP.NET itself.
Server-Based Control Architecture Before we look at the architecture of ASP.NET, we must first understand the basic architecture behind any server-side processing over HTTP. The diagram below illustrates this process:
C H A P T E R
8
Web Server
Request
Resource Processor (MIME processor)
Response
Resource
During any server-side processing, an HTTP request usually comes from outside the physical environment of the web server and its resources. Once the web server receives the request, it parses the request to determine the appropriate action to be taken. With IIS Web Server, this is normally done using file extensions and MIME (multipurpose internet mail extensions) types. IIS would simply use the file extension of the requested resource, and then invoke its corresponding MIME application or processor. The processor would then perform its duty and return the resource to IIS, which then returns the resource to the requestor. Actually, there is a bit more going on behind the scenes. Notice that we did not mention anything about security. A more detailed architecture will be shown when we discuss ASP.NET.
CGI CGI, common gateway interface, was one of the first Internet Server Application processors to process requests over the web. They were simple executable files written in the C language, in which each executable performed a specific action. Each CGI request launched a new process.
178
I N T R O D U C T I O N
T O
W E B
F O R M S
So if 50 users simultaneous requested a particular resource, there would be 50 processes loaded into memory at that instance. The syntax for calling a CGI script over the web is shown below: http://server/myscript.exe?Param1&Param2&Param3
MFC ISAPI Extensions Following CGI was ISAPI. ISAPI stands for Internet Server Application Programming Interface. An ISAPI extension is a DLL that acts almost identical to CGI. The primary difference between CGI and ISAPI is that ISAPI extensions could be loaded only once, and the same process could be shared by multiple requests. Here is the syntax for calling an ISAPI extension: http://server/myisapi.dll?Param1&Param2&Param3
Microsoft has provided a number of MFC classes to ease programming with ISAPI. ISAPI extensions are components that act as resource processors for incoming requests from a Web Server. They normally include three basic functions: Initialization, Processing and Termination. Once an ISAPI component is loaded into memory by the first resource request, it may remain in memory as long as the Web Server is running, depending on the server setup. Because of the possibility of a single object serving multiple threads, developers had to cook up their own synchronization to prevent crashes and deadlocks. IIS communicated with the extension through callbacks, which had to be implemented by the developer. Because earlier versions of IIS loaded extensions into the same process as IIS, other extensions could crash the web server. With later versions of IIS, there was support to load each extension into a separate process. To ease the development of ISAPI extensions, MFC provided a set of classes that encapsulated the underlying HTTP calls. Visual Studio included an MFC ISAPI Extension Wizard to generate sample code for you. One of the main benefits of ISAPI extensions was the protection of source code. Third party hosting companies could not access your processing code without hacking it. But this same benefit led to an enormous amount of effort involved in debugging. Developers would typically have to write their own test applications for walking through the code in Visual Studio. If that wasn’t enough, they would even go as far as creating NT services on remote
179
C H A P T E R
8
machines. Even this was not entirely accurate testing, because many problems were discovered over a distributed system in a real-world browser request scenario.
ASP Active Server Pages, known as ASP, is a Microsoft server-side scripting technology that uses ISAPI to deliver dynamic HTTP content to clients. An ASP page is a simple HTML page that can combine server side scripts using VBScript, client side scripts using JScript, and data transformation using XML and XSLT. Similar to ISAPI extensions and CGI scripts, ASP pages are processed when a file with an .asp extension is requested over HTTP. The ASP processor, which is implemented as an ISAPI extension, then loads the ASP file, parses it sequentially, and generates content based on special tags and scripts found during the parse. Once requested, the ASP processor compiles the script for subsequent requests and faster response time. ASP is, by some, thought of as language-independent. But the only major languages playing a role in ASP are JScript, VBScript, and Perl. ASP has a set of built-in objects to help with coding. The primary ones are the Request and Response object, which represent a request and a response, respectively.
ASP.NET ASP.NET is the next version of ASP. Syntactically, it is compatible with ASP. But it provides a newer programming model to help build more stable applications. It is entirely based on the .NET framework, so we inherently get to use C# for building web forms. ASP.NET allows code to be compiled and delivered to the client dynamically, via HTTP. Like ASP, it is also defined as language-independent. And as of this writing, the languages that target ASP.NET include JScript.NET, Python, Perl, Eiffel, C#, and VB.NET. Developers get to use the full range of managed classes available in the .NET Framework. With ASP.NET, there is a good separation between code and content. The architecture of an ASP.NET process is detailed below:
180
I N T R O D U C T I O N
T O
W E B
F O R M S
IIS Web Server
.ASPX Request
ASP.NET and .NET Framework
HTML Response
HTML Resource
The processing is very similar to the processing of an ASP page. But there is one small difference in the compilation process. ASP.NET first transforms the ASPX page into C# code, and places the transformed file into a temporary directory. It then compiles that C# file into an assembly with the same name as the pre-compiled assembly (code-behind assembly), and places the compiled file into a subdirectory of the temporary directory. It then creates an instance of your page by deriving from the page defined in the code-behind assembly. To see for yourself, after creating and deploying an ASP.NET application, locate the following directory or a similar directory on your web server: C:\WINNT\Microsoft.NET\Framework\v1.0.3705\Temporary ASP.NET Files\
Each subdirectory beneath this directory contains various web applications that have been deployed on the server, with each directory containing either code files or assemblies. The compilation process is shown here:
181
C H A P T E R
8
Page.ASPX
CodeBehindPage.DLL
Parsing
ASP.NET Compiler and .NET Framework
Inheritance Compilation
Runtime Compiled Code
Windows File System
<System>\Microsoft.NET\Framework\\Temporary ASP.NET Files\
182
I N T R O D U C T I O N
T O
W E B
F O R M S
ASP.NET supports both Web Forms and Web Services. In this book, we will only fully discuss web forms, but we will mention web services briefly. Web services allow access to server processing and functionality remotely, similar to DCOM in the old world. All data passed between servers is marshaled into XML on the client and then unmarshaled on the server. Actually, to appropriate terms to use these days are serialized and deserialized. The client must create a proxy using the web service’s schema, and afterwards it can call methods on remote objects as if the objects existed on the same server. Web Forms allow the building and deployment of powerful web applications. Programming a web form is almost as easy as programming any windows application, because of the new object oriented features available with ASP.NET. The .NET Framework also includes a set of reusable controls, along with a framework for creating custom controls. The types of controls available with ASP.NET are described below: HTML server controls: These are controls that have been designed to represent common HTML elements. They map one to one with their HTML counterparts. Web server controls: These controls have been developed to serve as a sibling to their Windows counterparts. These controls include the Calendar, AdRotator and DataGrid. They are described later. Validation controls: These are a set of controls that have the capability to perform validation of user input for other controls. User and Custom controls: These controls are defined by you.
Web Server Controls Here is the syntax for declaring a web server control in an ASP.NET page:
In the simple snippet above, the runat=”server” attribute instructs IIS to execute the code on the server. Here are some common web server controls that are available with the .NET framework: Label: Represents a label, which is a simple text display control for a web page.
183
C H A P T E R
8
TextBox: Provides a text box for user editing. If the AutoPostBack property is set to true, then the TextChanged event is raised whenever text has changed and focus leaves the control. Otherwise, the TextChanged event is raised only when text has changed and the page has been submitted. DropDownList: Provides a combo box-like control which displays a list of values. The Items property is used to add and remove items from the list. Items in an instance of ListItemCollection. The SelectedIndex property gets or sets the index of the selected item in the list. The SelectedItem property gets the first matched selected item in the list. If AutoPostBack is set to true, selection changes raises the SelectedIndexChanged event ListBox: Provides a list for multiple selections of items. Image: Displays an image on a web page. The ImageUrl property is used to reference the image, and the AlternateText property provides text if the referenced image could not be loaded. AdRotator: Provides a mechanism to display several consecutive images, each representing a different advertisement. The AdvertisementFile is used to specify an XML file that contains the images for each advertisement. Here is the syntax of the advertisement file: http://server/ad1.jpg http://server/ad1.html Ad1 goes here… 80 Ad1 Ad1 Caption
The properties of the Ad elements are described below: ImageUrl: Represents the URL of the image for displaying. NavigateUrl: Represents the URL that is navigated to when the ad is clicked. The Target property of the AdRotator determines which window or frame is used for navigating to the URL of the advertisement. AlternateText: Represents the text to display if the image could not be loaded.
184
I N T R O D U C T I O N
T O
W E B
F O R M S
Keyword: Represents a specific user-defined category for the advertisement. This is used by the AdRotator control to filter advertisements based on specific categories. This value will be used by the KeywordFilter property of the AdRotator for filtering. Impressions: Represents a value that indicates the duration of an advertisement relative to other advertisements.
CheckBox: Provides a box for checking and unchecking. The control provides an AutoPostBack property that determines if postbacks should occur when the checked state of the control changes. When AutoPostBack is set to true, the control’s CheckedChanged event is raised during postbacks. CheckBoxList: This control provides a group of check boxes. RadioButton: Displays a button that can be turned on or off. When the AutoPostBack property is set to true, the CheckedChanged event is raised when the checked state of the button is changed. This control also has a GroupName property to specify a radio button group, so that only one control in the group can be checked at a time. RadioButtonList: This control provides the ability for RadioButton controls to be grouped. Calendar: Provides a graphical calendar for selection of dates. Button: This represents standard button for clicking. The Text property provides the text for the button, and the Click event responds to user clicks. There is no AutoPostBack property on the button, because post backs are automatic. LinkButton: This control provides the same functionality as a button, but it is rendered as a hyperlink. ImageButton: This control provides the same functionality as a button, but it is rendered as an image. HyperLink: Represents an HTML hyperlink. The NavigateUrl corresponds to the page to be navigated to when the hyperlink is clicked. The ImageUrl specifies an image to be displayed.
185
C H A P T E R
8
Table: The control mimics a table. It uses the TableRow and TableCell controls to build the table. TableRow: This control specifies an individual row within a Table control. It has typical properties that mimic an HTML table’s attributes. TableCell: This control specifies a cell within a Table control. Panel: This control is simply a container for organizing other controls. Repeater: This is one of the data binding controls provided with the .NET framework. Using templates, this control can display items from a variety of data sources. DataList: This control is similar to the Repeater control, but it provides options in regards to formatting. DataGrid: Provides the same functionality as the Repeater and DataList, but it automatically renders a grid.
Validation Controls Validation controls provide a way of validating user input for other controls. Each validation control is associated with a single web control. Whenever a post back occurs, each validation control uses the data in the control it is validating to set the IsValid property of the validation control. A validation control also has an ErrorMessage property, which is displayed whenever a post back occurs and an IsValid property returns false. All validation controls inherit from System.Web.UI.WebControls.BaseValidator, in which the ErrorMessage and IsValid property is defined. A third property exposed by BaseValidator is ControlToValidate. With this property, you specify the ID of the control that this validation control validates. Here are some common validation controls provided with the .NET framework: RequiredFieldValidator: This validation control is used to check whether data has been entered in a control. RangeValidator: Uses the MinimumValue and MaximumValue property to validate controls using a specified range.
186
I N T R O D U C T I O N
T O
W E B
F O R M S
CompareValidator: This control uses comparison the validate controls. Several properties of this control are used to aid in the comparison. RegularExpressionValidator: This control validates data based on a regular expression provides with the ValidationExpression property. Controls that would use this validator include controls that display phone numbers, IP addresses, and zip codes. CustomValidator: This validator allows custom validation using a user-defined function. The ServerValidate event is used to specify a delegate that represents the custom function for server side validation. It also has a ClientValidateFunction property to allow client side validation.
Custom and User Controls Normally, with web development and windows development, there will always be the case when a suite of tools or controls just don’t provide what you need to match the requirements of a specific project. This leads to custom control development. Historically, developing custom web controls has been a daunting task. Developers mainly had three options. They could use DHTML to create client-side scripts that used HTML elements to build up controls. But with this method, there was really no protection of code. Or, developers would use Visual C++ or Visual Basic to create ActiveX controls, hoping that client browsers would trust them enough to allow them to run. A third way was to use Java applets, forcing every client machine to have the virtual machine installed and to deal with the slow performance for applets. The .NET Framework and ASP.NET changes all this. With user controls, a developer can turn an ASP.NET page snippet into a reusable, application-specific, snap-in. User controls are defined in .ascx files. But just like an .aspx, user controls may also reference a code-behind file. The best way to understand user controls is to create one. First, create a web forms project in Visual Studio .NET by choosing File | New | Project... from the menu, and then choosing ASP.NET Web Application from the Visual C# Projects tab. To be consistent with the book, name the project “WebFormsSample.” Once the standard files have been generated, go ahead and add three (3) web forms by choosing Project | Add Web Form... from the menu. Name each page as follows: Welcome, AboutUs, and ContactUs.
187
C H A P T E R
8
Your Solution Explorer should now look similar to the following:
Our goal now is to add a header to each page that shows the title and an image relating to the page being viewed. To achieve this, let’s create a user control. On the Project menu, choose Add Web User Control... and give it the name Header, as shown:
188
I N T R O D U C T I O N
T O
W E B
F O R M S
Click the Open button to generate the Header.ascx and Header.ascx.cs files. Header.ascx is the file that contains the HTML code, while Header.ascx.cs is the code-behind file. If you open the Header.ascx file in HTML view, you will see the following generated code:
As we saw earlier in the diagram on the architecture of ASP.NET, the code in the Header.ascx file will be parsed into C# code and compiled at runtime, and the compiled user control class will be derived from the Header class found in the Header.ascx.cs file. Because of this, every variable that we declare as public or protected in the Header.ascx.cs file will be accessible in the Header.ascx file, via script. The Codebehind attribute indicates the source file that contains the base class. This attribute, however, is not required for Release versions of Visual Studio .NET. It existed in beta versions so that Visual Studio .NET could serialize code after adding web controls in design mode. Now, Visual Studio .NET uses a naming convention for associating source files. It simply appends the source file extension, for example, .cs, to the original file name. Add the following html code to the Header.ascx file, or use the designer:
189
C H A P T E R
8
If you now switch to design view, this is what you will see:
190
I N T R O D U C T I O N
T O
W E B
F O R M S
The font that you see used by the text MyCompany.com can be found in the directory for this sample. Let’s now edit the Header.cs file and add two properties. But first, if you did not use the designer to layout the controls above, you must add declarations to these controls in the codebehind file as shown: public abstract class Header : System.Web.UI.UserControl { protected System.Web.UI.WebControls.Label _pageTitle; protected System.Web.UI.WebControls.Label _companyLabel; protected System.Web.UI.WebControls.Image _pageImage;
The variable names for the controls should correspond to the IDs of the controls on the page. These declarations are necessary so that the controls can be invoked during post backs using the rich object-oriented model of .NET. Also note that the variables are declared protected so that they can be accessed by the page. If the variables are not declared at all, the CLR will declare the variables in the temporary code created during runtime compilation. Just in case you are curious, here is a section of the temporarily created code file for the Header control: private System.Web.UI.Control __BuildControl_companyLabel() { System.Web.UI.WebControls.Label __ctrl; #line 4 "http://localhost/WebFormsSample/Header.ascx" __ctrl = new System.Web.UI.WebControls.Label(); #line default this._companyLabel = __ctrl; #line 4 "http://localhost/WebFormsSample/Header.ascx" __ctrl.ID = "_companyLabel"; #line default #line 4 "http://localhost/WebFormsSample/Header.ascx" __ctrl.Width = System.Web.UI.WebControls.Unit.Parse("203px", System.Globalization.CultureInfo.InvariantCulture); #line default
191
C H A P T E R
8
#line 4 "http://localhost/WebFormsSample/Header.ascx" __ctrl.Font.Bold = true; #line default #line 4 "http://localhost/WebFormsSample/Header.ascx" __ctrl.Font.Names = new string[] { "Crackhouse"}; #line default #line 4 "http://localhost/WebFormsSample/Header.ascx" __ctrl.Font.Size = System.Web.UI.WebControls.FontUnit.Parse("XX-Large", System.Globalization.CultureInfo.InvariantCulture); #line default System.Web.UI.IParserAccessor __parser = ((System.Web.UI.IParserAccessor)(__ctrl)); #line 4 "http://localhost/WebFormsSample/Header.ascx" __parser.AddParsedSubObject(new System.Web.UI.LiteralControl("MyCompany.com"));
}
#line default return __ctrl;
That’s enough of that. Here are the definitions of the properties to add: // // This property is used to modify the Title label. // [System.ComponentModel.Browsable(true)] public String Title { get { return _pageTitle.Text; } set { _pageTitle.Text = value; _pageImage.AlternateText = value;
192
I N T R O D U C T I O N
}
T O
W E B
F O R M S
}
// // This property is used to modify the Image web control. // [System.ComponentModel.Browsable(true)] public String ImageUrl { get { return _pageImage.ImageUrl; } set { _pageImage.ImageUrl = value; } }
As far as we’re concerned, this is all that we need for the Header user control. We will now add this user control to each of the three web forms pages created earlier. We will only demonstrate how to do this with the Home page. Before we do this, it is important to learn a little about registering user controls. User controls and custom controls are registered with the Register directive in an .aspx file. Here is the syntax for registering user controls:
TagPrefix is just the shorthand of a namespace. It is used to distinguish user controls with the same tag names (element names). TagName and Src work together to associate tags with user control files. In the snippet, all tags named “Header” will be associated with the user control in the Header.ascx file. When dragging user controls from Solution Explorer to web forms, the Register directive is automatically generated with default values. For each user control added to a form, the TagPrefix will have values of uc1, uc2, uc3, and so on. The TagName will be given the same name as the user control itself. Here is the syntax for registering custom web controls:
193
C H A P T E R
8
The TagPrefix has the same meaning as before. But this time, we do not have the TagName and Src attributes; instead, we have Namespace and Assembly attributes. The reason is because with web controls, we will normally have an assembly containing a suite of controls. We will discuss this further, after we finish demonstrating this part of the sample. Now, just drag the user control from Solution Explorer to the Home web form, as shown:
If you now view the Home.aspx file in the HTML editor, this is what you will see: Home <meta name="GENERATOR" Content="Microsoft Visual Studio 7.0"> <meta name="CODE_LANGUAGE" Content="C#"> <meta name="vs_defaultClientScript" content="JavaScript"> <meta name="vs_targetSchema" content="http://schemas.microsoft.com/intellisense/ie5">
194
I N T R O D U C T I O N
T O
W E B
F O R M S
You can change the uc1 tag prefix to whatever name suits you. We will now set the two properties that we added earlier to suitable values. To do this, we can either use the Home.aspx file or the Home.aspx.cs file. For demonstration purposes, we will set the properties in the Home.aspx file, as follows:
Follow the same technique for the AboutUs and ContactUs pages. You can then build the application and run it, but you will have to first set the Start Page property of the Debugging tab in the project’s properties. A better way to view the output is to choose View In Browser from the File menu for each page. The three pages viewed in the browser are shown below:
195
C H A P T E R
196
8
I N T R O D U C T I O N
T O
W E B
F O R M S
As you can see, user controls are just HTML snippets that can be reused on multiple web forms in a project. It’s easy to take existing HTML or ASP.NET pages and convert them to user controls. But user controls still have their downfalls. For example, they don’t support validation by any classes in the .NET Framework. We will now develop a custom web control that will be used for submitting information for the ContactUs page. The idea behind web controls is that they can be distributed and reused in other projects without exposing the source code. Let’s go ahead and create a new project in the same solution, named CustomWebControls. On the File menu, choose Add Project | New Project... and choose Web Control Library, as shown:
Choosing OK will generate the project files, one of which is a sample control file named WebCustomControl1. Rename this class to PhoneNumberControl and the associated code file to PhoneNumberControl.cs. At the beginning of the class definition for the control, you will notice two attributes, ToolboxData and DefaultProperty. The DefaultProperty attribute is used by Visual Studio .NET during design time for making sure a certain property receives focus when the designer is invoked. The DefaultProperty, by default, references the Text property. We will not be using the Text property, so remove the DefaultProperty attribute, as well as the Text definition and member variable.
197
C H A P T E R
8
The ToolboxData attribute is used by Visual Studio .NET to generate HTML markup when the control is dragged and dropped from the toolbox. Because we changed the name of the control, we must change the ToolboxData attribute as follows: [ToolboxData(" ")] public class PhoneNumberControl : WebControl
The {0} is a string format specification that will be used by the ToolboxDataAttribute class to replace that format holder with a tag prefix. The default tag prefixes are generated, with the first custom control being cc1, the second cc2, and so on. With custom controls, we can assign and associate a default tag prefix to a namespace. To do this, add the following to the AssemblyInfo.cs file of the CustomWebControls project: // // Default Tag Prefixes // [assembly: System.Web.UI.TagPrefix("CustomWebControls", "samples")]
Now, dragging the control from the toolbox to any web forms page will generate code similar to the following: <samples:PhoneNumberControl runat=”server”/>
The purpose of the PhoneNumberControl is to limit the way phone numbers can be entered into a text box. Therefore, it makes sense to embed three controls derived from System.Web.UI.WebControls.TextBox and add a Number property to represent the telephone number. Let’s do just that, as shown: private private private private
TextBox _areaCodeTextBox = new TextBox(); TextBox _prefixTextBox = new TextBox(); TextBox _numberTextBox = new TextBox(); LiteralControl _seperator = new LiteralControl(“-“);
We also need to set certain properties of the text boxes, so that each one truly behaves like part of a phone number control. Override OnInit and add the following code: protected override void OnInit(EventArgs e)
198
I N T R O D U C T I O N
{
T O
W E B
F O R M S
base.OnInit(e); _areaCodeTextBox.MaxLength = 3; _prefixTextBox.MaxLength = 3; _numberTextBox.MaxLength = 4;
}
_areaCodeTextBox.Width = new Unit("30px"); _prefixTextBox.Width = new Unit("30px"); _numberTextBox.Width = new Unit("40px");
Any initialization for a server control must be performed in the OnInit method. As you can see, we set the MaxLength property of the text boxes as well as the Width. Now, implement the Number property as shown: public long Number { get { string text = _areaCodeTextBox.Text + _prefixTextBox.Text + _numberTextBox.Text; return Int64.Parse(text); }
}
set { string text = value.ToString(); _areaCodeTextBox.Text = text.Substring(0, 3); _prefixTextBox.Text = text.Substring(3, 3); _numberTextBox.Text = text.Substring(6, 4); }
One last thing to do is to override the Render method. This method, which will be discussed in more detail in the next chapter, is used to generate HTML markup via the HtmlTextWriter object. Here is the overridden method: protected override void Render(HtmlTextWriter output) { _areaCodeTextBox.RenderControl(output); _separator.RenderControl(output);
199
C H A P T E R
}
8
_prefixTextBox.RenderControl(output); _separator.RenderControl(output); _numberTextBox.RenderControl(output);
In this method, we just rely on each embedded control to render itself. First, we let the area code text box render itself. We then render the separator, which is a literal control. A literal control is just plain text, implemented by System.Web.UI.LiteralControl. Next, we render the prefix text box, followed by another literal. We finally render the number text box. Now build the assembly and add the control to the toolbox by choosing Customize Toolbox from the Tools menu. Go to the .NET Framework Components tab and browse for the CustomWebControls.dll assembly. If you now scroll down, the PhoneNumberControl should already be selected. If not, select it, as shown:
Click OK and the control will be added to the toolbox, to whatever toolbox tab that is the current one. Notice that the control has some default icon associated with it. This can be changed with the ToolboxBitmapAttribute, which will be discussed in Chapter 13. Drag the control to the ContactUs form of the WebFormsSample project. Viewing the ContactUs page in the browser should yield an output similar to the following:
200
I N T R O D U C T I O N
T O
W E B
F O R M S
Summary This chapter introduced the server-based architecture by first providing a brief history. We touched on some common controls available with the .NET Framework. We then discussed how to author your own user controls. In the next chapter, we will look into the details involved with rendering a control. We will then move on to more advanced topics such as state management and templates. In conclusion, ASP.NET is a very powerful tool for building web forms and web applications, as well as authoring custom web controls. C# and Visual Studio .NET are valuable tools to help benefit from this new technology.
201
9
Chapter
Rendering Server Controls Overview Rendering is defined as the process of creating a visual representation of an object on a display or design surface. When talking about .NET Web Forms, this display surface is either a web browser at runtime, or typically the Visual Studio .NET designer at design time. The rendered output is in the form of markup, such as HTML, XML, or WML.
Runtime Rendering Rendering is done through an HtmlTextWriter object. The HtmlTextWriter class is part of the System.Web.UI namespace. It derives from TextWriter, so the standard Write and WriteLine methods can be used. Another class, Html32TextWriter is derived from HtmlTextWriter to render content to HTML 3.2 clients. Some of the methods of the HtmlTextWriter class are described below: RenderBeginTag: This method writes an opening tag to the output stream. The method takes either a string, or an HtmlTextWriterTag enumeration value. HtmlTextWriterTag values include a set of constants that represent standard HTML elements. Here is a couple of sample snippets using this method: writer.RenderBeginTag(HtmlTextWriterTag.Img); writer.RenderBeginTag(“img”);
RenderEndTag: This method should follow any previous calls to the RenderBeginTag method, such as:
C H A P T E R
9
writer.RenderBeginTag(HtmlTextWriterTag.Img); writer.RenderEndTag(); writer.RenderBeginTag(“hr”); writer.RenderEndTag();
AddAttribute: This method renders an attribute to the element that is currently being rendered via RenderBeginTag. The writer remembers any attributes added through AddAttribute, so that when RenderBeginTag is called, these attributes are also rendered. The temporary list containing the attributes will then be cleared. AddStyleAttribute Similar to AddAttribute, this method renders the child properties and child attributes of the style attribute. This method takes two parameters. The first parameter is either a string or an HtmlTextWriterStyle instance which specifies the style’s property or attribute name. The second parameter is a string that specifies the value. Like the HtmlTextWriterTag enumeration, the HtmlTextWriterStyle enumeration defines a set of values corresponding style properties and attributes. For example, the following code: writer.AddStyleAttribute(HtmlTextWriterStyle.Width, “100%”); writer.AddStyleAttribute(“height”, “100%”); writer.RenderBeginTag(HtmlTextWriterTag.Table); writer.RenderEndTag();
produces the following output:
WriteBeginTag: This method is similar to RenderBeginTag. However, it does not write the > symbol after writing the tag. Write methods allow more freedom for the developer, but the developer must take special care when using them: Here is some sample code that illustrates this method in use: writer.WriteBeginTag(“hr”); // output symbol after the tag. However, no attributes will be able to be written. WriteEndTag: This method must be called after WriteBeginTag, to render an end tag for the current element. This method lets you specify the tag name for the end tag. WriteAttribute: This method is similar to AddAttribute, but it must be called after WriteBeginTag. Write: This method allows the most freedom of all the Write methods. With this method, you must specify the exact HTML to be written to the output stream. WriteLine: This method is the same as Write, except that it appends the end-of-line character. It writes tabs to the output stream in the process. WriteLineNoTab This method does the same as WriteLine, except that it doesn’t write tabs to the output stream. The base control, System.Web.UI.Control, contains a public method, RenderControl, which is responsible for rendering content to an HtmlTextWriter object. This class also contains to virtual methods, Render and RenderChildren. The default implementation of Render calls RenderChildren. Render is responsible for rendering any content for the control, while RenderChildren is responsible for rendering child controls. Here are the definitions for the three methods: public virtual void RenderControl( HtmlTextWriter output ); protected virtual void Render( HtmlTextWriter output ); protected virtual void RenderChildren( HtmlTextWriter output );
We will now demonstrate the rendering process by implementing a web-based color picker control. To get started, open the WebFormsSample project that you created in the last chapter if you have been following along. Add a new WebCustomControl to the project, naming it ColorPicker in the process, as shown:
205
C H A P T E R
9
Click OK to add the file to the project. The ColorPicker control will consist of a window with 4 tabs. The first tab, Basic, will contain a collection of basic, administrator-defined colors. The second tab, System, will contain all system colors, as defined by the public static properties of System.Drawing.SystemColors. The third tab, Web, will contain all of the named colors that are defined by the System.Drawing.Color structure. The fourth and final tab, Custom, will contain sliders for creating custom colors. First things are first, so let’s go ahead and override the OnInit method and re-implement the already overridden Render method. First, remove the Text property, its data member, and erase everything in the Render method, as shown: protected override void OnInit(EventArgs e) { base.OnInit(e); } protected override void Render(HtmlTextWriter output) { }
We will now add a Color property and define a SelectedColorChanged event, as shown:
206
R E N D E R I N G
S E R V E R
C O N T R O L S
public event EventHandler SelectedColorChanged; private Color _selectedColor; public Color SelectedColor { get { return _color; } set { _color = value; } }
Let’s also add three member variables for the color collections: private ArrayList _basicColors = new ArrayList(); private ArrayList _systemColors = new ArrayList(); private ArrayList _webColors = new ArrayList();
Make sure you add a using System.Collections at the beginning of the source file, so that the above code will be recognized by Visual Studio .NET intellisense and the compiler. Initialize the width and height of the control in the constructor, as shown: public ColorPicker() { this.Width = Unit.Pixel(250); this.Height = Unit.Pixel(300); }
this.SelectedColor = Color.Transparent;
Now, inside the OnInit method, we will do something with these variables. Specifically, we will enumerate through the static properties of the Color structure and the System.Drawing.SystemColors class to fill the list. This is done using reflection, as shown below: protected override void OnInit(EventArgs e) {
207
C H A P T E R
9
base.OnInit(e); // Load the web colors foreach (PropertyInfo property in (typeof(Color)).GetProperties(BindingFlags.Public
|
BindingFlags.Static)) { _webColors.Add(property.Name); } // Load the system colors foreach (PropertyInfo property in (typeof(SystemColors)).GetProperties(BindingFlags.Public | BindingFlags.Static)) { _systemColors.Add(property.Name); } }
We are now ready to render part of the control. We won’t worry about the Basic tab and Custom tab as of yet. To render the control, we will render a table which will contain two rows. The first row will contain the tab headers. The second row will contain the view associated with each header. As each header is clicked, all views except the associated view will be hidden. We start by rendering the table’s attributes and begin tag, as shown: protected override void Render(HtmlTextWriter output) { output.AddAttribute(HtmlTextWriterAttribute.Width, this.Width.ToString()); output.AddAttribute(HtmlTextWriterAttribute.Height, this.Height.ToString()); output.AddAttribute(HtmlTextWriterAttribute.Cellpadding, "0"); output.AddAttribute(HtmlTextWriterAttribute.Cellspacing, "0"); output.AddAttribute(HtmlTextWriterAttribute.Border, "0"); output.AddAttribute(HtmlTextWriterAttribute.Bgcolor, "menu"); output.AddStyleAttribute("cursor", "default");
208
R E N D E R I N G
S E R V E R
C O N T R O L S
output.RenderBeginTag(HtmlTextWriterTag.Table);
We simply set the width and height of the table to the width and height of the control. We then set the background color to menu, to make it look more like a windows control. We must now create the tab headers by creating a single row with four columns, as shown: output.RenderBeginTag(HtmlTextWriterTag.Tr); // Basic Tab Header output.AddAttribute(HtmlTextWriterAttribute.Class, "TabHorizontal_Header"); output.RenderBeginTag(HtmlTextWriterTag.Td); output.Write("Basic"); output.RenderEndTag(); // System Tab Header output.AddAttribute(HtmlTextWriterAttribute.Class, "TabHorizontal_Header"); output.RenderBeginTag(HtmlTextWriterTag.Td); output.Write("System"); output.RenderEndTag(); // Web Tab Header output.AddAttribute(HtmlTextWriterAttribute.Class, "TabHorizontal_HeaderSelected"); output.RenderBeginTag(HtmlTextWriterTag.Td); output.Write("Web"); output.RenderEndTag(); // Custom Tab Header output.AddAttribute(HtmlTextWriterAttribute.Class, "TabHorizontal_Header"); output.RenderBeginTag(HtmlTextWriterTag.Td); output.Write("Custom"); output.RenderEndTag(); output.RenderEndTag();
We use the AddStyleAttribute method to add styles to the table’s columns. The border styles give each column a 3-D look. We initially set the Web tab as the current one, so notice that we don’t set a border bottom style for it. We will now render each tab view. We will use the display style to control which tab view is shown. Here is the code:
209
C H A P T E R
9
// Basic Tab View output.AddStyleAttribute("display", "none"); output.RenderBeginTag(HtmlTextWriterTag.Tr); output.AddAttribute(HtmlTextWriterAttribute.Colspan, "4"); output.AddAttribute(HtmlTextWriterAttribute.Class, "TabHorizontal_Content"); output.RenderBeginTag(HtmlTextWriterTag.Td); output.Write("TODO"); output.RenderEndTag(); output.RenderEndTag(); // System Tab View output.AddStyleAttribute("display", "none"); output.RenderBeginTag(HtmlTextWriterTag.Tr); output.AddAttribute(HtmlTextWriterAttribute.Colspan, "4"); output.AddAttribute(HtmlTextWriterAttribute.Class, "TabHorizontal_Content"); output.RenderBeginTag(HtmlTextWriterTag.Td); output.AddAttribute(HtmlTextWriterAttribute.Size, "10"); output.RenderBeginTag(HtmlTextWriterTag.Select); foreach (String colorName in _systemColors) { output.RenderBeginTag(HtmlTextWriterTag.Option); output.Write(colorName); output.RenderEndTag(); } output.RenderEndTag(); output.RenderEndTag(); output.RenderEndTag(); // Web Tab View output.AddStyleAttribute("display", "block"); output.RenderBeginTag(HtmlTextWriterTag.Tr); output.AddAttribute(HtmlTextWriterAttribute.Colspan, "4"); output.AddAttribute(HtmlTextWriterAttribute.Class, "TabHorizontal_Content"); output.RenderBeginTag(HtmlTextWriterTag.Td); output.AddAttribute(HtmlTextWriterAttribute.Size, "10"); output.RenderBeginTag(HtmlTextWriterTag.Select); foreach (String colorName in _webColors) { output.RenderBeginTag(HtmlTextWriterTag.Option); output.Write(colorName); output.RenderEndTag(); } output.RenderEndTag(); output.RenderEndTag();
210
R E N D E R I N G
S E R V E R
C O N T R O L S
output.RenderEndTag(); // Custom Tab View output.AddStyleAttribute("display", "none"); output.RenderBeginTag(HtmlTextWriterTag.Tr); output.AddAttribute(HtmlTextWriterAttribute.Colspan, "4"); output.AddAttribute(HtmlTextWriterAttribute.Class, "TabHorizontal_Content"); output.RenderBeginTag(HtmlTextWriterTag.Td); output.Write("TODO"); output.RenderEndTag(); output.RenderEndTag();
We must now add the table’s closing tag with the following code:
}
output.RenderEndTag();
We can now compile and run this code to see the rendered control. The control will be rendered the same in the designer and the browser, but this will be changed later on in this chapter when we discuss design-time rendering. You must note, however, that clicking the tab headers won’t actually do anything, because we haven’t rendered any client-side script. First, create or open a web form. Click Tools | Customize Toolbox... and add the control to the toolbox. Then drag it to the web form. The control should render similar to the following:
211
C H A P T E R
9
Ideally, we would like each color item in the color list box to have a background color that is the same. For instance, the AliceBlue item would have a background color of Alice Blue. In that case, we would have to probably render an inner table instead of a list box, with each table’s row having a background color equal to the associated color item.
Resource-Based Scripts and Style sheets In this section, we will illustrate how to use scripts and style sheets in our controls. This could easily be done by using the Render method and rendering the script and styles via the HtmlTextWriter methods. But we will take it a step further to demonstrate how to embed and extract script and stylesheet files from resources. This provides better readability for the developer. Also, if the developer needed to unit test the script, this could be easily done using a script file. In this section we will also implement the basic tab and custom tab for the control. Since the basic colors can be defined and changed outside the application, it makes sense to add them to an XML file. Add a subfolder, Resources, to the project. Then add a new XML file named BasicColors.xml, and set its Build Action to Embedded Resources, as shown:
212
R E N D E R I N G
S E R V E R
C O N T R O L S
Edit this XML file and add a number of elements similar to the following:
213
C H A P T E R
9
Actually, you can get away by adding about 16 of the above Color elements. Now, in code, we will enumerate through this document and construct the basic colors collection. When making a change to a file stored in the resources, you must rebuild the project in order for the changes to be embedded into the assembly. First, declare the following member variable for converting colors to and from strings: private static ColorConverter _colorConverter = new ColorConverter();
Now, add the following to the OnInit override: // Load the basic colors
214
R E N D E R I N G
S E R V E R
C O N T R O L S
Assembly thisAssembly = Assembly.GetExecutingAssembly(); String resourceName = String.Format("{0}.Resources.{1}", this.GetType().Namespace, "BasicColors.xml"); Stream inputStream = thisAssembly.GetManifestResourceStream(resourceName); if (!File.Exists(Page.Server.MapPath("BasicColors.xml"))) { Stream outputFile = new FileStream(Page.Server.MapPath("BasicColors.xml"), FileMode.Create, FileAccess.Write); StreamReader reader = new StreamReader(inputStream); StreamWriter writer = new StreamWriter(outputFile); writer.Write(reader.ReadToEnd()); reader.Close(); writer.Close(); outputFile.Close(); inputStream.Close(); inputStream = File.Open(Page.Server.MapPath("BasicColors.xml"), FileMode.Open); } XPathDocument doc = new XPathDocument(inputStream); XPathNavigator navigator = doc.CreateNavigator(); XPathNodeIterator iterColor = navigator.Select("//Color"); while (iterColor.MoveNext()) { XPathNavigator colorNavigator = iterColor.Current; string colorValue = colorNavigator.GetAttribute("Value", ""); Color color = (Color) _colorConverter.ConvertFromString(colorValue); _basicColors.Add(color); } inputStream.Close();
Also, note that you must add using directives for System.IO, System.Xml and System.Xml.XPath; In the code above, we first get a default XML file containing the basic colors from the resources, as shown here: Assembly thisAssembly = Assembly.GetExecutingAssembly(); String resourceName = String.Format("{0}.Resources.{1}", this.GetType().Namespace, "BasicColors.xml");
215
C H A P T E R
9
Stream inputStream = thisAssembly.GetManifestResourceStream(resourceName);
We then check to see if the XML file has been extracted on the client. If not, we extract the file, as shown: if (!File.Exists(Page.Server.MapPath("BasicColors.xml"))) { Stream outputFile = new FileStream(Page.Server.MapPath("BasicColors.xml"), FileMode.Create, FileAccess.Write); StreamReader reader = new StreamReader(inputStream); StreamWriter writer = new StreamWriter(outputFile); writer.Write(reader.ReadToEnd()); reader.Close(); writer.Close(); outputFile.Close(); inputStream.Close();
Finally, we load the extracted file and iterate through all of the basic colors, filling an ArrayList object in the process, as shown here: inputStream = File.Open(Page.Server.MapPath("BasicColors.xml"), FileMode.Open); } XPathDocument doc = new XPathDocument(inputStream); XPathNavigator navigator = doc.CreateNavigator(); XPathNodeIterator iterColor = navigator.Select("//Color"); while (iterColor.MoveNext()) { XPathNavigator colorNavigator = iterColor.Current; string colorValue = colorNavigator.GetAttribute("Value", ""); Color color = (Color) _colorConverter.ConvertFromString(colorValue); _basicColors.Add(color); } inputStream.Close();
We will now render the Basic tab, which will display the basic colors loaded from the resources. The Basic tab will contain boxes, or panels. Each panel will be painted a color from the basic colors list. At rendering time, we will determine the number of color panels per row.
216
R E N D E R I N G
S E R V E R
C O N T R O L S
Any number of colors may be added to the basic colors list, so the Basic tab will display scroll bars as appropriate. First, let’s modify the Basic tab definition so that scroll bars can be drawn: // Basic Tab View output.AddStyleAttribute("display", "none"); output.RenderBeginTag(HtmlTextWriterTag.Tr); output.AddAttribute(HtmlTextWriterAttribute.Colspan, "4"); output.AddAttribute(HtmlTextWriterAttribute.Class, "TabHorizontal_Content"); output.RenderBeginTag(HtmlTextWriterTag.Td); output.AddStyleAttribute("overflow", "auto"); output.AddStyleAttribute(HtmlTextWriterStyle.Width, "100%"); output.AddStyleAttribute(HtmlTextWriterStyle.Height, "100%"); output.RenderBeginTag(HtmlTextWriterTag.Div); output.Write(“TODO”); output.RenderEndTag(); output.RenderEndTag(); output.RenderEndTag();
In the code above, we inserted a DIV element inside the TD element. A DIV element is simply a container. The DIV element has three styles: width, height, and overflow. The width and height properties are set to 100% so that the DIV fills the view. The overflow property is used to specify how scroll bars should be displayed when any of the content of the DIV is not viewable. We set this property value to auto, so that the content will be clipped and the scroll bars displayed as needed. Other values include scroll, which clips the content and always shows the scroll bars; visible, which does not clip the content; and hidden, which does not show the content that exceeds the width and height of the container. We must now add the logic for rendering the individual color panels. Each color panel will be 20 pixels wide and high. There will be a spacing of 4 pixels between each panel, as well as 4 pixel margins. The number of color panels per row will be calculated using the current width of the control. Here is the modified code: output.RenderBeginTag(HtmlTextWriterTag.Div); const int colorPanelSize = 20; // given in pixels const int colorPanelSpacing = 4; // given in pixels int basicColorsPerRow = (short)(this.Width.Value / (colorPanelSize + colorPanelSpacing)); int x = colorPanelSpacing; int y = colorPanelSpacing; for (short colorCount = 0; colorCount < _basicColors.Count; colorCount++)
217
C H A P T E R
{
9
if (colorCount != 0) { if (colorCount % basicColorsPerRow == 0) { x = colorPanelSpacing; y += colorPanelSpacing + colorPanelSize; } else { x += colorPanelSpacing + colorPanelSize; } } Color color = (Color) _basicColors[colorCount]; Panel panel = new Panel(); panel.TabIndex = colorCount; panel.BorderStyle = BorderStyle.Outset; panel.BorderWidth = Unit.Pixel(2); panel.ToolTip = color.Name; panel.BackColor = color; panel.Style.Add("position", "absolute"); panel.Style.Add("left", Unit.Pixel(x).ToString()); panel.Style.Add("top", Unit.Pixel(y).ToString()); panel.Width = Unit.Pixel(colorPanelSize); panel.Height = Unit.Pixel(colorPanelSize); panel.RenderControl(output);
} output.RenderEndTag();
We use absolute positioning so that each panel will be positioned relative to its parent. If we had used relative positioning, each panel would be positioned relative to the previous panel, or in case of the first panel, the DIV. We also add a tool tip that displays the panel’s color name when the mouse moves over a panel. We can test this code by letting the Basic tab be the start tab. To do so, change the display style of the Basic tab view to block; change the display style of all other tab views to none. Also change the class attribute of the Basic tab header to TabHorizontal_HeaderSelected; change the class attribute of all other tab headers to TabHorizontal_Header. Compile the code and open a browser that uses the control. The rendered output should be similar to the following.
218
R E N D E R I N G
S E R V E R
C O N T R O L S
Let’s now add some styles and scripts to the resources. Create two new subfolders, one called StyleSheets and one called Scripts, beneath the Resources subfolder. Add a new JScript file to the Scripts subfolder named ColorPicker.htc. Add a new Cascading Style Sheet file to the StyleSheets folder named ColorPicker.css. Set the Build Action for both of these files to Embedded Resource. The Resources folder of Solution Explorer should now look similar to the following:
Now, let’s add all of the styles that were previously rendered in code to the ColorPicker.css file. After adding all styles, the file should look like this: .TabHorizontal_Header { behavior:url(ColorPicker.htc); border-left:thin outset white; border-right:thin outset white; border-top:thin outset white; border-bottom:thin inset window; padding-left:5px; padding-left:0px; padding-left:0px; padding-left:0px;
219
C H A P T E R
}
9
margin: 0px; height: 20px; width: 25%; font-size:smaller; font-weight:400; font-family:Menu, Verdana, Arial; color:menutext; background-color:eeeeee;
.TabHorizontal_HeaderSelected { behavior:url(ColorPicker.htc); border-left:thin outset white; border-right:thin outset white; border-top:thin outset white; padding-left:5px; padding-right:0px; padding-left:0px; padding-bottom:0px; margin:0px; height:20px; width:25%; font-size:smaller; font-weight:bold; font-family:Menu, Verdana, Arial; color:menutext; background-color:menu; } .TabHorizontal_Content { border-top:medium-none; border-bottom:thin outset white; border-left:thin outset white; border-right:thin outset white; padding:5px; margin:0px; height:100%; width:100%; background-color:menu; cursor:auto; }
Here is the ColorPicker.htc file:
220
R E N D E R I N G
S E R V E R
C O N T R O L S
<script> function TabHeader_OnClick() { var nTabHit = 0; var tdTabHeaderHit = event.srcElement; var trHeader = tdTabHeaderHit.parentNode; var tdTabs = trHeader.childNodes; for (var nTab = 0; nTab < tdTabs.length; nTab = nTab + 1) { var tdTabHeader = tdTabs.item(nTab); tdTabHeader.className = "TabHorizontal_Header"; if (tdTabHeader == tdTabHeaderHit) { nTabHit = nTab; document.cookie = trHeader.parentNode.parentNode.id + "_SelectedIndex=" + nTab; } } tdTabHeaderHit.className = "TabHorizontal_HeaderSelected"; var tBody = trHeader.parentNode; var trTabViews = tBody.childNodes; if ( trTabViews.length