This content was uploaded by our users and we assume good faith they have the permission to share this book. If you own the copyright to this book and it is wrongfully on our website, we offer a simple DMCA procedure to remove your content from our site. Start by pressing the button below!
.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
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
Overview........................................................................................................ 111 Data Binding Concepts ................................................................................ 111 Data Providers...........................................................................................................111 Data Consumers .......................................................................................................113
Binding and BindingContext ....................................................................... 113 Binding.......................................................................................................................114 BindingContext ..........................................................................................................114
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
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
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
Persistence ................................................................................................... 336 Persistence in Windows Forms .................................................................................336 Persistence in Web Forms ........................................................................................336
Transaction Support .................................................................................... 347 Revisiting the Designer Host .....................................................................................348 Working with Transactions ........................................................................................349
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); } }
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 {
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;
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) { ...
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; } }
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”; } }
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();
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.
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.
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) { ...
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
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
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
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
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";
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());
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”),
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”);
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;
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”);
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) {
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);
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);
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;
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)
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.jpghttp://server/ad1.htmlAd1 goes here…80Ad1
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: <%@ Control Language="c#" AutoEventWireup="false" Codebehind="Header.ascx.cs" Inherits="WebFormsSample.Header" TargetSchema="http://schemas.microsoft.com/intellisense/ie5"%>
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
MyCompany.com
Page Title
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
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: <% Register TagPrefix=”samples” TagName=”Header” Src=”Header.ascx” %>
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:
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: <%@ Page language="c#" Codebehind="Home.aspx.cs" AutoEventWireup="false" Inherits="WebFormsSample.Home" %> <%@ Register TagPrefix="uc1" TagName="Header" Src="Header.ascx" %> 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("<{0}:PhoneNumberControl runat=server> {0}:PhoneNumberControl>")] 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)
_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);
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:
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
204
R E N D E R I N G
S E R V E R
C O N T R O L S
WriteFullBeginTag: This method writer the full begin tag for an element, including the > 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:
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");
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;
<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 <= 1) return; for (var nTabView = 1; nTabView < trTabViews.length; nTabView = nTabView + 1) { var trTabView = trTabViews.item(nTabView); if (nTabHit == nTabView - 1) { trTabView.style.display = "block"; } else { trTabView.style.display = "none"; } } }
Now, in order to use the style sheet and script, we must extract them from the resources to the server at runtime. Add the following method to your class:
221
C H A P T E R
9
private void ExtractServerResources(String resourceNamespace, params String[] resources) { foreach(string resource in resources) { String fullExtractedFileName = Page.Server.MapPath(resource); string cacheKey = this.GetType().Name + "_" + fullExtractedFileName; string fullResourceName = String.Format("{0}.{1}", resourceNamespace, resource); // Check to see if the resource has already been extracted. // If it has, make sure it is the most recent one by checking the assembly's date. // If the assembly's date has changed, it's possible that the resource has changed. bool alreadyExtracted = File.Exists(fullExtractedFileName); Assembly thisAssembly = Assembly.GetExecutingAssembly(); String assemblyPath = thisAssembly.CodeBase.Substring("file:///".Length); DateTime assemblyDateTime = File.GetLastWriteTime(assemblyPath); object cacheValue = HttpRuntime.Cache[cacheKey]; if(alreadyExtracted && cacheValue != null) { DateTime cachedDateTime = (DateTime)cacheValue; if (DateTime.Compare(cachedDateTime, assemblyDateTime) >= 0) { continue; } } Stream inputFile = thisAssembly.GetManifestResourceStream(fullResourceName); System.Diagnostics.Debug.Assert(inputFile != null, "inputFile is null", "The resource file is not embedded in the assembly at the specified path."); Stream outputFile = new FileStream(fullExtractedFileName, FileMode.Create, FileAccess.Write);
222
R E N D E R I N G
S E R V E R
C O N T R O L S
StreamReader reader = new StreamReader(inputFile); StreamWriter writer = new StreamWriter(outputFile); writer.Write(reader.ReadToEnd()); reader.Close(); writer.Close(); outputFile.Close(); inputFile.Close(); string[] dependencies = { assemblyPath }; HttpRuntime.Cache.Insert(cacheKey, assemblyDateTime, new CacheDependency(dependencies)); } }
The code above uses HTTP caching when extracting the resources to the server. In order to support caching, add using directives for System.Web and System.Web.Caching. The code should be self explanatory. But in simple terms, we load each resource and check to see if it has already been extracted. If not, we extract the resource. If a resource has been extracted, we get the date and time of the assembly and compare it to a cached version. If the assembly is newer than the cached version, we extract the resource again, assuming that the resource may have been updated. We must now call this method from OnInit, as shown: 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(); ExtractServerResources( "CustomWebControls.Resources.Scripts", _behaviorFile); ExtractServerResources(
We must also declare the two variables utilized above: private const String _behaviorFile = "ColorPicker.htc"; private const String _styleSheet = "ColorPicker.css";
If we compile the code and attempt to load it in a web forms designer, we will not get the results we expect. The reason is due to the extraction of the resources. The extraction process uses HTTP caching, which will not be available during design time. Therefore, somewhere in the implementation an exception will be thrown, and the control will never finish rendering in the designer. This brings us to our next section, design time rendering.
Design Time Rendering Visual Studio .NET first calls the GetDesignTimeHtml method of the ControlDesigner. The default implementation of that method simply calls the Render method of the control. If nothing is rendered, the GetEmptyDesignTimeHtml method of the ControlDesigner is invoked. The default implementation of GetEmptyDesignTimeHtml simply renders text with the control type and control ID. Both methods return a string that represents the HTML to be rendered in the designer. Because we can’t take full advantage of the ColorPicker control’s rendering due to HTTP caching constraints, we will apply a designer to the control, implement a designer, and override the GetDesignTimeHtml method of the designer. To associate a control with a designer, we use the System.ComponentModel.DesignerAttribute class. This class is similar to the TypeConverterAttribute and EditorAttribute classes, in terms of the available parameters and constructor overloads. Here is the syntax to associate the designer with our control: [ToolboxData("<{0}:ColorPicker runat=server>{0}:ColorPicker>"), Designer(typeof(ColorPickerDesigner))] public class ColorPicker : System.Web.UI.WebControls.WebControl
Now, add a new class named ColorPickerDesigner, and derive it from System.Web.UI.Design.ControlDesigner. Also, override the GetDesignTimeHtml method, adding the following code:
The ColorPicker will display as a tab control with 4 tabs.
"; strHtml += "
The first tab, Basic, contains a set of defined basic colors.
"; strHtml += "
Simply extract the BasicColors.xml file from the resources, or wait
"; strHtml += "
until the control is rendered on a form, in which the file will
"; strHtml += "
automatically be extracted. The second tab, System, contains all
"; strHtml += "
system colors defined on the machine. The third tab, Web, contains
"; strHtml += "
known colors that can be used on web pages. The fourth and final tab,
"; strHtml += "
Custom, provides controls so that a custom color can be selected.
"; strHtml += "
"; }
return strHtml;
If you now compile the project, and view it in a web forms designer, you will see something like this:
225
C H A P T E R
9
This is exactly what the .NET Framework controls do sometimes to avoid complex rendering in design mode.
Summary In conclusion, Visual Studio .NET and the .NET Framework provide the backbone for supporting rich control rendering at both runtime and design time. Runtime rendering provides an extra benefit by enabling scripting and control styling. These scripts and styles can be created in text editors and added to the resources, which can be extracted from the control when needed. This makes it easy for developers to debug JScript, XSLT and other scripts and style sheets that are supported. Web controls can render in different ways in the designer versus the browser, by associating the control with a designer through metadata. This metadata is used by Visual Studio .NET to aid in the design process. In this chapter, we simply rendered a custom web control. We did not raise any events or save the state of the control between post backs. In the next chapter, we will examine state management in detail.
226
10 Chapter
ASP.NET State Management Overview ASP.NET pages are stateless by default. No data is saved on the server between page requests automatically. ASP.NET pages are recreated each time a web page is posted to the server. For instance, a checkbox will not preserve its state if it is checked and then the page is posted back to the server. ASP.NET provides several mechanisms to overcome this drawback. The sections to follow will cover each mechanism in detail.
ASP.NET Intrinsic Objects The most common intrinsic objects provided by ASP.NET are the Session object and Application object. Intrinsic objects can be thought of as “built-in” objects. The Application object is represented by the HttpApplicationState class, while the Session object is represented by the HttpSessionState class. Both of these classes are part of the System.Web namespace. The objects can be accessed by properties of the HttpContext class. HttpContext provides a static property, Current, which returns the current instance of the object. The System.Web.UI.Page class exposes these two intrinsic objects through its Application and Session properties. These properties are actually implemented to retrieve the objects from the current HttpContext instance. Furthermore, the current HttpContext instance can be obtained from the HttpApplication class, from which the global.asax page is derived. Note that the HttpApplication class and the HttpApplicationState class are not the same. The global.asax page defines seven event handlers relating to application and session state. These handlers are briefly described below:
C H A P T E R
1 0
Session_Start: This handler is called by the ASP.NET framework when a session has been created. Session_End: This handler is called when the session has been abandoned or when it has timed out. Application _Start: The handler is called right before the first session is created. Application_End: This handler is called when the application has been terminated; right after the last session has ended. Application_Error: This handler is called when an unhandled exception has been thrown. Application_BeginRequest: This handler is called when ASP.NET first receives an HTTP request. This is the first handler called in the event process. Application_EndRequest This handler is called when ASP.NET responds to an HTTP request. This is the last handler called in the event process.
HttpApplicationState An ASP.NET application consists of all resources that can be requested in a given virtual directory. These resources include web pages, files, page handlers, modules, scripts, images, and subdirectories, among others, that reside in the virtual directory. The HttpApplicationState object is used to share global information across an application. This class is part of the System.Web namespace. This class implements a dictionary-based collection for storing keyvalue pairs. Some of the common properties and methods are described below: AllKeys: This property returns a string array of all keys stored in the application object. Contents: This property simply returns a reference to the current HttpApplicationState object. It is only provided for backward compatibility with earlier versions of ASP. StaticObjects: This property returns an HttpStaticObjectCollection that can be used to enumerate through all objects declared by an object tag with application scope in the global.asax file. An object tag similar to the following would be added to this collection:
230
A S P . N E T
S T A T E
M A N A G E M E N T
Item: This property is the indexer that returns the object mapped to either the specified key name or the specified numeric key index. GetKey: This method returns the key name found at the specified numeric index. Get: This method returns the object mapped to either the specified key name or the specified numeric key index. Set: This method updates the value of an object mapped to the specified key name. Lock: This method synchronizes access to the object by implementing a locking mechanism. Because the HttpApplicationState object is shared, you should always call this method before making changes to the object. After the changes have been made, you should call Unlock. Unlock: This method removes the lock that was previously placed by the Lock method.
Although the HttpApplicationState object may be appropriate for storing global data, there are a few warnings we must be aware of when utilizing the object. Memory allocated for objects stored in the HttpApplicationState object will not be freed until the object is removed or replaced, or the web server is restarted. You should therefore be cautious about what you store in the HttpApplicationState object. Also, objects stored in the Application state are not shared across web farms and web gardens. A web farm is an application that is hosted across multiple machines. A web garden is an application that is hosted across multiple processes on one machine.
HttpSessionState Similar to the HttpApplicationState class, the HttpSessionState class is a dictionary-based collection for sharing objects. The Session state is used to store data on the server for multiple browser requests by the same user. When I say user, I mean the browser, or whoever or whatever initiated the first HTTP request. The HttpSessionState class has some of the same properties and methods that are part of the HttpApplicationState class. Some of the different ones are described below:
231
C H A P T E R
1 0
CodePage: This property gets or sets the numeric identifier of the codepage for the current session. A codepage is a character set that can be mapped to byte values. IsCookieless: This property returns a value indicating whether the session is cookieenabled. If the session is stored in the URL, the return value is true. Otherwise, it is false. IsNewSession: This property returns true if the current session was created with the current HTTP request. It returns false if the session was created with a previous request. IsReadOnly: This property returns a value indicating whether the session object can be modified. LCID: This property gets or sets the numeric value of the locale. For example, the LCID for US English is 1033. Mode: This property returns one of the SessionStateMode enumeration values. This enumeration contains values for indicating how session state is stored, as in a SQL Server database. SessionID: This property returns a unique string identifier representing a session. Timeout: This property gets or sets a value to act as a timeout for a session. The timeout is given in minutes, and once it the time has elapsed, the current session is terminated. This can be useful for bank accounting applications. Abandon: This method is used to immediately terminate the current session.
Hidden Fields Hidden fields provide a way of maintaining state by hiding values on a page. These values are page-specific. The HTML Input tag is responsible for implementing hidden fields, as shown:
The tag above is represented in ASP.NET by the System.Web.UI.HtmlControls.HtmlInputHidden class. The Page class also has a method, RegisterHiddenField, which is responsible for dynamically creating a hidden field on the page
232
A S P . N E T
S T A T E
M A N A G E M E N T
and assigning it an initial value. The hidden field’s value can be accessed by implementing IPostBackDataHandler. This interface will be discussed later.
Advantages With hidden fields, the data is stored in the page so no server resources are required. Because a hidden field is an HTML tag, it is as simple to implement as any other HTML markup. Also, almost all browsers support hidden fields, since the HTML input tag is universal.
Disadvantages Since hidden fields are stored on the page, large amounts of data can cause the page to grow tremendously. This increase can degrade performance. Therefore, hidden fields should only be used to store simple values. And because hidden fields are designed to allow only one value per hidden field, a developer must write custom code to parse fields that contain delimited values. Security is also a factor. Because the data is stored in the page, it is possible for the source to be viewed, thereby viewing the data in the hidden field.
Query Strings Query strings are parameters that are appended to a page’s URL. Here is an example: http://www.mycompany.com/Products.aspx?ID=123456&Version=2
The question mark indicates the beginning of the query string. The rest of the query string contains key and value pairs separated by ampersands. Each key is associated with its value with the equal sign. Query strings help maintain state by sending data explicitly to pages. A common example, such as the snippet above, is sending a product id to a page for processing product specific information.
Advantages Using query strings is one of the simplest ways to maintain state throughout a web application. They do not use any server resources since they are contained in the HTTP Request object for the URL.
233
C H A P T E R
1 0
Disadvantages Some client browsers place a 255-character limit on query strings. And because query strings are URL based, there is really no secure way of passing them to URLs. Another disadvantage is that you can only access query string values during an HTTP GET method.
Cookies Before we introduce cookies, you must know that you can reportedly go to jail in Germany for using them. So, with that said, what is a cookie? A cookie is just a small piece of information that is stored either in a file on a browser’s client machine or in memory in the client’s session. Cookies should be used for storing temporary data and small amounts of data.
Advantages One of the big advantages of cookies is that they do not require server resources. Cookies are stored on the client. They can then be read by the server after a post. Cookie information is sent with the request to the server. Another advantage of using cookies is that they inherently support expiration. Client browsers can configure the expiration of cookies. Cookies can expire when the session ends. Or they can remain on the client forever, which could end of being a disadvantage if hacked by someone. The final advantage of using cookies is that they are lightweight. Because they are simple text files storing key and value pairs, they take up minimal disk space.
Disadvantages One of the biggest disadvantages of using cookies is the issue of customizable expiration. Since a browser can be configured to keep cookies for a long period of time, some web sites may possibly be able to read information that was saved by some other site. For instance, some online banking web sites use cookies to store account information, so that when you return to the site, you don’t have to re-enter that information. If a hacker knows the cookie’s key, he/she could retrieve the account information, provided the cookie data can be deciphered or it is not encrypted. Another disadvantage relating to customizable expiration is that a user can configure a browser not to use cookies at all. Web sites that require the use of cookies will degrade in functionality. The final disadvantage, which was also an advantage, is that cookies are designed to be lightweight. Because of this, they have an 8192-byte limit on their sizes.
234
A S P . N E T
S T A T E
M A N A G E M E N T
Usage in ASP.NET HTTP classes in .NET are available in two namespaces, System.Net and System.Web. An HTTP cookie can be represented either by System.Web.HttpCookie or System.Net.Cookie. A cookie in ASP.NET is represented by the System.Web.HttpCookie class. Classes in the System.Net namespace are normally used in non web-based applications that utilize HTTP, such as instant messengers and web browsers. Classes in the System.Web namespace will normally be used by ASP.NET applications. Before we look at an example using cookies, we will go over the properties of both the System.Net.Cookie class and System.Web.Cookie class. The properties of the System.Net.Cookie class are described below: Comment: This property gets or sets a comment that the server can add to the cookie. Any information may be included in the cookie’s comment, such as cookie usage. CommentUri: Similar to the Comment property, this property gets or sets a URI to be used as the comment that the server can add to the cookie. Discard: This property gets or sets a Boolean value indicating whether the client should destroy the cookie when the session ends. If the property is set to true, the cookie will not be saved on the client when the session ends. Domain: This property gets or sets the server(s) where the cookie is generated. This property must include the domain of the cookie. Expired: This property returns true if the cookie has expired; false otherwise. When the property returns true, the client application is responsible for destroying the cookie. Expires: This property gets or sets the date and time that the cookie will expire. Name: This property gets or sets the name of the cookie, which will be used to lookup the cookie’s value. Certain characters cannot be used in the string value. These characters include the equal sign, semicolon, comma, carriage return character, newline character, and the tab character. Also, the dollar sign cannot be used as the first character of the name. Path: This property gets or sets the relative paths on the server to which the cookie is associated. This value is a subset of the domain. Port: This property gets or sets a comma-delimited string of TCP ports to which the cookie can be sent. A null value indicates that there is no restriction, whereas an empty string indicates that the port used in the Response object should be used.
235
C H A P T E R
1 0
Secure: This property gets or sets a Boolean value indicating whether this cookie can only be sent with secure connections, or https:// requests. TimeStamp: This property gets the date and time when the cookie was issued to the client. Value: This property gets or sets the value of a cookie, with some restrictions. The value cannot contain the semicolon or comma characters. Also, the value cannot be null. Version: This property gets or sets a numeric version that the cookie conforms to. Instantiation of a non ASP.NET cookie is provided by four constructor overloads, which are described below: public Cookie();
The default constructor initializes the Name, Value, Path, and Domain fields to an empty string. Before using a Cookie instance, the Name property must be set.
public Cookie( string name, string value, );
This constructor initializes the Name and Value fields with the parameters passed in. The Domain and Path fields are initialized to an empty string.
public Cookie( string name, string value, string path );
This constructor initializes the Name, Value, and Path fields with the parameters passed in. The Domain field is initialized to an empty string.
This constructor initializes the Name, Value, Path, and Domain fields with the parameters passed in. The properties of the System.Web.HttpCookie class are described below: Domain: This property gets or sets the server(s) where the cookie is generated. This property must include the domain of the cookie. Expires: This property gets or sets the date and time that the cookie will expire. HasKeys: This property returns a value indicating whether the cookie has child keys and values. If the property returns true, you may enumerate through the Values property. Item: This property is the string indexer for the child keys and values. Name: This property gets or sets the name of the cookie, which will be used to lookup the cookie’s value. Path: This property gets or sets the relative paths on the server to which the cookie is associated. This value is a subset of the domain. Port: This property gets or sets a comma-delimited string of TCP ports to which the cookie can be sent. A null value indicates that there is no restriction, whereas an empty string indicates that the port used in the Response object should be used. Secure: This property gets or sets a Boolean value indicating whether this cookie can only be sent with secure connections, or https:// requests. Value: This property gets or sets the value of a cookie, with some restrictions. The value cannot contain the semicolon or comma characters. Also, the value cannot be null.
237
C H A P T E R
1 0
Values: This property returns a NameValueCollection instance used for navigating through the key and value pairs. Instantiation of an ASP.NET cookie is provided by two constructor overloads, which are described below: public HttpCookie( string name );
This constructor initializes the Name field with the parameter passed in.
public HttpCookie( string name, string value, );
This constructor initializes the Name and Value fields with the parameters passed in. A cookie container manages a collection of cookies. A cookie container is represented by the System.Net.CookieContainer class. It provides methods to add and retrieve cookies using the System.Net.CookieCollection class. A cookie container can be accessed via the Cookies property of the System.Net.HttpWebRequest class. This may be a little confusing, because the System.Web.HttpRequest also has a Cookies property which returns a CookieCollection instance. But this CookieCollection class is part of the System.Web namespace. It also includes methods to add and retrieve cookies, as well as methods to remove cookies. In general, the System.Web.HttpRequest and System.Web.HttpResponse classes represent the intrinsic ASP equivalents of the Request object and Response object, respectively. Here is a sample snippet that sets and retrieves the selected index of a tab control. System.Web.HttpCookie cookie = new System.Web.HttpCookie(“SelectedIndex”); cookie.Value = “0”; Page.Response.Cookies.Add(cookie); ... int selectedIndex = 0;
Using the View State The ViewState property is provided by the System.Web.UI.Control class. It is an instance of System.Web.UI.StateBag, which is a state management mechanism. Items in the ViewState are stored as name and value string pairs. Items can be added, modified and removed in the ViewState. But make sure you do not attempt any modification during control rendering, as unexpected results may occur. Here is the syntax for saving and retrieving property values of a control in the ViewState: public class OvalButton : Button { public double Radius { get { return (double) this.ViewState[“Radius”]; } set { this.ViewState[“Radius”] = value; } } }
In the code above, the Radius property is persisted in the ViewState. The ViewState actually saves values in hidden fields on the page. Therefore, the ViewState can be used to store any data on the page, not just properties of controls, unless any property uses a ReferenceConverter. In that case, an exception is thrown. Here is another example: public class CollectionPropertyGrid : Control {
239
C H A P T E R
1 0
private DataSet _dsItems = new DataSet(); private SqlDataAdapter _da; ...
Advantages Similar to hidden fields, the data is stored on the page so no server resources are required. Using a ViewState is simple because of its object-oriented dictionary-based structure. Data stored in a ViewState is more secure than using hidden fields directly, because the data is compressed and encoded.
Disadvantages Just like hidden fields, large amounts of data stored in the ViewState will increase the size of the page and decrease performance. Only types that can be serialized or have an associated TypeConverter can be persisted using the ViewState. Serialized types are represented by the Serializable attribute. Client-side scripts cannot access the data in the ViewState.
Handling Post-back Scenarios Post backs in web forms are primarily performed as a result of an event that is raised. A post back is defined as an HTTP POST request that is transmitted to the server, usually resulting from clicking a submit button on a form. All input element fields are available for further
240
A S P . N E T
S T A T E
M A N A G E M E N T
processing on the server. The events are raised on the client and handled on the server, unlike traditional windows forms or client-side event processing. Some web server controls, such as the button, automatically sends a post request to the server to handle the click event. Not all events are handled this way. For instance, it wouldn’t make since for a web control to send a post in response to the mousemove event. That would be ridiculous.. We will now discuss the interfaces and methods related to post back processing. Afterwards, we will discuss the event model and lifecycle of a web forms control. First, let’s discuss a property and some of the methods of the Page that are related to post back processing.
Interfaces, Properties and Methods Related to Post Back IsPostBack Use this property to minimize server processing during page loading or page rendering. For instance, some server controls use this property to determine whether control properties should be initialized and read from a cache. The property returns true if the page is being rendered in response to an HTTP POST. Otherwise, it returns false. RegisterRequiresPostBack This method registers a control as one that requires post back processing. The Page object maintains these controls by adding their unique IDs to an array list. Here is the syntax: public void RegisterRequiresPostBack( Control control );
RegisterOnSubmitStatement This method allows a control to register a submit statement for post back processing. The registered submit statements are stored by the Page object in a HybridDictionary. Here is the syntax: public void RegisterOnSubmitStatement(
241
C H A P T E R
);
1 0
string key, string script
The first parameter is a unique key that identifies the script block. The second parameter is a string containing the script. RegisterRequiresRaiseEvent This method registers a control that requires an event to be raised when the control is processed on the page. Only one server control can be registered during a single page request. This is a virtual method. The default implementation simply stores and assigns the control to a member variable for later processing. Here is the syntax: public virtual void RegisterRequiresRaiseEvent( IPostBackEventHandler control );
RegisterHiddenField This method allows controls to register hidden fields on a form. The hidden fields are stored by the Page object in a HybridDictionary. Here is the syntax: public virtual void RegisterHiddenField( string hiddenFieldName, string hiddenFieldInitialValue );
RaisePostBackEvent This method informs the control that caused the post back that it should handle the post back event. Here is the syntax: protected virtual void RaisePostBackEvent( IPostBackEventHandler sourceControl, string eventArgument );
242
A S P . N E T
S T A T E
M A N A G E M E N T
The first parameter is the control that caused the post back. The control must implement IPostBackEventHandler, which will be described later. The second parameter is the event argument that was sent by the control. The method simply invokes the RaisePostBackEvent method of IPostBackEventHandler. GetPostBackEventReference This method returns a string that represents the client-side function for posting back to the server. This method normally returns a string similar to the following: __doPostBack(‘myControl1’, ‘myArgument’)
Here is the syntax for this method: public string GetPostBackEventReference( Control control, string argument );
The first parameter is used to get the control’s unique ID. The second parameter is any argument that is provided by the control itself. GetPostBackClientEvent As of this writing, this method just calls GetPostBackEventReference. I’m guessing that sometime in the future, it will be re-implemented to perform some slightly different task. GetPostBackClientHyperlink This method also calls GetPostBackEventReference. But it prefixes the returned string from that method with “javascript:”. This method will normally return a string similar to the following: javascript:__doPostBack(‘myControl1’, ‘myArgument’)
243
C H A P T E R
1 0
IPostBackDataHandler This interface is a contract which contains methods that controls must implement in order to automatically load post back data. The page framework only invokes the methods of this interface for a control if the control has been registered as one that requires post back. This registration is done by means of the RegisterRequiresPostBack method. Here are the methods of this interface: LoadPostData: Implement this method to retrieve data values that have been posted to the page. These values can then be used to repopulate properties if necessary. Here is the syntax of the method: bool LoadPostData( string postDataKey, NameValueCollection postCollection );
The first parameter is always the key identifier for the control, which is the control’s unique ID. The second parameter is a dictionary of all posted keys and values. The keys and values, in simple cases, should typically represent a control’s properties and values. Return true from this method if any data or properties of the control have changed. Return false otherwise. Note that, however, returning true will cause the RaisePostDataChangedEvent method to be called. RaisePostDataChangedEvent: Implement this method to notify the control or subscribers to the control’s events of any changes to the data that has taken place. The text box control uses this method to raise its TextChanged event. Here is the syntax for this method: void RaisePostDataChangedEvent();
IPostBackEventHandler This interface is a contract that defines a single method that server controls must implement in order to handle post back events automatically. Here is the only method that this interface supports:
244
A S P . N E T
S T A T E
M A N A G E M E N T
RaisePostBackEvent: This method is called by the page framework on a control whenever the page is posted back to the server. Here is the syntax: void RaisePostBackEvent( string eventArgument );
The only parameter is an event argument that was passed from the client. For controls that support multiple post back events, this parameter may contain a string that should be parsed to determine which type of event occurred. Controls that implement this method will typically call a virtual method that in turn raises an event.
Life Cycle of a Web Forms Control The life cycle of a web forms control can be described in a series of steps, which are described below. Processing of a web form usually involves round trips, which includes processing on the client and processing on the server. The web browser displays a form to the user, on which the user interacts with. The form is then posted back to the server for further processing, passing any necessary data values to the server. The form is then returned back to the browser, re-rendered, with any processed data. This sequence of events is called a round trip. The goal of writing any server control or web form is to reduce the number of round trips during a control’s life time. Initialization All properties and settings needed during the full life cycle of the request should be initialized here. This process is handled by overriding the OnInit method of the control. When overriding the OnInit method, be sure to call the base version so that the Init event is raised. Loading of View State The ViewState property is populated as a result of this process. This process is represented by the LoadViewState method of the control. The base version of OnInit automatically calls
245
C H A P T E R
1 0
LoadViewState before exiting. LoadViewState can be overridden to change the default implementation of storing a control’s state. Processing of Post Back Data All controls that participate in post back processing will update their properties using incoming form data. Controls will participate in post back processing by implementing the IPostBackDataHandler interface, and implementing its LoadPostData. The control must also call the RegisterRequiresPostBack method of its Page object. Loading In this stage, actions common to all requests are performed. All server controls in the control tree have been created and initialized before this stage. The ViewState has been restored and a control’s properties reflect the client-side data. The client-side data is the post back data, if the page has been posted to the server. This stage is represented by the Load event and the OnLoad virtual method of the Page class. Sending Post Back Change Notifications This process is represented by the RaisePostDataChanged method of the IPostBackDataHandler interface. State changes are processed in response to the previous and current post backs. Only controls that are registered as ones requiring post back will participate in this stage. Registration is done by means of the RegisterRequiresPostBack method of the Page class. Handling of Post Back Events This process is represented by the RaisePostBackEvent method of the IPostBackEventHandler. This process is used to handle events that were raised on the client that caused the post back.
246
A S P . N E T
S T A T E
M A N A G E M E N T
Prerendering This stage is represented by the PreRender event and OnPreRender virtual method. Last minute changes can be made to the control at this stage. This is the last chance that changes can be saved before being rendered. Saving of Vew State The ViewState property is saved during this stage. It is encoded and saved to a string as a hidden field. The SaveViewState virtual method is used to handle this. This method can be overridden in order to improve effiency. Rendering Rendering, which is represented by the Render method, is the process of generating markup to be delivered to the client. No more changes can be saved to the control at this point. Disposing Represented by the famous Dispose method, this process performs final cleanup for the control. You must override this method in order to release resources such as file handles. All resources must be released before the control is disposed of. Unloading This process is represented by the UnLoad event and OnUnLoad virtual method. The default implementation of the OnUnLoad method unloads any dangling event handlers found in the event handler list of the control. You can override this method to perform processing other than the default behavior, though it should never be necessary.
247
C H A P T E R
1 0
Completing the ColorPicker Control We will now use the knowledge we have just learned to complete the ColorPicker control. In the last chapter, we fully rendered the control, but we didn’t save any changes or raise any events. First, we will add code to remember the selected tab of the color picker control. This is necessary so that the tab won’t keep changing between post backs. So let’s define an enumeration that represents that tab types used by the control. Modify the inheritance list of the class and add the following code to the class definition: public class ColorPicker : System.Web.UI.WebControls.WebControl, IPostBackDataHandler private enum ColorTabType { Basic = 0, System = 1, Web = 2, Custom = 3 }; private const string _selectedTabHiddenFieldFormat = "{0}_SelectedTab"; public ColorPicker() { this.Width = Unit.Pixel(250); this.Height = Unit.Pixel(300);
public SelectedColor { get { return (Color) ViewState[“SelectedColor”]; } set { ViewState[“SelectedColor”] = value; } }
248
A S P . N E T
S T A T E
M A N A G E M E N T
private ColorTabType SelectedTab { get { return (ColorTabType) ViewState[“SelectedTab”]; } set { ViewState[“SelectedTab”] = value; } }
Because we are deriving from IPostBackDataHandler, we must implement its methods. Here is the code: bool IPostBackDataHandler.LoadPostData(string postDataKey, NameValueCollection postCollection) { string selectedTabName = postCollection[String.Format(_selectedTabHiddenFieldFormat, this.UniqueID)]; ColorTabType newTab = (ColorTabType) Enum.Parse(typeof(ColorTabType), selectedTabName); bool dataChanged = newTab != _selectedTab; _selectedTab = newTab; return dataChanged; } void IPostBackDataHandler.RaisePostDataChangedEvent() { // Notify the control of any changes to its state due to LoadPostData. }
In the code above, we implement the LoadPostData method to set the selected tab based on the value of a hidden field. If the selected tab has changed, we return true so that the RaisePostDataChangedEvent method is called. We don’t show the implementation of that method above, but you should get the basic idea. In order for this code to be meaningful, we must register a hidden field and register the control as one that requires post back, as shown: protected override void OnInit(EventArgs e) { base.OnInit(e);
249
C H A P T E R
1 0
Page.RegisterRequiresPostBack(this); }
...
protected override void Render(HtmlTextWriter output) { // Store a local variable to increase performance. // Accessing the property directly reads from the view state. ColorTabType selectedTab = this.SelectedTab; Page.RegisterHiddenField( String.Format(_selectedTabHiddenFieldFormat, this.UniqueID), selectedTab.ToString()); }
...
Let’s now modify the definitions of system colors collection and web colors collection so that they behave as sets, instead of ordered lists, as shown: private SortedList _systemColors = new SortedList(); private SortedList _webColors = new SortedList();
You should automatically see the reason for this from the code below that we have added after overriding the OnPreRender method. Here is the new code: protected override void OnPreRender(HtmlTextWriter output) { // Determine the selected tab based on the selected color. if (_webColors.ContainsKey(this.SelectedColor.Name)) { _selectedTab = ColorTabType.Web; } else if (_systemColors.ContainsKey(this.SelectedColor.Name)) { _selectedTab = ColorTabType.System; } else if (this.SelectedColor.Name == "0") { _selectedTab = ColorTabType.Basic; } else {
250
A S P . N E T
}
}
S T A T E
M A N A G E M E N T
_selectedTab = ColorTabType.Custom;
The code above will actually override the behavior that we added to the LoadPostData method of the IPostBackDataHandler, because this code sets the selected tab based on the selected color, whereas the previous code simply remembered the previously selected tab. To test the different behaviors, simply comment out the implantation for each method. The ColorPicker class found in the sample will not use the behavior that you see here as part of the IPostBackDataHandler interface. We only added it here to demonstrate its use. We will now modify the rendering behavior of the control so that it displays the tabs based on the SelectedTab property. Here are the modifications to the Render method: // Basic Tab Header output.AddAttribute(HtmlTextWriterAttribute.Class, selectedTab == ColorTabType.Basic ? "TabHorizontal_HeaderSelected" : "TabHorizontal_Header"); ... // System Tab Header output.AddAttribute(HtmlTextWriterAttribute.Class, selectedTab == ColorTabType.System ? "TabHorizontal_HeaderSelected" : "TabHorizontal_Header"); ... // Web Tab Header output.AddAttribute(HtmlTextWriterAttribute.Class, selectedTab == ColorTabType.Web ? "TabHorizontal_HeaderSelected" : "TabHorizontal_Header"); ... // Custom Tab Header output.AddAttribute(HtmlTextWriterAttribute.Class, selectedTab == ColorTabType.Custom ? "TabHorizontal_HeaderSelected" : "TabHorizontal_Header"); ... // Basic Tab View output.AddStyleAttribute("display", selectedTab == ColorTabType.Basic ? "block" : "none");
Let’s now handle events raised when clicking on the color panels of the Basic tab. Add the following line to the code that renders the color panels of the basic tab: Panel panel = new Panel(); panel.TabIndex = colorCount; panel.BorderStyle = BorderStyle.Outset; panel.BorderWidth = Unit.Pixel(2); panel.Attributes.Add("onclick", Page.GetPostBackClientHyperlink(this, color.Name)); 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);
As stated earlier, the GetPostBackClientHyperlink will generate a string that contains javascript code for posting back to the server. The event argument, in this case, is the color that is represented by and displayed on the panel. In order to take advantage of this post back, we must implement the single method of IPostBackEventHandler. First add that interface to the inheritance list of the control as shown:
252
A S P . N E T
S T A T E
M A N A G E M E N T
public class ColorPicker : System.Web.UI.WebControls.WebControl, IPostBackDataHandler, IPostBackEventHandler
Now implement the RaisePostBackEvent method as shown: void IPostBackEventHandler.RaisePostBackEvent(string eventArgument) { if (eventArgument != String.Empty) { this.SelectedColor = Color.FromName(eventArgument); } }
Because the event argument is the panel’s color, we use it to set the selected color when the page has been posted to the server. When the control is rendered, the new color will be used to select the appropriate tab, if the OnPreRender method contains the implementation from our discussion earlier. We will now raise the SelectedColorChanged event whenever the color has been changed. It is recommended to always add a virtual method that will raise the appropriate event, so we will do that. We will also add a Boolean indicator variable to determine when the event should be raised, so that setting the color in the constructor won’t raise the event unnecessarily. Here is the code: public class ColorPicker : System.Web.UI.WebControls.WebControl, IPostBackDataHandler, IPostBackEventHandler { ... private bool _shouldRaiseSelectedColorChangedEvent = true; public ColorPicker() { this.Width = Unit.Pixel(250); this.Height = Unit.Pixel(300);
So whenever a color panel is clicked, the page is posted back to the server passing the name of the color as an argument. The unique ID of the control is stored in the __EVENTTARGET hidden field on the page. The argument is stored in the __EVENTARGUMENT hidden field on the page. When the page framework processes the post back event, it finds the control that caused the post back by using the FindControl method of the Page and the unique ID. Once found, the framework checks to see if the control implements IPostBackEventHandler. If it does, the page calls its own RaisePostBackEvent virtual method, passing the casted IPostBackEventHandler reference and the event argument. As stated earlier, this method simply calls the RaisePostBackEvent method of the interface. So when deriving your own Page class, you could override this method to alter post back processing by the page, or prevent it altogether. In the SelectedColor property, we call a virtual OnSelectedColorChanged method. This method then raises the SelectedColorChanged event. This is the preferred way to raise events, so that inheritors can simply override the default handling of events. Finally, we must add more code for the Web and System tabs, so that clicking a color in the list would generate a post back. We are using a select element to render the lists for both of these pages. A select element can have multiple child option elements that represent the items
254
A S P . N E T
S T A T E
M A N A G E M E N T
in a list. Similar to coding the color panels, we must attach the onchange event of the select element to a JScript function that produces a post back. Therefore, we will use the GetPostBackEventReference. But there is one small thing. The GetPostBackEventReference takes an optional parameter which is the event argument. The event argument will definitely have to be the color of the item that is clicked, but we will not know this value on the server. It must somehow be calculated on the client and sent to the server. In order to handle this situation, we must create some client-side script that sends the post data to the server along with the event argument. So add another script file named ColorPicker_Select.htc to the Scripts folder, as shown:
Add the following script code to this file: <script language="jscript"> function SelectionList_OnChange() { // Determine the selected color. var select = event.srcElement; var colorPickerID = select.parentNode.parentNode.parentNode.parentNode.id; var colorName = select.options[select.selectedIndex].text; // Modify the PostBackEventReference property by adding the color name as the argument. // Perform the PostBack to the server. if (PostBackEventReference != null) { var argument = "'" + colorName + "'"; PostBackEventReference = PostBackEventReference.replace("''", argument); window.setTimeout(PostBackEventReference, 0, 'JavaScript'); } }
255
C H A P T E R
1 0
Don’t forget to set its Build Action to Embedded Resource. In order for the script to be extracted on the server at runtime, we must add it to our parameter list of the ExtractServerResources method. In case you haven’t realized, it would be a lot easier and less time consuming to create a custom attribute for extracting resources. But custom attributes are not within the scope of this book, so we will stick with the original plan. Add the following private member variable to the class definition: private const String _behaviorFile2 = "ColorPicker_Select.htc";
Now we must extract the new behavior file as shown: ExtractServerResources( "WebFormsSample.Resources.Scripts", _behaviorFile, _behaviorFile2);
Now add the following styles to the ColorPicker.css style sheet file: .SelectionList { behavior:url(ColorPicker_Select.htc); }
Finally, we must reference the above style sheet class on the select element in order for the event to be attached to, as shown: // System Tab View output.AddStyleAttribute("display", selectedTab == ColorTabType.System ? "block" : "none"); output.RenderBeginTag(HtmlTextWriterTag.Tr); output.AddAttribute(HtmlTextWriterAttribute.Colspan, "4"); output.AddAttribute(HtmlTextWriterAttribute.Class, "TabHorizontal_Content"); output.RenderBeginTag(HtmlTextWriterTag.Td); output.AddAttribute(HtmlTextWriterAttribute.Class, "SelectionList");
Notice that in the rendering code above, we use the one parameter method overload of the GetPostBackEventReference method. This is because we do not know the color of the item at this point. We simply pass this method as a string to the PostBackEventReference property that we have declared in the script. We use GetPostBackEventReference instead of GetPostBackClientHyperlink because we do not need the javascript: prefix, since the code will be called from JavaScript. Also, the class attribute of the select element is set to SelectionList, so that the new script file is loaded and executed. This is caused by the following statement: behavior:url(ColorPicker_Select.htc);
In the script, we determine the id of the control by navigating up the hierarchy of the select element, as shown: var select = event.srcElement; var colorPickerID = select.parentNode.parentNode.parentNode.parentNode.id;
We then get the selected index of the select element and query the item at that index for its text, as shown here:
257
C H A P T E R
1 0
var colorName = select.options[select.selectedIndex].text;
We now modify the PostBackEventReference property, by inserting the event argument between the empty single quotes. The one parameter method returns a string similar to the following: __doPostBack(‘ColorPicker1’, ‘’)
Here is the code we use to insert the name of the color as an event argument, and then invoke the method: if (PostBackEventReference != null) { var argument = "'" + colorName + "'"; PostBackEventReference = PostBackEventReference.replace("''", argument); window.setTimeout(PostBackEventReference, 0, 'JavaScript'); }
Since the method is in the form of a string, we just use the window object and its setTimeout method for invocation. Once this method is invoked, the page is posted to the server. The page framework then uses the values in the __EVENTTARGET and __EVENTARGUMENT hidden fields to call the RaisePostBackEvent method of the IPostBackEventHandler interface. Now compile and load a web form containing the control. Clicking on a color on any tab should cause a post back to the server. Interestingly, clicking a known color on the Basic tab will select that color on one of the other tabs upon post back. This is by design, as the Windows Forms color picker provided with the .NET Framework does the same thing.
Summary In this chapter, we discussed some of the client-based and server-based state management options. These included query strings, cookies, hidden fields, the view state, and some intrinsic objects. We talked about some of the advantages and disadvantages of using most of them. We then proceeded to discuss the event model of web forms post back processing involving controls. We talked about the interfaces, methods and properties involved, as well as the detailed stages in a control’s life cycle.
258
A S P . N E T
S T A T E
M A N A G E M E N T
Finally, we used the knowledge learned to complete the ColorPicker control. The ColorPicker control is a simple composite control that raises events upon color selection. In the next chapter, we will look at composite controls in more detail. We will also delve into user controls and templates, and describe the differences among all of these. And as we frequently do, we will also provide real world examples along the way.
259
11 Chapter
Templated and Composite Server Controls Overview Developing custom controls will sometimes involve grouping two or more existing controls together, creating what are known as “composite controls.” The ColorPicker control developed in the last couple of chapters is an example of a composite control. The Basic tab page contained a collection of Panel controls. So was the PhoneNumberControl, which consisted of three TextBox controls.
Managing Child Controls All composite and templated controls must be able to manage their child controls. This includes controlling which elements are parsed on an ASP.NET page, as well as naming the child controls in a way to help prevent ambiguity.
The Naming Container In order for child controls of a composite of templated control to have unique ids, they must be associated with some type of namespace for id assignments. For example, consider a button that is added to a form named Button1. Also, consider a button that is part of a composite control that is added to the form. In order to ensure that the button that is part of the composite control doesn’t have a name that clashes with other buttons on the form, we must associate it with some type of naming scope. This naming scope in ASP.NET is called the naming container, and it is represented by the System.Web.UI.INamingContainer interface. This
C H A P T E R
1 1
interface is simply a marker interface, which means that it has no methods or properties. Some may argue that the naming container could have been implemented as an attribute instead. Whenever a control implements the INamingContainer interface, ASP.NET creates a new naming scope for the control’s child controls; thus, child controls are almost guaranteed to have unique ids. Here are two examples that illustrate the use of the INamingContainer interface. Each example assumes a button named Button1 that is part of a composite control named CustomControl1. Example 1: The unique ID of the button when CustomControl1 does not derive from INamingContainer. Button1
Example 2: The unique ID of the button when CustomControl1 derives from INamingContainer CustomControl1:Button1
From Example 2, you can see that the name of a child control is expanded using the name of its parent, if the parent implements INamingContainer. If the parent control is also part of a control that derives from INamingContainer, the name of the child control is further expanded.
Parsing Behavior When controls are place declaratively on an ASP.NET page, the page framework must somehow use the declarative tag syntax to build a control and its hierarchy. The parsing behavior is primarily controlled by the System.Web.UI.ParseChildrenAttribute class. This attribute helps to determine how nested elements on a page should be interpreted by a control. The attribute, which can only be applied at the class level, has three constructor overloads, as shown: public ParseChildrenAttribute();
This overload creates a new instance of the attribute, with the ChildrenAsProperties property set to true. See the one parameter overload for more details.
262
T E M P L A T E D
A N D
C O M P O S I T E
S E R V E R
C O N T R O L S
public ParseChildrenAttribute( bool childrenAsProperties );
This overload creates a new instance of the attribute, setting the ChildrenAsProperties property to the value of the parameter passed in. When ChildrenAsProperties is set to false, the page framework does not parse any nested elements of the control. When the property is set to true, the page framework tries to match nested XML elements with public properties of the control by using name equality. For example, consider the following ASP.NET page code fragment:
In order for the page framework to process the above declarative code successfully, WebCustomControl1 must contain a public property named Display. The class represented by Display would then need a public property named Value.
public ParseChildrenAttribute( bool childrenAsProperties, string defaultProperty );
This overload creates a new instance of the attribute, setting the ChildrenAsProperties property to the value of the first parameter, and the DefaultProperty property to the value of the second parameter. When ChildrenAsProperties is set to true and DefaultProperty is specified, the control must define a public property whose name is the same as DefaultProperty. During page processing, the framework matches nested XML elements to properties of the DefaultProperty property. This overload is normally used to parse collection items. Here is an example:
Assuming MyCollectionControl has the ParseChildrenAttribute applied with ChildrenAsProperties set to true and DefaultProperty set to Items, the control must also expose a public property named Items which is a collection of Item instances. The Item class must also have a public property named Value. But it should also be noted that Item elements are not the only elements that can be nested within the MyCollectionControl element. Remember that because ChildrenAsProperties is true, any element can be nested as long as it corresponds to either a property of the control, or a property of DefaultProperty. When dealing with collections, the child elements must correspond to item instances of the collection.
System.Web.UI.WebControls.WebControl is already marked with ParseChildrenAttribute(true).
IParserAccessor This interface defines a method that a server control must implement in order to recognize parsed XML or HTML elements. The supported method, AddParsedSubObject, allows the control to manage the child objects that have been parsed and that are being added to the control hierarchy. System.Web.UI.Control implements this interface. It implements the AddParsedSubObject method by providing a protected virtual AddParsedSubObject method. Here is the syntax: protected virtual void AddParsedSubObject( object obj );
The base implementation of this method simply tries to cast the parameter passed in to a Control, and if it succeeds, it adds the control to its ControlsCollection. You will typically override this method to permit only certain types of controls to be added to the
264
T E M P L A T E D
A N D
C O M P O S I T E
S E R V E R
C O N T R O L S
ControlsCollection. For example, the TableCell control overrides this method to prevent literal controls from being added to the control’s hierarchy. It instead uses the text in a literal control to set its Text property. Sometimes, you may not want the default behavior of automatically mapping page elements to public properties by name. ASP.NET provides a way for deviating from the default through the use of control builders, which are discussed next.
Control Builders A control builder works along with the page parser in building a control and its child controls. Control builders are instances of or subclassed from the System.Web.UI.ControlBuilder class. Control builders can be applied to controls through the use of the System.Web.UI.ControlBuilderAttribute. This attribute takes one parameter, which is the type of the ControlBuilder, as shown: [ControlBuilderAttribute(typeof(MyControlBuilder))]
Every web control is assigned the default control builder by the page parser, if one has not been assigned explicitly. The purpose of a control builder is to determine the composition of a control from declarative syntax. Only one of either the ControlBuilderAttribute or the ParseChildrenAttribute will be used in determining the composition of a control. Therefore, in order for a custom control builder to be used for building a control, the ParseChildrenAttribute(false) overload must be used. Control builders will typically be used when a control does not derive from System.Web.UI.WebControls.WebControl, but instead derives from System.Web.UI.Control. As mentioned, all controls deriving from WebControl will have a default control builder, which is why the ParseChildrenAttribute is enough for these controls. Here are some of the properties and methods of the ControlBuilder class: FIsNonParserAccessor: This protected readonly property returns a Boolean value indicating whether the control implements the IParserAccessor interface. FChildrenAsProperties: This protected readonly property returns a Boolean value indicating whether a control has been assigned the ParseChildrenAttribute with ChildrenAsProperties set to true. If this property returns true, the control builder will not be used to build the control. ControlType: This readonly property returns the type of control to be created.
265
C H A P T E R
1 1
HasAspCode: This readonly property returns a Boolean value indicating whether the control represented by the control builder contains any ASP code blocks. Code blocks are blocks of code between the <% and %> delimiters. ID: This property gets or sets the ID of the control to be created. TagName: This readonly property returns the tag name of the control to be created. AllowWhiteSpaceLiterals: This method returns a Boolean value indicating whether white space should be allowed or ignored when building the control. This method is overridable. AppendLiteralString: This method appends a literal string to a control. The string is added between the control’s opening and closing tags. This method is overridable. AppendSubBuilder: This method appends the control builders for child controls of the container represented by this builder. This method is overridable. CloseControl: The page parser calls this method to notify the builder that parsing is complete. This method is overridable. GetChildControlType: This method returns the type of a control given the tag name and a collection of attributes. This method is overridable. You would typically override this method in order for the parser to create instances of controls that cannot be interpreted using tag names. HasBody: This method returns a Boolean value indicating whether the control has both an opening tag and closing tag. The default implementation returns true. This method is overridable. HtmlDecodeLiterals: This method returns a Boolean value indicating whether the literal string of an HTML control must be decoded. A literal string is the text between the opening tag and closing tag of a control. The default implementation returns false. This method is overridable. Init: This method initializes the control builder during a web request. The default implementation sets the values of its members to the values passed in. This method is overridable. Here is the syntax: public virtual void Init( TemplateParser parser, ControlBuilder parentBuilder, Type type,
266
T E M P L A T E D
A N D
C O M P O S I T E
S E R V E R
C O N T R O L S
String tagName, String id, IDictionary attribs );
The first parameter is an instance of the parser that invoked this control builder. This will normally be an instance of System.Web.UI.PageParser. Most of the methods and properties of the PageParser class are either protected, private or internal. So it is not intended to be used directly by third parties. NeedsTagInnerText: This method returns a Boolean value that indicates whether the control builder should retrieve the inner text of a control. Inner text is the text between the opening and closing tags of the control’s element. If this method returns true, SetTagInnerText will be called. The default implementation returns false. OnAppendToParentBuilder: This method notifies a control builder that it is about to be added to a parent control builder. The parent builder for a control builder that you customize will generally be the default, which is System.Web.UI.ControlBuilder. The control builder of a top level control will generally be a System.Web.UI.RootBuilder. SetTagInnerText: This method will be called if NeedsTagInnerText returns true. This method has one parameter representing the inner text of the control. CreateBuilderFromType: This is a static method that creates a control builder from the specified type. Here is the syntax: public static ControlBuilder CreateBuilderFromType( TemplateParser parser, ControlBuilder parentBuilder, Type type, String tagName, String id, IDictionary attribs, int line, string sourceFileName );
267
C H A P T E R
1 1
Data Binding Data binding is performed by means of data binding expressions. A data binding expression is code contained within <%# and %> delimiters. The code contained in a data binding expression must reflect bindings of a server control to properties or fields of either the Page or of the server control’s immediate naming container, provided they conform to the accessibility rules. For instance, a server control can be bound to both protected and public fields of the containing page, but it can only bind to public properties and fields of other controls on the page. Here is an example of a data binding expression: BlueVisionCompany ABCCompany 123” runat=server/>
The label would contain text similar to the following: Company ABC
A helper class, DataBinder, is also available for evaluating data binding expressions. This class provides a static method, Eval, used for evaluating an expression against an object during runtime. The Eval method is overloaded, as shown: public static object Eval( object container, string expression ); public static object Eval( object container, string expression, string format );
The first parameter is the object against which the expression is evaluated. This object must be accessible by the page. For templated and list controls, this is typically the naming container.
268
T E M P L A T E D
A N D
C O M P O S I T E
S E R V E R
C O N T R O L S
We will discuss templated controls later. The second parameter is a data binding expression. The third parameter is a formatting string, used to format the output. Here is the same example above, but this time using the DataBinder: ” runat=server/>
Using this code, the label would now have an output similar to the following: Customer: Company ABC
As you can see, the DataBinder reduces the amount of code it would require for formatting. Here is the same formatting above, but this time using the String.Format method: <%# String.Format(“Customer: {0}”, ((ListItem) CustomersList[CustomersList.SelectedIndex]).Text ) %>
Data binding expressions on a page are evaluated when the page’s DataBind method is called. This method also raises the DataBinding event. At design time, data binding expressions are enough to bind data to server controls. At runtime, you typically will want to handle the DataBinding event.
Implementing a Templated Control Now that we know more about parsing, control building, and data binding, we can use that knowledge to build templated controls, or templates. Templates help enforce the separation of data and presentation. Templates allow page developers to provide some or all of the UI for a particular control. Templates can be created for both user controls and custom controls. The best way to understand templates is to create one and use one. So we will do just that. There are a number of steps that we must adhere to when implementing a templated control, as shown below: 1. Derive a class from Control or one of its descendents. 2. Implement INamingContainer on the templated control. 3. Apply the ParseChildrenAttribute(true) overload to the templated control. Alternatively, you can apply a control builder using the ControlBuilderAttribute
269
C H A P T E R
1 1
to determine which child controls can be created inside the template. However, doing so would take away the meaning of template. 4. Create a logical container for holding templated items. A logical container is a class derived from Control which also implements INamingContainer. You can generally think of the template container as a holder of a single item. 5. Implement one or more properties of type ITemplate. Some examples of these properties include HeaderTemplate, FooterTemplate, and ItemTemplate. 6. For each ITemplate property implemented, apply a TemplateContainerAttribute to the property. The parameter to the constructor is the type of logical template container that the item will be instantiated in. If you do not apply this attribute, the container will be an instance of System.Web.UI.Control. 7. Override the CreateChildControls method of the templated control. Within this method, instantiate the template container by calling its constructor. Then call the InstantiateIn method of the ITemplate property, passing the logical template container as an argument. A framework internal class, SimpleTemplate, implements the ITemplate interface and simply adds itself to the control’s hierarchy. Finally, add the template container to the control’s collection of the templated control. 8. Override the OnDataBinding method of the templated control and call EnsureChildControls. This ensures that child controls in the templates are created before the page evaluates any data binding expressions. You must also call the base method to ensure that any registered event handlers receive the DataBinding event.
Example: AddressControl To demonstrate these steps, we are going to develop the AddressControl, which makes the use of templates for displaying address information. In each step, new code appears shaded, while code already discussed has a white background. Step 1: We start by adding a new class to the CustomWebControls project, naming the class AddressControl in the process. Derive this class from System.Web.UI.Control, as shown: public class AddressControl : Control {
270
T E M P L A T E D
}
A N D
C O M P O S I T E
S E R V E R
C O N T R O L S
public AddressControl() { // // TODO: Add constructor logic here // }
In order to use the control in the most efficient way for displaying addresses, we will also add a class named Address. This class will contain public properties to set and retrieve address details. Here is the class: public class Address { private String _addressLine1, _addressLine2, _city, _state, _postalCode; public String AddressLine1 { get { return _addressLine1; } set { _addressLine1 = value; } } public String AddressLine2 { get { return _addressLine2; } set { _addressLine2 = value; } } public String City { get { return _city; } set { _city = value; } } public String State { get { return _state; } set { _state = value; } } public String PostalCode { get { return _postalCode; } set { _postalCode = value; } }
271
C H A P T E R
1 1
}
Now, we will define a public property and member field for an address to the control, as shown: public class AddressControl : Control, INamingContainer { private Address _address; public AddressControl() { // // TODO: Add constructor logic here // } public Address Address { get { return _address; } set { _address = value; } }
Step 2: In order to ensure that all child controls of the address control have unique ids, we must implement INamingContainer on the control. Remember that INamingContainer is a marker interface that is used by the framework to generate unique names. Specifically, implementing INamingContainer here will ensure that each template container has a unique id. This is especially useful for templated list controls, where the control will have more than one child template container. Here is the code: public class AddressControl : Control, INamingContainer
Step 3: Apply the ParseChildrenAttribute(true) overload to the control. This will cause child elements to be mapped to properties of the control. Here is the class with the attribute defined:
272
T E M P L A T E D
A N D
C O M P O S I T E
S E R V E R
C O N T R O L S
[ParseChildren(true)] public class AddressControl : Control, INamingContainer
Step 4: We must now create a logical container control for holding the template items. We do this simply to wrap all of the template code that is added to the control into one container. This way, we only need to add the template container to the control’s hierarchy for rendering. Because the container must be added to the hierarchy, it must be derived from System.Web.UI.Control or one of its descendents. Also, to ensure that each element in the template itself is unique, we must implement INamingContainer on this control. Implementing INamingContainer on the main control ensures that each template container has a unique id. Several template containers may be created at runtime in the case of templated list controls. Implementing INamingContainer on the template container ensures that the elements of the template are unique. Here is the code to do that: public class AddressTemplateContainer : Control, INamingContainer { private Address _address; public AddressTemplateContainer(Address address) { _address = address; }
}
public Address Address { get { return _address; } set { _address = value; } }
We add an Address property to this control also, so that the template user will be able to access address information using a data binding expression. Any template container will generally need at least one property to bind data to. The convention for templated list control containers is to provide a DataItem property of type object. The following code shows how to access the Address property on a page: <%# Container.Address.City %>
273
C H A P T E R
1 1
Even though we only get, never set, the address on the page, the property must support both get and set accessors due to the way data binding works. Data binding demands that it works bidirectional. Only in the case of non list bound templates will you need to provide a property on both the control and its template containers. Step 5: We will now add an AddressTemplate property of type ITemplate. We could have also derived a class from ITemplate, and then assigned that class type to the AddressTemplate property. ITemplate supports one method, InstantiateIn, which by default adds itself to the hierarchy of the control passed in as a parameter. Here is the syntax for the method: void InstantiateIn( Control container );
The container is the template container for the template. All templates on a page are by default parsed into a framework internal class called SimpleTemplate. The InstantiateIn method of SimpleTemplate creates a new template control and adds it to the container. You would typically implement ITemplate if you needed to change the way template controls are added to their containers. Because we do not need to change the default behavior, we will not implement the interface. Here is the code: public class AddressControl : Control, INamingContainer { private Address _address; private ITemplate _addressTemplate; ... public ITemplate AddressTemplate { get { return _addressTemplate; } set { _addressTemplate = value; } }
274
T E M P L A T E D
A N D
C O M P O S I T E
S E R V E R
C O N T R O L S
Step 6: Now let’s apply the TemplateContainerAttribute to the template property, so that the framework knows which container the template belongs to. Here is the property with the attribute applied: [TemplateContainer(typeof(AddressTemplateContainer))] public ITemplate AddressTemplate
Step 7: We must now override CreateChildControls of the templated control in order to instantiate the template, add it to the container, and add the container to the control’s hierarchy. Here is the code: protected override void CreateChildControls() { // If a template has been supplied, add it to the hierarchy. // Otherwise, add a predefined literal control to the hierarchy. if (this.AddressTemplate != null) { this.Controls.Clear(); AddressTemplateContainer templateContainer = new AddressTemplateContainer(_address); this.AddressTemplate.InstantiateIn(templateContainer); this.Controls.Add(templateContainer); } else { StringBuilder addressBuilder = new StringBuilder(); addressBuilder.Append(_address.AddressLine1); addressBuilder.Append(", "); addressBuilder.Append(_address.AddressLine2); addressBuilder.Append(", "); addressBuilder.Append(_address.City); addressBuilder.Append(", "); addressBuilder.Append(_address.State); addressBuilder.Append(", "); addressBuilder.Append(_address.PostalCode);
275
C H A P T E R
1 1
LiteralControl literal = new LiteralControl(addressBuilder.ToString()); this.Controls.Add(literal); } }
We first check to see if AddressTemplate is not null. If it is not, we remove any controls from the control’s hierarchy. Then we create the address template container and instantiate the template into it. Instantiating the template into the container simply adds the template to the container. This is because we did not implement ITemplate. We finally add the template container to the hierarchy. If the AddressTemplate property is null, we must supply a default representation for our control. We do this by creating a literal control with formatted address information, and adding the literal control to the hierarchy. This way, a template user only needs to supply an Address. Step 8: Finally, we must override the OnDataBinding method of the templated control to ensure that the templates are created before the data binding expressions are evaluated. We do this by calling EnsureChildControls, as shown: public override void OnDataBinding(EventArgs e) { // This method will only be called if the DataBind method is called on the page. this.EnsureChildControls(); base.OnDataBinding(e); }
We also call the base OnDataBinding method to make sure that subscribed event handlers receive the DataBinding event. The general rule in overriding any event raising method is to also call the base method. We can now compile this control and add it to a page to test it. We will evaluate two test cases. First we will add the control to a page without applying a template. Then we will add the control and design a template for the address. Go ahead and add the control to an ASP.NET page. You must also add the following code to the Page’s Load event handler. This handler is normally Page_Load. If the page is set up to automatically wire page events, the handler will always be Page_Load. Here is the code:
276
T E M P L A T E D
A N D
C O M P O S I T E
S E R V E R
C O N T R O L S
private void Page_Load(object sender, System.EventArgs e) { // Put user code to initialize the page here this.DataBind(); }
Calling the DataBind method causes all data binding expressions to be evaluated. Because we are overriding the OnDataBinding method and calling EnsureChildControls, all child controls will be created before the expressions are evaluated. Here is the code for the first test case: <samples:AddressControl id="addressControl1" runat="server">
If you view this page in a browser, you will see an output similar to the following:
277
C H A P T E R
1 1
Let’s now implement the second test case and create the following template: <samples:AddressControl id="addressControl1" runat="server"> This is my eCard:
Address Line1:
Address Line2:
City:
State:
Zip:
Viewing the code above in a web browser should yield an output similar to the following:
278
T E M P L A T E D
A N D
C O M P O S I T E
S E R V E R
C O N T R O L S
Summary In this chapter, we learned everything we needed to know to become proficient building composite and templated controls. We talked about control builders and how they relate to parsing and control instantiation. We discussed the naming container and its involvement in implementing templated controls. This is the last chapter on Web Forms that is directly related to custom control implementation. In the next few chapters, we will focus on another major issue when dealing with control development, designer support.
279
12 Chapter
Introduction to Designers Overview Designers are objects that have the ability to modify a component’s design time behavior on a design surface. A designer can display a component’s user interface as well as allow property changes to the component. It can also provide other services and perform additional processing specific to the component it is associated with. In this chapter, we will start by discussing the many designers that are available with the .NET framework. In most instances, you will never have to write a designer from scratch. Later, we will move on to talk about a designer’s overall architecture as it relates to designing components in the .NET framework. We will also mention some of the other classes that relate indirectly to designers.
The Designer Hierarchy The .NET framework includes a base interface, System.ComponentModel.Design.IDesigner, in which all designers should inherit from. The class designer hierarchy can be split into two separate hierarchies. First, we have the Windows Forms Designers, which will be discussed shortly. These classes consist of designers for Windows Forms controls. Secondly, we have the Web Forms Designers. These designers target both HTML and Web Forms controls.
C H A P T E R
1 2
IDesigner The IDesigner interface provides the basic framework for creating custom designers in .NET. All custom designers should inherit from this class or from a class derived from this interface. The IDesigner interface includes two public properties and two methods that should be implemented. These members are described below: Component: This property gets or sets the base component that the designer is designing. The base component is any component derived from IComponent or from a class derived from IComponent. Verbs: This property gets or sets the collection of design-time verbs that are supported by the designer. These are commands that are available to the designer’s context menu and to the area right below the designer’s Property Grid. The collection is an instance of DesignerVerbCollection. Design-time verbs will be discussed in more detail in Chapter 13. DoDefaultAction: Implement this method in order to perform a default action on the designer. A default action is usually invoked by a user action on the component, such as a double-click. Some designers use this method to serialize code for the default event of the component. Designer serialization will be discussed in Chapter 13. Initialize: This method is usually invoked by the designer framework. Implement this method in order to initialize the designer with the specified component. Here is the syntax for this method: void Initialize( IComponent component );
IRootDesigner This interface derives from IDesigner. It indicates the root designer. A root designer is simply a top-level designer for other designers. This interface also provides support for view technologies, which are discussed later in this chapter. Here are the members of this interface: SupportedTechnologies: This property gets or sets an array of view technologies that the designer supports for display purposes.
282
I N T R O D U C T I O N
T O
D E S I G N E R S
GetView: This method returns a view object for the specified view technology.
Because the class designers are split up into two hierarchies, we will illustrate these in two separate figures. We will show the classes related to Windows Forms designers first, followed by Web Forms designer classes.
System.ComponentModel.Design.ComponentDesigner The ComponentDesigner will typically be the base designer from which you write your custom designers. You will generally never have to write a designer from scratch by deriving directly from IDesigner. All of the .NET framework designers derive from this class. In actuality, any object that derives from IComponent will be assigned a ComponentDesigner by default. Specifically, a component is assigned the System.Windows.Forms.Design.ComponentDocumentDesigner, which derives from ComponentDesigner. We will discuss the assignment of designers later. But for a quick note, like most other class associations in .NET, they are assigned with an attribute, System.ComponentModel.DesignerAttribute. Here are the members of ComponentDesigner: AssociatedComponents: This property returns a collection of components associated with the designer. Associated components are components that can be considered grouped together on a form. For example, a group box’s associated components are its child controls. Therefore, any cut or paste operation to the group box also affects the child controls. This method is overridable. Component: This property returns the component that the designer is designing, which is the component that was passed to the Initialize method of the designer. This property implements IDesigner.Component. Verbs: This property returns the collection of design-time verbs that are supported by the designer. This property is overridable and implements IDesigner.Verbs. DoDefaultAction: This virtual method implements IDesigner.DoDefaultAction. The default implementation creates an event handler signature for the default event and serializes it in code. Initialize: This virtual method initializes the designer for viewing and editing the component. It also saves the component passed to it to be returned by the Component property. This method implements IDesigner.Initialize. InitializeNonDefault: This method allows non default initialization on a component and designer. You would typically override this method when default properties should not be maintained as a result of a component being added to a designer in a non default way, such as through copy and paste. During a copy and paste scenario, you may wish to override certain property values that would have otherwise received default values. This method is overridable. OnSetComponentDefaults: This method is called during designer initialization and it allows defaults to be set after the component has been initialized. The default
285
C H A P T E R
1 2
implementation of this method sets the default property to its default value, as specified by the DefaultPropertyAttribute. This method is overridable. InheritanceAttribute: This protected property returns an instance of the InheritanceAttribute for the associated component. An InheritanceAttribute is applied to properties, fields, and events to indicate their inheritance type and level. The InheritanceAttribute class includes three static fields to indicate the inheritance type and level. Here are the members that represent the inheritance type: •
Inherited: This readonly field specifies that the member is inherited.
•
InheritedReadOnly: This readonly field specifies that the member is inherited and readonly.
•
NotInherited: This readonly field specifies that the member is not inherited.
Inherited: This protected readonly property returns a Boolean value indicating whether the component is inherited. ShadowProperties: This protected readonly property returns a collection of shadow properties, which are properties that override user settings. Shadowing a property is the process of keeping track of the property values set or changed by a user, and then determining whether to save those property values to the actual component. The collection returned is an instance of ComponentDesigner.ShadowPropertyCollection. It is implemented as a Hashtable with no add / remove functionality. GetService: This virtual method is used to obtain additional services exposed by the designer. The default implementation of this method calls the GetService method of the component’s Site property. Sites will be discussed later. InvokeGetInheritanceAttribute: This method is called to return the InheritanceAttribute for a particular designer. The default implementation simply invokes the InheritanceAttribute property for the designer passed to it. PreFilterAttributes: This virtual method allows a designer to add attributes that it exposes for the component that it is designing. If you override this method, call the base version before adding your own logic. The default implementation of this method does nothing.
286
I N T R O D U C T I O N
T O
D E S I G N E R S
PostFilterAttributes: This virtual method allows a designer to change or remove attributes it exposes for the component that it is designing. This method is called after PreFilterAttributes. You must call the base version after your custom logic when overriding. The default implementation of this method sets the InheritanceAttribute of the component to InheritanceAttribute.NotInherited. PreFilterEvents: This virtual method is similar to PreFilterAttributes, except that it allows the addition of events during design-time. PostFilterEvents: This virtual method is similar to PostFilterAttributes, except that it allows the removal and change of events during design-time. PreFilterProperties: This virtual method is similar to PreFilterAttributes, except that it allows the addition of properties during design-time. The default implementation loads the dictionary passed in with the browsable properties of the component. PostFilterProperties: This virtual method is similar to PostFilterAttributes, except that it allows the removal and change of properties during design-time. RaiseComponentChanging: This method notifies the IComponentChangeService that the component is about to be changed by calling its OnComponentChanging method. Here is the syntax for this method: protected void RaiseComponentChanging( MemberDescriptor member );
Note that the OnComponentChanging method of IComponentChangeService is different from the OnComponentChanging method of ITypeDescriptorContext. RaiseComponentChanged: This method notifies the IComponentChangeService that the component has changed by calling its OnComponentChanged method. Here is the syntax for this method: protected void RaiseComponentChanged( MemberDescriptor member, object oldValue, object newValue );
The first parameter indicates the member that has changed. The second and third parameters represent the old and new values respectively.
287
C H A P T E R
1 2
To make components invisible at design time, apply the DesignTimeVisibleAttribute(false) attribute to their class definition. System.Windows.Forms.Design.ComponentDocumentDesigner This designer inherits from ComponentDesigner and implements IRootDesigner. Any component that derives from IComponent will be assigned this designer by default. Here is an example of this type of designer’s view in Visual Studio .NET:
Here are the new and overridden properties and methods of this class: Control: This property returns the control that the designer is designing. For this designer, the control returned is an instance of System.Windows.Forms.Design.ComponentTray. The ComponentTray class is used to represent components that do not provide a visual surface for designing. As you can see from the above picture, that particular component tray consists of
288
I N T R O D U C T I O N
T O
D E S I G N E R S
a number of labels and hyperlinks when no components have been added to the designer. TrayAutoArrange: This property gets or sets a Boolean value indicating whether the component tray used by this designer is in auto-arrange mode. The default is true. TrayLargeIcon: This property gets or sets a Boolean value indicating whether the component tray used by this designer is in large icon mode. The default is false. GetToolSupported: This protected method returns a Boolean value indicating whether the specified ToolboxItem instance is supported by the designer. The default implementation of this method returns true. This method is overridable. Initialize: This method is overridden in order to add certain services to this designer. Specifically, the IEventHandlerService, IComponentChangeService, ISelectionUIService, and IInheritanceService are added. PreFilterProperties: This method is overridden to add the TrayLargeIcon property to the dictionary.
System.Windows.Forms.Design.ControlDesigner The ControlDesigner class is intended to extend designer support for components that inherit from Control. This is the default designer for all controls. Here are the members of this class: AccessibilityObject: This virtual property returns the AccessibilityObject instance assigned to the control. This is an instance of ControlDesigner.ControlDesignerAccessibleObject. AssociatedComponents: This method is overridden to return all child controls of the control that this designer is designing. Recall that associated components are components that can be considered to be grouped together on the designer. Control: This property simply casts the Component property to Control and returns the result. This property is overridable. SelectionRules: This virtual property returns a combination of System.Windows.Forms.Design.SelectionRule values that represent the
289
C H A P T E R
1 2
movement capabilities of the control while in design mode. The SelectionRule values are described below in no particular order: •
AllSizeable: The control can be sized in all directions.
•
BottomSizeable: The control can be sized vertically from the bottom.
•
TopSizeable: The control can be sized vertically from the top.
•
LeftSizeable: The control can be sized horizontally from the left.
•
RightSizeable: The control can be sized horizontally from the right.
•
Locked: The control cannot be moved nor sized.
•
Moveable: The control can be moved.
•
Visible: The control is visible.
•
None: There are no selection rules set.
CanBeParentedTo: This virtual method returns a Boolean value indicating whether the control managed by the current designer can be a child of a control managed by the designer passed in. This method returns false if the designer passed in is not a ParentControlDesigner instance or if the control being designed by the parent designer does not contain the control for this designer. Otherwise, it returns true. Here is the implementation of this method: public virtual bool CanBeParentedTo(IDesigner parentDesigner) { if (parentDesigner is ParentControlDesigner) { if (((ParentControlDesigner)parentDesigner).Control.Contains(t his.Control) { return true; } } }
290
return false;
I N T R O D U C T I O N
T O
D E S I G N E R S
Initialize: This method is overridden to initialize internal members with the control that this designer is designing. It also places a special bitmap over each inherited visual component of the control that this designer is designing. Here is an example of an inherited form showing inheritance glyphs: Base Form
Inherited Form
IntitializeNonDefault: This method is overridden to initialize the window targets for the children of the control that this designer is designing. OnSetComponentDefaults: This method is overridden to set the Text property of a control to the value of the site’s Name property. If there is no Text property, the method simply returns without doing anything. accessibilityObj: This protected field holds the value to be returned by the AccessibilityObject property. InvalidPoint: This protected static readonly field contains an invalid point for the component in the designer. This point always has its X and Y properties set to the minimum value for an integer.
291
C H A P T E R
1 2
EnableDragRect: This protected property returns a Boolean value indicating whether drag rectangles should be drawn for the component that this designer is designing. The default implementation returns false. BaseWndProc: This protected method processes windows messages of type System.Windows.Forms.Message. The default implementation of this message calls DefWndProc with fields of the Message value passed in. DefWndProc: This protected method provides default processing for windows messages. DisplayError: This method uses the Exception instance passed to it to display an error message, either through the IUIService, if available, or by displaying a simple message box. The IUIService will be discussed later. EnableDragDrop: This method enables drag and drop based on the Boolean value passed to it. If dragging is enabled, several event handlers, such as OnDragDrop, are set up for receiving notifications. GetHitTest: This method returns a Boolean value indicating whether the given point is within the bounds of the control represented by this designer. This method is overridable. The default implementation of this method returns false. HookChildControls: This protected method re-routes messages from the specified child control and its children to the designer. OnContextMenu: This protected virtual method allows the designer to perform processing right before a context menu is about to be displayed. OnCreateHandle: This protected virtual method allows the designer to perform additional processing right after its control’s window handle has been created. OnDragEnter: This protected virtual method allows the designer to perform processing when a drag and drop operation is entered into the designer’s view. OnDragLeave: This protected virtual method allows the designer to perform processing when a drag and drop operation leaves the designer’s view. OnDragOver: This protected virtual method allows the designer to perform processing when a drag and drop operation drags an object over the designer’s view.
292
I N T R O D U C T I O N
T O
D E S I G N E R S
OnDragDrop: This protected virtual method allows the designer to perform processing when a drag and drop operation drops an object onto the designer’s view. OnGiveFeedback: This protected virtual method is called when a drag and drop operation is in process. Its purpose is to allow the designer to provide additional feedback about the location where the object is currently dragged over so that the caller can determine where to drop an object. OnMouseDragBegin: This protected virtual method is called when the left mouse button has been pressed and has begun dragging over the component that this designer is designing. OnMouseDragMove: This protected virtual method is called during each movement of a drag and drop operation. OnMouseDragEnd: This protected virtual method is called when a drag and drop operation has been completed or canceled. OnMouseEnter: This protected virtual method is called when the mouse first enters the control that this designer is designing. OnMouseHover: This protected virtual method is called when the mouse moves over the control that this designer is designing. OnMouseLeave: This protected virtual method is called when the mouse leaves the control that the designer is designing. OnPaintAdornments: This protected virtual method is called after the control has painted itself so that the designer can paint any additional graphics on its surface. The default implementation uses this method to paint any inheritance glyphs on the control. OnSetCursor: This protected virtual method is called whenever a cursor needs to be set for the designer. The default implementation uses the IToolboxService to determine the cursor that should be set. PreFilterProperties: This method is overridden to add several properties to be available to the designer. UnhookChildControls: This protected method unhooks messages that were previously hooked through HookChildControls.
293
C H A P T E R
1 2
WndProc: This protected virtual method processes windows messages.
System.Windows.Forms.Design.ParentControlDesigner ParentControlDesigner is inherited from ControlDesigner. It extends design time support by providing the ability to add nested components to it. Here are some of the members of this class: DefaultControlLocation: This protected virtual property returns the default location for a control added to the designer. The default implementation of this method returns the 0-origin. DrawGrid: This protected virtual property gets or sets a Boolean value indicating whether a grid should be drawn on the control managed by this designer. The default implementation of this method uses the options set in Visual Studio .NET for determining whether to draw a grid. Here is a view of the options dialog:
EnableDragRect: This property is overridden to return true.
294
I N T R O D U C T I O N
T O
D E S I G N E R S
GridSize: This protected virtual property gets or sets the grid size for each square drawn when the DrawGrid property is set to true. The default implementation uses the options set in Visual Studio .NET. CanParent: This virtual method returns a Boolean value indicating whether the control managed by this designer can be a parent of the control managed by the designer passed in. CreateTool: This protected method creates a component from a ToolboxItem instance passed to it and adds the component to the designer. CreateToolCore: This protected virtual method is used to call one of the CreateTool overloads. GetControl: This protected method returns the control for the specified child component. GetUpdateRect: This protected method returns the updated Rectangle bound by the component in the designer.
System.Windows.Forms.Design.ScrollableControlDesigner This class derives from ParentControlDesigner. It extends the designer framework to provide support for scrollable controls. System.Windows.Forms.Design.DocumentDesigner This class derives from ScrollableControlDesigner and implements IRootDesigner. Therefore, this designer is considered to be a root designer, similar to ComponentDocumentDesigner. Here are some of the properties and methods of this class: SelectionRules: This property is overridden. This implementation returns a SelectionRules combination of values that prevent the document from being resized. EnsureMenuEditorService: This protected virtual method creates a menu editing service if one has not already been created for this designer.
295
C H A P T E R
1 2
GetToolSupported: This protected virtual method returns a Boolean value indicating whether the ToolboxItem passed in is supported by this designer. This method returns true if the tool should be enabled in the toolbox, indicating the designer knows how to create to component represented by the toolbox item. ToolPicked: This protected method is called when a toolbox item is clicked. An instance of ToolboxItem is passed to this method, indicating the tool picked. This method is overridable.
System.Web.UI.Design.HtmlControlDesigner This designer derives from ComponentDesigner, and provides the basic framework for designing ASP.NET server controls. The following properties and methods are supported by this class: Behavior: This property gets or sets a System.Web.UI.Design.IHtmlControlDesignerBehavior instance that is associated with this designer. This interface enables html behavior extensions to be attached to the designer. The default value for this property is a null reference. DataBindings: This property gets an instance of DataBindingsCollection that represents the data bindings for the control that this designer is designing. The IDataBindingsAccessor is used to return these bindings. DesignTimeElement: This protected method uses the IHtmlControlDesignerBehavior represented by the Behavior property to return its DesignTimeElement property. ShouldCodeSerialize: This virtual property gets or sets a Boolean value indicating whether a field declaration should be created in code for the control represented by this designer. The default is true. OnBehaviorAttached: This protected virtual property is called when an IHtmlControlDesignerBehavior is attached to the designer. OnBehaviorDetaching: This protected virtual property is called when an IHtmlControlDesignerBehavior is detached from the designer. OnBindingsCollectionChanged: This protected virtual method is called whenever a data binding has changed. The method has a string parameter that indicates the
296
I N T R O D U C T I O N
T O
D E S I G N E R S
property name for which the data binding was changed. The designer should call this method whenever it changes a control’s data bindings. OnSetParent: This protected virtual method is called whenever the control represented by this designer becomes a child control of another control. PreFilterProperties: This method is overridden to provide the Name, DataBindings, and Modifiers properties. The Modifiers property at design time is used to specify the accessibility of the serialized control, such as private, protected, or public. System.Web.UI.Design.HtmlIntrinsicControlDesigner This class derives from HtmlControlDesigner. It provides a basic designer framework for all intrinsic HTML controls. In the .NET framework, these are the controls that are part of the System.Web.UI.HtmlControls namespace. System.Web.UI.Design.ControlDesigner This class derives from HtmlControlDesigner. It provides a basic designer framework for all server controls. In the .NET framework, these are the controls that are part of the System.Web.UI.WebControls namespace. The following properties and methods are exposed by this class: ID: This virtual property gets or sets a string that uniquely identifies the component on the designer. The default implementation of this property sets the component’s id that is associated with the designer. It also sets the component’s site’s id to the same value. Control: This virtual property returns the control that the designer is designing. In most instances, this control will be the value of the Component property, but cast to Control. Some non visual controls may also use this property to return the UI of their designer. AllowResize: This virtual property returns a Boolean value indicating whether the control can be resized on the designer. The default implementation returns false. DesignTimeHtmlRequiresLoadComplete: This virtual property returns a Boolean value indicating whether the designer must be done loading before the design time html can be used.
297
C H A P T E R
1 2
IsDirty: This virtual property gets or sets a value indicating whether the control has been changed by the designer. ReadOnly: This property gets or sets a value indicating whether the properties of the control that this designer is designing are readonly while in design mode. The default is false. GetDesignTimeHtml: This virtual method returns a string that represents the control’s design time html. The default implementation of this method temporarily sets the control to visible, and then calls the control’s RenderControl method. If nothing was rendered, it calls GetEmptyDesignTimeHtml. If an exception is thrown during the rendering process, it calls the GetErrorDesignTimeHtml method. GetEmptyDesignTimeHtml: The default implementation of this virtual method returns a string containing the name of the control. You would typically override this method to provide cues on how to set properties for the control. Some list controls use this method to indicate that the list is empty. GetErrorDesignTimeHtml: The default implementation of this virtual method returns an empty string. The purpose of this method is to display error specific information using the Exception instance passed in as a parameter. OnComponentChanged: This protected virtual method is called whenever the component changes. A ComponentChangedEventArgs that contains the change data is passed as a parameter. If the old and new values are not equal, the designer sets IsDirty to true and calls UpdateDesignTimeHtml. OnBindingsCollectionChanged: This method is overridden to write the data binding expression for the property passed in as a parameter to the ASPX file. OnBehaviorAttached: If the control managed by this designer is a server control, this method is overridden to remove the following CSS styles from the control: position, top, bottom, left, right. IsPropertyBound: This method returns a Boolean value indicating whether the specified property is data bound. It does this by testing the DataBindings property to see if it contains the specified property name. OnControlResize This protected virtual method is called when the control associated with the designer has been resized. When this method is called, the width and height have already been set. This method can also be called several times when resizing a control to give designers a chance to display the control before resizing is complete.
298
I N T R O D U C T I O N
T O
D E S I G N E R S
RaiseResizeEvent: This method simply calls the OnControlResize method. PreFilterProperties: This method is overridden to add the ID and DynamicProperties properties to the control during design time. DesignTimeElementView: This protected property returns a view for the DesignTimeElement property via the IHtmlControlDesignerBehavior interface. UpdateDesignTimeHtml: This virtual method is used to refresh the designer by updating the html for the control. This method is normally called in response to component changes. The default implementation of this method calls GetDesignTimeHtml if the control is readonly.
System.Web.UI.Design.ReadWriteControlDesigner This designer is derived from ControlDesigner. The designer supports controls that support adding child controls. The ReadOnly property is set to false by default. Any controls that wish to support the addition of child controls should apply this designer. Otherwise, apply the ControlDesigner. Here are the methods of this class: OnComponentChanged: This method is overridden in order to update the display correctly for font and color changes. This method calls MapPropertyToStyle to map the control’s property changes to CSS styles. MapPropertyToStyle: This protected virtual method maps a property value of a control to a CSS style to be used by the IHtmlControlDesignerBehavior interface, but only if the control is an instance of WebControl. The WebControl class has properties that represent CSS style properties. As styles are mapped, the AddStyleAttribute of the interface is called, passing the style name and the style value. The IHtmlControlDesignerBehavior interface is discussed later, but here is a snippet of what is actually going on: if (propertyName.Equals("BackColor")) { Behavior.SetStyleAttribute("backgroundColor", true, val, true); } else if (propertyName.Equals("ForeColor")) { Behavior.SetStyleAttribute("color", true, val, true);
299
C H A P T E R
1 2
}
OnBehaviorAttached: This overridden method behaves similar to OnComponentChanged, except that is maps all styles.
System.Web.UI.Design.WebControls.PanelDesigner This designer derives from ReadWriteControlDesigner and supports the Panel web control. A Panel is used to group other web controls. And as stated earlier, controls that wish to support addition of child controls should apply the ReadWriteControlDesigner. Here are the methods of this class: MapPropertyToStyle: This method is overridden to map the BackImageUrl and HorizontalAlign properties to their CSS equivalents. OnBehaviorAttached: This method is overridden to provide almost the same functionality as MapPropertyToStyle. System.Web.UI.Design.TemplatedControlDesigner This class derives from ControlDesigner. Its purpose is to extend the designer framework in order to provide support for templated server controls. This class is the pure base class for all templated server control designers; hence, it is marked abstract. Here are the members of this class: ActiveTemplateEditingFrame: This property returns an instance implementing ITemplateEditingFrame for managing the template editing area. There can be several template editing frames, so this property only returns the current one. An ITemplateEditingFrame is simply the UI used by a template editor. Most template editing frames can be created through the ITemplateEditingService. CanEnterTemplateMode: This property returns a Boolean value indicating whether the designer will allow the viewing and editing of templates. HidePropertiesInTemplateMode: This protected virtual property returns a Boolean value indicating whether the properties of the control that this designer is designing should be hidden while in template editing mode.
300
I N T R O D U C T I O N
T O
D E S I G N E R S
InTemplateMode: This property returns a Boolean value indicating whether the designer is in template mode. CreateTemplateEditingFrame: This protected abstract method should be overridden to create an instance of ITemplateEditingFrame for the specified verb. Verbs are designer commands that are available to the designer’s context menu. These will be discussed in the next chapter. The idea is that a templated control’s designer will expose several verbs on the context menu for creating templates at design time. EnterTemplateMode: This method is used to open the specified template editing frame in the designer for editing. This method first calls ExitTemplateMode if one template is already being edited in the designer, as determined by the InTemplateMode property. ExitTemplateMode: This method is used to exit all nesting templates and close their active template editing frames. Here is the syntax for this method: public void ExitTemplateMode( bool switchingTemplate, bool nested, bool save );
The first parameter determines whether the method was called because the designer is entering into a new template mode. The second parameter is true if the designer is nested within another designer that is also in template editing mode. If this parameter is true, the designer calls ExitTemplateMode for every templated control in the designer hierarchy, starting with the root designer. The last parameter specifies whether the templates should be saved before exiting. GetCachedTemplateEditingVerbs: This method returns an array of template editing verbs, each typed TemplateEditingVerb, which is a type derived directly from DesignerVerb. Designer verbs are discussed in the next chapter. GetPersistInnerHtml: This method always returns null. It is overridden simply to call SetActiveTemplatedEditingFrame and to set the IsDirty property to true if the template mode changed after the call. GetTemplateContainerDataItemProperty: This method returns an empty string. You should override this method to indicate the property name that will mimic the “Container.DataItem” property. This is normally used for templated list controls.
301
C H A P T E R
1 2
GetTemplateContainerDataSource: This virtual method defaults to return null. You should override this method to return the data source of the template’s container. GetTemplateContent: This method is marked abstract, and should be overridden to return the template’s content given the template name and the editing frame. GetTemplateEditingVerbs: This method returns an array of template editing verbs that the designer exposes. It does this through a call to GetCachedTemplateEditingVerbs. GetTemplateFromText: This method creates an instance of ITemplate given the name of the template. The name of the template must match exactly to the name of the template property. The control parser is responsible for ensuring this. GetTemplatePropertyParentType: This method returns the type of the object that the template property exists on. The implementation simply calls the GetType method of the Component property. GetTextFromTemplate: This method retrieves the inner text for the given ITemplate instance by using a template builder. Specifically, it casts ITemplate to TemplateBuilder, and invokes the Text property of TemplateBuilder. HidePropertiesInTemplateMode: This virtual method returns a Boolean value indicating whether the properties of the control that this designer is designing should be hidden when the designer is in template editing mode. The default implementation returns true. OnBehaviorAttached: This method is overridden to close the active template editing frame if it is open. OnComponentChanged: This method is overridden to update the templated editing frame’s name if the ID of the control has changed. OnSetParent: This method is overridden to determine if the parent designer supports nested template editing. OnTemplateModeChanged: This protected virtual method is called whenever the template mode has changed. SaveActiveTemplateEditingFrame: This method calls the Save method for the ITemplateEditingFrame instance that represents the active template editing frame.
302
I N T R O D U C T I O N
T O
D E S I G N E R S
SetTemplateContent: This abstract method should be implemented to set the content of the template to the specified string content. UpdateDesignTimeHtml: If the designer is not in template editing mode, it calls the base version of this method. Otherwise, this method simply returns. System.Web.UI.Design.WebControls.BaseDataListDesigner This designer is used as a base designer for templated list controls. Both the DataGrid and DataList controls offer design time functionality that extends from this designer. It can actually provide support for any control that inherits from BaseDataList. Here are some interesting members of this class: Verbs: This property is overridden to provide two verbs: the AutoFormatVerb, which is used to auto format the data bound list control, and the PropertyBuilderVerb, which pops up a dialog for setting the DataSource and DataMember properties. DataSource: This property gets or sets the value of the control’s DataSource property. This is a collection used for binding to the control. If the collection is a DataSet, then the DataMember property must also be set to specify the data table. DataMember: This property gets or sets the value of the control’s DataMember property. This is a string property that represents the table name of a data set, or a nested collection property of a collection. DataKeyField: This property gets or sets the value of the control’s DataKeyField property. The DataKeyField property is used to uniquely identify a record in the list. Having the key field separate from the data source’s fields allows the control to display without necessarily displaying private keys. DesignTimeHtmlRequiresLoadComplete: This property returns false if the data source has no items. Otherwise, it returns true. GetDesignTimeDataSource: This method returns an exact clone of the runtime data source. This method can either return an exact copy of the data source or a dummy data source, depending on the parameters passed to it.
303
C H A P T E R
1 2
System.Web.UI.Design.WebControls.DataGridDesigner This class is used to provide design time support for the DataGrid control. It derives from BaseDataListDesigner, while the DataGrid derives from BaseDataList. This class overrides GetCachedTemplateEditingVerbs to create a verb collection for each TemplateColumn found on the DataGrid. These verbs can be invoked from the context menu of the designer. System.Web.UI.Design.WebControls.DataListDesigner This designer is used primarily by the DataList control. This class overrides GetCachedTemplateEditingVerbs to create a verb collection for the HeaderTemplate, FooterTemplate, ItemTemplate, and SeparatorTemplate. System.Web.UI.Design.TextControlDesigner This designer derives from ControlDesigner, and is used to provide design time support for text controls. There is a constraint on using this designer: any control that applies this designer must have a Text property, or an ArgumentException will be thrown. This designer is used by the Label control and Hyperlink control. System.Web.UI.Design.WebControls.HyperlinkDesigner This class derives from TextControlDesigner. This designer is used by the Hyperlink control. Only the GetDesignTimeHtml method has been overridden in this class. If the Text property or ImageUrl property has not been set for the Hyperlink control that this designer is designing, then the designer uses the control’s ID to set the Text property. If the NavigateUrl property has not been set, then the designer sets the property to “url”. It then calls the base GetDesignTimeHtml method and resets the Hyperlink’s properties to their original values. System.Web.UI.Design.WebControls.LabelDesigner The LabelDesigner also derives from TextControlDesigner. It does not provide any new or overridden members. It is used by the Label control.
304
I N T R O D U C T I O N
T O
D E S I G N E R S
System.Web.UI.Design.WebControls.LinkButtonDesigner The LinkButtonDesigner is used by the LinkButton control. It derives directly from TextControlDesigner, and provides no additional or overridden methods or properties. System.Web.UI.Design.UserControlDesigner This designer extends the design time support for user controls. Web user controls can be thought of as pagelets, or reusable pieces of HTML or ASP.NET snippets. This class derives from ControlDesigner, and overrides the following members: AllowResize: This property always returns false. The user control itself cannot be redefined by a user. It provides no external UI for resizing. ShouldCodeSerialize: This property always returns false, and does nothing when it is set. Because user controls cannot be instantiated directly, no declarations will be serialized into code. GetDesignTimeHtml: This method calls CreatePlaceHolderDesignTimeHtml of the base. This simply creates a place holder since there is no detailed information on the control. GetPersistInnerHtml: This method returns null, since no persistent inner HTML should be generated by default. System.Web.UI.Design.WebControls.AdRotatorDesigner This designer extends the design time support for the AdRotator web control. The AdRotator is used to display several consecutive images, with each image representing an advertisement. System.Web.UI.Design.WebControls.BaseValidatorDesigner The BaseValidatorDesigner is intended to provide designer support for controls that derive from BaseValidator. This designer is derived from ControlDesigner. It overrides the GetDesignTimeHtml method to display the ErrorMessage property of the validator control. If the ErrorMessage property or the Display property has not been set, it temporarily sets the ErrorMessage property to the ID of the validator control and calls the base
305
C H A P T E R
1 2
GetDesignTimeHtml method. Remember that the base version of GetDesignTimeHtml simply tells the control to render itself via RenderControl. System.Web.UI.Design.WebControls.ButtonDesigner This class derives from ControlDesigner to provide design time support for the Button control. It overrides GetDesignTimeHtml to set the Text property to the ID of the control if the Text property has not already been set. System.Web.UI.Design.WebControls.CalendarDesigner This designer provides support for the Calendar web control. It derives from ControlDesigner and exposes an AutoFormatVerb to display a dialog for auto-formatting the Calendar control. System.Web.UI.Design.WebControls.CheckBoxDesigner The CheckBoxDesigner extends the design time support for CheckBox controls. This designer is similar to the ButtonDesigner, in that it only overrides one method. It overrides GetDesignTimeHtml to set the Text property to the ID of the control, if the Text property was not previously explicitly set. System.Web.UI.Design.WebControls.ListControlDesigner The ListControlDesigner derives from ControlDesigner. Its purpose is to provide designer support for classes that are typed ListControl. The ListControl class is an abstract class, from which the following controls derive: CheckBoxList, used to specify a group of checkboxes; DropDownList, for providing a combo box control; ListBox; and RadioButtonList, used to group a list of radio buttons that are mutually exclusive. The designer also implements IDataSourceProvider, which defines an interface that provides designer access to data sources.
306
I N T R O D U C T I O N
T O
D E S I G N E R S
System.Web.UI.Design.WebControls.TableDesigner This class derives from ControlDesigner, and is used to extend the design time support for the Table control. This designer overrides GetDesignTimeHtml to generate dummy columns if the data source is not bound at design time.
Designer Architecture In a typical designer environment, you will have a root component that is the main component being designed. This component is associated with a designer, which is most likely an instance of IRootDesigner. This component itself may have the ability to “house” other components. These components themselves will be attached to their own designers. A designer host is used to manage all of the components and designers for a given designer document. Most of the objects mentioned here will be discussed shortly, but here is the basic architecture shown visually.
Designer Host
Root Designer
Service Provider
Designer A Service Provider
Root Component
Component A
Service Provider
Designer B Component B
The dotted rectangle represents the designer host, which indicates that the host is not really visual. It does have a Container property, however, used for managing the document. This Container, when working through Visual Studio .NET, is the same instance as the host itself,
307
C H A P T E R
1 2
which is System.VisualStudio.Designer.DesignerHost. The thick rectangles represent the components, where their outer thin rectangles represent their associated designers. As you can see, designers can actually contain other designers, just as components can contain other components. However, the containment of designers is not actually physical containment, but rather logical containment.
Sites The particular location where a component resides on its container is called a site. A site is implemented using the System.ComponentModel.ISite interface, and a site is also a service provider. A site simply associates a component with its container. Using sites, components can be named even if they do not expose a name property. Here are the properties of this interface: Component: This property returns the component associated with the particular site. Container: This property returns the container associated with the particular site. DesignMode: This property returns a Boolean value indicating whether the component is currently in design mode. Name: This property returns the name of the site, which in turn can be used as the name of the component.
Windows Forms Designer Architecture In Windows Forms development, designers will typically be created for components and controls that are designed to be placed on a windows form. A form is associated with a root designer, which is used to display a component when viewed as a top component. The root designer then needs someway to display the component to the user. This is done using a designer view. The components that are assigned the DocumentDesigner are, by default, viewed through a DesignerFrame, which is an instance of Control. Therefore, there is actually another layer in the designer framework for displaying root components. View Technologies A view technology is an object that a designer can use for displaying a user interface. In the .NET framework, view technologies are represented by the
308
I N T R O D U C T I O N
T O
D E S I G N E R S
System.ComponentModel.Design.ViewTechnology enumeration. This enumeration currently supports only two values: ViewTechnology.WindowsForms, which indicates that a Windows Forms control will be used to display the user interface; and ViewTechnology.Passthrough, which indicates that the display should be passed to the development environment. The development environment understands view objects that are ActiveX controls, active documents, and IVsWindowPane instances. A view object that uses the Passthrough technology should implement any required interfaces that the development environment expects. When using the WindowsForms view technology, the development environment knows that the view will always be an instance of Control. You will be able to see how view technologies are used in Chapter 15, when we implement a custom windows forms designer. But for starters, the view object is the area in the designer of a root component that is visible to you, mainly the white area surrounding the control being designed.
Web Forms Designer Architecture Web forms designers are very much similar to windows forms designers, except that a web forms designer typically begins with HtmlControlDesigner. This designer utilizes DHTML behaviors when displaying an HTML control or web control to the user. Also, with web forms, the Page is typically the root component. IHtmlControlDesignerBehavior The System.Web.UI.Design.IHtmlControlDesignerBehavior interface is used by the designer to attach a behavior to a control that is being designed. This behavior is similar to styles that are part of HTML elements, or behaviors that are attached to DHTML components. Here are the properties and methods of this interface: Designer: This property gets or sets the HtmlControlDesigner associated with this behavior. DesignTimeElement: This property returns an object that represents the html element that this behavior is associated with. GetAttribute: This method returns an object that represents the specified html attribute.
309
C H A P T E R
1 2
GetStyleAttribute: This method returns an object that represents the specified html style attribute. RemoveAttribute: This method removes the specified html attribute from the design time element. RemoveStyleAttribute: This method removes the specified html style attribute from the design time element. SetAttribute: This method sets the specified html attribute to the specified object. SetStyleAttribute: This method sets the specified html style attribute to the specified object.
The Root Designer Among all of the designers for a particular document or component, there must be one root designer, which is the master designer that provides the user interface for interaction. This designer will also support drag and drop of other components. The component that the root designer is associated with is called the root component. For example, in a typical Windows Forms project, you will most likely have a Form that is part of the project. You are able to drag several components and controls onto this form. Each component or control will have its own associated designer, while the form itself must have its own designer. Because the form is the topmost component in which drag and drop and other UI interaction can take place, it is referred to as the root component. User interaction on the form is enabled through its designer, which is the root designer. Similarly, in ASP.NET, the root component is normally the Page object. Root designers are represented by an interface, IRootDesigner. This interface provides design time support for the topmost component, and enables user interaction on it. As stated earlier, two classes in the .NET framework implement this interface directly: DocumentDesigner, which provides scrollable support for top level designers and ComponentDocumentDesigner, which enables the addition of child components to the root component. Here are the members of this interface again, which were also mentioned at the beginning of this chapter: SupportedTechnologies: This property returns an array of ViewTechnology values that the designer can support for its display. GetView: This method returns a view object for the specified view technology. The object returned will be an instance of Control if the view technology is defined as
310
I N T R O D U C T I O N
T O
D E S I G N E R S
ViewTechnology.WindowsForms. However, the caller should cast the return value with caution, as the object returned can in general be of any type.
Service Providers Designers and components can obtain other services through the use of service providers. A .NET service provider is represented by the System.IServiceProvider interface. The interface provides a mechanism for retrieving a specific service object, given the type of object to retrieve. Here is the only member of this interface: GetService: This method, when overridden, will return an object that represents the specified service type. If the service provider cannot return an object of the specified type, then callers should expect the return value to be null. Services are the basic building blocks of the .NET design-time architecture. They help to expose access to specific features from external objects. Some common designer services will be discussed soon. But first, we must talk about the mechanism for adding and removing services so that a service provider is aware.
Service Container A service container allows for services to be added and removed to it. Therefore, external objects can have more control over what a service provider exposes. A service container is actually a service provider. The interface that represents a service container is System.IServiceContainer, which is derived from IServiceProvider, as shown: public interface IServiceContainer : IServiceProvider
Here are the two methods of this interface: AddService: This method adds the specified service to the service container. This method is overloaded, as follows: void AddService( Type serviceType, object service );
311
C H A P T E R
1 2
This overload adds the specified service to the container, using its service type as the key.
void AddService( Type serviceType, ServiceCreatorCallback callback );
This overload adds the specified service to the container, using its service type as the key and a delegate to create the service. Using this method, the service will not be created until it is actually requested. This makes since for services that consume lots of resources.
void AddService( Type serviceType, object service, bool promote );
This method works exactly as the first overload, except that it promotes the service to parent service containers if the third parameter is true.
void AddService( Type serviceType ServiceCreatorCallback callback bool promote );
This method works exactly as the second overload, except that it promotes the service to parent service containers if the third parameter is true.
RemoveService: This method removes the specified service from the service container. This method has two overloads. The first overload takes a Type
312
I N T R O D U C T I O N
T O
D E S I G N E R S
parameter to be used as the key for removing the service. The second overload uses both a Type parameter and a Boolean parameter, indicating whether the service should be removed from any parent service containers as well. The .NET Framework comes with a class that is already derived from IServiceContainer to be used with the designer framework. This class is System.ComponentModel.Design.ServiceContainer. This class has two constructor overloads: the default constructor, and one that takes a parent service provider as a parameter. This class uses a Hashtable to store the available services. It stores both service instances and service callbacks into this table for retrieval. Here is a snippet of how the GetService method is implemented: private IServiceProvider _parentServiceProvider; private Hashtable _services = new Hashtable(); ... public object GetService(Type serviceType) { object service = null; if (serviceType == typeof(IServiceContainer)) { service = this; } else { service = Services[serviceType]; } if (service is ServiceCreatorCallback) { service = ((ServiceCreatorCallback)service)(this, serviceType); if (service != null && !service.GetType().IsCOMObject && !serviceType.IsAssignableFrom(service.GetType())) { service = null; } Services[serviceType] = service; } if (service == null && parentProvider != null) { service = _parentServiceProvider.GetService(serviceType); } return service; }
313
C H A P T E R
1 2
In the code snippet above, all available services are contained in the Hashtable. When a particular service is requested, the method first checks to see if the service type requested is of type IServiceContainer. If it is, the method returns the current service container. If not, it checks the Hashtable. If the value returned from the Hashtable is of type ServiceCreatorCallback, the method invokes the handler attached to the delegate, as shown: service = ((ServiceCreatorCallback)service)(this, serviceType);
Otherwise, the value that is retrieved from the Hashtable will be returned from the method, even if it is null. Now you can see that a service container is not only a service provider, but is also a service itself, since its own instance can be returned.
Common Designer Services Many useful designer services come prepackaged with the .NET framework. These services are part of the System.ComponentModel.Design namespace. Besides the IServiceContainer described above, the common designer services are discussed next. IUIService This service allows interaction with the object that is hosting a designer. It is used to display message boxes, error messages, tool windows, and the component editor; and retrieve ambient properties of the host. For those unfamiliar with ActiveX development, an ambient property of an object is a property that is held and exposed by the object’s container. This way, a particular property of an object will have values that are dependent on the environment that the object is operating in. IToolboxService This interface provides access to the toolbox in a particular development environment. For Visual Studio .NET control developers, the development environment will most likely be the Visual Studio .NET IDE. Through this service, designers can configure the toolbox by adding and removing toolbox items, among other things.
314
I N T R O D U C T I O N
T O
D E S I G N E R S
IMenuCommandService This interface provides access to the Visual Studio .NET menu. It allows the addition and removal of designer verbs. It also exposes a way to show a context menu.
IInheritanceService This service allows a designer to identify inherited components of a particular component. The System.ComponentModel.Design.InheritanceService class implements this interface for tracking which components are inherited and to select which inherited components that should be ignored. It also returns the InheritanceAttribute for a specified component. Remember that designers use the InheritanceAttribute to draw an inheritance glyph on a particular component. ISelectionService This service provides an interface so that designers can select components. Designers can then use the selected component to show its properties in the properties window. The selection service supports multiple selections, but always defines only one primary selection. IDesignerEventService This service provides event notifications for the addition and selection of designers. Whenever a designer is created and destroyed, DesignerCreated and DesignerDisposed events are raised, respectively. Whenever the design-view changes, the SelectionChanged event is raised. Whenever the active designer changes, ActiveDesignerChanged event is raised. This service will typically be queried through the IDesignerHost interface, since it manages all designers. IDesignerOptionService This service provides access to the Options dialog available on the Tools menu of Visual Studio .NET. The options that can currently be set and retrieved include GridSize, ShowGrid, and SnapToGrid.
315
C H A P T E R
1 2
IComponentChangeService This service allows a designer to raise notifications of component changes that occurred in association with the designer.
Introducing the DesignerHost As stated briefly, a designer host is used to manage all of the designers related to a particular component at design time. A designer host is represented by the System.ComponentModel.Design.IDesignerHost interface. This interface implements both IServiceContainer and IServiceProvider. This service inherently provides support for designer transactions and component management. Here are some of the methods, properties and events of this interface, with the exception of transaction related members, which are discussed with designer transactions in Chapter 13. Container: This property returns the container for the designer host. Through the container, you can retrieve all components on the designer document. Loading: This property returns a Boolean value indicating whether the designer is currently loading the document. RootComponent: This property returns an instance of IComponent that represents the base root component of the designer document. For example, you may derive a class from the root class, thereby automatically assigning the derived class the root designer. When the derived component is being designed, this property will return the base component, since that is the first component to which the Root Designer was applied. RootComponentClassName: This property returns a string representing the fully qualified class name of the root component. If you derive a component from the root component, then the name of the derived component’s class will be returned. Activate: This method activates the designer to be used for display. Designers must be activated before being displayed. CreateComponent: This method creates a component of the specified type and adds it to the designer document. This method is overloaded, with the second overload providing a string parameter that represents the component’s name. The first overload creates a default name for the component using the INameCreationService.
316
I N T R O D U C T I O N
T O
D E S I G N E R S
DestroyComponent: This method destroys the specified component and removes it from the designer document. GetDesigner: This method returns an IDesigner instance for the specified designer type. Here is this method in action: IDesignerHost host = ... IDesigner designer = host.GetDesigner(typeof(MyRootDesigner));
GetType: This method returns a Type instance given a fully qualified string name of the type. Activated: This event is raised when the designer is activated. Deactivated: This event is raised when the designer is deactivated. LoadComplete: This event is raised when the designer document has been completely loaded.
Summary The designer architecture exposed by the .NET framework is very rich and extensible. A designer can be written for any object that derives from IComponent. Both Windows Forms and Web Forms utilize the same designer concepts and framework modules. Designers allow a different presentation to the user at design time than what is actually available at runtime. This separation of logic allows runtime code to live separately from design time code, which can reduce the number of redistributables that will be packaged with your application. In this chapter, we have introduced all of the basics of the designer architecture in .NET. This included the designer hierarchy as well as the interfaces that allow designing a component to be possible. In the next chapter, we will talk about some of the objects that can support designers.
317
13 Chapter
Design-Time Support Overview Design time support refers to the behavior and display of a component or control in a visual designer. It includes showing components in toolboxes, displaying and invoking menu commands related to components, drawing the component on the design surface, and serializing the component into code. In the last chapter we introduced the design time framework for both Windows Forms and Web Forms. We discussed the many designers that ship with the .NET framework and the relationship between each. In this chapter, we will discuss each of the elements mentioned above on how each supports the designer.
The Toolbox The Visual Studio .NET toolbox displays a variety of components and controls that can be added to a designer document. The items in the toolbox, as well as the selected toolbox tab, will change depending on the designer that is currently in use. There are generally two ways to apply a toolbox item to a component. The first and simple way is to use an attribute. The second way involves a little more work. Specifically, it involves instantiating a class of type ToolboxItem, and using the IToolboxService to add the item to the toolbox. The attribute concept is discussed next.
C H A P T E R
1 3
ToolboxItemAttribute Toolbox items can be associated with a component through the use of the System.ComponentModel.ToolboxItemAttribute. This class has two static fields: Default, which indicates that a default ToolboxItem instance will be associated with the component; and None, which indicates that no toolbox item will be associated with the component. The constructor for this attribute is overloaded as follows: public ToolboxItemAttribute( bool defaultType );
This constructor associates the default toolbox item if the parameter is true, or no item if it is false.
public ToolboxItemAttribute( string toolboxItemTypeName );
This constructor creates a ToolboxItem instance given the fully qualified type name of the toolbox item.
public ToolboxItemAttribute( Type toolboxItemType );
This constructor creates a ToolboxItem instance given the type of ToolboxItem to create.
Here is the general syntax in associating a toolbox item with a component: [ToolboxItem(true)] public class MyControl : Control
320
D E S I G N - T I M E
S U P P O R T
ToolboxItem As stated earlier, toolbox items can also be applied to components by creating an instance of ToolboxItem directly. Before we look into adding an item to the toolbox, we will first examine the structure of the ToolboxItem class. Here are the members of this class: AssemblyName: This property gets or sets an AssemblyName instance that specifies the assembly that contains the type that this toolbox item represents. Bitmap: This property gets or sets a bitmap that will represent the toolbox item on the toolbox. DisplayName: This property gets or sets the display name for the toolbox item. This is the name that will be displayed alongside the bitmap on the toolbox. Filter: This property gets or sets an array of ToolboxFilterAttribute instances that represent the filter for the toolbox. Toolbox filters allow toolbox items to only, never, or always be available for certain designers. TypeName: This property gets or sets the fully qualified name of the type of component that the toolbox item will create. Locked: This protected property returns a Boolean value indicating whether the toolbox item is currently locked. CreateComponents: This method creates an array of IComponent instances for the type that this toolbox item represents. This method actually calls CreateComponentsCore. It also calls OnComponentsCreating first and OnComponentsCreated last. Initialize: This virtual method initializes the toolbox item with the specified component type. Lock: This method synchronizes access to the toolbox item, by setting the Locked property to true. CheckUnlocked: This method throws an InvalidOperationException if the toolbox item is not locked. Several of the properties use this method to make sure only one user is modifying the toolbox item. User, in this context, means code.
321
C H A P T E R
1 3
CreateComponentsCore: This protected virtual method is used to create components for the specified designer host. It returns an array of IComponent instances that were created on the host. Serialize: This protected virtual method saves the state of the ToolboxItem into the specified serialization object. Deserialize: This protected virtual method deserializes and loads the state of the ToolboxItem from the specified serialization object. OnComponentsCreating: This protected virtual method raises the ComponentsCreating event. OnComponentsCreated: This protected virtual method raises the ComponentsCreated event. ComponentsCreating: This event is raised whenever components that are associated with this toolbox item are about to be created. This event is also raised while the components are in the process of being created. The components need not be created from the toolbox in order for this event to be raised. The delegate for this event is a ToolboxComponentsCreatingEventHandler. This delegate has a parameter, ToolboxComponentsCreatingEventArgs, which exposes the designer host as a property. The designer host exposed is the host on which the components are being created. ComponentsCreated: This event is raised whenever components are created that are associated with this toolbox item. The components need not be created from the toolbox in order for this event to be raised. The delegate for this event is a ToolboxComponentsCreatedEventHandler. This delegate has a parameter, ToolboxComponentsCreatedEventArgs, which exposes an array of IComponent instances that should be added to the toolbox.
IToolboxService The System.Drawing.Design.IToolboxService interface provides access to the toolbox in the development environment. This service can be used as an alternative to using the ToolboxItemAttribute. Here are the members of this interface:
322
D E S I G N - T I M E
S U P P O R T
CategoryNames: Returns a CategoryNameCollection that includes all of the categories currently on the toolbox. This collection derives from System.Collections.ReadOnlyCollectionBase, which indicates that the collection cannot be modified externally. A toolbox category is the name that is displayed on a toolbox tab. Some common categories include “Windows Forms”, “Web Forms”, “General”, and “Components”. SelectedCategory: This property gets or sets the name of the currently selected toolbox tab. AddCreator: This method adds a new toolbox item creator to the toolbox. A toolbox item creator is a delegate instance of type ToolboxItemCreatorCallback. RemoveCreator: This method removes a previously added toolbox item creator. AddLinkedToolboxItem: This method adds a linked toolbox item to the toolbox. A linked toolbox item exists only when the specified designer host is alive. Once the designer host is destroyed, the toolbox item will be destroyed also. Therefore, the toolbox item will only be created when a project and a particular designer are opened, and will be destroyed when the project is closed. AddToolboxItem: This method adds the specified toolbox item to the toolbox. RemoveToolboxItem: This method removes the specified toolbox item from the toolbox. GetSelectedToolboxItem: This method returns the currently selected ToolboxItem. SetSelectedToolboxItem: This method sets the currently selected ToolboxItem to a new value. GetToolboxItems: This method returns a collection of ToolboxItem instances from the toolbox. This method is overloaded to either return all items, items specific to a category, items specific to a designer host, or items specific to both a category and a designer host. IsSupported: This method returns a Boolean value indicating whether the specified serialized toolbox item can be used by the designer host. This method is also overloaded to return a Boolean value indicating whether the serialized toolbox item matches the specified collection of attributes. IsToolboxItem: This method returns a Boolean value indicating whether the specified object is serialized as a toolbox item. This method is typically used in conjunction
323
C H A P T E R
1 3
with drag and drop, so that a designer can make sure that the item being dropped is a toolbox item before allowing the drop to take place. Refresh: This method refreshes the state of the toolbox. How the refresh operates is up to the inheritor. SelectedToolboxItemUsed: This method notifies the toolbox that the selected toolbox item has been used. The toolbox will then generally set the currently selected tool to a null reference. SerializeToolboxItem: This method serializes a ToolboxItem into an object that can be persisted. DeserializeToolboxItem: This method creates a ToolboxItem instance from an object that represents a serialized toolbox item. SetCursor: This method sets the application’s cursor to a cursor that represents the currently selected toolbox item.
ToolboxItemFilterAttribute Designers and components have the ability to filter the toolbox items that they support. This is done through the ToolboxItem class and the ToolboxItemFilterAttribute class. This class has overloads used to filter toolbox items based on a particular string, and optionally a ToolboxItemFilterType, which is used to indicate the type of filtering, such as required or prohibited. When the filter is applied, any ToolboxItem instances with a Filter property equal to the filter of this attribute will match the condition. The action taken when the condition is matched depends on the ToolboxItemFilterType value. Both the designer and the component must have the same filter applied in order to enforce restrictions. Here are the values of the ToolboxItemFilterType enumeration, and the descriptions of what they mean: Allow: This value simply indicates that the filter exists. It doesn’t support any restrictions. Prevent: When designers and components have the same filter name , this value disables the toolbox item. Require: This value requires that components and designers have the same filter name in order for the toolbox item to be enabled.
324
D E S I G N - T I M E
S U P P O R T
Custom: This value indicates that the availability of the toolbox item will be determined at “designer runtime”. For this to happen, the designer must implement the IsSupported method of the IToolboxUser interface. Here is a snippet of how this attribute is used: [ToolboxItemFilter("12345", ToolboxItemFilterType.Require)] public abstract class MyRootDesigner : ComponentDesigner, IRootDesigner [ToolboxItemFilter("12345", ToolboxItemFilterType.Prevent)] public abstract class MyComponent : Component
The designer says, “Only components that have the filter 12345 should be enabled.” This component says, “Only components that do not match my filter should be enabled.”
ToolboxBitmapAttribute Every toolbox item will be assigned a default bitmap. However, it is possible to assign custom bitmaps to your components. This is done via the ToolboxBitmapAttribute. This attribute is overloaded to take a filename, a type, or a fully qualified resource name for creating a toolbox bitmap. Creating and Applying a Toolbox Bitmap When you design a bitmap to be used on the toolbox, make sure it is 16 X 16 pixels. The perimeter of the bitmap is used as a mask, so make sure it is the same color all the way around, as shown:
325
C H A P T E R
1 3
If you do not allow a mask, you will get unpredicted results when trying to view your bitmap on the toolbox. Sometimes, it may not even show up at all. So remember the perimeter. When applying a bitmap through the use of the ToolboxItem class, call the “MakeTransparent” method of the class’s Bitmap property. When assigning the attribute, there are generally three ways to associate a bitmap with your component. The first way is the use a filename for the bitmap, as shown: [ToolboxBitmap(@“C:\Wizard.bmp”)]
You can also specify a resource name if the bitmap is stored in the assembly as an embedded resource. Here is the syntax: [ToolboxBitmap(typeof(Wizard), “ToolboxBitmaps.Wizard.bmp”)]
The first parameter identifies the assembly from which the bitmap will be loaded. This is done by providing a type. The type may either be defined in the current assembly or in a referenced assembly. Any type in the assembly may be used, as long as its namespace corresponds to the namespace of the resource. The second parameter specifies the resource name of the bitmap. The resource name, together with the namespace of the type, must represent the fully qualified name of the resource, which is the resource namespace plus the resource name. A resource namespace is equal to the Default Namespace, as defined in the project’s properties, plus any subfolders in the project that contains the resource. Using an example, “ToolboxBitmaps.Wizards.bmp” indicates to look for the Wizard.bmp file in the ToolboxBitmaps sub-namespace. Sub-namespaces for resources are created simply by creating subfolders in your project. In other words, the “ToolboxBitmaps” sub-namespace is really a subfolder in the project. One way to determine the namespace for your resource is to use ILDASM. You can also specify a class type to load a bitmap, as shown: [ToolboxBitmap(typeof(Wizard)]
326
D E S I G N - T I M E
S U P P O R T
When specifying a class type, the bitmap must be part of the same namespace as the class. Remember, a resource namespace is equal to the Default Namespace, as defined in the project’s properties, plus any subfolders that contain the resource.
Designer Verbs If your control or component supports common actions, such as auto formatting a grid or adding buttons to a toolbar, it can be useful to expose “verbs” for the component. Verbs are displayed on the bottom pane of the property browser, and on the context menu for the component when in design mode. In short, a verb is simply a command. Verbs are termed “designer verbs” when they exist in design mode only. To see an example of a designer verb, drag a Calendar web control to a web form and select the control in the designer, as shown:
Now look at the property browser. If the property browser is not visible, chose View | Properties Window from the Visual Studio .NET main menu. Your properties window should look similar to the following, provided you did not change any properties of the control:
327
C H A P T E R
1 3
Notice the “Auto Format...” link at the bottom of the properties window. This link was added by the Visual Studio .NET designer framework to invoke a designer verb. This verb is also added to the context menu of the Calendar control, as shown:
328
D E S I G N - T I M E
S U P P O R T
Notice the “Auto Format...” menu item at the bottom of the menu. Both the link and the menu item will invoke the same command that is attached to the verb. Now that we know what designer verbs are used for, let’s look at their role and structure in the designer framework. A designer verb is represented by the System.ComponentModel.Design.DesignerVerb class, which is derived from System.ComponentModel.Design.MenuCommand. A formal definition of a designer verb is that it is a menu command that is linked to an event handler. The DesignerVerb class has two constructor overloads, as shown: public DesignerVerb( string text, EventHandler handler );
This overload initializes a new instance of the DesignerVerb class using the first parameter as the display of the verb, and the second parameter as the event handler to be called when the verb is invoked. It is a common practice to append “...” to the verb when an external window
329
C H A P T E R
1 3
or dialog will be invoked in conjunction with the verb. Using the ellipsis conforms to UI guidelines.
public DesignerVerb( string text, EventHandler handler, CommandID startCommandID );
This overload works exactly the same as the first overload, except that a specific CommandID will be associated with the verb. The CommandID is used as the new start id for commands to follow. A default CommandID is assigned if the first constructor is used. Before we look at the other members of the DesignerVerb class, we must first understand the usage of the CommandID. A CommandID is a unique identifier for a command, which includes both a GUID identifier to represent a menu group, and a numeric identifier to represent a menu item within that group. The .NET framework comes with a set of standard commands, which come from the System.ComponentModel.Design.StandardCommands and System.Windows.Forms.Design.MenuCommand classes. Both of these classes contain a set of static fields that return CommandID values for standard commands exposed by a particular host or IDE. Now, here are the members of the DesignerVerb class: Checked: This virtual property gets or sets a Boolean value indicating whether the menu command should be checked. Naturally, this does not apply to the command when it is displayed on the properties window. The default value of this property is false. CommandID: This virtual property returns the CommandID associated with the menu command. Enabled: This virtual property gets or sets a Boolean value indicating whether the menu command should be enabled. The default is true. OleStatus: This virtual property returns an integer representing the OLE status code for this menu command. These flags determine the state of the menu item. These flags reflect the properties that have been set on this class, in bitwise form.
330
D E S I G N - T I M E
S U P P O R T
Supported: This virtual property returns a Boolean value indicating whether this menu item is supported. When this property returns false, the Visual Studio .NET context menu does not display this command, but the properties window will continue to do so. The default is true. Text: This property returns a string that represents the text that is displayed for this menu item. Visible: This virtual property gets or sets a value indicating whether this menu item is visible. The default is true. OnCommandChanged: This protected virtual method raises the CommandChanged event. This method is invoked in response to certain OLE status changes. In particular, whenever the command becomes checked or disabled, or vice versa, this method will be called. CommandChanged: The event provides notification that the command has become checked or disabled, or the opposite of those states. The IDesigner interface has a virtual property, Verbs, which returns an instance of System.ComponentModel.Design.DesignerVerbCollection. The designer framework invokes this property each time the context menu is invoked or the control is selected. Therefore, you should create a verb collection only once, and return that collection from the Verbs property. An alternative is to add your collection to the base Verbs property in the constructor, but doing so would not override any already added verbs by the base class. Here is a sample control and designer that illustrate the use of designer verbs. Suppose that your company has a standard which states that any Panel controls added to a web form must be one of three colors: red, white, or blue. To make it easier for developers to adhere to this standard, you could override the Panel control just to assign it your own designer, which will use verbs for easy color assignment. Here is the code: [Designer(typeof(ColorPanelDesigner))] public class ColorPanel : Panel { } public class ColorPanelDesigner : PanelDesigner { public ColorPanelDesigner() { DesignerVerb verbRed = new DesignerVerb(“Red”, new EventHandler(this.OnVerbRed));
331
C H A P T E R
1 3
DesignerVerb verbWhite = new DesignerVerb(“White”, new EventHandler(this.OnVerbWhite)); DesignerVerb verbBlue = new DesignerVerb(“Blue”, new EventHandler(this.OnVerbBlue)); base.Verbs.AddRange(new DesignerVerb[]{verbRed, verbWhite, verbBlue}); } protected Panel Control { get { return (Panel)this.Component; } } private void ChangeColor(Color newColor) { PropertyDescriptor pd = TypeDescriptor.GetProperties(Control)[“BackColor”]; pd.SetValue(newColor); } private void OnVerbRed(object sender, EventArgs e) { ChangeColor(Color.Red); } private void OnVerbWhite(object sender, EventArgs e) { ChangeColor(Color.White); }
Note that we use a PropertyDescriptor to change the BackColor of the Panel, instead of changing the property directly. This is because the designer framework relies on ComponentChanged events in order to refresh the designer’s view. This is normally done via the IComponentChangeService. When setting properties through the PropertyDescriptor, these events are raised automatically, as necessary. Here is a view of the designer for the code above:
332
D E S I G N - T I M E
S U P P O R T
An alternate way to add new commands to the Visual Studio .NET designer menu is to retrieve the IMenuCommandService from the designer host, and invoke its AddCommand method. This method takes an event handler and a CommandID. But because the IDesigner interface already exposes a Verbs collection, it’s much easier to use it instead.
Extender Provider An extender provider is a component that provides other components with its own properties. The other components are said to be extended. For example, when a ToolTip is placed on a windows form, it provides a string property named ToolTip to all other controls that are on the form. At runtime, whenever the mouse hovers over a particular control, the tool tip text that was set for that control is displayed. Extender providers are not directly related to designers. But using the designer to manipulate an extender provider will show you that the properties of a component are extended, as if the properties were part of the original component. This concept is similar to the
333
C H A P T E R
1 3
PreFilterProperties method of a designer, except that extender providers work during runtime as well. To see an extender provider in action, drag a ToolTip component from the toolbox to a Windows Form. Also, drag some more controls to the form, such as a textbox and a button. Your designer and properties window should look similar to the following picture:
Notice that when selecting the button, a new unfamiliar property named “ToolTip on toolTip1” suddenly displays in the properties window. This indicates that the ToolTip property of the toolTip1 component has been extended to the button1 control. After setting the text and running the application, the text is displayed whenever the mouse hovers over the button. The same is true for the textBox1 control.
Implementing an Extender Provider Several extender providers are shipped with the .NET framework. The ErrorProvider provides a UI that indicates an error in user input for a control. The HelpProvider provides popup and online help for controls. We have already discussed the ToolTip. We will now discuss the process in implementing a custom extender provider.
334
D E S I G N - T I M E
S U P P O R T
An extender provider is represented by the System.ComponentModel.IExtenderProvider interface. This interface supports a single method, CanExtend, which takes an object instance as a parameter and returns a Boolean value indicating whether it can extend that object. When a component implements this interface, each property of the component that should be extended to other components should be identified. This is done through an attribute. We will now go over the steps. 1. Derive a component from IExtenderProvider and implement the CanExtend method. Return true from CanExtend if the component can be extended. Otherwise return false. public class MyComponent : Component, IExtenderProvider { public bool IExtenderProvider.CanExtend(object component) { return (component is Control); } }
2. Define a set of properties that should be extended to other components. These properties are actually methods, because they must be prefixed with Get and Set. For example, to extend a property named MyProperty, you would create two methods named GetMyProperty and SetMyProperty. Both of these methods should take an extra parameter identifying the component to be extended. This is by convention. Here are some examples: public string GetMyProperty(Control control) { ... } public SetMyProperty(Control control, string value) { ... }
3. Add a ProvidePropertyAttribute to the class definition for each property that will be extended. This attribute accepts two parameters. The first parameter identifies the property name to be extended. The second parameter identifies the type of object that can be extended. This type should match the type of the parameter on the Get and Set methods. [ProvideProperty(“MyProperty”, typeof(Control)]
335
C H A P T E R
1 3
public class MyComponent : Component, IExtenderProvider
Extender providers make it easy to extend an existing architecture. You can simply plug new functionality into existing components, without touching the existing components at all.
Persistence Persistence is defined as the art of the loading and saving of data to and from a persistent medium. This medium may be a database, flat file, or even memory. The .NET framework allows objects to be saved in any format. Visual Studio .NET persists data either through code generation or resources, with code generation set as the default. Because persistence is supported by the framework, both the web forms and windows forms architectures support persistence. However, there are differences in the ways and mechanisms that data is persisted for each architecture.
Persistence in Windows Forms As stated above, persistence can be done through either code generation or resources. Windows forms data can be persisted in resources simply by setting the Localization property of a form to true. The data will then be serialized in an XML based “resx” file. Visual Studio .NET, by default, persists object values in code. Consequently, persistence in windows forms goes hand in hand with serialization. Serialization is the process of converting an object’s state in a way so that it can be persisted to some medium. We will discuss this further in the section on Serialization.
Persistence in Web Forms Persistence in web forms can be done only through code generation. However, the code generation technique is two-fold. First, components that derive from IComponent are all persisted as code in the code-behind file. On the other hand, html controls and server controls are persisted on the page. Because of the html markup supported by web pages, property values can either be persisted as attributes, elements, or text.
336
D E S I G N - T I M E
S U P P O R T
PersistenceModeAttribute The System.Web.UI.PersistenceModeAttribute specifies how a property should be saved by the designer. This attribute is usually necessary when creating custom web server controls. It can only be applied to properties of an object. This attribute takes one parameter, which is a System.Web.UI.PersistenceMode enumeration value. Here are the supported values of the PersistenceMode enumeration: Attribute: This value specifies that the property should be persisted as an HTML attribute of the server control. You would typically use this value for simple, or primitive, properties. InnerProperty: This value specifies that the property should be persisted as a nested element within the web server control’s opening and closing tags. You would typically use this value for complex, or non-primitive, properties; collections; and templates. InnerDefaultProperty: This value specifies that the property should be persisted as text within the web server control’s opening and closing tags. The Label web control applies uses this value to persist its Text property. Only one property of a control should have this persistence mode applied. EncodedInnerDefaultProperty: This value is the same as InnerDefaultProperty, except that the persisted value is HTML encoded. Let’s look at an example for clarity: public class SearchControl : Control { private TextBox _searchTextBox;
}
[PersistenceMode(PersistenceMode.InnerProperty)] public TextBox SearchTextBox { get { return _searchTextBox; } set { _searchTextBox = value; } }
The above example would be persisted as follows:
337
C H A P T E R
1 3
<SearchTextBox Text=”Some Value” />
PersistChildrenAttribute System.Web.UI.PersistChildrenAttribute is another metadata extension that can be applied to server controls. This attribute takes a Boolean property that indicates whether the properties of the server control should be persisted as nested server controls. If the parameter passed is true, properties are persisted as nested server controls, with the runat attribute set to true. If the parameter is false, properties are persisted as nested elements only, with no tag prefix and no runat attribute. Note, however, that the PersistenceModeAttribute still determines whether a property is persisted as an inner property. PersistChildren simply controls the type of nesting. The PersistChildrenAttribute can only be applied to classes. If this attribute is not applied, the default value of false is assumed by the designer. ControlPersister The System.Web.UI.Design.ControlPersister class provides the methods necessary to save the properties of a control. It uses the metadata of a property, including the PersistenceModeAttribute discussed above, to determine how the persistence should be done. You will probably never have to call any methods of this class directly. It is called automatically be the web forms designer framework.
Serialization Designer serialization is the process of converting object and property values into source code for recreation of the object at runtime. This includes generating code to call the appropriate constructors as well as to set and retrieve property values.
DesignerSerializationVisibility The System.ComponentModel.DesignerSerializationVisibilityAttribute specifies what part of a property should be serialized by the designer. It can only be applied to properties. This attribute is usually necessary when creating custom web server controls. This attribute takes one parameter, which is a System.ComponentModel.DesignerSerializationVisibility
338
D E S I G N - T I M E
S U P P O R T
enumeration value specifying how a property should be serialized. Here are the supported values of the DesignerSerializationVisibility enumeration: Visible: This value specifies that the object should be serialized in a default way. Default means that for a non-collection property, the property’s value is serialized; for a collection property, the values of the property’s child properties are serialized. You would typically use this value to serialize simple properties. Content: This value specifies that the contents of the object should be serialized. You would typically use this value for complex objects and collections. Note that collections do not need this value applied, however, since the designer will serialize their contents this way by default. Hidden: This value specifies that the contents of the object should not be serialized. When this value is applied, the code generator does not produce code for the property. Applying the System.ComponentModel.DesignOnlyAttribute to a control’s property will cause the code generator to ignore the property when serializing the control.
Example of DesignerSerializationVisibility in Windows Forms Here is an example of DesignerSerializationVisibility on a windows forms control: public class SearchControl : Control { private TextBox _searchTextBox; [DesignerSerializationVisibility(DesignerSerializationVisibilit y.Content)] public TextBox SearchTextBox { get { return _searchTextBox; } set { _searchTextBox = value; } } }
On a windows form, the above control would get serialized as follows:
339
C H A P T E R
1 3
this.MySearchControl = new SearchControl(); this.MySearchControl.SearchTextBox.Length = 1; this.MySearchControl.SearchTextBox.BackColor = System.Drawing.Color.White;
Because the Content value was used, the properties of the SearchTextBox control were serialized. The output above assumes that the properties Length and BackColor of the control were set on the designer. Example of DesignerSerializationVisibility in Web Forms Let’s now look at the same example as above, but this time we will assume that the control is a web server control. Using the same DesignerSerializationVisibility value, the code generator would produce the following markup on a web page:
This output also assumes the PersistenceMode.Attribute value. Otherwise, the DesignerSerializationVisibility.Content value would have no effect. Here are the possible combinations and results for using the DesignerSerializationVisibilityAttribute and the PersistenceModeAttribute together on a web control: PersistenceMode
DesignerSerializationVisibility
Result
______________
Hidden
Not Serialized
Attribute
Visible
Property Serialized
InnerProperty
Visible
Not Serialized
InnerDefaultProperty
Visible
Not Serialized
EncodedInnerDefaultProperty
Visible
Not Serialized
Attribute
Content
Child Properties Serialized
340
D E S I G N - T I M E
S U P P O R T
Designer Serializers A designer uses the services from the System.ComponentModel.Design.Serialization.IDesignerSerializationManager to serialize components into code. Visual Studio .NET, be default, uses the System.ComponentModel.Design.Serialization.CodeDomSerializer to generate C# code for property values of components on a designer. In general, it will support any language that targets the .NET framework. We only mention C# because that is what this book is based on. The advantages that code generation has over using resources are twofold. First and foremost, code generation makes it easier for a developer to understand what’s going on. Much of the time, hard coded strings tell a developer exactly what a particular line of code is supposed to do, versus using a constant or some value from a resource file. Problems can easily be isolated in a debugging cycle. Secondly, performance is improved because the setting of a property value can be configured to happen as fast as the assembly instructions and CLR will execute. The downside is that code generation can become extremely complex. Luckily, the .NET designer framework has provided a model that is very flexible and should suit most needs. Designer Serialization Architecture Serialization in the designer framework begins with serialization managers. These managers allow the addition and removal of designer serialization providers. A designer serialization provider has access to one or more designer serializers. There is always one default serialization provider, which may or may not be a physical provider, used to provide serializers to a requestor. A pictorial view of the architecture is shown here:
341
C H A P T E R
1 3
Designer Serialization Manager
Manages
Offers Custom Designer Serialization Provider (Plus one default provider)
Designer Serializer
We will now discuss each of the actors identified above in detail. Designer Serialization Manager Serialization managers are represented by the IDesignerSerializationManager interface. This interface can be used by designers to access the services that manage the design-time serialization process. Therefore, this interface is also a service provider. In general, you may not ever have the need to implement this interface. Most custom serialization and deserialization techniques should be handled by the serializer itself. Here are the members that this interface supports: Context: This property returns an instance of System.ComponentModel.Design.Serialization.ContextStack. A ContextStack is a user-defined storage area, with Push and Pop methods to behave like any normal stack. This ContextStack class is primarily used for cross-serializer communication. Care should be taken when implementing a custom designer serializer and using the context stack. Serializers should know exactly how and what objects were pushed onto a stack by a calling serializer.
342
D E S I G N - T I M E
S U P P O R T
Properties: This property returns a PropertyDescriptorCollection containing custom properties that can be serialized. These properties are normally not the properties of the type being serialized. They are primarily helpers that serializers can use to control the serialization process. AddSerializationProvider: This method adds the specified serialization provider to the current IDesignerSerializationManager instance. You would call this method to alter the process of how the serialization manager retrieves a serializer for a particular type. During serialization, the serialization manager will ask each serialization provider to return a serializer for a specified type. If there are no serialization providers, or if all of them return null, the default serializer will be used. RemoveSerializationProvider: This method removes a custom serialization provider from the manager. GetSerializer: This method returns a serializer of the specified type for the specified object type. In general, implementers of this method should look at each of the available custom serialization providers, calling the GetSerializer method of each. If none of the providers return a serializer, then this method should use some default way of obtaining a serializer. This method should return null if no serializer can be obtained for the specified type. CreateInstance: This method creates a named instance of an object of the specified type and adds it to a collection of named types, to be later used by GetInstance. This method takes four parameters, as shown: object CreateInstance( Type type, ICollection arguments, string name, bool addToContainer );
The first parameter specifies the type of object to create. The second parameter is a collection of arguments to be passed to the constructor when creating the object. The third parameter is a string that can be used to access this object via GetInstance. If this value is null, the object will not be retrievable via GetInstance. The last parameter is a Boolean value indicating whether the created object should be added to the design container. For this to have any effect, the type must implement IComponent.
343
C H A P T E R
1 3
GetInstance: This method retrieves an instance of a created object by the specified name. GetName: This method returns a string representing the name of the specified object. SetName: This method sets the name of the specified object to the specified string. ReportError: Reports an error contained in the specified object. If the object is an Exception, the message of the exception is reported; if the object is any other type, this method calls the ToString method of the object and reports the value returned. ResolveName: This event is raised by the GetName method when it cannot locate an object’s name in the name table. This event has a delegate of type ResolveNameEventHandler, which exists in the same namespace as this interface. This delegate has an EventArgs parameter of type ResolveNameEventArgs, which contains two properties: Name, which returns the name of the object to resolve; and Value, which gets or sets the object that matches the name. SerializationComplete: This event is raised when the serialization process is complete. Designer Serialization Provider Designer serialization providers are represented by the IDesignerSerializationProvider interface. This interface has a single method, GetSerializer, which returns a serializer using the specified serialization manager, default serializer, object type, and serializer type. Serializers should be registered and unregistered via the AddSerializationProvider and RemoveSerializationProvider methods respectively. Designer Serializer A designer serializer is an object whose responsibility is to serialize and deserialize objects from one form to another. An object can tell the designer framework that it will use a custom serializer for saving its state. This is done via the System.ComponentModel.Design.Serialization.DesignerSerializerAttribute. This attribute takes two parameters. The first parameter specifies the type of designer serializer to use. The second parameter specifies the base type of the designer serializer that is being added. The use of a base type allows a collection of designer serializers to be applied to a particular
344
D E S I G N - T I M E
S U P P O R T
component, as long as each one supports a different base type. This allows your components to be serialized differently in different hosting environments. Visual Studio .NET only understands one type of base designer serializer, which is an abstract class named System.ComponentModel.Design.Serialization.CodeDomSerializer. This class in an abstract base class, so any custom serializers should inherit from it. Before we examine the structure of this class, we must first understand what the CodeDOM is. CodeDOM The CodeDOM is an object model provided by the .NET framework that represents source code. Hence, it stands for code document object model. It enables code generation and interpretation in any language that targets the .NET framework, though we only focus on C# in this book. The CodeDOM architecture allows CodeDOM elements to be linked together, forming a CodeDOM graph, which can then be rendered as source code in the desired language. We won’t go into too much detail of the CodeDOM elements, but we will list some common ones with a brief description of each. All of them are represented as classes and are part of the System.CodeDom namespace. Here they are: CodeAssignStatement: This class represents an assignment statement. CodeComment: This class represents a code comment. CodeCommentStatement: This class represents a code statement that contains a single comment. CodeConstructor: This class represents the declaration for a constructor. CodeStatement: This class represents a code statement. This is the base class for other code statement objects. CodeStatementCollection: This class represents a collection of CodeStatement objects. CodeVariableDeclarationStatement: This class represents a statement that declares a variable. CodeThisReferenceExpression: This class represents the current instance of a class. This is often referred to as “this” in C#. CodeMemberMethod: This class represents a single method of a class.
345
C H A P T E R
1 3
CodeMemberField: This class represents a single field of a class. CodeMemberEvent: This class represents a single event of a class. CodeMemberProperty: This class represents a single property of a class. CodeIndexerExpression: This class represents the indexer of a class. There are many more CodeDOM classes; the above list just names a few. We will now discuss the framework-provided serializer that utilizes these elements. CodeDomSerializer Here are the members that the CodeDomSerializer supports: Deserialize: This method deserializes the specified serialized CodeDom object into an object. This method is abstract, and must be implemented in a derived class. Serialize: This method serializes the specified object into a CodeDom object. This method is abstract, and must be implemented in a derived class. DeserializeExpression: This protected method deserializes the specified CodeExpression. This method returns an object that is either a deserialized object or another code expression, depending on the specified expression. For example, if the specified expression can be further simplified, a simplified expression is returned. If the expression cannot be further simplified, it is returned as a deserialized object. DeserializeStatement: This protected method deserializes the specified CodeStatement by executing the statement. DeserializePropertiesFromResources: This protected method deserializes properties from the specified object that contain the specified attributes. This method uses the ResourceCodeDomSerializer to retrieve the properties from the resources. SerializePropertiesToResources: This protected method serializes the properties of the specified object, containing the specified attributes, to the resources. Internally, this method pushes the specified CodeStatementCollection onto the context stack of the serialization manager to be retrieved by the ResourceCodeDomSerializer.
346
D E S I G N - T I M E
S U P P O R T
SerializeProperties: This protected method serializes all properties of the specified object, containing the specified attributes, adding them to the specified CodeStatementCollection. SerializeEvents: This protected method serializes the events of the specified object, containing the specified attributes, adding them to the specified CodeStatementCollection. SerializeResource: This protected method serializes the specified resource value to the resources, via the WriteResource method of ResourceCodeDomSerializer. SerializeResourceInvariant: This protected method serializes the specified invariant resource value to the resources, via the WriteResourceInvariant method of ResourceCodeDomSerializer. SerializeToExpression: This protected method serializes the specified object into a CodeExpression object. SerializeToReferenceExpression: This protected method is similar to SerializeToExpression, except that it is used to serialize references, which will normally need to be used as parameters to methods.
Transaction Support Designer transactions provide a mechanism for enabling undo and redo of changes that occur during design time. A transaction can delay processing until the transaction is marked complete. This enables a series of changes to be undone. Transactions are particularly important when you need to add or remove components from the designer. Creating new components can be achieved via the CreateComponent method of the IDesignerHost interface. Similarly, you can remove components using the DestroyComponent method. In order for creates and deletes to be successful, they must occur within the context of a transaction. A designer transaction is represented by the System.ComponentModel.Design.DesignerTransaction class. Here are the members of this class: Canceled: This property returns a Boolean value indicating whether the transaction has canceled. Committed: This property returns a Boolean value indicating whether the transaction has committed.
347
C H A P T E R
1 3
Description: This property returns a string that represents the description of the transaction. As a common practice, this string will typically consist of the operation name followed by the Site name of the component. Cancel: This method tries to cancel a transaction, and rolls back any changes made by the events of the transaction. Commit: This method tries to commit a transaction, and saves any changes made by the events of the transaction. OnCancel: This protected abstract method cancels a transaction. This method is called by the Cancel method, and should typically be overridden to raise a user defined Cancel event. OnCommit: This protected abstract method commits a transaction. This method is called by the Commit method, and should typically be overridden to raise a user defined Commit event.
Revisiting the Designer Host We introduced the designer host in Chapter 12. However, we failed to mention any transaction related members of the IDesignerHost interface. We will now examine these members and see how they relate to transactional operations. Here are the members: InTransaction: This property returns a Boolean value indicating whether a transaction is in progress. TransactionDescription: This property returns a string that represents the description of the current transaction. CreateTransaction: This method creates and returns a DesignerTransaction instance that can be used for committing and canceling sequential operations. This method is overloaded to optionally take a string that represents the description of the transaction. This is the string that will be returned from TransactionDescription. TransactionOpening: This event is raised when the transaction is about to start. This event is only raised once after the first CreateTransaction method has been called, until all transactions have been committed or canceled. In other words, subsequent calls to CreateTransaction do not raise this event. TransactionOpened: This event is raised once the transaction has started.
348
D E S I G N - T I M E
S U P P O R T
TransactionClosing: This event is raised when the transaction is about to end. The delegate for this event is DesignerTransactionCloseEventHandler, which contains a single Boolean property, TransactionCommitted, indicating whether the transaction is closing due to a commit. TransactionClosed: This event is raised once the transaction has ended. The delegate for this event is DesignerTransactionCloseEventHandler.
Working with Transactions When working with designer transactions, there are a few rules that you must adhere to so that transactions are used effectively. First and foremost, you must be able to obtain an IDesignerHost from a service provider. Creating a Transaction Creating a transaction requires a call to the CreateTransaction method of the designer host. Always call this method within a try-catch block or a using block. A good idea is to use the second constructor overload, which takes a string parameter identifying the description of the transaction. It is common practice to include the name of the action as well as the component’s Site name in the description, as shown: IDesignerHost host = (IDesignerHost)GetService(typeof(IDesignerHost)); if (host != null) { string description = “AddSubComponent – “ + this.Component.Site.Name; using (DesignerTransaction trx = host.CreateTransaction(description)) { } }
You must also call the OnComponentChanging method of the IComponentChangeService for each action that occurs within the context of the transaction, as shown: IDesignerHost host = (IDesignerHost)GetService(typeof(IDesignerHost)); if (host != null)
In the code above, instead of accessing the IComponentChangeService directly, we use the PropertyDescriptor, which automatically provides the notifications for us. In general, you would do this for each property change. Above, we only have one property change. Committing a Transaction Call the Commit method on the transaction object when you are done with processing. If you did not create the transaction in a using block, you must commit the transaction in a finally block. When a transaction is created in a using block, its Dispose method automatically ends the transaction. {
Canceling a Transaction Canceling a transaction is similar to committing, except that you call the Cancel method of the DesignerTransaction object that was created.
350
D E S I G N - T I M E
S U P P O R T
Handling the CheckoutException When modifying components in design mode, there is always the case when the corresponding source file is under source control. Whenever an attempt is made to modify components at design time, Visual Studio .NET tries to check out any relevant source files. If this checkout fails, a System.ComponentModel.Design.CheckoutException is raised. You must catch this exception in order to cancel any pending transactions. Here is some sample code that does this: IDesignerHost host = (IDesignerHost)GetService(typeof(IDesignerHost)); if (host != null) { string description = “AddSubComponent – “ + this.Component.Site.Name; DesignerTransaction trx = null; try { trx = host.CreateTransaction(description) IComponent subComp = host.CreateComponent(typeof(MySubComponent)); PropertyDescriptor pd = TypeDescriptor.GetProperties(this.Component)[“SubComponent”]; pd.SetValue(this.Component, subComp); } catch (CheckoutException ce) { if (ce == CheckoutException.Canceled) trx.Cancel(); } finally { trx.Commit(); } }
We simply test the CheckoutException parameter against a static instance, CheckoutException.Canceled, which indicates that the checkout has been canceled. We then cancel the transaction if the check out was canceled.
351
C H A P T E R
1 3
Summary In conclusion, the designer architecture includes a very rich architecture and object model. It provides services which can be customized for a particular host. From code generation to extending the host, the designer architecture is highly customizable.
352
14 Chapter
Licensing Overview Many components and controls are implemented with the intention to be sold for making money. Licensing helps to ensure components are only used in certain contexts or by certain users. It allows authors to protect intellectual property by making sure that users are authorized to use components. Components and controls have typically been distributed freely, through one-time licensing fees, royalties, per CPU installations, and per usage. With .NET, all components and controls, windows and web-based, are licensed the same way. Licensing is built directly into the framework. Visual Studio .NET handles all design-time based licensing automatically. In this chapter, we will look at some of the classes associated with component licensing. We will then look at the default licensing scheme which is provided with the .NET framework. Finally, we will implement our own license provider.
LicenseProvider .NET separates the licensing validation logic from the component or control through a license provider. A license provider is represented by the System.ComponentModel.LicenseProvider class. Components and controls are associated with license providers through an attribute, specifically the System.ComponentModel.LicenseProviderAttribute. This attribute takes one argument, which is the type of license provider. Here is an example: [LicenseProvider(typeof(LicFileLicenseProvider))] public class MyLicensedControl : Control
C H A P T E R
1 4
The LicenseProvider class supports a single abstract method, GetLicense, which is responsible for returning the license for a given component. A license is represented by the System.ComponentModel.License class. This class will be discussed soon. Here is the syntax for the GetLicense method: public abstract License GetLicense( LicenseContext context, Type type, object instance, bool allowExceptions );
The first parameter of this method is a LicenseContext that specifies where the licensed component or control can be used. This class will be discussed in more detail later. The second parameter is the type of component that is requesting the license. The third parameter is an instance of the component that is requesting the license. The fourth parameter is a Boolean variable that indicates whether a LicenseException should be thrown if the component could not be granted a license.
LicFileLicenseProvider The .NET framework ships with a sample license provider, called LicFileLicenseProvider. This class derives from the base LicenseProvider class. This license provider uses a text file for validating license keys in order to use a component or control. We will talk more about this license provider later.
LicenseException The System.ComponentModel.LicenseException is an exception class that is typically thrown when a component could not be granted a valid license for execution. Besides the standard properties inherited from System.Exception, LicenseException provides a public property, LicensedType, which represents the type of component that could not be granted a valid license.
356
L I C E N S I N G
License The System.ComponentModel.License class is the abstract base class for all licenses. This class implements IDisposable, and it should always be disposed of when it’s no longer needed. Here is the syntax for disposing of a license: private License _license = null; ... public void Dispose() { if (_license != null) { _license.Dispose(); } }
The License class supports a single public property, LicenseKey, which is of a string type. This property represents the license key that was granted to a licensed component. Any Unicode string can represent a license key. For security reasons, however, a license key should not be stored in simple text. It should be stored with some form of hidden structure.
LicenseContext As stated earlier, the LicenseContext indicates where a licensed object can be used. LicenseContext implements IServiceProvider so that additional services can be obtained to support licenses running within the domain of the license context. Here are the members of this class: UsageMode: This property is of type LicenseUsageMode, and specifies when a particular license can be used. LicenseUsageMode is an enumeration with two values: Designtime, which indicates that the license should be used during design time by the visual designer or compiler; Runtime, which indicates that the license can be used during runtime. The default implementation of this property returns LicenseUsageMode.Runtime. This property is overridable. GetSavedLicenseKey: This property returns a saved license key of the specified type from the specified resource assembly. Here is the syntax for this method:
357
C H A P T E R
1 4
public virtual string GetSavedLicenseKey( Type type, Assembly resourceAssembly );
The first parameter is the type of the component associated with the license. The second parameter is the assembly that contains the embedded license key. The default implementation of this method returns a null reference. This method is overridable. SetSavedLicenseKey: This method sets a license key for the specified component type. This method is overridable. Here is the syntax: public virtual void SetSavedLicenseKey( Type type, String LicenseKey );
GetService: This method returns a service of the specified type. Here is the syntax for this method: public virtual object GetService( Type serviceType );
LicenseManager The license manager is represented by the System.ComponentModel.LicenseManager class. This is a sealed class that contains static methods and properties for creating and validating licenses for components and controls. It works together with the license provider. Here are the members of this class: CurrentContext: This property gets or sets the current LicenseContext for specifying when the licensed object can be used. UsageMode: This readonly property returns the LicenseUsageMode for specifying when the license can be used.
358
L I C E N S I N G
CreateWithContext: This method is used to create a licensed object given a type and a LicenseContext. The method is overloaded, as shown: public static object CreateWithContext( Type objectType, LicenseContext context ); public static object CreateWithContext( Type objectType, LicenseContext context, object[] args );
The first parameter specifies the type of object to create. The second parameter is a reference to a LicenseContext for determining when the licensed object can be used. The third parameter is an array of arguments to be passed to the constructor for creation of the object. IsLicensed: This method returns a Boolean value indicating whether the given object type is licensed. Here is the syntax for this method: public static bool IsLicensed( Type objectType );
IsValid: This method returns a Boolean value indicating whether the given object or type can be granted a license. This method is overloaded, as shown: public static bool IsValid( Type objectType) ); public static bool IsValid( Type objectType, object instance, out License license );
359
C H A P T E R
1 4
The first parameter specifies the type of object that is requesting the license. The second parameter is the instance of the object requesting the license. The third parameter will contain a reference to a License if a license was granted to the requestor. If a license could not be granted, this parameter will contain a null value. If a valid License object is returned, it must be disposed of after its use. This method returns true if the License object is not null or if the object is not licensed at all; it returns false otherwise. Validate: This method, similar to IsValid, determines whether a given object or type can be granted a license. This method is overloaded, as shown: public static void Validate( Type type ); public static License Validate( Type type, object instance );
The first parameter specifies the type of the object requesting the license. The second parameter specifies the instance of the object requesting the license. If a valid license could not be granted to the requestor, a LicenseException is thrown. The second overload returns a reference to a License object if a valid license was granted. Here is a sample snippet illustrating this method’s use: namespace SampleLicensing { [LicenseProviderAttribute(typeof(LicFileLicenseProvider))] public class MyClass : IDisposable { private License license = null; public MyClass() { license = LicenseManager.Validate(typeof(MyClass), this); } public void Dispose() { if (license != null)
360
L I C E N S I N G
{
}
}
}
}
license.Dispose(); license = null;
LockContext: This method synchronizes access to the LicenseContext of the LicenseManager. This method prevents a License object from being retrieved from the LicenseContext. Here is the syntax: public static void LockContext( object contextUser );
The parameter to the method is the object associated with the current context that should be locked. UnlockContext: This method allows changes to be made to the current LicenseContext for the given object. This method should be called after a call to LockContext. Here is the syntax: public static void UnlockContext( object contextUser );
Using the LicFileLicenseProvider As we stated earlier, the .NET framework ships with a simple license provider, represented by the System.ComponentModel.LicFileLicenseProvider class. This class validates license keys stored in text files. These text files have a special naming convention, and must be named [Namespace].[ClassName].LIC . [Namespace] is the namespace of the licensed class. [ClassName] is the class name of the licensed class. And LIC is the extension. Along with the naming convention, the content of the license file must also conform to certain rules. Specifically, it must contain the following string format: [Namespace].[ClassName] is a licensed component. Here is an example of a license file represented by the sample code of the LicenseManager Validate discussed earlier:
361
C H A P T E R
1 4
SampleLicensing.MyClass is a licensed component.
The LicFileLicenseProvider looks for that exact format when performing license validation. The LicFileLicenseProvider supports a licensing mechanism similar to COM’s IClassFactory2 in the way it supports validation. At design time, LIC files are used. Visual Studio then embeds a runtime license into the executing assembly. Use of the LicFileLicenseProvider requires following a number of steps. The steps are outlined below: 1. Create a class that derives from IDisposable. 2. Apply the LicenseProviderAttribute to the class, specifying the LicFileLicenseProvider type. 3. Add a member variable of type License to the class. 4. In the constructor of the class, call the Validate method of the LicenseManager, storing the return value into the License member variable. 5. Implement or override the Dispose method to dispose of the license.
To illustrate this concept, we will create a licensed class that uses the LicFileLicenseProvider. We will then create a host application for instantiating the licensed class.
Step 1: First, create a new class library project named ComponentLicensing. Rename the automatically created class and class file to LicFileLicensedComponent. Then derive this class from IDisposable, as shown: public class LicFileLicensedComponent : IDisposable { public LicFileLicensedComponent() { // // TODO: Add constructor logic here // }
362
L I C E N S I N G
}
Step 2: Apply the LicenseProviderAttribute to the class and specify the LicFileLicenseProvider as the type. Also, add a using directive for System.ComponentModel at the top of the file. Here is the code: using System; using System.ComponentModel; namespace ComponentLicensing { /// <summary> /// Summary description for LicFileLicensedComponent. /// [LicenseProvider(typeof(LicFileLicenseProvider))] public class LicFileLicensedComponent : IDisposable { public LicFileLicensedComponent() { // // TODO: Add constructor logic here // } } }
Step 3: Add a member variable of type License to the class for holding a reference to a valid license, as shown: public class LicFileLicensedComponent : IDisposable { private License _license = null; }
...
363
C H A P T E R
1 4
Step 4: Validate the license by calling the static Validate method of the LicenseManager class in the constructor of the licensed class. Store the returned license into the member variable created in the last step. Here is the constructor: public LicFileLicensedComponent() { _license = LicenseManager.Validate(typeof(LicFileLicensedComponent), this); }
If license validation fails, an exception of type LicenseException will be thrown here, and the licensed class will never get instantiated. Sometimes, when validation fails, you may still wish to allow instantiation of a particular component, but prevent access to special features. In such a case it may be better to use the IsValid method of the LicenseProvider. But for the sake of this sample, a LicenseException is just fine.
Step 5: Because we are implementing IDisposable, and because we have a License object stored in our class, we must dispose of the license when it is no longer needed. To do so, we implement the Dispose method of IDisposable, as shown: public void Dispose() { if (_license != null) { _license.Dispose(); _license = null; } }
This is all that is needed to build a component that supports LIC file licensing. At runtime, this component will check the calling assembly’s manifest for an embedded license key. If the key is not found there, the component checks the runtime directory of the calling assembly. A license key can be embedded into a calling assembly by either using Visual Studio .NET or the license compiler tool, lc.exe and the assembly linker tool, al.exe.
364
L I C E N S I N G
To embed a license key using Visual Studio .NET, you must add a licenses.licx file to the calling assembly’s project. Note that when dragging licensed components and controls to the designer, this file is automatically added to the calling assembly’s project. Be sure that this file has a Build Action of Embedded Resource. This file should contain the assembly qualified names of all components whose licenses should be embedded into the assembly. When doing so, the LIC file for each component must reside in that component’s directory. When embedding a license key at design time, the LIC file must reside in the licensed component’s directory. When validating a license at runtime, the LIC file must reside in the calling assembly’s directory. To embed a license key using the license compiler tool, use the following syntax: lc /target:[target] /complist:[licx file] /i:[licensed control assembly]
[target] is the target name of the binary licenses file. The output will be in the form [target].licenses . The runtime license keys will be embedded into this file. [licx file] specifies the list of components whose licenses should be extracted and embedded into the binary licenses file. [licensed control assembly] is the name of the assembly from which the licensed components’ keys should be extracted. Once the binary licenses file is created, you can manually embed it into the calling assembly either by using the assembly linker, or by compiling with the “/res:.licenses” option in Visual Studio .NET. When embedding license keys, the LIC file must reside in the output directory of the licensed component. Then at runtime, no LIC files will be needed. If you are not embedding license keys, a LIC file must exist for each licensed component in the calling assembly’s output directory. We will now create the host application. In doing so, we will illustrate both embedding and not embedding license keys. So go ahead and create a new console application project named ComponentLicensingTest. Then add a reference to the ComponentLicensing assembly created earlier. In the main method of the console application class, add the following code: static void Main(string[] args) {
365
C H A P T E R
1 4
ComponentLicensing.LicFileLicensedComponent c = new ComponentLicensing.LicFileLicensedComponent(); Console.WriteLine("No exception was thrown. Valid license!"); c.Dispose(); }
This method simply tries to instantiate an instance of the LicFileLicensedComponent class. If it succeeds, meaning no LicenseException is thrown, it displays text on the console. We will first illustrate license validation using license keys found in LIC files, meaning they are not embedded. To do so, add a LIC file to the output directory of the console application, naming it ComponentLicensing.LicFileLicensedComponent.LIC. Add the following text to the LIC file: ComponentLicensing.LicFileLicensedComponent is a licensed component.
Remember that this is the exact format expected by the LicFileLicenseProvider class. This text is referred to as the license key; although this type of key can easily be hacked, since you already know the exact format. We will talk more about this in the next section. All you need to do now is to build the console application and run it. Provided the LIC file is in the correct format and is placed in the output directory of the application, you will see an output similar to the following:
366
L I C E N S I N G
Because the license provider did not raise a LicenseException, the Console.WriteLine method was executed. Let’s now illustrate using embedded license keys. First move the LIC file you just created to the output directory of the ComponentLicensing assembly. Remember that in order to embed license keys, the LIC file must reside in the licensed component’s output directory. Now add the licenses.licx file to the console application project, making sure that it has its Build Action set to Embedded Resource. Then add the assembly qualified name of the licensed component, as shown: ComponentLicensing.LicFileLicensedComponent,ComponentLicensing
Finally, build the application. During compilation, Visual Studio .NET invokes the license compiler and embeds the license keys for all components specified in the LICX file. If you run the application, you should get the same output as that above.
Implementing A Custom LicenseProvider Because the format of the license key for the LicFileLicenseProvider is known, it will generally not be a good idea to use it as a license provider for licensing your components. All a user would need to do is gain access to your component, create a LIC file, and place the “MyNamespace.MyComponent is a licensed component.” into the file. This would not be good at all. Therefore, you should never use the LicFileLicenseProvider directly. You should instead derive a class from LicenseProvider or LicFileLicenseProvider. We will first illustrate what’s going on behind the scenes with the LicFileLicenseProvider by deriving a class from it and overriding its methods. So open the ComponentLicensing project and add the following code: [LicenseProvider(typeof(CustomLicenseProvider))] //[LicenseProvider(typeof(LicFileLicenseProvider))] public class LicFileLicensedComponent : IDisposable { private License _license = null; public LicFileLicensedComponent() { _license = LicenseManager.Validate(typeof(LicFileLicensedComponent), this); }
367
C H A P T E R
}
1 4
public void Dispose() { if (_license != null) { _license.Dispose(); _license = null; } }
Now you must either rebuild the console application, or copy the ComponentLicensing assembly to the console application’s output directory. During my testing, I have noticed that I sometimes have to shut down Visual Studio .NET and restart it whenever I change licensing schemes. If you get a raised LicenseException when you know everything is correct, then you may need to restart Visual Studio .NET also. Removing the referenced licensed component and re-referencing it will also help. We will walk through the licensing validation process by setting break points in the overridden methods of the LicFileLicenseProvider. In the ComponentLicensing project, go to the debugging tab of the configuration properties for the project. Then, in the Start Action group,
368
L I C E N S I N G
set the Debug Mode to Program and the Start Application to point to your console application. Here is what mine looks like:
Now set break points in each overridden method, as well as setting one in the constructor of LicFileLicensedComponent. Press F5 in Visual Studio to start debugging. The code should stop at the first breakpoint, as shown:
369
C H A P T E R
1 4
Pressing F10 or F11 should take you into the GetLicense method of the license provider, as shown:
The code will now use the base method for trying to retrieve a valid license for the component. Stepping through this code will cause a break here:
370
L I C E N S I N G
The purpose of this method is to validate the license key found. It compares the license key found to a specific string format. This method is only called if the key was successfully found in the LIC file or embedded in the calling assembly. The base version of this method calls GetKey, as shown:
This method generates the expected key based on the object type passed to it. Once this method returns, the base version of IsKeyValid compares the two keys for equality. If they are equal, it creates a LicFileLicense object and returns it from the GetLicense method. The LicFileLicense class is internal to the framework, and is derived from System.ComponentModel.License. The license is then returned from the Validate method of the LicenseManager. To support LIC file licensing, you would typically derive from LicFileLicenseProvider and only override the GetKey method. You can then return any key to be compared with the key in the LIC file.
371
C H A P T E R
1 4
To provide other forms of license validation, instead of file based validation, you should derive your class from the base LicenseProvider. You would then override the GetLicense method to provide your custom implementation, such as invoking a web service to determine if the caller’s machine is licensed to use your components.
Summary Licensing in .NET is very easy and flexible. Because the licensing model is embedded into the framework, it relieves the control developer of the time consumed in implementing licensing schemes. And with licensing built into the runtime, all a component needs to do is to specify the provider it wants to use for license validation. Then, at any time, the license provider can easily be changed through the use of a single attribute. With that said, .NET provides very easy but powerful ways to protect your intellectual property.
372
15 Chapter
Developing the Windowsbased Wizard Control Overview Even though the .NET framework ships with some of the most commonly used controls, it fails to provide any wizard-like components, other than the TabControl. A wizard is similar to a tabbed control in that it supports multiple “pages” that a user will navigate through in order to accomplish a specific task. The downside of a tabbed control, however, is that the tasks exposed by each page infer that each task can be accomplished in no particular order. A wizard, on the other hand, enforces order. It provides a sequence of pages, with the ability to force task completion on one page before moving to the next. Our goal in this chapter is to design and implement a reusable Wizard. Microsoft has provided a set of guidelines, coined “Wizard 97,” and our Wizard will be designed to match them. According to the guidelines, all wizards should contain a welcome page and completion page, which will correspond to the first page and last page, respectively. Between these two pages will be the actual task-oriented pages. Each of these pages should have a header with text describing the page’s tasks. The overall wizard should be wrapped in a form with navigation buttons anchored at the bottom. Now that we have this background, let’s get to the fun stuff!
Step 1: The Architecture Based on the background, we can design the object model shown here:
C H A P T E R
1 5
Wizard
WelcomePage
CompletionPage
ExteriorWizardPage
InteriorWizardPageCollection (Collection)
InteriorWizardPage
BaseWizardPage
The Wizard class is considered to be the root of the model. The Wizard contains a WelcomePage instance, CompletionPage instance, and InteriorWizardPageCollection instance. Both the WelcomePage and CompletionPage classes are derived from ExteriorWizardPage, because they will share a common user interface. Similarly, we create a class named InteriorWizardPage that will be the base for all interior pages. Any shared UI for the interior pages will be found in this class. All pages have BaseWizardPage as the root base, so that any common functionality for a page, regardless of whether it is interior or exterior, can be reused.
Step 2: The User Interface Now that we have a basic architecture, it will be easy for us the design the UI, placing common user interface elements in their appropriate base class. Go ahead and create a new Windows Control Library project, and add a new user control to it, named Wizard.
376
D E V E L O P I N G
T H E
W I N D O W S - B A S E D
W I Z A R D
C O N T R O L
Wizard The Wizard will be the holder of all the wizard pages, as well as the navigation buttons. To enable this, we will need a page panel and a navigation panel on the Wizard. Modify the Wizard user control by adding two panels, as shown:
_pagePanel
_navigationPanel
The top panel is named _pagePanel while the bottom panel is named _navigationPanel. Also drag 5 buttons to the navigation panel as shown above. The buttons are named, _backButton, _nextButton, _finishButton, _cancelButton, and _helpButton, in order of their appearance.
BaseWizardPage The BaseWizardPage, the root of all pages, will be designed such that all inherited pages contain a default size. This size should be the same as the size of the PagePanel created on the Wizard, since this is where instances of the BaseWizardPage will be placed. Go ahead and create a new User Control named BaseWizardPage, and modify its size to be the same as that of the PagePanel.
377
C H A P T E R
1 5
BaseWizardPage
ExteriorWizardPage Exterior pages consist of only the Welcome page and Completion page. On an exterior page, the left side of the page should contain a watermark bitmap, while the right side contains descriptive text for the wizard. To achieve these goals, we drag two panels to the ExteriorWizardPage. But first, create a User Control named ExteriorWizardPage and derive it from BaseWizardPage.
378
D E V E L O P I N G
T H E
W I N D O W S - B A S E D
W I Z A R D
C O N T R O L
WatermarkPanel MainPanel
The panel on the left is defined as the WatermarkPanel, while the panel on the right is defined as the MainPanel. The MainPanel itself consists of six child controls: five panels and one label. Here is the MainPanel: 3 2
5 1 6
4
379
C H A P T E R
1 5
1. TextAreaPanel – This is the area where additional text or controls will be added. For example, most wizards use this area to place a check box titled, “Do not show this welcome screen again.” 2. WelcomeCompletionLabel – This is the area where the welcome and completion titles will go. The type of this control is Label. 3. NoZoneTopPanel – This is a no-zone area. Nothing should be added here. This panel has a DockStyle of Top. 4. NoZoneBottomPanel – This is a no-zone area. Nothing should be added here. This panel has a DockStyle of Bottom. 5. NoZoneLeftPanel – This is a no-zone area. Nothing should be added here. This panel has a DockStyle of Left. 6. NoZoneRightPanel – This is a no-zone area. Nothing should be added here. This panel has a DockStyle of Right.
InteriorWizardPage An interior page will consist of a header and an area for placing other controls. To accomplish this, we drag a single panel to the control to act as the HeaderPanel. Go ahead and create a new UserControl derived from BaseWizardPage, named InteriorWizardPage.
380
D E V E L O P I N G
T H E
W I N D O W S - B A S E D
W I Z A R D
C O N T R O L
The HeaderPanel has a DockStyle of Top. It consists of a TitleLabel, SubtitleLabel, and PictureBox. The picture box is used as the symbol for the wizard. Typically, this should be a thumbnail view of the watermark found on the exterior wizard pages, since that is the approach that most wizard designers take.
Step 3: Runtime Implementation We will now continue the implementation of the Wizard. In a serious implementation, we would have not created any classes until now. But because Visual Studio .NET and C# are both RAD (Rapid Application Development) tools, it was very easy to create some of the UI elements as they were being designed, especially since we already had a model. The first thing to do is to make sure the WelcomePage and CompletionPage classes both derive from ExteriorWizardPage. Also, make sure ExteriorWizardPage and InteriorWizardPage both derive from BaseWizardPage. We will now begin with the BaseWizardPage. We know that each page of the wizard should probably expose some events. Particularly, a user will want to know when a navigation button was clicked, as well as when the current page has changed. These should represent two separate events, because clicking on a navigation button may not necessary yield a page change. Consider the Help navigation button. It is only called a navigation button since it is placed on the navigation pane, but will probably not cause any real navigation to the wizard
381
C H A P T E R
1 5
itself. It will seem most useful to place an Activate event on the BaseWizardPage, so that the event is available to all pages. Here is the code for that: public class BaseWizardPage : System.Windows.Forms.UserControl { /// <summary> /// Required designer variable. /// private System.ComponentModel.Container components = null; private static readonly object _ActivateEvent = new object(); public event EventHandler Activate { add{Events.AddHandler(_ActivateEvent, value);} remove{Events.RemoveHandler(_ActivateEvent, value);} } protected internal virtual void OnSetActive(EventArgs e) { if ((EventHandler)Events[_ActivateEvent] != null) { ((EventHandler)Events[_ActivateEvent])(this, e); } }
}
Similarly, the wizard itself will want to notify when the active page is about to change, as well as when the active page has changed, as shown: public class Wizard : System.Windows.Forms.UserControl { private System.Windows.Forms.Button _cancelButton; private System.Windows.Forms.Button _helpButton; private System.Windows.Forms.Button _nextButton; private System.Windows.Forms.Button _backButton; private System.Windows.Forms.Panel _navigationPanel; private System.Windows.Forms.Panel _pagePanel; private CustomWindowsControls.WelcomePage _welcomePage; private CustomWindowsControls.CompletionPage _completionPage; private System.Windows.Forms.Button _finishButton; /// <summary> /// Required designer variable. /// private System.ComponentModel.Container components = null;
382
D E V E L O P I N G
T H E
W I N D O W S - B A S E D
W I Z A R D
C O N T R O L
private InteriorWizardPageCollection _interiorWizardPages = null; private readonly object _activePageChangingEvent = new object(); private readonly object _activePageChangedEvent = new object(); private Image _symbolImage = null; private Image _watermark = null; private Font _titleFont = new Font("Verdana", 12F, FontStyle.Bold, GraphicsUnit.Point, ((Byte)(0))); private Font _headerTitleFont = new Font("Microsoft Sans Serif", 8.25F, FontStyle.Bold, GraphicsUnit.Point, ((Byte)(0))); private Font _headerSubtitleFont = new Font("Microsoft Sans Serif", 8.25F); public event EventHandler ActivePageChanging { add{Events.AddHandler(_activePageChangingEvent, value);} remove{Events.RemoveHandler(_activePageChangingEvent, value);} } public event EventHandler ActivePageChanged { add{Events.AddHandler(_activePageChangedEvent, value);} remove{Events.RemoveHandler(_activePageChangedEvent, value);} } protected internal virtual void OnActivePageChanging(EventArgs e) { if ((EventHandler)Events[_activePageChangingEvent] != null) { ((EventHandler)Events[_activePageChangingEvent])(this, e); } } e)
Because the number of interior pages is virtually countless, we will create a collection for storing only InteriorWizardPage instances. As a helper for the collection, we will also create an enumerator, for cycling through the pages. The enumerator will also consider the WelcomePage and CompletionPage when it cycles through the pages. Here is the code: public class InteriorWizardPageCollection : CollectionBase { private readonly Wizard _owner = null; public InteriorWizardPageCollection(Wizard owner) { _owner = owner; } public InteriorWizardPage this[int nIndex] { get { return (InteriorWizardPage) base.List[nIndex]; } } public void Add(InteriorWizardPage page) { _owner.Controls.Add(page); base.List.Add(page); } public new void RemoveAt(int nIndex) { _owner.Controls.Remove(this[nIndex]); base.List.RemoveAt(nIndex); } protected override void OnClear() { _owner.Controls.Clear(); base.OnClear();
384
D E V E L O P I N G
T H E
W I N D O W S - B A S E D
W I Z A R D
C O N T R O L
}
}
public int IndexOf(InteriorWizardPage page) { return base.List.IndexOf(page); }
The collection takes one parameter in its constructor, so that it knows who the owner of the collection is. By design, the CollectionEditor is aware of this one parameter constructor. Whenever a page is added to the collection, it is also added to the control hierarchy of the Wizard. Similarly, when a page is removed from the collection, it is also removed from the control hierarchy of the Wizard.
public class WizardPageEnumerator : IEnumerator { private readonly Wizard _Wizard; private BaseWizardPage _currentPage; public WizardPageEnumerator(Wizard Wizard) { _Wizard = Wizard; _currentPage = Wizard.WelcomePage; } public object Current { get { return _currentPage; } } public bool MoveNext() { BaseWizardPage page = _currentPage; if (page == _Wizard.CompletionPage) { return false; } int nPageCount = _Wizard.InteriorWizardPages.Count + 2; int nPage = 0;
public bool MovePrevious() { BaseWizardPage page = _currentPage; if (page == _Wizard.WelcomePage) { return false; } int nPageCount = _Wizard.InteriorWizardPages.Count + 2; int nPage = nPageCount-3; if (page is InteriorWizardPage) { nPage = ((_Wizard.InteriorWizardPages.IndexOf((InteriorWizardPage)page) 1) % nPageCount); } page = (nPage != -1 && nPageCount > 2) ? (BaseWizardPage) _Wizard.InteriorWizardPages[nPage] : _Wizard.WelcomePage; _currentPage = page; return true; }
}
386
public void Reset() { _currentPage = _Wizard.WelcomePage; }
D E V E L O P I N G
T H E
W I N D O W S - B A S E D
W I Z A R D
C O N T R O L
The purpose of the WizardPageEnumerator class is to allow a user to cycle through the collection of pages that the Wizard supports. Some nasty logic is added so that the wizard knows how to consider the exterior pages when it cycles.
public class Wizard : System.Windows.Forms.UserControl { private readonly WizardPageEnumerator _wizardPageEnumerator = null; public Wizard() { InitializeComponent(); _interiorWizardPages = new InteriorWizardPageCollection(this); _welcomePage = new WelcomePage(); _completionPage = new CompletionPage(); _welcomePage.Dock = DockStyle.Fill; _completionPage.Dock = DockStyle.Fill; _pagePanel.Controls.Add(_welcomePage); _pagePanel.Controls.Add(_completionPage); _titleFont = new Font("Verdana", 12F, FontStyle.Bold, GraphicsUnit.Point, ((Byte)(0))); }
_wizardPageEnumerator = new WizardPageEnumerator(this);
[Browsable(false)] public WelcomePage WelcomePage { get { return _welcomePage; } } [Browsable(false)] public CompletionPage CompletionPage { get { return _completionPage; } } [Browsable(false)] public WizardPageEnumerator WizardPageEnumerator
387
C H A P T E R
{
}
1 5
get { return _wizardPageEnumerator; }
[Browsable(true), DesignerSerializationVisibility(DesignerSerializationVisibility.C ontent)] public InteriorWizardPageCollection InteriorWizardPages { get { return _interiorWizardPages; } } [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.H idden)] public BaseWizardPage ActivePage { get { return _pagePanel.Controls[0] as BaseWizardPage; } set { OnActivePageChanging(EventArgs.Empty); BaseWizardPage page = (BaseWizardPage) value; page.BringToFront(); page.OnSetActive(EventArgs.Empty); OnActivePageChanged(EventArgs.Empty); } }
The Wizard class exposes some necessary properties. ActivePage is the page that is currently being displayed on the wizard. InteriorWizardPages returns an instance of InteriorWizardPageCollection, which we just discussed. WizardPageEnumerator returns an enumerator for cycling through the pages, which was also just discussed. Finally, we have a WelcomePage property and a CompletionPage property, which are readonly properties. Their sole purpose is to allow the active page to be set to one of them. Other useful properties that are exposed by the Wizard are shown here:
388
D E V E L O P I N G
T H E
W I N D O W S - B A S E D
W I Z A R D
C O N T R O L
[Browsable(true)] public String BackButtonText { get{return _backButton.Text;} set{_backButton.Text = value;} } [Browsable(true)] public String NextButtonText { get{return _nextButton.Text;} set{_nextButton.Text = value;} } [Browsable(true)] public String FinishButtonText { get{return _finishButton.Text;} set{_finishButton.Text = value;} } [Browsable(true)] public String CancelButtonText { get{return _cancelButton.Text;} set{_cancelButton.Text = value;} } [Browsable(true)] public String HelpButtonText { get{return _helpButton.Text;} set{_helpButton.Text = value;} } [Browsable(false)] public Panel WelcomePageTextArea { get { return GetTextArea(_welcomePage); } set { SetTextArea(_welcomePage, value); } }
389
C H A P T E R
1 5
[Browsable(false)] public Panel CompletionPageTextArea { get { return GetTextArea(_completionPage); } set { SetTextArea(_completionPage, value); } } private Panel GetTextArea(ExteriorWizardPage page) { return page.Controls[0].Controls[0] as Panel; } private void SetTextArea(ExteriorWizardPage page, Panel panel) { page.Controls[0].Controls.RemoveAt(0); page.Controls[0].Controls.Add(panel); page.Controls[0].Controls.SetChildIndex(panel, 0); } [Browsable(true)] public String WelcomeTitle { get { return GetTitle(_welcomePage); } set { SetTitle(_welcomePage, value); } } [Browsable(true)] public String CompletionTitle { get { return GetTitle(_completionPage); }
390
D E V E L O P I N G
}
T H E
W I N D O W S - B A S E D
W I Z A R D
C O N T R O L
set { SetTitle(_completionPage, value); }
private String GetTitle(ExteriorWizardPage page) { Label l = (Label) page.Controls[0].Controls[1]; return l.Text; } private void SetTitle(ExteriorWizardPage page, String strTitle) { Label l = (Label) page.Controls[0].Controls[1]; l.Text = strTitle; } [Browsable(true)] public Font TitleFont { get { return _titleFont; } set { _titleFont = value; Label l = (Label) _welcomePage.Controls[0].Controls[1]; l.Font = value; l = (Label) _completionPage.Controls[0].Controls[1]; l.Font = value; } } [Browsable(true)] public Image Watermark { get { return _watermark; } set { _watermark = value; Panel p = (Panel) _welcomePage.Controls[1];
391
C H A P T E R
}
}
1 5
p.BackgroundImage = value; p = (Panel) _completionPage.Controls[1]; p.BackgroundImage = value;
[Browsable(true)] public Image SymbolImage { get { return _symbolImage; } set { _symbolImage = value; foreach (InteriorWizardPage page in _interiorWizardPages) { ((PictureBox)page.Controls[0].Controls[2]).Image = value; } } } public Font HeaderTitleFont { get { return _headerTitleFont; } set { _headerTitleFont = value; SetHeaderFont(value, false); } } public Font HeaderSubtitleFont { get { return _headerSubtitleFont; } set { _headerSubtitleFont = value;
Now, let’s hook up the Click events of the navigation buttons to some user defined events. To make things simple, we will create only one event, named WizardCommandEvent, with a corresponding WizardCommandEventHandler and WizardCommandEventArgs. The event will be raised whenever any navigation button is clicked. In order for the event to give information on which button was clicked, we will add a property to the WizardCommandEventArgs class. But first, we should define a command. Let’s do so by creating the enumeration shown here: public enum WizardCommandButton { Back, Next, Cancel, Finish, Help, }
Each of the values in the enumeration will correspond to a button that was clicked. At times, however, some buttons should not be clicked at all, in which case the buttons should be disabled. To accomplish this, we can add an enumeration with a FlagsAttribute, specifying which buttons should be enabled and which should be disabled. Here is the enumeration: [Flags] public enum WizardButtons { None = 0, Back = 1, Next = 2,
In this code, the Wizard itself will not raise the event. Instead, it will forward the event to the active page and let the page raise it. This way, a subscriber will know exactly what “page” the event is raised from. The BaseWizardPage will expose a Wizard property in case you need to know the parent wizard of a page. Here is the additional code for the BaseWizardPage class: public delegate void WizardCommandEventHandler(object sender, WizardCommandEventArgs e); public class WizardCommandEventArgs : EventArgs { private WizardCommandButton _wizardCommandButton; public WizardCommandEventArgs(WizardCommandButton wizardCommandButton) {
395
C H A P T E R
}
}
1 5
_wizardCommandButton = wizardCommandButton;
public WizardCommandButton WizardCommandButton { get { return _wizardCommandButton; } }
if ((WizardCommandEventHandler)Events[_WizardCommandEvent] != null) { ((WizardCommandEventHandler)Events[_WizardCommandEvent])(this, e); } }
A new method, AssignWizard, was added so that once a page is created, it can be assigned to a parent wizard. The InteriorWizardPageCollection uses this method to assign pages as they are added. We will now focus on the ExteriorWizardPage and InteriorWizardPage classes. Most of the code in the ExteriorWizardPage class will be added by the designer, once controls are dropped onto the page. There is something extra we must do, which is to make sure the TextAreaPanel is the topmost control, so that controls can be dropped onto it. Here is the modified constructor for this class: public ExteriorWizardPage() : base() { InitializeComponent(); }
_textAreaPanel.BringToFront();
397
C H A P T E R
1 5
We simply call the BringToFront method of the control to ensure that it is the topmost control. As far as the InteriorWizardPage is concerned, we want to expose more properties that are useful to the user. Here is the modified code for this class: protected internal override void AssignWizard(Wizard parentWizard) { base.AssignWizard(parentWizard); _headerImagePictureBox.Image = parentWizard.SymbolImage; _headerTitleLabel.Font = parentWizard.HeaderTitleFont; _headerSubtitleLabel.Font = parentWizard.HeaderSubtitleFont; } [Browsable(true)] public String HeaderTitle { get { return _headerTitleLabel.Text; } set { _headerTitleLabel.Text = value; } } [Browsable(true)] public String HeaderSubtitle { get { return _headerSubtitleLabel.Text; } set { _headerSubtitleLabel.Text = value; } }
These properties make the page more developer friendly. We also override the AssignWizard method so that some of the details of the properties remain consistent with what was set at the
398
D E V E L O P I N G
T H E
W I N D O W S - B A S E D
W I Z A R D
C O N T R O L
Wizard level. We have now added all the implementation needed to successfully create a fullfledged wizard. In the next section, we will focus on modifying the design-time behavior of the wizard.
Step 4: Design-Time Support One of the first things that every custom control needs is a bitmap to associate it with. So let’s jump right into this section by creating a 16x16 bitmap with 16 colors. The outer perimeter of the bitmap will be used as the mask. Here is ours:
We placed this bitmap in a ToolboxBitmaps folder, but in general, it can be placed anywhere, as long as it is marked as Embedded Resource and you know how to reference it. We reference our bitmap as follows: [ToolboxBitmap(typeof(Wizard), "ToolboxBitmaps.Wizard.bmp")] public class Wizard : System.Windows.Forms.UserControl {
Here is the solution explorer showing where our bitmap resides:
399
C H A P T E R
1 5
The Wizard will be added to the Customize Toolbox dialog automatically. But we do want to prevent classes derived from BaseWizardPage from being able to be added to the toolbox. To do so, we apply the ToolboxItemAttribute, as shown: [ToolboxItem(false)] public class BaseWizardPage : System.Windows.Forms.UserControl
Other interesting design-time attributes include the DefaultEventAttribute and DefaultPropertyAttribute. The DefaultEventAttribute specifies the type of handler that should be created when a user double clicks on a control. The DefaultPropertyAttribute specifies what the property that should immediately take focus in the properties editor. Here are these attributes as they are applied to our classes: [DefaultEvent("ActivePageChanged")] [DefaultProperty("InteriorWizardPages")] [ToolboxBitmap(typeof(Wizard), "ToolboxBitmaps.Wizard.bmp")] public class Wizard : System.Windows.Forms.UserControl [DefaultEvent("Activate")] public class BaseWizardPage : System.Windows.Forms.UserControl [DefaultProperty("HeaderTitle")] public class InteriorWizardPage : BaseWizardPage
400
D E V E L O P I N G
T H E
W I N D O W S - B A S E D
W I Z A R D
C O N T R O L
Now let’s move on to the real fun. It’s time to create the designers. We want all designers to be part of the Design sub-namespace, so create a folder in the project, named Design. To this folder, add a C# file named WizardDesigner. All designer classes relating to the Wizard control will be found in this source file. We will start with the simple designers, and work our way to the more complex ones. Because all wizard pages inherit from a base class, it makes sense to create a base designer. So create a class named WizardPageDesigner, and add the DesignerAttribute to the BaseWizardPage class, as shown: [Designer(typeof(WizardPageDesigner))] [ToolboxItem(false)] public class BaseWizardPage : System.Windows.Forms.UserControl public class WizardPageDesigner : ScrollableControlDesigner { public override bool CanBeParentedTo(IDesigner designer) { if (designer is WizardDesigner) { return true; } return false; } }
We derive the WizardPageDesigner from ScrollableControlDesigner, since a wizard page can act as a scrollable control. We override the CanBeParentedTo method so that the page can only be added to a Wizard control. Note that there is a reference to WizardDesigner in this method. Hold your horses, it will be created later. You can also go ahead and create a skeleton WizardDesigner class, just in case you are compiling as you read. Next, we will apply and create a designer for the base exterior page. We name this designer ExteriorWizardPageDesigner. Here is the code: [Designer(typeof(ExteriorWizardPageDesigner))] public class ExteriorWizardPage : BaseWizardPage public class ExteriorWizardPageDesigner : WizardPageDesigner { private bool _drawGrid = true;
We override the DrawGrid and OnPaintAdornments methods of this class so that the grid is not drawn on the surface of an exterior page. Note that this rule only applies when the exterior page is shown on an actual Wizard control. For example, when double clicking the ExteriorWizardPage class in the solution explorer, this rule does not apply. But after dropping a Wizard control onto a form, you will see the purpose of overriding this method. We will now create and apply the InteriorWizardPageDesigner for the interior pages. This designer is more complex than the others, because it contains limited real estate which must be taken into account when a control is being dropped onto it. Here is the code: [Designer(typeof(InteriorWizardPageDesigner))] [DefaultProperty("HeaderTitle")] public class InteriorWizardPage : BaseWizardPage public class InteriorWizardPageDesigner : WizardPageDesigner { ISelectionService _selectionService = null; IToolboxService _toolboxService = null; IDesignerHost _designerHost = null; public override void Initialize(IComponent component) { base.Initialize(component); _selectionService = (ISelectionService) GetService(typeof(ISelectionService));
402
D E V E L O P I N G
T H E
W I N D O W S - B A S E D
W I Z A R D
C O N T R O L
if (_selectionService != null) { _selectionService.SelectionChanged += new EventHandler(OnSelectionChanged); } _designerHost = (IDesignerHost) GetService(typeof(IDesignerHost)); _toolboxService = (IToolboxService) GetService(typeof(IToolboxService)); } protected override void Dispose(bool bIsDisposing) { if (_selectionService != null) { _selectionService.SelectionChanged -= new EventHandler(OnSelectionChanged); } }
base.Dispose(bIsDisposing);
public override SelectionRules SelectionRules { get { return SelectionRules.Visible; } } protected virtual void OnSelectionChanged(object sender, EventArgs e) { // Force the designer to repaint itself. // (The designer is repainted only when a property of the control has been changed.) this.Control.Enabled = false; this.Control.Enabled = true; } protected override void OnPaintAdornments(PaintEventArgs e) { if (_selectionService.PrimarySelection == this.Control) {
403
C H A P T E R
1 5
ControlPaint.DrawSelectionFrame(e.Graphics, true, Rectangle.Inflate(PageArea, -5, -5), Rectangle.Inflate(PageArea, -10, -10), this.Control.BackColor); } if (DrawGrid) { ControlPaint.DrawGrid(e.Graphics, PageArea, GridSize, Control.BackColor); } } protected Rectangle PageArea { get { Rectangle pageArea = Rectangle.Empty; InteriorWizardPage page = this.Component as InteriorWizardPage; if (page != null) { int x = page.ClientRectangle.X + 1; int width = page.ClientRectangle.Width - 1; int y = page.Controls[0].ClientRectangle.Y + 1 + page.Controls[0].ClientRectangle.Height; int height = page.ClientRectangle.Height - 1 - y; pageArea = new Rectangle(x, y, width, height); } return pageArea; } } protected override void OnDragOver(DragEventArgs de) { Point pt = this.Control.PointToClient(new Point(de.X, de.Y)); // Here, we could have utilized GetHitTest() if (PageArea.X <= pt.X && pt.X <= PageArea.X + PageArea.Width && PageArea.Y <= pt.Y && pt.Y <= PageArea.Y + PageArea.Height) { if ((de.KeyState & 1) == 1) { de.Effect = DragDropEffects.Move; } else if ((de.KeyState & 8) == 8)
404
D E V E L O P I N G
{ }
}
T H E
W I N D O W S - B A S E D
W I Z A R D
C O N T R O L
de.Effect = DragDropEffects.Copy;
} else { de.Effect = DragDropEffects.None; }
protected override IComponent[] CreateToolCore(ToolboxItem item, int x, int y, int nWidth, int nHeight, bool bHasLocation, bool bHasSite) { IComponent[] components = null; Rectangle itemBounds = this.Control.RectangleToClient(new Rectangle(x, y, nWidth, nHeight)); if (PageArea.Contains(itemBounds)) { components = base.CreateToolCore(item, x, y, nWidth, nHeight, bHasLocation, bHasSite); } return components; } }
We override OnPaintAdornments to force a selection frame around the page. This way, the user will know when the page is selected versus the entire Wizard control. If we didn’t do this, the user could mistakenly modify properties of the Wizard control when the intent is to modify properties of the page. We create a protected property named PageArea. This is a rectangle that contains the available bounds of the page, which excludes the header area. This is done so that no additional components and controls can be added to the header. We also draw a grid on the PageArea. CreateToolCore is overridden to only create a component if it is being created within the real estate allowed. Along with that, we override OnDragOver so that the appropriate drag-drop effect is displayed by the cursor, depending on where the mouse is being dragged. The WizardDesigner will serve as the designer for the Wizard control. We implement it in order to add designer verbs for adding, removing, and cycling through pages. We also add the ability to cycle through pages by clicking one of the navigation buttons. Here is the code: [Designer(typeof(WizardDesigner))] [DefaultEvent("ActivePageChanged")]
405
C H A P T E R
1 5
[DefaultProperty("InteriorWizardPages")] [ToolboxBitmap(typeof(Wizard), "ToolboxBitmaps.Wizard.bmp")] public class Wizard : System.Windows.Forms.UserControl public class WizardDesigner : ParentControlDesigner { private readonly DesignerVerb _verbAddPage; private readonly DesignerVerb _verbShowNextPage; private readonly DesignerVerb _verbRemovePage; private private private private
The following methods handle the changing of the active page. WndProc is overridden to allow clicking the navigation buttons at design-time. By default, clicking a control on a designer does nothing, except for possibly making that control the active selection. In the WndProc method, messages are simply redirected from the Wizard to an appropriate child control. protected virtual void OnActivePageChanging(object sender, EventArgs e) { this.Wizard.ActivePage.WizardCommand -= new WizardCommandEventHandler(OnWizardCommand); } protected virtual void OnActivePageChanged(object sender, EventArgs e) { this.Wizard.ActivePage.WizardCommand += new WizardCommandEventHandler(OnWizardCommand); } protected override void WndProc(ref Message m) { switch (m.Msg) { case 513: { Point pt = new Point(m.LParam.ToInt32()); IntPtr pHandle = IntPtr.Zero; if (m.HWnd == this.Control.Controls[1].Controls[1].Handle) { pHandle = m.HWnd; } else if (m.HWnd == this.Control.Controls[1].Controls[2].Handle) { pHandle = m.HWnd;
protected virtual void ModifyMenu() { _verbRemovePage.Enabled = false; if (this.Wizard.ActivePage is InteriorWizardPage) { _verbRemovePage.Enabled = true; } } protected Wizard Wizard { get { return this.Control as Wizard; } }
The following methods are the verb handlers. The AddPage and RemovePage methods utilize designer transactions so that their processing can be undone. We use the designer host to create and remove pages from the Wizard, so that they are removed form the designer as well. private void AddPage(object sender, EventArgs e) { DesignerTransaction transaction = null; PropertyDescriptor pd = TypeDescriptor.GetProperties(this.Wizard)["InteriorWizardPages"]; if (_designerHost != null) { try { String siteName = Component.Site.Name; transaction = _designerHost.CreateTransaction(siteName + "WizardAddWizardPage"); RaiseComponentChanging(pd);
We override these methods so that the grid is not drawn on the Wizard control. protected override void OnPaintAdornments(PaintEventArgs e) { _drawGrid = false; base.OnPaintAdornments(e); }
_drawGrid = true;
protected override bool DrawGrid { get { return _drawGrid; } set
413
C H A P T E R
{ }
}
1 5
_drawGrid = value;
We override the following methods so that this designer can only be a parent of a BaseWizardPage or a WizardPageDesigner. public override bool CanParent(Control control) { if (control is BaseWizardPage) return true; }
return false;
public override bool CanParent(ControlDesigner designer) { if (designer is WizardPageDesigner) return true;
}
}
return false;
We have now added all of the code necessary to pose a complete design-time solution. But in order to end the chapter in a cool and fun way, we should illustrate the use of a root designer. Recall that a root designer is a designer that is associated with a root component. Root components are components such as Forms and User Controls. Anything that can be opened in a designer and designed alone is a short and easy way of defining a root designer. We will create a root designer for the BaseWizardPage so that a copyright notice appears on the designer. We will also change the color of the background just for fun. So create a new class named WizardPageRootDesigner, and apply the designer to the BaseWizardPage. Here is the code: [DefaultEvent("Activate")] [Designer(typeof(WizardPageDesigner))] [Designer(typeof(WizardPageRootDesigner), typeof(IRootDesigner))] [ToolboxItem(false)] public class BaseWizardPage : System.Windows.Forms.UserControl
414
D E V E L O P I N G
T H E
W I N D O W S - B A S E D
W I Z A R D
C O N T R O L
public class WizardPageRootDesigner : DocumentDesigner { public override void Initialize(IComponent component) { base.Initialize(component); IRootDesigner rootDesigner = (IRootDesigner) this; System.Windows.Forms.Control view = rootDesigner.GetView(ViewTechnology.WindowsForms) as Control; if (view != null) { view.BackColor = Color.LightSkyBlue; Label label = new Label(); label.Text = "(c) 2002 BlueVision, LLC. All Rights Reserved."; label.Dock = DockStyle.Bottom; view.Controls.Add(label); } } }
In this class, we override the Initialize method and get a reference to the IRootDesigner. Because DocumentDesigner implements the IRootDesigner interface, we simply use a cast. IRootDesigner rootDesigner = (IRootDesigner) this;
Recall that using a view technology of ViewTechnology.WindowsForms should cause the GetView method to return an instance of Control. The control returned is the area on which the control being designed is shown. After retrieving the control, we change the background color and add a copyright label to the bottom. If you compile the code and open any wizard page in the designer, you see a view similar to the following:
415
C H A P T E R
1 5
Even though this designer wasn’t the most complex, we decided to save it for last.
Summary The chapter illustrated the necessary concepts involved in creating a custom control. Using preexisting guidelines, we were able to design a model and create the UI. Because Visual Studio .NET is remarkable for rapid prototyping, we were able to generate the UI during the architectural design phase. In this chapter, we learned about docking and control layout, which was introduced in Chapter 5. We also defined events according to our knowledge from Chapter 2. The ControlPaint class of chapter 7 was utilized for drawing a selection frame around our interior wizard pages. And finally, we used Chapters 12 and 13 to spice up our control with design-time support. In the
416
D E V E L O P I N G
T H E
W I N D O W S - B A S E D
W I Z A R D
C O N T R O L
next chapter, we will switch our focus away from Windows Forms, and revisit Web Forms with a sample.
417
16 Chapter
Developing the Web-based Tab Control Overview Another useful control that was not available as a component in the ASP.NET control framework is the TabControl. A TabControl displays multiple tabs, of which each can be clicked to bring focus to a new tab page. The tabs are analogous to dividers in a notebook, where flipping a divider will yield the divider page. Our goal is to implement an ASP.NET server control that simulates a TabControl. The control will support both vertical tabs and horizontal tabs. Vertical tabs are those that can be found on the Visual Studio .NET toolbox. In this chapter we will illustrate rendering, state management, parsing, and design-time support.
Step 1: The Architecture The model for the TabControl is very simple. It can be illustrated as follows:
C H A P T E R
1 6
TabControl
TabCollection (Collection)
Tab
Simply stated, the TabControl contains a collection of type TabCollection. Each item in the collection is of type Tab.
Step 2: The User Interface Now that we know the model, we will create the user interface. When writing server controls, you will sometimes need to utilize the HTML designer and the View In Browser function for brainstorming. In the case of this example, the best way to design your control is to first design it in an HTML designer separate from your project. So let’s do that by creating an HTML file in Visual Studio .NET. We will first start with the horizontal rendering. We will render the horizontal view as a table with two rows. The first row will be used as the header while the second row will be used as the view, as shown:
420
D E V E L O P I N G
T H E
W E B - B A S E D
T A B
C O N T R O L
Now, we will render the tab headers by creating columns in the tab header. We must also set the view to span the number of columns that are contained in the header. Here is the output with the tab headers:
421
C H A P T E R
1 6
Because 5 columns were added in this example, we must span 5 columns in the view, so that the view virtually has only one column, as shown:
422
D E V E L O P I N G
T H E
W E B - B A S E D
T A B
C O N T R O L
Before we move further, let’s glance at the HTML code used to generate the output:
We need to now make the output above look more like a true tab control. We start by changing the background color of the table and modifying the borders of the view, as shown:
423
C H A P T E R
1 6
The code above will produce the following output:
The view now has more of a 3-D look. Similarly, we can do the same to the tab headers. At the same time, we will distinguish the selected tab from the others through border modifications and colors, as shown:
424
D E V E L O P I N G
T H E
W E B - B A S E D
T A B
C O N T R O L
We now have some handy HTML for help in rendering our control horizontally. So let’s do the same for vertical rendering. To jump right into things, here is the code for the vertical tab, with the output following:
thin thin
thin thin
thin thin
425
C H A P T E R
1 6
white thin white thin
white thin white thin
Here is the output:
The vertical tab control will use the same 3-D effects that were used by the horizontal one. Now that we have everything we need to know from a UI point of view, we can begin implementation.
Step 3: Runtime Implementation For the implementation phase, we can concentrate on four specific areas: implementing the model, parsing and persistence support, providing the rendering logic, and maintaining the state between post backs. The first thing we will do is implement the model. We know that the
426
D E V E L O P I N G
T H E
W E B - B A S E D
T A B
C O N T R O L
TabControl will contain a collection of tabs and that it can be rendered both vertically and horizontally, so let’s create three classes: TabControl, TabCollection and Tab; and one enumeration: TabOrientation, to reflect the model. Create a new C# file and add these classes and enumeration to the file, as shown: public class TabControl : Control { public enum TabOrientation { Vertical, Horizontal } private TabCollection _tabs = new TabCollection(); private Unit _unitWidth = new Unit("100%"); private Unit _unitHeight = new Unit("100%"); private int _selectedIndex = 0; private TabOrientation _tabOrientation = TabOrientation.Vertical; public TabCollection Tabs { get{ return _tabs; } } public Unit Width { get { return _unitWidth; } set { _unitWidth = value; } } public Unit Height { get { return _unitHeight; } set { _unitHeight = value; } } public TabOrientation Orientation { get { return _tabOrientation; } set { _tabOrientation = value; } } public int SelectedIndex { get { return _selectedIndex; } set { _selectedIndex = value; }
427
C H A P T E R
}
1 6
}
public class TabCollection : CollectionBase { public Tab this[int nIndex] { get { return (Tab) base.List[nIndex]; } } public void Add(Tab tab) { base.List.Add(tab); }
}
public int IndexOf(Tab tab) { return base.List.IndexOf(tab); }
public class Tab : Control { private String m_strHeaderText; public String HeaderText { get { return m_strHeaderText; } set { m_strHeaderText = value; } } }
Now, we must support parsing and persistence so that the tabs can be saved and retrieved from a web form correctly. This is done through the PersistChildrenAttribute, PersistenceModeAttribute and the ParseChildrenAttribute. They are applied as follows: [PersistChildren(false)] [ParseChildren(true, "Tabs")] public class TabControl : Control, IPostBackDataHandler, IPostBackEventHandler { ... [PersistenceMode(PersistenceMode.InnerDefaultProperty)]
428
D E V E L O P I N G
T H E
W E B - B A S E D
T A B
C O N T R O L
public TabCollection Tabs { get{ return _tabs; } } }
...
The ParseChildrenAttribute tells the control parser to construct the Tabs property using any nested elements of the TabControl element. Because of this attribute, we do not need to specify and define a custom control builder. The PersistChildrenAttribute tells the control persister not to prefix immediate child elements with a tag. The PersistenceModeAttribute tells the control persister to write the Tabs collection as a nested element. Note that we use the InnerDefaultProperty member instead of the InnerProperty member because we used the DefaultProperty overload of ParseChildrenAttribute. Both attributes must be able to work hand in hand. Because we know the styles that will be involved in our TabControl rendering, we can go ahead and create a style sheet named TabControl.css for storing these styles. Create a new style sheet in the project, and set its Build Action to Embedded Resource, because we will want to extract this style sheet at runtime. Here is what we came up with: .TabHorizontal_Header { border-left:thin outset white; border-right:thin outset white; border-top:thin outset white; border-bottom:thin inset window; padding:0,0,0,5; margin:0px; height:20px; width:25%; font-size:smaller; font-weight:400; font-family:Menu, Verdana, Arial; color:menutext; background-color:eeeeee; } .TabHorizontal_HeaderSelected { border-left:thin outset white; border-right:thin outset white; border-top:thin outset white;
To aid in the extraction process, we created a utility class named WebControlUtility that contains a static method, ExtractServerResources. We will not show the details of this method here, but it basically extracts a file from the resources and writes it to disk on the web server. Therefore, the ASP.NET user account will need write permissions on that directory. The extraction process only writes when the assembly’s date changes. An assembly date change reflects a possible resource change. If you do not wish to enable write access, you can always extract the resources manually using some of the .NET framework tools, and then place the extracted file in the directory of the web application that will host the control. Note that we could have easily embedded all the styles in the Render method of the control, but adding them to the resources will allow easy modification. With that said, we create a private method named RenderSupportScripts for rendering scripts and styles sheets, as shown: private static readonly String _extractedResourcePath = WebControlUtility.AppRootPath; private const String _styleSheet = "TabControl.css"; private void RenderSupportScripts(HtmlTextWriter output) { // Extract the HTC file and the CSS file from this assembly to the application directory. Assembly controlAssembly = Assembly.GetAssembly(typeof(TabControl));
We now override the Render method of the control, and call RenderSupportScripts immediately, as shown: protected override void Render(HtmlTextWriter output) { // Register the client script. if (Page != null && !Page.IsClientScriptBlockRegistered(this.GetType().FullName)) { Page.RegisterClientScriptBlock(this.GetType().FullName, ""); RenderSupportScripts(output); } }
We first check to see if the styles link has already been rendered by another control or this control. If it has not, we render the styles link. We can now add two more private methods, RenderControlHorizontal and RenderControlVertical, and call these methods from our Render method, as shown: protected override void Render(HtmlTextWriter output) { // Register the client script. if (Page != null && !Page.IsClientScriptBlockRegistered(this.GetType().FullName)) { Page.RegisterClientScriptBlock(this.GetType().FullName, ""); RenderSupportScripts(output); } if (_tabs.Count > 0)
432
D E V E L O P I N G
{
T H E
W E B - B A S E D
T A B
C O N T R O L
_selectedIndex = 0;
// Call the appropriate rendering method, depending on the TabOrientation // (This provides easier maintenance and better readability) if (_tabOrientation == TabOrientation.Vertical) { RenderTabControlVertical(output); } else if (_tabOrientation == TabOrientation.Horizontal) { RenderTabControlHorizontal(output); } } } private void RenderTabControlVertical(HtmlTextWriter output) { // TabControl HtmlTable table = new HtmlTable(); table.ID = this.UniqueID; table.Width = this.Width.ToString(); table.Height = this.Height.ToString(); table.CellSpacing = 1; table.CellPadding = 0; table.Border = 1; table.BgColor = "Menu"; table.Style.Add("cursor", "default"); // Render each tab in the TabCollection. foreach(Tab tab in _tabs) { String strDisplay = "none"; if (_selectedIndex == _tabs.IndexOf(tab) ) strDisplay = "block";
// TabHeader HtmlTableRow tr = new HtmlTableRow(); HtmlTableCell td = new HtmlTableCell(); if (_selectedIndex == _tabs.IndexOf(tab) ) {
If we compiled and placed this control on a web form, we would only see a tab control with the first tab always selected. We will now allow the tab selection to be changed by defining a SelectedIndexChanged event and supporting post back. The basic idea is that when a user clicks on a tab, the current view should change to correspond to the tab clicked. To be consistent with other .NET server controls, we will give our control the capability to support both client side selection changes and server-side selection changes. We will do this by providing an AutoPostBack property. For server-side selection changes, we only need to rerender the control with the selected view shown. For client-side changes, however, things will get a little more difficult. On the client, the entire tab control should be rendered, with the inactive views hidden. Therefore, we must add client-side script to activate the hidden views as the tabs are changed. Like the style sheet, we add a TabControl.htc file to our project. Here is our script: <script language="jscript"> function TabControl_OnClick() { if (orientation == "horizontal") { TabHorizontal_OnClick(); } else if (orientation == "vertical") {
436
D E V E L O P I N G
}
}
T H E
W E B - B A S E D
T A B
C O N T R O L
TabVertical_OnClick();
function TabHorizontal_OnClick() { var nTabHit = 0; var tdTabHeaderHit = event.srcElement; var trHeader = tdTabHeaderHit.parentNode; var tdTabs = trHeader.childNodes; for (var nTab = 0; nTab < tdTabs.length - 1; 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 <= 1) return; for (var nTabView = 1; nTabView < trTabViews.length; nTabView = nTabView + 1) { var trTabView = trTabViews.item(nTabView); if (nTabHit == nTabView - 1) { trTabView.style.display = "block"; } else { trTabView.style.display = "none"; } } } function TabVertical_OnClick() { var tdTabHeaderHit = event.srcElement; var tBody = tdTabHeaderHit.parentNode.parentNode; var trTabs = tBody.childNodes;
Because this script is a behavior script, we need to modify the style sheet to make it work, as shown: .TabHorizontal_Header { behavior:url(TabControl.htc); ... } .TabHorizontal_HeaderSelected { behavior:url(TabControl.htc); ... } .TabVertical_Header { behavior:url(TabControl.htc); ... } .TabVertical_HeaderSelected { behavior:url(TabControl.htc); ... }
438
D E V E L O P I N G
T H E
W E B - B A S E D
T A B
C O N T R O L
Here is the new code for our class: public class TabControl : Control, IPostBackDataHandler, IPostBackEventHandler { ... private bool _autoPostBack = true; private const String _behaviorFile = "TabControl.htc"; ... public bool AutoPostBack { get { return _autoPostBack; } set { _autoPostBack = value; } } protected override void OnInit(EventArgs e) { base.OnInit(e);
}
if (AutoPostBack && Page != null) { Page.RegisterRequiresPostBack(this); }
protected override void LoadViewState(Object viewState) { if (viewState != null && _autoPostBack) { _selectedIndex = Int32.Parse((String) viewState); } else { base.LoadViewState(viewState); } } protected override Object SaveViewState() { // If AutoPostBack is set, save the SelectedTab to the view state for postback scenarios if (_autoPostBack) {
public event EventHandler SelectedIndexChanged; public virtual void OnSelectedIndexChanged(EventArgs e) { if (SelectedIndexChanged != null) SelectedIndexChanged(this, e); } protected override void Render(HtmlTextWriter output) { ... if (_tabs.Count > 0) { // Get the Selected Index. // For a non-AutoPostBack TabControl, the selected index is stored in a cookie. // Otherwise, it is stored as a property of the TabControl. if (!_autoPostBack && Page != null) { _selectedIndex = 0; System.Web.HttpCookie cookie = Page.Request.Cookies[this.UniqueID+"_SelectedIndex"]; if (cookie != null) _selectedIndex = Int32.Parse(cookie.Value); if (_selectedIndex < 0 || _selectedIndex > _tabs.Count - 1) _selectedIndex = 0; } ... }
}
void IPostBackDataHandler.RaisePostDataChangedEvent() { // Stub Implementation, Required for IPostBackDataHandler
440
D E V E L O P I N G
T H E
W E B - B A S E D
T A B
C O N T R O L
// This control must derive from IPostBackDataHandler, even though it doesn't use its methods. } bool IPostBackDataHandler.LoadPostData(string strPostDataKey, System.Collections.Specialized.NameValueCollection postDataCollection) { // Stub Implementation, Required for IPostBackDataHandler // This control must derive from IPostBackDataHandler, even though it doesn't use its methods. return false; } void IPostBackEventHandler.RaisePostBackEvent(String strEventArgument) { if (strEventArgument == null) return; _selectedIndex = Int32.Parse(strEventArgument);
}
EventArgs e = new EventArgs(); OnSelectedIndexChanged(e);
private void RenderTabControlVertical(HtmlTextWriter output) { ... // Render each tab in the TabCollection. foreach(Tab tab in _tabs) { ... // TabHeader HtmlTableRow tr = new HtmlTableRow(); HtmlTableCell td = new HtmlTableCell(); if (_selectedIndex == _tabs.IndexOf(tab) ) { td.Attributes.Add("class", "TabVertical_HeaderSelected"); } else { td.Attributes.Add("class", "TabVertical_Header");
441
C H A P T E R
1 6
} td.Attributes.Add("orientation", this.Orientation.ToString().ToLower()); if (_autoPostBack) { td.Attributes.Add("onclick", "jscript:" + Page.GetPostBackEventReference(this, _tabs.IndexOf(tab).ToString())); } td.Attributes.Add("oncontextmenu", "TabVertical_OnContextMenu()"); td.Controls.Add(new LiteralControl(tab.HeaderText)); tr.Controls.Add(td); table.Controls.Add(tr); // TabView // Only render the TabView under the following conditions: // (1) AutoPostBack is set to false. // (2) AutoPostBack is set to true, and the TabView is for the Selected Tab if (!_autoPostBack || (_autoPostBack && _selectedIndex == _tabs.IndexOf(tab)) ) { tr = new HtmlTableRow(); td = new HtmlTableCell(); td.Attributes.Add("class", "TabVertical_Content"); td.Controls.Add(tab); tr.Controls.Add(td); tr.Style.Add("display", strDisplay); table.Controls.Add(tr); } } }
table.RenderControl(output);
private void RenderTabControlHorizontal(HtmlTextWriter output) { ... // Render each tab in the TabCollection. foreach(Tab tab in _tabs) { // TabHeader
442
D E V E L O P I N G
T H E
W E B - B A S E D
T A B
C O N T R O L
HtmlTableCell td = new HtmlTableCell(); if (_selectedIndex == _tabs.IndexOf(tab) ) { td.Attributes.Add("class", "TabHorizontal_HeaderSelected"); } else { td.Attributes.Add("class", "TabHorizontal_Header"); } if (_autoPostBack) { td.Attributes.Add("onclick", "jscript:" + Page.GetPostBackEventReference(this, _tabs.IndexOf(tab).ToString())); } td.Attributes.Add("orientation", this.Orientation.ToString().ToLower()); td.Style.Add("width", nWidthPercent.ToString() + "%"); td.Controls.Add(new LiteralControl(tab.HeaderText)); tr.Controls.Add(td); } HtmlTableCell tdSpace = new HtmlTableCell(); tdSpace.Controls.Add(new LiteralControl(" ")); tdSpace.Attributes.Add("class", "TabHorizontal_Space"); tr.Controls.Add(tdSpace); table.Controls.Add(tr); // TabView foreach (Tab tab in _tabs) { String strDisplay = "none"; if (_selectedIndex == _tabs.IndexOf(tab) ) strDisplay = "block"; // Only render the TabView under the following conditions: // (1) AutoPostBack is set to false. // (2) AutoPostBack is set to true, and the TabView is for the Selected Tab if (!_autoPostBack || (_autoPostBack && _selectedIndex == _tabs.IndexOf(tab)) ) { tr = new HtmlTableRow(); HtmlTableCell td = new HtmlTableCell(); td.ColSpan = _tabs.Count + 1;
We implement IPostBackEventHandler so that we can process the post back events raised by the client. We implement IPostBackDataHandler because we registered the page as one that requires post back. Thus, the .NET page framework will expect this interface to be implemented by our control, even though we only provide a stubbed implementation. If AutoPostBack is set to true, we use the GetPostBackEventReference of the Page class to set the click event of the tab headers. Otherwise, we use a user-defined click event. We also provide custom view state logic, so that only the selected index is stored in the view state. Because of this, the SelectedIndex property will always remember its value between post backs. This completes the runtime implementation for the TabControl.
Step 4: Design-Time Support In order for our control to be developer friendly in the Visual Studio .NET IDE, we must add some design-time support to it. One of the first things we must do is load our control with design-time attributes, such as DefaultValueAttribute, DefaultEventAttribute, and DefaultPropertyAttribute. Here are some attributes that we added: [ ToolboxData("<{0}:TabControl runat=server>{0}:TabControl>"), System.Drawing.ToolboxBitmap(typeof(TabControl), "Resources.ToolboxBitmaps.TabControl.bmp"), Designer(typeof(TabControlDesigner)), ParseChildren(true, "Tabs"), PersistChildren(false), DefaultProperty("Tabs"), DefaultEvent("SelectedIndexChanged"), ]
444
D E V E L O P I N G
T H E
W E B - B A S E D
T A B
C O N T R O L
public class TabControl : Control, IPostBackDataHandler, IPostBackEventHandler { ... [ DesignerSerializationVisibility(DesignerSerializationVisibility .Content), PersistenceMode(PersistenceMode.InnerDefaultProperty), ] public TabCollection Tabs { get{ return _tabs; } } ... [DefaultValue(true)] public bool AutoPostBack { get { return _autoPostBack; } set { _autoPostBack = value; } } ...
}
[Browsable(false)] public int SelectedIndex { get { return _selectedIndex; } set { _selectedIndex = value; } }
The ToolboxDataAttribute indicates what should be written to an ASP.NET page when the control is created from the toolbox. The ToolboxBitmap is also specified, indicating the bitmap that should be used for the TabControl. The BrowsableAttribute is set to false for the SelectedIndex property so that it can’t be set in the designer. And most interestingly, the DesignerSerializationVisibilityAttribute is set to a value of Content, indicating that the collection’s items should be serialized to the page by the designer. Because the DesignerAttribute was specified, we need to go ahead and implement a TabControlDesigner class. Our designer will add verbs to enable adding, removing and cycling through the tabs in the collection. We render the designer exactly as it would look
445
C H A P T E R
1 6
during runtime. However, if no tabs have been added to the collection, we render a note explaining how tabs should be added. Here is the code for this class: public class TabControlDesigner : System.Web.UI.Design.ControlDesigner { #region Fields private DesignerVerb m_verbAddTab; private DesignerVerb m_verbShowNextTab; private DesignerVerb m_verbRemoveTab; private int m_nDesignTimeSelectedIndex; #endregion public TabControlDesigner() { m_verbAddTab = new DesignerVerb("Add New Tab", new EventHandler(AddTab)); m_verbShowNextTab = new DesignerVerb("Show Next Tab", new EventHandler(ShowNextTab)); m_verbRemoveTab = new DesignerVerb("Remove Current Tab", new EventHandler(RemoveTab));
public override void Initialize(IComponent component) { base.Initialize(component); TabControl tabControl = (TabControl)component; m_nDesignTimeSelectedIndex = tabControl.SelectedIndex; ISelectionService ss = (ISelectionService)GetService(typeof(ISelectionService)); IComponentChangeService ccs = (IComponentChangeService)GetService(typeof(IComponentChangeServic e)); if (ss != null) { ss.SelectionChanged += new EventHandler(OnSelectionChanged); ccs.ComponentChanged += new ComponentChangedEventHandler(OnComponentChanged); } }
446
D E V E L O P I N G
T H E
W E B - B A S E D
T A B
C O N T R O L
protected override void Dispose(bool disposing) { ISelectionService ss = (ISelectionService)GetService(typeof(ISelectionService)); IComponentChangeService ccs = (IComponentChangeService)GetService(typeof(IComponentChangeServic e)); if (ss != null) { ss.SelectionChanged -= new EventHandler(OnSelectionChanged); ccs.ComponentChanged -= new ComponentChangedEventHandler(OnComponentChanged); } base.Dispose(disposing); } public override void OnComponentChanged(object sender, ComponentChangedEventArgs e) { base.OnComponentChanged(sender, e);
}
if (e.Component == this.Component) { ModifyMenu(); }
public override bool DesignTimeHtmlRequiresLoadComplete { get { return true; } } protected override String GetEmptyDesignTimeHtml() { // Provide the developer with info on how to add tabs to the TabControl. String strHtml = ""; strHtml += "
"; strHtml += "
TabControl - " + this.ID + "
"; strHtml += "
Please add Tabs through the Tabs (Collection) property in the Properties pane,
";
447
C H A P T E R
1 6
strHtml += "
or by right-clicking this Control and choosing 'Add New Tab'.
"; strHtml += "
Then switch to HTML view and edit each Tab's view by inserting inner content.
"; }
return strHtml;
public override String GetDesignTimeHtml() { TabControl tabControl = (TabControl) this.Component; if (tabControl != null && tabControl.Tabs.Count > 0) { StringWriter stringWriter = new StringWriter(); HtmlTextWriter textWriter = new HtmlTextWriter(stringWriter); if (tabControl.Orientation == TabControl.TabOrientation.Horizontal) { // TabControl HtmlTable table = new HtmlTable(); table.Width = tabControl.Width.ToString(); table.Height = tabControl.Height.ToString(); table.CellSpacing = 0; table.CellPadding = 0; table.Border = 0; table.BgColor = "Menu"; HtmlTableRow tr = new HtmlTableRow(); int nWidthPercent = 15; if (tabControl.Tabs.Count * nWidthPercent > 100) { nWidthPercent = 100/tabControl.Tabs.Count; } // Render each tab in the TabCollection. foreach(Tab tab in tabControl.Tabs) { // TabHeader HtmlTableCell td = new HtmlTableCell(); if (m_nDesignTimeSelectedIndex == tabControl.Tabs.IndexOf(tab) ) {
Summary We have now completed one of the most useful web server controls there is. This chapter focused on some topics that were introduced earlier in the book, such as state management, rendering, parsing, persistence and serialization. We saw how easy it was to implement design-time rendering so that it looks exactly like the actual runtime control. With the knowledge from this chapter and the topics discussed above, you should feel comfortable creating your own web server controls and designers.
453
Index A Activity Diagram, 82, 88 AddAttribute method, HtmlTextWriter class, 218 AddHandler method, Delegate class, 24, 25, 404, 405, 418 AddressControl server control, 287 AddService method, IServiceContainer interface, 333 AddStyleAttribute method, HtmlTextWriter class, 218 AdRotator, ASP.NET server controls, 196 AdRotatorDesigner class, 326 Advanced data binding, 148, 149, 150 text and tag property, 149, 150 Architecture Windows Forms, 104 Array, 124 ASP.NET Active server pages, 189, 192 architecture, 189 caching, 237 CGI comparison, 190, 191, 192 compilation process, 193 custom and user controls, 199 intrinsic objects, 243 ISAPI extensions, 191, 192 registering controls, 206 rendering, 212, 217 server control syntax, 195 state management, 243 types of server controls, 195 validation controls, 198 ATL Connection points, 17, 27 AutoPostBack property, TextBox control, 196