Sei sulla pagina 1di 480

BLUEVISION PUBLISHING

James Henry

NNEETT CCuussttoomm CCoonnttrroollss && DDeessiiggnneerrss

uussiinngg CC##

DDeevveellooppiinngg

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.

organization, or company is intended or should be inferred. Published by BlueVision, LLC, 3395 English Oaks

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

Project Administrator

James Henry

James Henry

Technical Reviewers

Illustrations

James Henry

James Henry

Tamala Matthews

Cover

Technical Editors

Alfred Griffin

James Henry

James Henry

Kerri Betts

Proof Readers

Kerri Betts

James Henry

Tamala Matthews

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 .

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.

. 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

Table of Contents

Chapter 1:Introduction

1

Overview Audience Requirements What This Book Will Cover History of Control Reusability Evolution of .NET Web Forms versus Windows Forms Inside Visual Studio .NET

1

1

2

2

4

4

5

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

39

System.Drawing.ColorConverter 39

System.ComponentModel.TypeListConverter

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

GetPaintValueSupported method

74

PaintValue method

74

Activity Diagram (Editing)

76

Activity Diagram (Painting)

80

Implementing a Simple UITypeEditor

81

Example: ColorEditor

Implementing a CollectionEditor

Example: ToolbarItemCollectionEditor

Summary

82

85

86

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

104

Controls Collection

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

Typed IList 127

127

127

IListSource 127

ITypedList

128

IList and IComponent

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

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

Summary

270

279

Chapter 12:Introduction to Designers

281

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

Persistence

334

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

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

Introduction

Overview

Chapter

1

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

CHAPTER

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 CD- ROM 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.

INTRODUCTION

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.

CHAPTER

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

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 re- implement, they typically have an end result of high quality. On the other hand, an API is

.NET.

.NET has actually

INTRODUCTION

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

CHAPTER

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:

introduced to an HTML start page, similar to the following: My Profile The start page displays

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,

INTRODUCTION

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:

you to search for samples on your machine, shown here: What’s New This tab searches the

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.

CHAPTER

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:

available services that match a certain category, as shown: Web Hosting The Web Hosting tab lists

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.

INTRODUCTION

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:

as read the event log and configure services. It’s like working on what you love without

CHAPTER

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:

INTRODUCTION

INTRODUCTION Solution Explorer With VS.NET, all projects are now managed as solutions. Visual J++ developers will

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.

CHAPTER

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:

in the bottom pane of the view. This is illustrated below: Internal Online Help Now, what

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:

INTRODUCTION

INTRODUCTION Task List This pane lists the common tasks that are related to your project. Whenever

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

CHAPTER

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.

Chapter

2

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.

CHAPTER

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.

}

EVENTS

AND

EVENT

HANDLERS

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.

CHAPTER

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.

EVENTS

AND

EVENT

HANDLERS

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:

CHAPTER

2

Client Object Client Object Client Object Event Handler Method Event Handler Method Event Handler Method
Client Object
Client Object
Client Object
Event Handler Method
Event Handler Method
Event Handler Method
Subscribes
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

EVENTS

AND

EVENT

HANDLERS

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.

CHAPTER

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

{

EVENTS

AND

EVENT

HANDLERS

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.

CHAPTER

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.

Type Converters

Chapter

3

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

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.

needed.

.NET solves this problem with

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.

CHAPTER

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

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.

type.

.NET enables this conversion through its

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:

TYPE

CONVERTERS

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

{

}

CHAPTER

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

{

TYPE

CONVERTERS

}

}

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.

CHAPTER

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:

TYPE

CONVERTERS

TYPE CONVERTERS The picture above shows the standard values for the FormBorderStyle enumeration type. For enumerations,

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.

CHAPTER

3

CHAPTER 3 In the picture above, the set of standard values include all available cultures supported

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.”

TYPE

CONVERTERS

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.

CHAPTER

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”.

TYPE

CONVERTERS

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.

CHAPTER

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.

TYPE

CONVERTERS

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:

CHAPTER

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

TYPE

CONVERTERS

{

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;

CHAPTER

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.

TYPE

CONVERTERS

If you compile and run the code, here is what you will see:

If you compile and run the code, here is what you will see: From the picture

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:

CHAPTER

3

CHAPTER 3 Note that the BodyStyle property is still disabled because no TypeConverter is associated with

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;

TYPE

CONVERTERS

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:

CHAPTER

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 (V-

12)”);

}

}

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

TYPE

CONVERTERS

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.

CHAPTER

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);

}

TYPE

CONVERTERS

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:

now, here are some screen shots of what you should see: Clicking the drop down button

Clicking the drop down button next to the Cylinder property, will yield a screen-shot similar to the following:

CHAPTER

3

CHAPTER 3 Now that that’s out of the way, let’s get to the fun stuff. We

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))

TYPE

CONVERTERS

{

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;

CHAPTER

3